use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use super::workflow_bundle::WorkflowBundle;
use super::workflow_patch::{bundle_capability_ceiling, CapabilityCeilingViolation};
use super::CapabilityPolicy;
#[derive(Clone, Debug)]
pub enum NestedInvocationTarget<'a> {
WorkflowBundle(&'a WorkflowBundle),
HarnScript { path: &'a str, source: &'a str },
BurinHarness { manifest: &'a serde_json::Value },
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct NestedInvocationCeilingReport {
pub target_kind: String,
pub target_label: String,
pub parent: CapabilityPolicy,
pub requested: CapabilityPolicy,
pub violations: Vec<CapabilityCeilingViolation>,
}
impl NestedInvocationCeilingReport {
pub fn allowed(&self) -> bool {
self.violations.is_empty()
}
}
pub fn requested_ceiling_for_target(target: &NestedInvocationTarget<'_>) -> CapabilityPolicy {
match target {
NestedInvocationTarget::WorkflowBundle(bundle) => bundle_capability_ceiling(bundle),
NestedInvocationTarget::HarnScript { source, .. } => scan_harn_script_ceiling(source),
NestedInvocationTarget::BurinHarness { manifest } => scan_burin_manifest_ceiling(manifest),
}
}
pub fn enforce_nested_invocation_ceiling(
parent: &CapabilityPolicy,
target: &NestedInvocationTarget<'_>,
) -> NestedInvocationCeilingReport {
let requested = requested_ceiling_for_target(target);
let violations = collect_violations(parent, &requested);
let (kind, label) = match target {
NestedInvocationTarget::WorkflowBundle(bundle) => {
("workflow_bundle".to_string(), bundle.id.clone())
}
NestedInvocationTarget::HarnScript { path, .. } => {
("harn_script".to_string(), path.to_string())
}
NestedInvocationTarget::BurinHarness { manifest } => {
let label = manifest
.get("id")
.and_then(|value| value.as_str())
.unwrap_or("<unknown>")
.to_string();
("burin_harness".to_string(), label)
}
};
NestedInvocationCeilingReport {
target_kind: kind,
target_label: label,
parent: parent.clone(),
requested,
violations,
}
}
fn collect_violations(
parent: &CapabilityPolicy,
requested: &CapabilityPolicy,
) -> Vec<CapabilityCeilingViolation> {
let mut violations = Vec::new();
if !parent.tools.is_empty() {
for tool in &requested.tools {
if !parent.tools.contains(tool) {
violations.push(CapabilityCeilingViolation {
kind: "tool".to_string(),
detail: format!("nested target requests tool '{tool}' outside parent ceiling"),
});
}
}
}
for (capability, ops) in &requested.capabilities {
match parent.capabilities.get(capability) {
Some(parent_ops) => {
for op in ops {
if !parent_ops.contains(op) {
violations.push(CapabilityCeilingViolation {
kind: "capability".to_string(),
detail: format!(
"nested target requests '{capability}.{op}' outside parent ceiling"
),
});
}
}
}
None if !parent.capabilities.is_empty() => {
violations.push(CapabilityCeilingViolation {
kind: "capability".to_string(),
detail: format!(
"nested target requests capability '{capability}' outside parent ceiling"
),
});
}
_ => {}
}
}
if let (Some(parent_level), Some(requested_level)) = (
parent.side_effect_level.as_deref(),
requested.side_effect_level.as_deref(),
) {
if rank(requested_level) > rank(parent_level) {
violations.push(CapabilityCeilingViolation {
kind: "side_effect_level".to_string(),
detail: format!(
"nested target requests side_effect_level '{requested_level}' outside parent ceiling '{parent_level}'"
),
});
}
}
if !parent.workspace_roots.is_empty() {
for root in &requested.workspace_roots {
if !parent.workspace_roots.contains(root) {
violations.push(CapabilityCeilingViolation {
kind: "workspace_root".to_string(),
detail: format!(
"nested target requests workspace_root '{root}' outside parent allowlist"
),
});
}
}
}
violations
}
fn rank(level: &str) -> usize {
match level {
"none" => 0,
"read_only" => 1,
"workspace_write" => 2,
"process_exec" => 3,
"network" => 4,
_ => 5,
}
}
fn scan_harn_script_ceiling(source: &str) -> CapabilityPolicy {
let stripped = strip_comments(source);
let mut capabilities: std::collections::BTreeMap<String, BTreeSet<String>> =
std::collections::BTreeMap::new();
let mut max_side_effect: Option<&'static str> = None;
for (token, capability, op, side_effect) in BUILTIN_CAPABILITIES {
if contains_call(&stripped, token) {
capabilities
.entry((*capability).to_string())
.or_default()
.insert((*op).to_string());
max_side_effect = match max_side_effect {
Some(current) if rank(current) >= rank(side_effect) => Some(current),
_ => Some(side_effect),
};
}
}
CapabilityPolicy {
tools: Vec::new(),
capabilities: capabilities
.into_iter()
.map(|(k, v)| (k, v.into_iter().collect()))
.collect(),
workspace_roots: Vec::new(),
side_effect_level: max_side_effect.map(|level| level.to_string()),
recursion_limit: None,
tool_arg_constraints: Vec::new(),
tool_annotations: std::collections::BTreeMap::new(),
}
}
fn scan_burin_manifest_ceiling(manifest: &serde_json::Value) -> CapabilityPolicy {
if let Some(ceiling) = manifest.get("capability_ceiling") {
if let Ok(parsed) = serde_json::from_value::<CapabilityPolicy>(ceiling.clone()) {
return parsed;
}
}
let tools = manifest
.get("tools")
.and_then(|value| value.as_array())
.map(|tools| {
tools
.iter()
.filter_map(|tool| tool.as_str().map(str::to_string))
.collect::<Vec<_>>()
})
.unwrap_or_default();
CapabilityPolicy {
tools,
capabilities: std::collections::BTreeMap::new(),
workspace_roots: Vec::new(),
side_effect_level: Some("network".to_string()),
recursion_limit: None,
tool_arg_constraints: Vec::new(),
tool_annotations: std::collections::BTreeMap::new(),
}
}
fn strip_comments(source: &str) -> String {
let mut out = String::with_capacity(source.len());
let mut in_block = false;
let mut chars = source.chars().peekable();
while let Some(c) = chars.next() {
if in_block {
if c == '*' && matches!(chars.peek(), Some('/')) {
chars.next();
in_block = false;
}
continue;
}
if c == '/' {
match chars.peek() {
Some('/') => {
for next in chars.by_ref() {
if next == '\n' {
out.push('\n');
break;
}
}
continue;
}
Some('*') => {
chars.next();
in_block = true;
continue;
}
_ => {}
}
}
if c == '#' {
for next in chars.by_ref() {
if next == '\n' {
out.push('\n');
break;
}
}
continue;
}
if c == '"' || c == '\'' {
out.push(' ');
let quote = c;
while let Some(next) = chars.next() {
if next == '\\' {
chars.next();
out.push(' ');
out.push(' ');
continue;
}
if next == quote {
out.push(' ');
break;
}
out.push(if next == '\n' { '\n' } else { ' ' });
}
continue;
}
out.push(c);
}
out
}
fn contains_call(source: &str, token: &str) -> bool {
let bytes = source.as_bytes();
let needle = token.as_bytes();
if bytes.len() < needle.len() + 1 {
return false;
}
let mut start = 0;
while let Some(pos) = find_subslice(&bytes[start..], needle) {
let absolute = start + pos;
let before = if absolute == 0 {
None
} else {
Some(bytes[absolute - 1])
};
let after = bytes.get(absolute + needle.len()).copied();
let valid_before = match before {
None => true,
Some(c) => !is_identifier_byte(c),
};
let valid_after = matches!(after, Some(b'(') | Some(b' ') | Some(b'\t'))
|| matches!(after, Some(b'\n') | Some(b'\r'));
if valid_before && valid_after {
return true;
}
start = absolute + needle.len();
}
false
}
fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
haystack
.windows(needle.len())
.position(|window| window == needle)
}
fn is_identifier_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
const BUILTIN_CAPABILITIES: &[(&str, &str, &str, &str)] = &[
("read_file", "workspace", "read_text", "read_only"),
("read_file_result", "workspace", "read_text", "read_only"),
("read_file_bytes", "workspace", "read_text", "read_only"),
("list_dir", "workspace", "list", "read_only"),
("file_exists", "workspace", "exists", "read_only"),
("stat", "workspace", "exists", "read_only"),
("write_file", "workspace", "write_text", "workspace_write"),
(
"write_file_bytes",
"workspace",
"write_text",
"workspace_write",
),
("append_file", "workspace", "write_text", "workspace_write"),
("mkdir", "workspace", "write_text", "workspace_write"),
("copy_file", "workspace", "write_text", "workspace_write"),
("delete_file", "workspace", "delete", "workspace_write"),
("apply_edit", "workspace", "apply_edit", "workspace_write"),
("exec", "process", "exec", "process_exec"),
("exec_at", "process", "exec", "process_exec"),
("shell", "process", "exec", "process_exec"),
("shell_at", "process", "exec", "process_exec"),
("http_get", "network", "http", "network"),
("http_post", "network", "http", "network"),
("http_put", "network", "http", "network"),
("http_patch", "network", "http", "network"),
("http_delete", "network", "http", "network"),
("http_request", "network", "http", "network"),
("http_download", "network", "http", "network"),
("connector_call", "connector", "call", "network"),
("secret_get", "connector", "secret_get", "read_only"),
("llm_call", "llm", "call", "network"),
("llm_call_safe", "llm", "call", "network"),
("llm_completion", "llm", "call", "network"),
("llm_stream", "llm", "call", "network"),
("agent_loop", "llm", "call", "network"),
("mcp_call", "process", "exec", "process_exec"),
("mcp_connect", "process", "exec", "process_exec"),
];
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
fn permissive_parent() -> CapabilityPolicy {
let mut capabilities = BTreeMap::new();
capabilities.insert(
"workspace".to_string(),
vec!["read_text".to_string(), "list".to_string()],
);
capabilities.insert("connector".to_string(), vec!["call".to_string()]);
capabilities.insert("process".to_string(), vec!["exec".to_string()]);
capabilities.insert("network".to_string(), vec!["http".to_string()]);
capabilities.insert("llm".to_string(), vec!["call".to_string()]);
CapabilityPolicy {
tools: Vec::new(),
capabilities,
workspace_roots: Vec::new(),
side_effect_level: Some("network".to_string()),
recursion_limit: None,
tool_arg_constraints: Vec::new(),
tool_annotations: BTreeMap::new(),
}
}
fn read_only_parent() -> CapabilityPolicy {
let mut capabilities = BTreeMap::new();
capabilities.insert(
"workspace".to_string(),
vec![
"read_text".to_string(),
"list".to_string(),
"exists".to_string(),
],
);
CapabilityPolicy {
tools: Vec::new(),
capabilities,
workspace_roots: Vec::new(),
side_effect_level: Some("read_only".to_string()),
recursion_limit: None,
tool_arg_constraints: Vec::new(),
tool_annotations: BTreeMap::new(),
}
}
#[test]
fn harn_script_with_only_reads_passes_under_read_only_parent() {
let source = r#"
let body = read_file("README.md")
let exists = file_exists("Cargo.toml")
"#;
let report = enforce_nested_invocation_ceiling(
&read_only_parent(),
&NestedInvocationTarget::HarnScript {
path: "test.harn",
source,
},
);
assert!(report.allowed(), "{report:#?}");
}
#[test]
fn harn_script_with_exec_is_rejected_under_read_only_parent() {
let source = r#"
let result = exec("ls", ["-la"])
"#;
let report = enforce_nested_invocation_ceiling(
&read_only_parent(),
&NestedInvocationTarget::HarnScript {
path: "exec.harn",
source,
},
);
assert!(!report.allowed());
let kinds: Vec<&str> = report.violations.iter().map(|v| v.kind.as_str()).collect();
assert!(kinds.contains(&"capability"));
assert!(kinds.contains(&"side_effect_level"));
}
#[test]
fn harn_script_with_http_is_rejected_under_read_only_parent() {
let source = r#"
http_get("https://example.com")
"#;
let report = enforce_nested_invocation_ceiling(
&read_only_parent(),
&NestedInvocationTarget::HarnScript {
path: "http.harn",
source,
},
);
assert!(!report.allowed());
}
#[test]
fn harn_script_keyword_inside_string_does_not_trigger() {
let source = r#"
let label = "exec is not invoked here"
let body = read_file("README.md")
"#;
let report = enforce_nested_invocation_ceiling(
&read_only_parent(),
&NestedInvocationTarget::HarnScript {
path: "string.harn",
source,
},
);
assert!(
report.allowed(),
"false positive on quoted token: {report:#?}"
);
}
#[test]
fn harn_script_keyword_in_comment_is_ignored() {
let source = r#"
// exec("rm -rf /") is what we used to do but no longer
let x = read_file("README.md")
"#;
let report = enforce_nested_invocation_ceiling(
&read_only_parent(),
&NestedInvocationTarget::HarnScript {
path: "comments.harn",
source,
},
);
assert!(
report.allowed(),
"false positive on commented token: {report:#?}"
);
}
#[test]
fn workflow_bundle_with_act_auto_is_rejected_under_read_only_parent() {
let mut bundle = super::super::workflow_test_fixtures::pr_monitor_bundle();
bundle.policy.autonomy_tier = "act_auto".to_string();
let report = enforce_nested_invocation_ceiling(
&read_only_parent(),
&NestedInvocationTarget::WorkflowBundle(&bundle),
);
assert!(!report.allowed());
}
#[test]
fn burin_manifest_with_explicit_ceiling_is_used_directly() {
let manifest = serde_json::json!({
"id": "burin.harness.repair",
"capability_ceiling": {
"capabilities": {
"workspace": ["read_text"]
},
"side_effect_level": "read_only"
}
});
let report = enforce_nested_invocation_ceiling(
&read_only_parent(),
&NestedInvocationTarget::BurinHarness {
manifest: &manifest,
},
);
assert!(report.allowed(), "{report:#?}");
}
#[test]
fn burin_manifest_without_ceiling_falls_back_to_network_and_is_rejected() {
let manifest = serde_json::json!({"id": "burin.harness.unknown"});
let report = enforce_nested_invocation_ceiling(
&read_only_parent(),
&NestedInvocationTarget::BurinHarness {
manifest: &manifest,
},
);
assert!(!report.allowed());
}
#[test]
fn permissive_parent_accepts_workflow_bundle() {
let bundle = super::super::workflow_test_fixtures::pr_monitor_bundle();
let report = enforce_nested_invocation_ceiling(
&permissive_parent(),
&NestedInvocationTarget::WorkflowBundle(&bundle),
);
assert!(report.allowed(), "{report:#?}");
}
}