use std::path::Path;
use std::sync::Arc;
use async_trait::async_trait;
use oxi_sdk::{AgentTool, AgentToolResult, ToolContext};
use serde_json::Value;
use tokio::sync::oneshot;
use crate::access_manager::{
AccessDenied, AccessGate, AgentContext, CheckRequest, DenyLayer, PathMode,
};
const FILE_TOOLS: &[&str] = &["read", "write", "edit", "ls", "find", "grep"];
fn extract_path_from_params(tool_name: &str, params: &Value) -> Option<String> {
if !FILE_TOOLS.contains(&tool_name) {
return None;
}
params
.get("path")
.and_then(|v| v.as_str())
.map(String::from)
}
fn path_mode_for_tool(tool_name: &str) -> PathMode {
match tool_name {
"write" | "edit" => PathMode::Write,
_ => PathMode::Read,
}
}
fn format_denied(denied: &AccessDenied) -> String {
let layer_tag = match denied.layer {
DenyLayer::Capability => "[CSpace]",
DenyLayer::Rbac => "[RBAC]",
DenyLayer::Permission => "[Permissions]",
DenyLayer::ExecPolicy => "[ExecPolicy]",
};
format!(
"🔒 권한 거부: {} — {} {}",
denied.reason,
denied.suggestion.as_deref().unwrap_or(""),
layer_tag
)
}
pub struct GatedTool<T: AgentTool> {
inner: T,
gate: Arc<AccessGate>,
context: AgentContext,
}
impl<T: AgentTool> GatedTool<T> {
pub fn new(inner: T, gate: Arc<AccessGate>, context: AgentContext) -> Self {
Self {
inner,
gate,
context,
}
}
}
impl<T: AgentTool> std::fmt::Debug for GatedTool<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GatedTool")
.field("name", &self.inner.name())
.finish()
}
}
#[async_trait]
impl<T: AgentTool + 'static> AgentTool for GatedTool<T> {
fn name(&self) -> &str {
self.inner.name()
}
fn label(&self) -> &str {
self.inner.label()
}
fn description(&self) -> &'static str {
"Execute commands and access system resources. Permissions enforced by AccessGate."
}
fn parameters_schema(&self) -> Value {
self.inner.parameters_schema()
}
async fn execute(
&self,
tool_call_id: &str,
params: Value,
signal: Option<oneshot::Receiver<()>>,
ctx: &ToolContext,
) -> Result<AgentToolResult, String> {
let tool_name = self.inner.name();
let check = CheckRequest::Tool {
context: &self.context,
tool_name,
};
if let Err(denied) = self.gate.check(check) {
tracing::warn!(
agent = %denied.agent,
tool = %tool_name,
layer = ?denied.layer,
"GatedTool: tool access denied"
);
return Ok(AgentToolResult::error(format_denied(&denied)));
}
if let Some(path) = extract_path_from_params(tool_name, ¶ms) {
let mode = path_mode_for_tool(tool_name);
let path_check = CheckRequest::Path {
context: &self.context,
path: Path::new(&path),
mode,
};
if let Err(denied) = self.gate.check(path_check) {
tracing::warn!(
agent = %denied.agent,
path = %path,
tool = %tool_name,
layer = ?denied.layer,
"GatedTool: path access denied"
);
return Ok(AgentToolResult::error(format!(
"🔒 경로 접근 거부: {}",
denied.reason
)));
}
}
self.inner.execute(tool_call_id, params, signal, ctx).await
}
}
pub fn gate_tool<T: AgentTool + 'static>(
tool: T,
gate: Arc<AccessGate>,
context: AgentContext,
) -> GatedTool<T> {
GatedTool::new(tool, gate, context)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::access_manager::{AccessManager, AgentPermissions, NoOpAuditSink, Role, Subject};
use crate::config::ExecConfig;
use oxi_sdk::ReadTool;
use parking_lot::Mutex;
fn make_gate_for_test() -> Arc<AccessGate> {
let mut access = AccessManager::new();
let perms = AgentPermissions::for_new_agent("test-agent");
access.set_permissions(perms);
let subject = Subject::Agent(
<crate::types::AgentId as std::convert::From<uuid::Uuid>>::from(uuid::Uuid::new_v4()),
);
access
.rbac_manager_mut()
.assign_role(subject, Role::Superuser);
Arc::new(AccessGate::new(
Arc::new(Mutex::new(access)),
Arc::new(ExecConfig::default()),
Arc::new(NoOpAuditSink),
))
}
#[test]
fn test_gated_tool_preserves_name() {
let gate = make_gate_for_test();
let ctx = AgentContext::test_fixture("test-agent");
let tool = GatedTool::new(ReadTool::new(), gate, ctx);
assert_eq!(tool.name(), "read");
}
#[test]
fn test_extract_path_read_tool() {
let params = serde_json::json!({"path": "/workspace/file.rs"});
assert_eq!(
extract_path_from_params("read", ¶ms),
Some("/workspace/file.rs".to_string())
);
}
#[test]
fn test_extract_path_exec_tool() {
let params = serde_json::json!({"command": "echo hello"});
assert_eq!(extract_path_from_params("exec", ¶ms), None);
}
#[test]
fn test_path_mode_for_tool() {
assert_eq!(path_mode_for_tool("write"), PathMode::Write);
assert_eq!(path_mode_for_tool("edit"), PathMode::Write);
assert_eq!(path_mode_for_tool("read"), PathMode::Read);
assert_eq!(path_mode_for_tool("ls"), PathMode::Read);
}
#[test]
fn test_format_denied() {
let denied = AccessDenied {
agent: "test".into(),
resource: "exec".into(),
layer: DenyLayer::ExecPolicy,
reason: "not in allowlist".into(),
suggestion: Some("add to config".into()),
};
let s = format_denied(&denied);
assert!(s.contains("🔒"));
assert!(s.contains("[ExecPolicy]"));
}
}