use serde_json::Value;
use crate::agent::config::{
permission_for_shell, HematiteConfig, PermissionDecision, PermissionMode,
};
use crate::agent::trust_resolver::{resolve_workspace_trust, WorkspaceTrustPolicy};
use crate::tools::RiskLevel;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthorizationSource {
SystemAdminMode,
ReadOnlyMode,
YoloMode,
WorkspaceTrusted,
WorkspaceApprovalRequired,
WorkspaceDenied,
McpExternal,
SafePathBypass,
ConfigAllow,
ConfigAsk,
ConfigDeny,
ShellBlacklist,
ShellRiskSafe,
ShellRiskModerate,
ShellRiskHigh,
DefaultToolPolicy,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthorizationDecision {
Allow {
source: AuthorizationSource,
},
Ask {
source: AuthorizationSource,
reason: String,
},
Deny {
source: AuthorizationSource,
reason: String,
},
}
impl AuthorizationDecision {
pub fn source(self) -> AuthorizationSource {
match self {
AuthorizationDecision::Allow { source }
| AuthorizationDecision::Ask { source, .. }
| AuthorizationDecision::Deny { source, .. } => source,
}
}
}
pub fn authorize_tool_call(
name: &str,
args: &Value,
config: &HematiteConfig,
yolo_flag: bool,
) -> AuthorizationDecision {
if config.mode == PermissionMode::SystemAdmin {
return AuthorizationDecision::Allow {
source: AuthorizationSource::SystemAdminMode,
};
}
if config.mode == PermissionMode::ReadOnly && is_destructive_tool(name) {
return AuthorizationDecision::Deny {
source: AuthorizationSource::ReadOnlyMode,
reason: format!(
"Action blocked: tool `{}` is forbidden in permission mode `{:?}`.",
name, config.mode
),
};
}
if yolo_flag {
return AuthorizationDecision::Allow {
source: AuthorizationSource::YoloMode,
};
}
let workspace_root = crate::tools::file_ops::workspace_root();
let trust = resolve_workspace_trust(&workspace_root, &config.trust);
if trust_sensitive_tool(name) {
match trust.policy {
WorkspaceTrustPolicy::Denied => {
return AuthorizationDecision::Deny {
source: AuthorizationSource::WorkspaceDenied,
reason: format!(
"Action blocked: workspace `{}` is denied by trust policy{}.",
trust.workspace_display,
trust
.matched_root
.as_ref()
.map(|root| format!(" ({})", root))
.unwrap_or_default()
),
};
}
WorkspaceTrustPolicy::RequireApproval => {
return AuthorizationDecision::Ask {
source: AuthorizationSource::WorkspaceApprovalRequired,
reason: format!(
"Workspace `{}` is not trust-allowlisted, so `{}` requires approval before Hematite performs destructive or external actions there.",
trust.workspace_display, name
),
};
}
WorkspaceTrustPolicy::Trusted => {}
}
}
if name.starts_with("mcp__") {
return AuthorizationDecision::Ask {
source: AuthorizationSource::McpExternal,
reason: format!(
"External MCP tool `{}` requires explicit operator approval.",
name
),
};
}
if matches!(name, "write_file" | "edit_file") {
if let Some(path) = args.get("path").and_then(|v| v.as_str()) {
if is_path_safe(path) {
return AuthorizationDecision::Allow {
source: AuthorizationSource::SafePathBypass,
};
}
}
}
if name == "shell" {
let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("");
match permission_for_shell(cmd, config) {
PermissionDecision::Allow => {
return AuthorizationDecision::Allow {
source: AuthorizationSource::ConfigAllow,
}
}
PermissionDecision::Ask => {
return AuthorizationDecision::Ask {
source: AuthorizationSource::ConfigAsk,
reason: "Shell command requires approval by `.hematite/settings.json`."
.to_string(),
}
}
PermissionDecision::Deny => {
return AuthorizationDecision::Deny {
source: AuthorizationSource::ConfigDeny,
reason: "Action blocked: shell command denied by `.hematite/settings.json`."
.to_string(),
}
}
PermissionDecision::UseRiskClassifier => {}
}
if let Err(e) = crate::tools::guard::bash_is_safe(cmd) {
return AuthorizationDecision::Deny {
source: AuthorizationSource::ShellBlacklist,
reason: format!("Action blocked: {}", e),
};
}
return match crate::tools::guard::classify_bash_risk(cmd) {
RiskLevel::Safe => AuthorizationDecision::Allow {
source: AuthorizationSource::ShellRiskSafe,
},
RiskLevel::Moderate => AuthorizationDecision::Ask {
source: AuthorizationSource::ShellRiskModerate,
reason: "Shell command classified as moderate risk and requires approval."
.to_string(),
},
RiskLevel::High => AuthorizationDecision::Ask {
source: AuthorizationSource::ShellRiskHigh,
reason: "Shell command classified as high risk and requires approval.".to_string(),
},
};
}
AuthorizationDecision::Allow {
source: if trust_sensitive_tool(name) {
AuthorizationSource::WorkspaceTrusted
} else {
AuthorizationSource::DefaultToolPolicy
},
}
}
fn is_destructive_tool(name: &str) -> bool {
crate::agent::inference::tool_metadata_for_name(name).mutates_workspace
}
pub(crate) fn is_path_safe(path: &str) -> bool {
let p = path.to_lowercase();
p.contains(".hematite/")
|| p.contains(".hematite\\")
|| p.contains("tmp/")
|| p.contains("tmp\\")
}
fn trust_sensitive_tool(name: &str) -> bool {
crate::agent::inference::tool_metadata_for_name(name).trust_sensitive
}