use std::io;
use std::path::{Component, Path, PathBuf};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::ToolNature;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ToolResourceKind {
Path,
Domain,
Command,
Remote,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolAuthorizationScope {
Once,
Persisted,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolExecutionAuthorization {
tool_name: String,
nature: ToolNature,
resource_kind: ToolResourceKind,
normalized_resource: PathBuf,
scope: ToolAuthorizationScope,
}
impl ToolExecutionAuthorization {
pub fn for_path(
tool_name: impl Into<String>,
nature: ToolNature,
workspace_root: &Path,
resource: &str,
scope: ToolAuthorizationScope,
) -> io::Result<Self> {
Ok(Self {
tool_name: tool_name.into(),
nature,
resource_kind: ToolResourceKind::Path,
normalized_resource: normalize_authorized_path(workspace_root, resource)?,
scope,
})
}
pub fn authorizes_path(
&self,
tool_name: &str,
nature: ToolNature,
workspace_root: &Path,
resource: &str,
) -> bool {
self.tool_name == tool_name
&& self.nature == nature
&& self.resource_kind == ToolResourceKind::Path
&& normalize_authorized_path(workspace_root, resource)
.is_ok_and(|path| path == self.normalized_resource)
}
#[must_use]
pub fn normalized_path(&self) -> &Path {
&self.normalized_resource
}
#[must_use]
pub fn scope(&self) -> ToolAuthorizationScope {
self.scope
}
}
fn normalize_authorized_path(workspace_root: &Path, resource: &str) -> io::Result<PathBuf> {
let requested = Path::new(resource);
let candidate = if requested.is_absolute() {
requested.to_path_buf()
} else {
workspace_root.join(requested)
};
let mut lexical = PathBuf::new();
for component in candidate.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if !lexical.pop() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"path traversal escapes filesystem root",
));
}
}
other => lexical.push(other.as_os_str()),
}
}
let mut existing = lexical.as_path();
let mut suffix = Vec::new();
while !existing.exists() {
let Some(name) = existing.file_name() else {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"path has no existing ancestor",
));
};
suffix.push(name.to_os_string());
existing = existing.parent().ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "path has no existing ancestor")
})?;
}
let mut normalized = existing.canonicalize()?;
for component in suffix.into_iter().rev() {
normalized.push(component);
}
Ok(normalized)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ToolPermissionFacet {
pub nature: ToolNature,
#[serde(default)]
pub resource: Option<String>,
#[serde(default)]
pub resource_kind: Option<ToolResourceKind>,
#[serde(default)]
pub description: Option<String>,
}
impl ToolPermissionFacet {
pub fn new(nature: ToolNature) -> Self {
Self {
nature,
resource: None,
resource_kind: None,
description: None,
}
}
pub fn with_resource(
nature: ToolNature,
resource: impl Into<String>,
resource_kind: ToolResourceKind,
) -> Self {
Self {
nature,
resource: Some(resource.into()),
resource_kind: Some(resource_kind),
description: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}