blob: 7d41101013869e792295208b3b7bdf89e1ea53d7 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use maka_agent_api::contract::{
Directories, Directory, DirectoryPage, DirectorySelection, Workspace,
};
use maka_fs_tools::{
directory,
workspace::{self, directory::PublishedDirectory},
};
use maka_host_runtime::DirectoryRootSpec;
use std::{path::Path, sync::Arc};
pub const PAGE_BYTES: usize = 48 * 1024;
/// Shared file/command preset and immutable Maka protections. Installation
/// ceilings may further restrict commands, but never supply default authority.
pub fn sandbox(
selected: &Workspace,
access: maka_agent_api::contract::WorkspaceAccess,
state_root: &Path,
) -> Result<(maka_sandbox::Sandbox, maka_sandbox::Sandbox), String> {
use maka_agent_api::contract::WorkspaceAccess;
use maka_sandbox::{
Mode, Network, Sandbox,
filesystem::{Access, Policy},
};
let mode = match access {
WorkspaceAccess::ReadOnly => Mode::ReadOnly,
WorkspaceAccess::WorkspaceWrite => Mode::WorkspaceWrite,
WorkspaceAccess::DangerFullAccess => return Ok((Sandbox::Disabled, Sandbox::Disabled)),
};
let (base, metadata) = workspace::permissions::resolve(mode, Path::new(&selected.path))
.map_err(|error| error.to_string())?;
let private = Sandbox::Managed {
filesystem: Policy {
default: Access::Write,
rules: private_rules(selected, state_root)?,
deny_globs: vec![],
},
network: Network::Allowed,
};
let ceiling = metadata
.intersect(&private)
.map_err(|error| error.to_string())?;
let base = base
.intersect(&ceiling)
.map_err(|error| error.to_string())?;
Ok((base, ceiling))
}
/// The native Session record is the authority for the sole exception inside
/// private state. A caller-supplied path can never open this exception.
pub fn private_rules(
selected: &Workspace,
state_root: &Path,
) -> Result<Vec<maka_sandbox::filesystem::Rule>, String> {
use maka_sandbox::filesystem::{Access, Rule};
let mut rules = vec![Rule::subtree(state_root, Access::Deny)];
if let Some(binding) = &selected.worktree {
if binding.directory() != Path::new(&selected.path) {
return Err("agent_workspace_unavailable".into());
}
maka_fs_tools::worktree::Worktrees::open(&state_root.join("subagent-worktrees"))
.and_then(|worktrees| worktrees.inspect(binding))
.map_err(|_| "agent_workspace_unavailable")?;
rules.push(Rule::subtree(binding.directory(), Access::Write));
}
Ok(rules)
}
#[derive(Clone)]
pub struct Root {
name: String,
directory: Arc<PublishedDirectory>,
}
pub fn open(specs: Vec<DirectoryRootSpec>, managed: &Path) -> Result<Vec<Root>, String> {
let roots: Vec<_> = DirectoryRootSpec::normalize(specs)
.map_err(|_| "agent_workspace_configuration_invalid")?
.into_iter()
.map(|spec| {
if spec.path.starts_with(managed) {
return Err("agent_workspace_configuration_invalid".into());
}
Ok(Root {
name: spec.label,
directory: Arc::new(
PublishedDirectory::open(&spec.path)
.map_err(|_| "agent_workspace_configuration_invalid")?,
),
})
})
.collect::<Result<_, String>>()?;
Ok(roots)
}
fn root<'a>(roots: &'a [Root], id: &str) -> Result<&'a Root, String> {
roots
.iter()
.enumerate()
.find(|(index, _)| id == format!("root-{}", index + 1))
.map(|(_, root)| root)
.ok_or("agent_directory_unavailable".into())
}
fn choice(
root: &Root,
root_id: String,
segments: Vec<String>,
name: String,
) -> Result<Directory, String> {
if segments.iter().any(|s| s.chars().any(char::is_control)) {
return Err("agent_directory_invalid".into());
}
let selected = root
.directory
.resolve(&segments)
.map_err(|_| "agent_directory_unavailable")?;
let (path, identity) =
directory::capture(&selected).map_err(|_| "agent_directory_unavailable")?;
root.directory
.validate(&path)
.map_err(|_| "agent_directory_unavailable")?;
let path = host_path(&path)?;
Ok(Directory {
name,
path,
selection: DirectorySelection {
root_id,
segments,
directory_identity: identity,
},
})
}
fn host_path(path: &Path) -> Result<String, String> {
let path = workspace::project::host_path(path).map_err(|_| "agent_directory_invalid")?;
Ok(path.into())
}
fn root_page(roots: &[Root]) -> Result<DirectoryPage, String> {
Ok(DirectoryPage::Roots {
roots: roots
.iter()
.enumerate()
.map(|(index, root)| {
choice(
root,
format!("root-{}", index + 1),
vec![],
root.name.clone(),
)
})
.collect::<Result<_, _>>()?,
})
}
pub fn fits<T: serde::Serialize>(value: &T) -> bool {
serde_json::to_value(value)
.is_ok_and(|value| maka_plugin_core::json::check_delivery(&value, PAGE_BYTES).is_ok())
}
pub async fn query(roots: Vec<Root>, input: Directories) -> Result<DirectoryPage, String> {
query_excluding(roots, input, None).await
}
pub async fn query_excluding(
roots: Vec<Root>,
input: Directories,
excluded: Option<std::path::PathBuf>,
) -> Result<DirectoryPage, String> {
tokio::task::spawn_blocking(move || {
let Directories::Children { selection, after } = input else {
return root_page(&roots);
};
let root = root(&roots, &selection.root_id)?;
let name = selection
.segments
.last()
.cloned()
.unwrap_or_else(|| root.name.clone());
let selected = choice(
root,
selection.root_id.clone(),
selection.segments.clone(),
name,
)?;
if selected.selection.directory_identity != selection.directory_identity
|| excluded
.as_ref()
.is_some_and(|path| Path::new(&selected.path).starts_with(path))
{
return Err("agent_directory_unavailable".into());
}
if after.as_ref().is_some_and(|name| {
name.is_empty()
|| name.contains(['/', '\\', '\0'])
|| name.chars().any(char::is_control)
}) {
return Err("agent_directory_invalid".into());
}
let mut names: Vec<_> = root
.directory
.directory_names(&selection.segments)
.map_err(|_| "agent_directory_unavailable")?
.into_iter()
.filter(|name| {
!name.chars().any(char::is_control)
&& after
.as_ref()
.is_none_or(|cursor| name.encode_utf16().cmp(cursor.encode_utf16()).is_gt())
})
.collect();
if let Some(excluded) = &excluded {
names.retain(|name| {
let mut segments = selection.segments.clone();
segments.push(name.clone());
root.directory
.resolve(&segments)
.is_ok_and(|path| !path.starts_with(excluded))
});
}
let mut children = Vec::new();
for name in names.iter().take(32) {
let mut segments = selection.segments.clone();
segments.push(name.clone());
children.push(choice(
root,
selection.root_id.clone(),
segments,
name.clone(),
)?);
}
let mut page = DirectoryPage::Children {
directory: selected,
after: (names.len() > children.len())
.then(|| children.last().expect("nonempty page").name.clone()),
children,
};
while !fits(&page) {
let DirectoryPage::Children {
children, after, ..
} = &mut page
else {
unreachable!()
};
if children.len() <= 1 {
break;
}
children.pop();
*after = children.last().map(|child| child.name.clone());
}
let path = root
.directory
.resolve(&selection.segments)
.map_err(|_| "agent_directory_unavailable")?;
directory::open(&path, &selection.directory_identity)
.map_err(|_| "agent_directory_unavailable")?;
Ok(page)
})
.await
.map_err(|_| "agent_directory_unavailable")?
}
/// None is a confirmed pre-effect refusal. Marker initialization errors remain Unknown.
pub async fn initialize(
roots: &[Root],
selection: DirectorySelection,
managed: &Path,
private_root: &Path,
control: &Path,
) -> Result<Option<Workspace>, String> {
let Ok(root) = root(roots, &selection.root_id) else {
return Ok(None);
};
if selection
.segments
.iter()
.any(|s| s.chars().any(char::is_control))
{
return Ok(None);
}
let published = root.directory.clone();
let captured = published.clone();
let managed = managed.to_owned();
let expected = selection.directory_identity.clone();
let prepared = tokio::task::spawn_blocking(move || {
let path = captured.resolve(&selection.segments).ok()?;
if path.starts_with(&managed) {
return None;
}
host_path(&path).ok()?;
let directory = directory::open(&path, &expected).ok()?;
captured.validate(&path).ok()?;
match workspace::read_directory_identity(path.clone(), directory.try_clone().ok()?) {
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(_) => return None,
}
Some((path, directory))
})
.await
.map_err(|_| "agent_workspace_unavailable")?;
let Some((path, directory)) = prepared else {
return Ok(None);
};
let identity = workspace::ensure_directory_identity(
path.clone(),
directory,
private_root.to_owned(),
control.to_owned(),
None,
)
.await
.map_err(|_| "agent_workspace_initialization_failed")?;
published
.validate(&path)
.map_err(|_| "agent_workspace_initialization_failed")?;
directory::open(&path, &selection.directory_identity)
.map_err(|_| "agent_workspace_initialization_failed")?;
Ok(Some(Workspace {
worktree: None,
project_id: None,
path: host_path(&path)?,
directory_identity: selection.directory_identity,
identity,
}))
}
pub async fn validate(roots: &[Root], selected: &Workspace, managed: &Path) -> Result<(), String> {
let roots = roots.to_vec();
let selected = selected.clone();
let managed = managed.to_owned();
tokio::task::spawn_blocking(move || {
let (path, observed) = directory::capture(Path::new(&selected.path))
.map_err(|_| "agent_workspace_unavailable")?;
if host_path(&path)? != selected.path || observed != selected.directory_identity {
return Err("agent_workspace_unavailable".into());
}
let authorized = if let Some(binding) = &selected.worktree {
directory::open(binding.source(), binding.source_identity())
.map_err(|_| "agent_workspace_unavailable")?;
if binding.directory() != path {
return Err("agent_workspace_unavailable".into());
}
maka_fs_tools::worktree::Worktrees::open(&managed)
.and_then(|worktrees| worktrees.inspect(binding))
.map_err(|_| "agent_workspace_unavailable")?;
binding.source()
} else {
if path.starts_with(&managed) {
return Err("agent_workspace_unavailable".into());
}
path.as_path()
};
let root = roots
.iter()
.find(|root| root.directory.validate(authorized).is_ok())
.ok_or("agent_workspace_unavailable")?;
let directory = directory::open(&path, &selected.directory_identity)
.map_err(|_| "agent_workspace_unavailable")?;
if workspace::read_directory_identity(path.clone(), directory)
.map_err(|_| "agent_workspace_unavailable")?
!= selected.identity
{
return Err("agent_workspace_unavailable".into());
}
root.directory
.validate(authorized)
.map_err(|_| "agent_workspace_unavailable".into())
})
.await
.map_err(|_| "agent_workspace_unavailable")?
}
/// Re-observe a published directory without creating a workspace marker.
pub async fn selected(
roots: Vec<Root>,
selection: DirectorySelection,
managed: std::path::PathBuf,
) -> Result<Option<Directory>, String> {
resolve_selection(roots, selection, managed, true).await
}
pub async fn observed(
roots: Vec<Root>,
selection: DirectorySelection,
excluded: std::path::PathBuf,
) -> Result<Option<Directory>, String> {
resolve_selection(roots, selection, excluded, false).await
}
async fn resolve_selection(
roots: Vec<Root>,
selection: DirectorySelection,
managed: std::path::PathBuf,
workspace_marker: bool,
) -> Result<Option<Directory>, String> {
tokio::task::spawn_blocking(move || {
let Ok(root) = root(&roots, &selection.root_id) else {
return None;
};
let selected = choice(
root,
selection.root_id.clone(),
selection.segments.clone(),
selection
.segments
.last()
.cloned()
.unwrap_or_else(|| root.name.clone()),
)
.ok()?;
if selected.selection.directory_identity != selection.directory_identity
|| Path::new(&selected.path).starts_with(&managed)
{
return None;
}
let path = std::path::PathBuf::from(&selected.path);
let directory = directory::open(&path, &selection.directory_identity).ok()?;
if !workspace_marker {
return Some(selected);
}
match workspace::read_directory_identity(path, directory) {
Ok(_) => Some(selected),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Some(selected),
Err(_) => None,
}
})
.await
.map_err(|_| "agent_directory_unavailable".into())
}
pub async fn selection_at(
roots: Vec<Root>,
path: String,
managed: std::path::PathBuf,
) -> Result<Option<DirectorySelection>, String> {
tokio::task::spawn_blocking(move || {
let path = Path::new(&path);
if path.starts_with(&managed) {
return None;
}
for (index, root) in roots.iter().enumerate() {
let Ok(relative) = path.strip_prefix(root.directory.path()) else {
continue;
};
let segments = relative
.components()
.map(|part| part.as_os_str().to_str().map(str::to_owned))
.collect::<Option<Vec<_>>>()?;
let Ok(selected) = choice(
root,
format!("root-{}", index + 1),
segments,
root.name.clone(),
) else {
continue;
};
if Path::new(&selected.path) == path {
return Some(selected.selection);
}
}
None
})
.await
.map_err(|_| "agent_directory_unavailable".into())
}