#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
//! default agent profile policy plugin.
use bcode_agent_permissions::evaluate_profile_tool_call;
use bcode_agent_policy::{
AgentConfig, AgentPermissionConfig, BUILD_AGENT, PLAN_AGENT, active_tools_for, agent_config,
default_config as policy_default_config,
};
use bcode_agent_profile::{
AGENT_PROFILE_INTERFACE_ID, AgentContextRequest, AgentContextResponse, AgentDecision,
AgentInfo, AgentList, AgentListRequest, EvaluateToolCallRequest, EvaluateToolCallResponse,
OP_AGENT_CONTEXT, OP_EVALUATE_TOOL_CALL, OP_LIST_AGENTS, OP_POLICY_STATUS,
OP_RESOLVE_POLICY_PROFILE_IDENTITY, PolicyStatusResponse,
ResolveAgentPolicyProfileIdentityRequest,
};
use bcode_plugin_sdk::prelude::*;
use serde::Deserialize;
use sha2::{Digest as _, Sha256};
use std::collections::BTreeSet;
use std::fmt::Write as _;
use std::path::PathBuf;
use toml::{Table, Value};
const MANIFEST: &str = include_str!("../bcode-plugin.toml");
#[derive(Debug, Deserialize)]
struct DefaultAgentsManifestExtension {
agent_defaults: AgentDefaultsManifestExtension,
}
#[derive(Debug, Deserialize)]
struct AgentDefaultsManifestExtension {
build_tools: Vec<String>,
plan_disabled_tools: Vec<String>,
build_preauthorizations: Vec<String>,
}
fn manifest_defaults() -> Result<AgentDefaultsManifestExtension, toml::de::Error> {
toml::from_str::<DefaultAgentsManifestExtension>(MANIFEST)
.map(|extension| extension.agent_defaults)
}
#[must_use]
pub fn default_config() -> AgentPermissionConfig {
let (config, _) = default_config_with_diagnostics();
config
}
fn default_config_with_diagnostics() -> (AgentPermissionConfig, Vec<String>) {
match manifest_defaults() {
Ok(defaults) => (default_config_with_defaults(defaults), Vec::new()),
Err(error) => (
policy_default_config(),
vec![format!(
"failed to parse plugin agent_defaults; using policy defaults without plugin tool enablement: {error}"
)],
),
}
}
fn default_config_with_defaults(defaults: AgentDefaultsManifestExtension) -> AgentPermissionConfig {
let mut config = policy_default_config();
if let Some(build) = config.agent.get_mut(BUILD_AGENT) {
set_default_tools(build, &defaults.build_tools, true);
build.permission.tools.extend(
defaults
.build_preauthorizations
.iter()
.map(|tool| (tool.clone(), bcode_agent_policy::Action::Allow)),
);
}
if let Some(plan) = config.agent.get_mut(PLAN_AGENT) {
set_default_tools(plan, &defaults.build_tools, true);
for tool_id in defaults.plan_disabled_tools {
plan.tools.insert(tool_id, false);
}
}
config
}
fn set_default_tools(agent: &mut AgentConfig, tool_ids: &[String], enabled: bool) {
agent
.tools
.extend(tool_ids.iter().map(|tool_id| (tool_id.clone(), enabled)));
}
/// Default plan/build agent profile plugin.
#[derive(Default)]
pub struct DefaultAgentsPlugin;
impl RustPlugin for DefaultAgentsPlugin {
fn invoke_service(&mut self, context: NativeServiceContext) -> ServiceResponse {
if context.request.interface_id != AGENT_PROFILE_INTERFACE_ID {
return ServiceResponse::error(
"unsupported_interface",
"unsupported agent profile service interface",
);
}
match context.request.operation.as_str() {
OP_LIST_AGENTS => list_agents(&context.request),
OP_AGENT_CONTEXT => agent_context(&context.request),
OP_EVALUATE_TOOL_CALL => evaluate_tool(&context.request),
OP_POLICY_STATUS => json_response(&policy_status()),
bcode_agent_profile::OP_PREVIEW_COMMAND => preview_command(&context.request),
OP_RESOLVE_POLICY_PROFILE_IDENTITY => resolve_policy_profile_identity(&context.request),
_ => ServiceResponse::error(
"unsupported_operation",
"unsupported agent profile operation",
),
}
}
}
/// Decode a client-supplied effective config into the raw `[agent]` layer and typed tool selection.
///
/// The raw [`Value`] is retained deliberately: `AgentConfig` and `PermissionConfig`
/// declare `#[serde(default)]` without `skip_serializing_if`, so a typed round-trip
/// materializes empty rule tables and default `external_directory` actions. Layering
/// those synthesized entries would clobber the built-in defaults beneath them, so the
/// raw table is composed instead, exactly as [`load_config`] composes `bcode.toml`.
fn effective_config_layers(contents: Option<&str>) -> Option<(Value, bcode_config::ToolsConfig)> {
let contents = contents?;
let typed = bcode_config::decode_effective_config(contents).ok()?;
let raw = toml::from_str::<Value>(contents).ok()?;
Some((raw, typed.tools))
}
fn policy_and_tools_from_effective_toml(
contents: Option<&str>,
) -> Option<(AgentPermissionConfig, bcode_config::ToolsConfig)> {
let (declarative, tools_config) = effective_config_layers(contents)?;
let (config, _) = compose_agent_policy(&declarative, load_permissions_state_layer().as_ref());
Some((config, tools_config))
}
fn agent_list_with_config(effective_config_toml: Option<&str>) -> AgentList {
let config = policy_and_tools_from_effective_toml(effective_config_toml)
.map_or_else(|| load_config().0, |(config, _)| config);
let plan = agent_config(&config, PLAN_AGENT);
let build = agent_config(&config, BUILD_AGENT);
AgentList {
agents: vec![
AgentInfo {
id: PLAN_AGENT.to_string(),
name: "Plan".to_string(),
description: "Read-only analysis agent with Pi/OpenCode-style command policy"
.to_string(),
badge: Some("plan".to_string()),
accent: plan.accent,
aliases: vec!["plan".to_string()],
is_default: false,
},
AgentInfo {
id: BUILD_AGENT.to_string(),
name: "Build".to_string(),
description: "Implementation agent with normal Bcode permission checkpoints"
.to_string(),
badge: Some("build".to_string()),
accent: build.accent,
aliases: vec!["build".to_string()],
is_default: true,
},
],
}
}
fn list_agents(request: &ServiceRequest) -> ServiceResponse {
let request = match request.payload_json::<AgentListRequest>() {
Ok(request) => request,
Err(error) => return invalid_request(&error),
};
json_response(&agent_list_with_config(
request.effective_config_toml.as_deref().map(String::as_str),
))
}
fn agent_context(request: &ServiceRequest) -> ServiceResponse {
let request = match request.payload_json::<AgentContextRequest>() {
Ok(request) => request,
Err(error) => return invalid_request(&error),
};
let (config, tools_config) = policy_and_tools_from_effective_toml(
request.effective_config_toml.as_deref().map(String::as_str),
)
.unwrap_or_else(|| (load_config().0, load_tools_config()));
let mut agent = agent_config(&config, &request.agent_id);
apply_tool_selection(&mut agent, &tools_config, &request.available_tools);
let enabled_tools = Some(enabled_tools_from_available_metadata(
active_tools_for(&agent),
&request.available_tools,
));
let system_prompt_suffix = Some(match request.agent_id.as_str() {
PLAN_AGENT => "[PLAN AGENT ACTIVE]\n\nInspect, analyze, and plan only. Do not edit source files or intentionally modify project state. You may run shell commands allowed by the active permission policy, including validation commands such as `cargo check` or `cargo test`, even if they create normal build/cache artifacts. If implementation changes are needed, ask the user to switch to the build agent. When the user asks what text an image or screenshot says, use `ocr.extract` instead of `filesystem.read`.".to_string(),
BUILD_AGENT => "[BUILD AGENT ACTIVE]\n\nImplementation is allowed subject to Bcode permissions, active agent policy, and project instructions. Use tools normally, keep changes focused, and report validation. When the user asks what text an image or screenshot says, use `ocr.extract` instead of `filesystem.read`.".to_string(),
other => format!("[AGENT ACTIVE: {other}]\n\nFollow this agent's configured tool and permission policy."),
});
let operating_mode = Some(
match request.agent_id.as_str() {
PLAN_AGENT => "read_only",
BUILD_AGENT => "implementation",
_ => "profile_defined",
}
.to_string(),
);
json_response(&AgentContextResponse {
system_prompt_suffix,
operating_mode,
enabled_tools,
})
}
fn enabled_tools_from_available_metadata(
configured_enabled_tools: Vec<String>,
available_tools: &[bcode_tool::ToolDefinition],
) -> Vec<String> {
if available_tools.is_empty() {
return configured_enabled_tools;
}
let available_tool_names = available_tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<std::collections::BTreeSet<_>>();
configured_enabled_tools
.into_iter()
.filter(|tool_id| available_tool_names.contains(tool_id.as_str()))
.collect()
}
fn load_tools_config() -> bcode_config::ToolsConfig {
bcode_config::load_config()
.map(|config| config.tools)
.unwrap_or_default()
}
fn apply_tool_selection(
agent: &mut AgentConfig,
tools_config: &bcode_config::ToolsConfig,
available_tools: &[bcode_tool::ToolDefinition],
) {
let mut selected = match tools_config.default {
bcode_config::ToolDefaultMode::Agent => active_tools_for(agent).into_iter().collect(),
bcode_config::ToolDefaultMode::None => BTreeSet::new(),
bcode_config::ToolDefaultMode::All => available_tools
.iter()
.map(|tool| tool.name.clone())
.collect::<BTreeSet<_>>(),
};
selected.extend(tools_config.enabled.iter().cloned());
for tool_id in &tools_config.disabled {
selected.remove(tool_id);
}
let mut known = agent.tools.keys().cloned().collect::<BTreeSet<_>>();
known.extend(selected.iter().cloned());
known.extend(tools_config.disabled.iter().cloned());
if tools_config.default == bcode_config::ToolDefaultMode::All {
known.extend(available_tools.iter().map(|tool| tool.name.clone()));
}
for tool_id in known {
agent
.tools
.insert(tool_id.clone(), selected.contains(&tool_id));
}
}
fn apply_tool_selection_for_evaluation(
agent: &mut AgentConfig,
tools_config: &bcode_config::ToolsConfig,
requested_tool: &str,
) {
apply_tool_selection(agent, tools_config, &[]);
if tools_config.default == bcode_config::ToolDefaultMode::All
&& !tools_config.disabled.contains(requested_tool)
{
agent.tools.insert(requested_tool.to_string(), true);
}
}
fn evaluate_tool_request(
request: &EvaluateToolCallRequest,
) -> Result<EvaluateToolCallResponse, String> {
let cwd =
request.cwd.as_deref().map(PathBuf::from).ok_or_else(|| {
"tool policy requires an explicit session working directory".to_string()
})?;
let (config, tools_config) = policy_and_tools_from_effective_toml(
request.effective_config_toml.as_deref().map(String::as_str),
)
.unwrap_or_else(|| (load_config().0, load_tools_config()));
let mut agent = agent_config(&config, &request.agent_id);
apply_tool_selection_for_evaluation(&mut agent, &tools_config, &request.tool_name);
if let Some(pinned) = &request.policy_profile {
let current = policy_profile_identity(
&request.agent_id,
request.effective_config_toml.as_deref().map(String::as_str),
)?;
if current != *pinned {
return Ok(EvaluateToolCallResponse {
decision: AgentDecision::Deny,
reason: Some(
"the pinned workflow authorization profile is no longer available exactly"
.to_string(),
),
shell: None,
});
}
}
Ok(evaluate_profile_tool_call(&agent, request, &cwd))
}
fn preview_command(request: &ServiceRequest) -> ServiceResponse {
let Ok(request) = request.payload_json::<bcode_agent_profile::PreviewCommandRequest>() else {
return ServiceResponse::error("invalid_request", "invalid command preview request");
};
match preview_command_request(&request) {
Ok(response) => json_response(&response),
Err(message) => ServiceResponse::error("invalid_preview", message),
}
}
fn preview_command_request(
request: &bcode_agent_profile::PreviewCommandRequest,
) -> Result<bcode_agent_profile::PreviewCommandResponse, &'static str> {
use bcode_agent_profile::{PreviewCommandSubject, PreviewCommandWord, ToolPolicyOperation};
use bcode_shell_command_analysis_models::{ShellAnalysisRequest, ShellWord};
let (declarative, _) = effective_config_layers(Some(&request.effective_config_toml))
.ok_or("invalid declarative configuration")?;
// Deliberately exclude persisted approvals: this operation is an offline declarative preview.
let (config, source) = compose_agent_policy(&declarative, None);
if !source.diagnostics.is_empty() {
return Err("declarative agent policy could not be loaded without degradation");
}
if !config.agent.contains_key(&request.agent_id) {
return Err("unknown agent profile");
}
let cwd = PathBuf::from(&request.cwd);
if !cwd.is_absolute() {
return Err("command preview requires an absolute working directory");
}
let (analysis, analysis_error) =
match bcode_shell_command_analysis::analyze(&ShellAnalysisRequest::posix(&request.command))
{
Ok(analysis) => (Some(analysis), None),
Err(error) => (None, Some(error)),
};
let subjects = analysis.as_ref().map_or_else(Vec::new, |analysis| {
analysis
.commands
.iter()
.map(|subject| PreviewCommandSubject {
span: subject.span,
words: std::iter::once(&subject.executable)
.chain(&subject.arguments)
.map(|word| PreviewCommandWord {
span: word.span(),
dynamic: matches!(word, ShellWord::Dynamic { .. }),
})
.collect(),
assignments: subject
.assignments
.iter()
.map(|assignment| assignment.span)
.collect(),
})
.collect()
});
let analysis_valid = analysis
.as_ref()
.is_some_and(|analysis| analysis.completeness.is_complete());
let evaluation = EvaluateToolCallRequest {
session_id: bcode_session_models::SessionId::new(),
agent_id: request.agent_id.clone(),
tool_name: "shell.run".to_owned(),
operation: ToolPolicyOperation::Command {
command: Some(request.command.clone()),
analysis,
analysis_error,
},
aliases: Vec::new(),
requires_permission: true,
policy_profile: None,
cwd: Some(request.cwd.clone()),
effective_config_toml: None,
};
let preview = bcode_agent_policy::preview_tool_call(
&agent_config(&config, &request.agent_id),
&evaluation,
&cwd,
);
Ok(bcode_agent_profile::PreviewCommandResponse {
agent_id: request.agent_id.clone(),
scope: "declarative_command_policy".to_owned(),
full_authorization: false,
decision: preview.decision,
matched_rule: preview.matched_rule,
matches: preview.matches,
subjects,
analysis_valid,
})
}
fn evaluate_tool(request: &ServiceRequest) -> ServiceResponse {
let request = match request.payload_json::<EvaluateToolCallRequest>() {
Ok(request) => request,
Err(error) => return invalid_request(&error),
};
match evaluate_tool_request(&request) {
Ok(evaluation) => json_response(&evaluation),
Err(error) => json_response(&EvaluateToolCallResponse {
decision: AgentDecision::Ask,
reason: Some(error),
shell: None,
}),
}
}
fn policy_profile_identity(
profile_id: &str,
effective_config_toml: Option<&str>,
) -> Result<bcode_agent_profile::AgentPolicyProfileIdentity, String> {
let (config, tools_config) = policy_and_tools_from_effective_toml(effective_config_toml)
.unwrap_or_else(|| (load_config().0, load_tools_config()));
let mut profile = agent_config(&config, profile_id);
apply_tool_selection(&mut profile, &tools_config, &[]);
let encoded = serde_json::to_vec(&profile).map_err(|error| error.to_string())?;
let digest = Sha256::digest(encoded);
let mut policy_digest_sha256 = String::with_capacity(64);
for byte in digest {
write!(policy_digest_sha256, "{byte:02x}").expect("writing to a String cannot fail");
}
bcode_agent_profile::agent_policy_profile_identity(
"bcode.default-agents",
profile_id,
policy_digest_sha256,
)
}
fn resolve_policy_profile_identity(request: &ServiceRequest) -> ServiceResponse {
let request: ResolveAgentPolicyProfileIdentityRequest = match request.payload_json() {
Ok(request) => request,
Err(error) => {
return ServiceResponse::error("invalid_request", error.to_string());
}
};
match policy_profile_identity(
&request.profile_id,
request.effective_config_toml.as_deref().map(String::as_str),
) {
Ok(identity) => json_response(&identity),
Err(error) => ServiceResponse::error("encode_failed", error),
}
}
fn policy_status() -> PolicyStatusResponse {
let (config, source) = load_config();
let mut build = agent_config(&config, BUILD_AGENT);
let mut plan = agent_config(&config, PLAN_AGENT);
let tools_config = load_tools_config();
apply_tool_selection(&mut build, &tools_config, &[]);
apply_tool_selection(&mut plan, &tools_config, &[]);
PolicyStatusResponse {
using_default: source.using_default,
source: source.label,
build_enabled_tools: active_tools_for(&build),
plan_enabled_tools: active_tools_for(&plan),
diagnostics: source.diagnostics,
}
}
#[derive(Debug, Clone)]
struct PolicySource {
label: String,
using_default: bool,
diagnostics: Vec<String>,
}
/// Load the runtime permissions state layer.
///
/// The transported effective config carries declarative layers only, so this
/// highest-priority layer is read here for both the local and client-supplied paths.
fn load_permissions_state_layer() -> Option<Value> {
match bcode_config::load_permissions_state_value() {
Ok(state) => state,
Err(error) => {
eprintln!(
"bcode.default-agents: failed to load runtime permissions state ({error}); using declarative config only"
);
None
}
}
}
fn load_config() -> (AgentPermissionConfig, PolicySource) {
let declarative = match bcode_config::load_composed_config_value() {
Ok(value) => value,
Err(error) => {
let (base_config, diagnostics) = default_config_with_diagnostics();
eprintln!(
"bcode.default-agents: failed to load declarative config ({error}); using built-in defaults"
);
return (
base_config,
PolicySource {
label: "built-in default agent policy".to_string(),
using_default: true,
diagnostics,
},
);
}
};
compose_agent_policy(&declarative, load_permissions_state_layer().as_ref())
}
/// Compose agent policy from the built-in defaults, a declarative `[agent]` layer, and
/// the runtime permissions state layer.
///
/// Precedence matches `docs/permissions.md`: built-in defaults (including the
/// plugin-owned manifest `build_tools`) sit beneath declarative config, which sits
/// beneath the runtime permissions state.
fn compose_agent_policy(
declarative: &Value,
state: Option<&Value>,
) -> (AgentPermissionConfig, PolicySource) {
let (base_config, diagnostics) = default_config_with_diagnostics();
let declarative_empty = agent_table_is_empty(declarative);
let state_empty = state.is_none_or(agent_table_is_empty);
if declarative_empty && state_empty {
return (
base_config,
PolicySource {
label: "built-in default agent policy".to_string(),
using_default: true,
diagnostics,
},
);
}
let mut merged = match Value::try_from(base_config.clone()) {
Ok(value) => value,
Err(error) => {
eprintln!(
"bcode.default-agents: failed to encode built-in agent policy ({error}); using built-in defaults"
);
return (
base_config,
PolicySource {
label: "built-in default agent policy".to_string(),
using_default: true,
diagnostics,
},
);
}
};
bcode_config::merge_config_values(&mut merged, agent_only_config_value(declarative));
if let Some(state) = state {
bcode_config::merge_config_values(&mut merged, agent_only_config_value(state));
}
let config = match merged.try_into() {
Ok(config) => config,
Err(error) => {
eprintln!(
"bcode.default-agents: failed to decode composed agent policy ({error}); using built-in defaults"
);
return (
base_config,
PolicySource {
label: "built-in default agent policy".to_string(),
using_default: true,
diagnostics,
},
);
}
};
let label = match (declarative_empty, state_empty) {
(false, false) => {
"built-in default agent policy + bcode.toml [agent] + runtime permissions state"
.to_string()
}
(false, true) => "built-in default agent policy + bcode.toml [agent]".to_string(),
(true, false) => "built-in default agent policy + runtime permissions state".to_string(),
(true, true) => unreachable!("handled above"),
};
(
config,
PolicySource {
label,
using_default: false,
diagnostics,
},
)
}
fn agent_table_is_empty(value: &Value) -> bool {
value
.get("agent")
.and_then(Value::as_table)
.is_none_or(Table::is_empty)
}
fn agent_only_config_value(value: &Value) -> Value {
let Some(agent) = value.get("agent").cloned() else {
return Value::Table(Table::new());
};
let mut root = Table::new();
root.insert("agent".to_string(), agent);
Value::Table(root)
}
fn json_response<T: serde::Serialize>(value: &T) -> ServiceResponse {
ServiceResponse::json(value)
.unwrap_or_else(|error| ServiceResponse::error("serialization_failed", error.to_string()))
}
fn invalid_request(error: &serde_json::Error) -> ServiceResponse {
ServiceResponse::error("invalid_request", error.to_string())
}
#[cfg(not(feature = "static-bundled"))]
export_plugin!(DefaultAgentsPlugin, MANIFEST);
#[cfg(feature = "static-bundled")]
#[must_use]
pub fn static_plugin() -> bcode_plugin_sdk::StaticPluginVtable {
bcode_plugin_sdk::static_plugin_vtable!(
DefaultAgentsPlugin,
include_str!("../bcode-plugin.toml")
)
}
#[cfg(test)]
mod preview_tests {
use super::*;
use bcode_agent_profile::PreviewCommandRequest;
fn request(command: &str) -> PreviewCommandRequest {
PreviewCommandRequest {
agent_id: "build".to_owned(),
command: command.to_owned(),
cwd: "/tmp".to_owned(),
effective_config_toml:
"[agent.build.permission.command]\n\"*\" = \"deny\"\n\"printf *\" = \"allow\"\n"
.to_owned(),
}
}
#[test]
fn preview_aggregates_without_executing_and_omits_values() {
let response =
preview_command_request(&request("TOKEN=supersecret printf credential; forbidden"))
.unwrap();
assert_eq!(response.decision, AgentDecision::Deny);
assert_eq!(response.subjects.len(), 2);
assert!(!response.full_authorization);
assert_eq!(response.scope, "declarative_command_policy");
assert_eq!(response.subjects[0].assignments.len(), 1);
let json = serde_json::to_string(&response).unwrap();
for secret in ["TOKEN", "supersecret", "credential", "forbidden"] {
assert!(!json.contains(secret));
}
}
#[test]
fn preview_syntax_dynamic_and_configuration_fail_closed() {
let invalid = preview_command_request(&request("printf 'unterminated")).unwrap();
assert_eq!(invalid.decision, AgentDecision::Deny);
assert!(!invalid.analysis_valid);
let dynamic = preview_command_request(&request("$COMMAND")).unwrap();
assert_eq!(dynamic.decision, AgentDecision::Deny);
assert!(dynamic.subjects[0].words[0].dynamic);
let mut invalid = request("printf ok");
invalid.effective_config_toml = "not [valid".to_owned();
assert!(preview_command_request(&invalid).is_err());
invalid = request("printf ok");
invalid.agent_id = "missing".to_owned();
assert!(preview_command_request(&invalid).is_err());
}
#[test]
fn preview_forwards_layer_carveout_locators_and_regex_flags() {
let mut request = request("git status && git diff");
request.effective_config_toml = r#"
[agent.build.permission]
command_policy = { version = 1, layers = [ { command = { "git *" = "deny" } }, { command_patterns = { "git status" = "allow" } } ] }
"#.to_owned();
let response = preview_command_request(&request).unwrap();
assert_eq!(response.decision, AgentDecision::Deny);
let winners: Vec<_> = response
.matches
.iter()
.filter(|matched| matched.winning)
.collect();
assert_eq!(winners.len(), 2);
assert_eq!(
(
winners[0].subject_index,
winners[0].layer_index,
winners[0].rule_index
),
(0, 2, 0)
);
assert_eq!(winners[0].rule_kind, "command_patterns");
assert_eq!(winners[0].decision, AgentDecision::Allow);
assert!(!winners[0].broad);
assert_eq!((winners[1].subject_index, winners[1].layer_index), (1, 1));
assert!(winners[1].broad);
assert!(
response
.matches
.iter()
.any(|matched| matched.layer_index == 1
&& matched.subject_index == 0
&& !matched.winning
&& matched.broad)
);
request.command = "git STATUS".to_owned();
request.effective_config_toml = r#"
[agent.build.permission]
command_policy = { version = 1, layers = [ { command_patterns = { "git /status/i" = "allow" } } ] }
"#
.to_owned();
let regex = preview_command_request(&request).unwrap();
assert_eq!(regex.decision, AgentDecision::Allow);
let winner = regex
.matches
.iter()
.find(|matched| matched.winning)
.unwrap();
assert_eq!(winner.regex_flags, ["i"]);
assert_eq!(winner.layer_index, 1);
assert!(!winner.uncertain);
request.command = "git $ACTION".to_owned();
request.effective_config_toml = r#"
[agent.build.permission]
command_policy = { version = 1, layers = [ { command_patterns = { "git /push/i" = "deny" } } ] }
"#
.to_owned();
let dynamic = preview_command_request(&request).unwrap();
assert_eq!(dynamic.decision, AgentDecision::Deny);
assert!(
dynamic
.matches
.iter()
.any(|matched| matched.uncertain && matched.regex_flags == ["i"])
);
let json = serde_json::to_string(&response).unwrap();
assert!(!json.contains("supersecret"));
assert!(!json.contains("git status"));
}
#[test]
fn preview_static_allow_is_only_declarative() {
let response = preview_command_request(&request("printf ok")).unwrap();
assert_eq!(response.decision, AgentDecision::Allow);
assert!(response.analysis_valid);
assert!(!response.full_authorization);
}
}
#[cfg(test)]
mod tests {
use super::*;
use bcode_agent_policy::Action;
use bcode_agent_profile::AgentDecision;
use bcode_session_models::SessionId;
use serde_json::json;
use std::path::Path;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn effective_config_scopes_agent_accent_and_permissions_without_ambient_state() {
let _guard = ENV_LOCK.lock().expect("env lock should not be poisoned");
let root = unique_temp_dir();
let _state = StateGuard::set(&root.join("missing-permissions.toml"));
let allow_toml = r##"
[agent.build]
accent = "#112233"
tools = { "filesystem.read" = true }
[agent.build.permission]
read = { "**" = "allow" }
"##;
let deny_toml = r##"
[agent.build]
accent = "#aabbcc"
tools = { "filesystem.read" = true }
[agent.build.permission]
read = { "**" = "deny" }
"##;
let allow_agents = agent_list_with_config(Some(allow_toml));
let deny_agents = agent_list_with_config(Some(deny_toml));
let build_accent = |agents: &AgentList| {
agents
.agents
.iter()
.find(|agent| agent.id == BUILD_AGENT)
.and_then(|agent| agent.accent.clone())
};
assert_eq!(build_accent(&allow_agents).as_deref(), Some("#112233"));
assert_eq!(build_accent(&deny_agents).as_deref(), Some("#aabbcc"));
let request = |config: &str| EvaluateToolCallRequest {
session_id: SessionId::new(),
agent_id: BUILD_AGENT.to_string(),
tool_name: "filesystem.read".to_string(),
operation: bcode_agent_profile::ToolPolicyOperation::Read {
paths: vec!["/tmp/project/file".to_string()],
},
aliases: vec!["read".to_string()],
requires_permission: true,
policy_profile: None,
cwd: Some("/tmp/project".to_string()),
effective_config_toml: Some(Box::new(config.to_string())),
};
assert_eq!(
evaluate_tool_request(&request(allow_toml))
.expect("allow evaluation")
.decision,
AgentDecision::Allow
);
assert_eq!(
evaluate_tool_request(&request(deny_toml))
.expect("deny evaluation")
.decision,
AgentDecision::Deny
);
}
#[test]
fn later_policy_changes_fail_closed_for_pinned_workflow_identity() {
let _guard = ENV_LOCK.lock().expect("env lock should not be poisoned");
let root = unique_temp_dir();
std::fs::create_dir_all(&root).expect("temp root should be created");
let config_path = root.join("bcode.toml");
let previous_config = std::env::var_os("BCODE_CONFIG");
unsafe { std::env::set_var("BCODE_CONFIG", &config_path) };
std::fs::write(
&config_path,
"[agent.build.permission]\ncommand = { \"*\" = \"allow\" }\n",
)
.expect("config");
let pinned = policy_profile_identity("build", None).expect("pinned identity");
std::fs::write(
&config_path,
"[agent.build.permission]\ncommand = { \"*\" = \"deny\" }\n",
)
.expect("changed config");
let evaluation = evaluate_tool_request(&EvaluateToolCallRequest {
session_id: SessionId::new(),
agent_id: "build".to_string(),
tool_name: "shell.run".to_string(),
operation: bcode_agent_profile::ToolPolicyOperation::Command {
command: Some("cargo check".to_string()),
analysis: None,
analysis_error: None,
},
aliases: vec!["command".to_string()],
requires_permission: true,
policy_profile: Some(pinned),
cwd: Some(root.to_string_lossy().into_owned()),
effective_config_toml: None,
})
.expect("evaluation");
assert_eq!(evaluation.decision, AgentDecision::Deny);
assert!(evaluation.reason.expect("reason").contains("pinned"));
match previous_config {
Some(value) => unsafe { std::env::set_var("BCODE_CONFIG", value) },
None => unsafe { std::env::remove_var("BCODE_CONFIG") },
}
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn policy_profile_identity_is_exact_and_changes_with_policy() {
let _guard = ENV_LOCK.lock().expect("env lock should not be poisoned");
let root = unique_temp_dir();
std::fs::create_dir_all(&root).expect("temp root should be created");
let config_path = root.join("bcode.toml");
let previous_config = std::env::var_os("BCODE_CONFIG");
unsafe { std::env::set_var("BCODE_CONFIG", &config_path) };
std::fs::write(
&config_path,
"[agent.build.permission]\ncommand = { \"*\" = \"ask\" }\n",
)
.expect("config");
let first = policy_profile_identity("build", None).expect("first identity");
let repeated = policy_profile_identity("build", None).expect("repeat identity");
assert_eq!(first, repeated);
std::fs::write(
&config_path,
"[agent.build.permission]\ncommand = { \"*\" = \"deny\" }\n",
)
.expect("updated config");
let changed = policy_profile_identity("build", None).expect("changed identity");
assert_ne!(first.policy_digest_sha256, changed.policy_digest_sha256);
match previous_config {
Some(value) => unsafe { std::env::set_var("BCODE_CONFIG", value) },
None => unsafe { std::env::remove_var("BCODE_CONFIG") },
}
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn list_agents_contains_plan_and_build() {
let agents = agent_list_with_config(None).agents;
assert!(agents.iter().any(|agent| agent.id == PLAN_AGENT));
assert!(agents.iter().any(|agent| agent.id == BUILD_AGENT));
}
#[test]
fn manifest_agent_defaults_are_valid() {
let defaults = manifest_defaults().expect("plugin manifest defaults should parse");
assert!(!defaults.build_tools.is_empty());
for disabled_tool in &defaults.plan_disabled_tools {
assert!(
defaults.build_tools.contains(disabled_tool),
"plan-disabled tool {disabled_tool} should be declared in build defaults"
);
}
}
#[test]
fn plan_denies_unlisted_shell_command_parts() {
let (config, _) = load_config();
let agent = agent_config(&config, PLAN_AGENT);
let request = EvaluateToolCallRequest {
session_id: SessionId::new(),
agent_id: PLAN_AGENT.to_string(),
tool_name: "shell.run".to_string(),
operation: bcode_agent_profile::ToolPolicyOperation::Command {
command: Some("git diff && git commit -m nope".to_string()),
analysis: None,
analysis_error: None,
},
aliases: vec!["command".to_string()],
requires_permission: true,
policy_profile: None,
cwd: Some("/tmp/project".to_string()),
effective_config_toml: None,
};
let result = evaluate_profile_tool_call(&agent, &request, Path::new("/tmp/project"));
assert_eq!(result.decision, AgentDecision::Deny);
}
#[test]
fn default_plan_tools_are_plugin_owned_and_disable_writes() {
let config = default_config();
let plan = agent_config(&config, PLAN_AGENT);
let tools = active_tools_for(&plan);
assert!(tools.contains(&"filesystem.read".to_string()));
assert!(tools.contains(&"filesystem.list".to_string()));
assert!(tools.contains(&"filesystem.find".to_string()));
assert!(tools.contains(&"filesystem.grep".to_string()));
assert!(tools.contains(&"filesystem.stat".to_string()));
assert!(!tools.contains(&"filesystem.write".to_string()));
assert!(!tools.contains(&"filesystem.edit".to_string()));
assert!(!tools.contains(&"filesystem.multi_edit".to_string()));
}
#[test]
fn runtime_permission_state_preserves_build_default_tools_and_merges_agent_metadata() {
let _guard = ENV_LOCK.lock().expect("env lock should not be poisoned");
let root = unique_temp_dir();
std::fs::create_dir_all(&root).expect("temp root should be created");
let config_path = root.join("bcode.toml");
let state_path = root.join("permissions.toml");
std::fs::write(
&config_path,
r##"
[agent.build]
accent = "#22d3ee"
"##,
)
.expect("config should be written");
std::fs::write(
&state_path,
r#"
[agent.build.permission]
command = { "python3 *" = "allow" }
"#,
)
.expect("state should be written");
let previous_config = std::env::var_os("BCODE_CONFIG");
let previous_state = std::env::var_os("BCODE_PERMISSIONS_STATE");
unsafe {
std::env::set_var("BCODE_CONFIG", &config_path);
std::env::set_var("BCODE_PERMISSIONS_STATE", &state_path);
}
let (config, source) = load_config();
let build = agent_config(&config, BUILD_AGENT);
let tools = active_tools_for(&build);
assert!(tools.contains(&"filesystem.write".to_string()));
assert!(tools.contains(&"filesystem.edit".to_string()));
assert!(tools.contains(&"filesystem.multi_edit".to_string()));
assert!(tools.contains(&"shell.run".to_string()));
assert_eq!(build.accent.as_deref(), Some("#22d3ee"));
assert_eq!(
build.permission.command.get("python3 *"),
Some(&Action::Allow)
);
assert!(matches!(
source.label.as_str(),
"built-in default agent policy + runtime permissions state"
| "built-in default agent policy + bcode.toml [agent] + runtime permissions state"
));
assert!(!source.using_default);
restore_env("BCODE_CONFIG", previous_config);
restore_env("BCODE_PERMISSIONS_STATE", previous_state);
}
#[test]
fn multi_edit_is_independently_selectable() {
let mut build = agent_config(&default_config(), BUILD_AGENT);
assert!(active_tools_for(&build).contains(&"filesystem.multi_edit".to_owned()));
let mut selection = bcode_config::ToolsConfig {
disabled: BTreeSet::from(["filesystem.multi_edit".to_owned()]),
..bcode_config::ToolsConfig::default()
};
apply_tool_selection(&mut build, &selection, &[]);
let tools = active_tools_for(&build);
assert!(!tools.contains(&"filesystem.multi_edit".to_owned()));
assert!(tools.contains(&"filesystem.edit".to_owned()));
assert!(tools.contains(&"filesystem.write".to_owned()));
selection.default = bcode_config::ToolDefaultMode::None;
selection.disabled.clear();
selection.enabled.insert("filesystem.multi_edit".to_owned());
apply_tool_selection(&mut build, &selection, &[]);
assert_eq!(
active_tools_for(&build),
vec!["filesystem.multi_edit".to_owned()]
);
}
#[test]
fn tool_selection_none_exposes_only_explicit_tools() {
let mut agent = agent_config(&default_config(), BUILD_AGENT);
let tools_config = bcode_config::ToolsConfig {
default: bcode_config::ToolDefaultMode::None,
enabled: BTreeSet::from(["filesystem.read".to_string()]),
disabled: BTreeSet::new(),
..bcode_config::ToolsConfig::default()
};
apply_tool_selection(&mut agent, &tools_config, &[]);
let tools = active_tools_for(&agent);
assert_eq!(tools, vec!["filesystem.read".to_string()]);
}
#[test]
fn tool_selection_disabled_wins_over_enabled_and_agent_defaults() {
let mut agent = agent_config(&default_config(), BUILD_AGENT);
let tools_config = bcode_config::ToolsConfig {
default: bcode_config::ToolDefaultMode::Agent,
enabled: BTreeSet::from(["shell.run".to_string()]),
disabled: BTreeSet::from(["shell.run".to_string()]),
..bcode_config::ToolsConfig::default()
};
apply_tool_selection(&mut agent, &tools_config, &[]);
let tools = active_tools_for(&agent);
assert!(!tools.contains(&"shell.run".to_string()));
}
#[test]
fn context_service_applies_multi_edit_selection_and_available_providers() {
let _guard = ENV_LOCK.lock().expect("env lock should not be poisoned");
let root = unique_temp_dir();
let _state = StateGuard::set(&root.join("missing-permissions.toml"));
let context_tools = |agent: &str, config: &str, names: &[&str]| {
let request = AgentContextRequest {
session_id: SessionId::new(),
agent_id: agent.to_owned(),
available_tools: names
.iter()
.map(|name| bcode_tool::ToolDefinition {
name: (*name).to_owned(),
description: String::new(),
input_schema: json!({}),
})
.collect(),
effective_config_toml: Some(Box::new(config.to_owned())),
};
let response = agent_context(&ServiceRequest {
interface_id: AGENT_PROFILE_INTERFACE_ID.to_owned(),
operation: OP_AGENT_CONTEXT.to_owned(),
payload: serde_json::to_vec(&request).unwrap(),
});
let decoded: AgentContextResponse = serde_json::from_slice(&response.payload).unwrap();
decoded.enabled_tools.unwrap()
};
let available = [
"filesystem.multi_edit",
"filesystem.edit",
"filesystem.write",
"question",
];
let build = context_tools(BUILD_AGENT, "", &available);
assert!(
available
.iter()
.all(|name| build.iter().any(|tool| tool == name))
);
let plan = context_tools(PLAN_AGENT, "", &available);
assert_eq!(plan, vec!["question"]);
for config in [
"[tools]\ndisabled = [\"filesystem.multi_edit\"]\n",
"[agent.build]\ntools = { \"filesystem.multi_edit\" = false }\n",
] {
let tools = context_tools(BUILD_AGENT, config, &available);
assert!(!tools.iter().any(|tool| tool == "filesystem.multi_edit"));
assert!(tools.iter().any(|tool| tool == "filesystem.edit"));
assert!(tools.iter().any(|tool| tool == "filesystem.write"));
}
assert_eq!(
context_tools(BUILD_AGENT, "", &["question"]),
vec!["question"]
);
let explicit = context_tools(
PLAN_AGENT,
"[agent.plan]\ntools = { \"filesystem.multi_edit\" = true }\n",
&available,
);
assert!(explicit.iter().any(|tool| tool == "filesystem.multi_edit"));
}
#[test]
fn tool_selection_all_uses_available_tool_metadata() {
let mut agent = agent_config(&default_config(), BUILD_AGENT);
let tools_config = bcode_config::ToolsConfig {
default: bcode_config::ToolDefaultMode::All,
enabled: BTreeSet::new(),
disabled: BTreeSet::from(["filesystem.write".to_string()]),
..bcode_config::ToolsConfig::default()
};
let available_tools = vec![
bcode_tool::ToolDefinition {
name: "filesystem.read".to_string(),
description: String::new(),
input_schema: json!({}),
},
bcode_tool::ToolDefinition {
name: "filesystem.write".to_string(),
description: String::new(),
input_schema: json!({}),
},
];
apply_tool_selection(&mut agent, &tools_config, &available_tools);
let tools = active_tools_for(&agent);
assert!(tools.contains(&"filesystem.read".to_string()));
assert!(!tools.contains(&"filesystem.write".to_string()));
assert!(!tools.contains(&"shell.run".to_string()));
}
fn restore_env(name: &str, value: Option<std::ffi::OsString>) {
unsafe {
if let Some(value) = value {
std::env::set_var(name, value);
} else {
std::env::remove_var(name);
}
}
}
/// Point the runtime permissions state at an explicit path for hermetic layering tests.
struct StateGuard {
previous: Option<std::ffi::OsString>,
}
impl StateGuard {
fn set(path: &Path) -> Self {
let previous = std::env::var_os("BCODE_PERMISSIONS_STATE");
unsafe {
std::env::set_var("BCODE_PERMISSIONS_STATE", path);
}
Self { previous }
}
}
impl Drop for StateGuard {
fn drop(&mut self) {
unsafe {
match self.previous.take() {
Some(value) => std::env::set_var("BCODE_PERMISSIONS_STATE", value),
None => std::env::remove_var("BCODE_PERMISSIONS_STATE"),
}
}
}
}
#[test]
fn effective_config_keeps_manifest_declared_default_tools() {
let _guard = ENV_LOCK.lock().expect("env lock should not be poisoned");
let root = unique_temp_dir();
let _state = StateGuard::set(&root.join("missing-permissions.toml"));
let effective = r#"
[agent.plan]
tools = { "filesystem.read" = true }
"#;
let (config, tools_config) = policy_and_tools_from_effective_toml(Some(effective))
.expect("effective config decodes");
let mut plan = agent_config(&config, PLAN_AGENT);
apply_tool_selection(&mut plan, &tools_config, &[]);
let tools = active_tools_for(&plan);
assert!(
tools.contains(&"question".to_string()),
"manifest-declared default tools must survive the effective-config path: {tools:?}"
);
assert!(tools.contains(&"ask".to_string()));
assert!(tools.contains(&"ocr.extract".to_string()));
assert!(tools.contains(&"filesystem.grep".to_string()));
}
#[test]
fn workflow_coordination_defaults_allow_but_explicit_policy_still_wins() {
let _guard = ENV_LOCK.lock().unwrap();
let root = unique_temp_dir();
let _state = StateGuard::set(&root.join("missing-permissions.toml"));
for tool in manifest_defaults().unwrap().build_preauthorizations {
let mut request = EvaluateToolCallRequest {
session_id: SessionId::new(),
agent_id: BUILD_AGENT.into(),
tool_name: tool.clone(),
operation: bcode_agent_profile::ToolPolicyOperation::Mutating,
aliases: Vec::new(),
requires_permission: true,
policy_profile: None,
cwd: Some("/tmp/project".into()),
effective_config_toml: Some(Box::new(String::new())),
};
assert_eq!(
evaluate_tool_request(&request).unwrap().decision,
AgentDecision::Allow
);
for (action, expected) in [("ask", AgentDecision::Ask), ("deny", AgentDecision::Deny)] {
request.effective_config_toml = Some(Box::new(format!(
"[agent.build.permission.tools]\n\"{tool}\" = \"{action}\"\n"
)));
assert_eq!(evaluate_tool_request(&request).unwrap().decision, expected);
}
request.effective_config_toml = Some(Box::new(format!(
"[agent.build.tools]\n\"{tool}\" = false\n"
)));
assert_eq!(
evaluate_tool_request(&request).unwrap().decision,
AgentDecision::Deny
);
request.agent_id = PLAN_AGENT.into();
request.effective_config_toml = Some(Box::new(String::new()));
assert_eq!(
evaluate_tool_request(&request).unwrap().decision,
AgentDecision::Deny
);
}
}
#[test]
fn contributor_evidence_is_selected_for_default_plan_and_build_and_disableable() {
let _guard = ENV_LOCK.lock().expect("env lock");
let root = unique_temp_dir();
let _state = StateGuard::set(&root.join("missing-permissions.toml"));
let (config, tools_config) = policy_and_tools_from_effective_toml(Some("")).unwrap();
let name = "workflow.contributor_evidence";
let available = vec![bcode_tool::ToolDefinition {
name: name.into(),
description: "bounded contributor evidence".into(),
input_schema: serde_json::json!({"type":"object","properties":{},"additionalProperties":false}),
}];
for profile in [PLAN_AGENT, BUILD_AGENT] {
let mut agent = agent_config(&config, profile);
apply_tool_selection(&mut agent, &tools_config, &available);
assert!(active_tools_for(&agent).contains(&name.into()));
let disabled = bcode_config::ToolsConfig {
disabled: BTreeSet::from([name.into()]),
..Default::default()
};
apply_tool_selection(&mut agent, &disabled, &available);
assert!(!active_tools_for(&agent).contains(&name.into()));
}
}
#[test]
fn coordination_tools_are_build_only_and_explicitly_disableable() {
let _guard = ENV_LOCK.lock().expect("env lock");
let root = unique_temp_dir();
let _state = StateGuard::set(&root.join("missing-permissions.toml"));
let (config, tools_config) = policy_and_tools_from_effective_toml(Some("")).unwrap();
let mut build = agent_config(&config, BUILD_AGENT);
let mut plan = agent_config(&config, PLAN_AGENT);
apply_tool_selection(&mut build, &tools_config, &[]);
apply_tool_selection(&mut plan, &tools_config, &[]);
assert!(active_tools_for(&plan).contains(&"workflow.execution_context".into()));
for tool in [
"workflow.stage_delegation",
"workflow.stage_task_group",
"workflow.publish_run_graph_edit",
"workflow.accept_run_graph_publication",
] {
assert!(active_tools_for(&build).contains(&tool.into()));
assert!(!active_tools_for(&plan).contains(&tool.into()));
}
{
let tool = "workflow.stage_task_group";
let disabled = bcode_config::ToolsConfig {
disabled: BTreeSet::from([tool.into()]),
..Default::default()
};
apply_tool_selection(&mut build, &disabled, &[]);
assert!(!active_tools_for(&build).contains(&tool.into()));
}
}
#[test]
fn effective_config_declarative_disable_wins_over_manifest_defaults() {
let _guard = ENV_LOCK.lock().expect("env lock should not be poisoned");
let root = unique_temp_dir();
let _state = StateGuard::set(&root.join("missing-permissions.toml"));
let effective = r#"
[agent.plan]
tools = { "filesystem.write" = false, "filesystem.edit" = false, "question" = false }
"#;
let (config, tools_config) = policy_and_tools_from_effective_toml(Some(effective))
.expect("effective config decodes");
let mut plan = agent_config(&config, PLAN_AGENT);
apply_tool_selection(&mut plan, &tools_config, &[]);
let tools = active_tools_for(&plan);
assert!(!tools.contains(&"filesystem.write".to_string()));
assert!(!tools.contains(&"filesystem.edit".to_string()));
assert!(
!tools.contains(&"question".to_string()),
"an explicit declarative disable must win over manifest defaults"
);
}
#[test]
fn effective_config_applies_runtime_permission_state_layer() {
let _guard = ENV_LOCK.lock().expect("env lock should not be poisoned");
let root = unique_temp_dir();
std::fs::create_dir_all(&root).expect("temp root should be created");
let state_path = root.join("permissions.toml");
std::fs::write(
&state_path,
r#"
[agent.build.permission]
read = { "**" = "allow" }
"#,
)
.expect("state should be written");
let _state = StateGuard::set(&state_path);
let effective = r#"
[agent.build]
tools = { "filesystem.read" = true }
[agent.build.permission]
read = { "**" = "ask" }
"#;
let request = EvaluateToolCallRequest {
session_id: SessionId::new(),
agent_id: BUILD_AGENT.to_string(),
tool_name: "filesystem.read".to_string(),
operation: bcode_agent_profile::ToolPolicyOperation::Read {
paths: vec!["/tmp/project/file".to_string()],
},
aliases: vec!["read".to_string()],
requires_permission: true,
policy_profile: None,
cwd: Some("/tmp/project".to_string()),
effective_config_toml: Some(Box::new(effective.to_string())),
};
assert_eq!(
evaluate_tool_request(&request)
.expect("evaluation succeeds")
.decision,
AgentDecision::Allow,
"runtime permissions state must override declarative rules on the effective-config path"
);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn effective_config_without_agent_table_uses_builtin_defaults() {
let _guard = ENV_LOCK.lock().expect("env lock should not be poisoned");
let root = unique_temp_dir();
let _state = StateGuard::set(&root.join("missing-permissions.toml"));
let (config, _) =
policy_and_tools_from_effective_toml(Some("[tools]\ndefault = \"agent\"\n"))
.expect("effective config decodes");
assert_eq!(config, default_config());
}
#[test]
fn effective_config_path_matches_direct_composition() {
let _guard = ENV_LOCK.lock().expect("env lock should not be poisoned");
let root = unique_temp_dir();
std::fs::create_dir_all(&root).expect("temp root should be created");
let state_path = root.join("permissions.toml");
std::fs::write(
&state_path,
r#"
[agent.plan.permission]
command = { "cargo check *" = "allow" }
"#,
)
.expect("state should be written");
let _state = StateGuard::set(&state_path);
let effective = r##"
[agent.plan]
accent = "#6b7280"
tools = { "filesystem.read" = true }
"##;
let (from_effective, _) = policy_and_tools_from_effective_toml(Some(effective))
.expect("effective config decodes");
let declarative = toml::from_str::<Value>(effective).expect("declarative TOML parses");
let state = load_permissions_state_layer();
let (from_composition, _) = compose_agent_policy(&declarative, state.as_ref());
std::fs::remove_dir_all(&root).ok();
assert_eq!(
from_effective, from_composition,
"the effective-config path must apply the same layering as direct composition"
);
let plan = agent_config(&from_effective, PLAN_AGENT);
assert_eq!(
plan.permission.command.get("cargo check *"),
Some(&Action::Allow),
"runtime state rules must survive the effective-config path"
);
assert!(
plan.tools.get("question").copied().unwrap_or_default(),
"manifest defaults must survive the effective-config path"
);
}
fn unique_temp_dir() -> std::path::PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock should be after unix epoch")
.as_nanos();
std::env::temp_dir().join(format!("bcode-default-agents-test-{nanos}"))
}
}