| /* | |
| * 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. | |
| */ | |
| //! Explicit standard directories scoped to a checked workspace observation. | |
| use super::*; | |
| use maka_agent_api::contract::Workspace; | |
| use maka_fs_tools::{ | |
| directory, | |
| workspace::{self, directory::PublishedDirectory}, | |
| }; | |
| use maka_skills::{SkillScope, SkillSource, api::LocationId}; | |
| use std::path::Path; | |
| #[derive(Clone)] | |
| pub(super) struct CapturedSource { | |
| pub metadata: Source, | |
| pub root: ReadRoot, | |
| pub directory: PathBuf, | |
| pub prefix: String, | |
| scope: SkillScope, | |
| source: SkillSource, | |
| } | |
| impl CapturedSource { | |
| pub fn custom(metadata: Source, root: ReadRoot) -> Self { | |
| Self { | |
| prefix: format!("source:{}", metadata.id), | |
| metadata, | |
| root, | |
| directory: PathBuf::new(), | |
| scope: SkillScope::Custom, | |
| source: SkillSource::Custom, | |
| } | |
| } | |
| pub fn project(&self) -> bool { | |
| self.scope == SkillScope::Project | |
| } | |
| pub fn contains(&self, reference: &str) -> bool { | |
| reference.starts_with(&format!("{}:", self.prefix)) | |
| } | |
| pub fn bind(&self, scope: &Scope) -> maka_skills::Source { | |
| let access = self.root.bind_scope(scope); | |
| maka_skills::Source { | |
| root: access.location(), | |
| access, | |
| directory: self.directory.clone(), | |
| scope: self.scope.clone(), | |
| source: self.source.clone(), | |
| reference_prefix: self.prefix.clone(), | |
| } | |
| } | |
| } | |
| fn standard(root: ReadRoot, path: &Path, project: Option<&str>) -> Vec<CapturedSource> { | |
| let ids = if project.is_some() { | |
| [LocationId::ProjectMaka, LocationId::ProjectAgents] | |
| } else { | |
| [LocationId::UserMaka, LocationId::UserAgents] | |
| }; | |
| ids.into_iter() | |
| .map(|id| { | |
| let prefix = project.map_or_else( | |
| || id.reference().to_owned(), | |
| |identity| { | |
| format!( | |
| "project:{identity}:{}", | |
| id.reference().split(':').nth(1).unwrap() | |
| ) | |
| }, | |
| ); | |
| CapturedSource { | |
| metadata: Source { | |
| id: id.reference().replace(':', "-"), | |
| label: format!( | |
| "{} {}", | |
| if project.is_some() { "Project" } else { "User" }, | |
| id.directory() | |
| ), | |
| path: path.join(id.directory()).to_string_lossy().into_owned(), | |
| }, | |
| root: root.clone(), | |
| directory: id.directory().into(), | |
| prefix, | |
| scope: id.scope(), | |
| source: id.source(), | |
| } | |
| }) | |
| .collect() | |
| } | |
| pub(super) fn home( | |
| path: &str, | |
| policy: Arc<maka_sandbox::filesystem::Compiled>, | |
| ) -> Result<Vec<CapturedSource>, String> { | |
| if !Path::new(path).is_absolute() { | |
| return Err("skills_configuration_invalid".into()); | |
| } | |
| let root = ReadRoot::capture(path) | |
| .map_err(|_| "skills_source_unavailable")? | |
| .restrict(policy) | |
| .map_err(|_| "skills_source_unavailable")?; | |
| Ok(standard(root, Path::new(path), None)) | |
| } | |
| pub(super) fn projects( | |
| specs: Vec<maka_host_runtime::DirectoryRootSpec>, | |
| private: &Path, | |
| ) -> Result<Vec<Arc<PublishedDirectory>>, String> { | |
| maka_host_runtime::DirectoryRootSpec::normalize(specs) | |
| .map_err(|_| "skills_configuration_invalid")? | |
| .into_iter() | |
| .map(|spec| { | |
| if spec.path.starts_with(private) { | |
| return Err("skills_configuration_invalid".into()); | |
| } | |
| PublishedDirectory::open(&spec.path) | |
| .map(Arc::new) | |
| .map_err(|_| "skills_configuration_invalid".into()) | |
| }) | |
| .collect() | |
| } | |
| impl Library { | |
| pub(super) async fn for_workspace( | |
| &self, | |
| workspace: Option<Workspace>, | |
| scope: &Scope, | |
| ) -> Result<Self, String> { | |
| if scope.cancellation.is_cancelled() { | |
| return Err("skills_cancelled".into()); | |
| } | |
| let mut library = self.clone(); | |
| library.workspace = workspace.clone(); | |
| library.sources.retain(|source| !source.project()); | |
| let Some(workspace) = workspace.filter(|_| !self.projects.is_empty()) else { | |
| return Ok(library); | |
| }; | |
| let mut ticket = scope | |
| .resources | |
| .reserve() | |
| .map_err(|_| "skills_call_closed")?; | |
| let cancellation = scope.cancellation.clone(); | |
| tokio::task::spawn_blocking(move || { | |
| ticket.start(); | |
| let result = (|| { | |
| if cancellation.is_cancelled() { | |
| return Err("skills_cancelled".into()); | |
| } | |
| let (path, observed) = directory::capture(Path::new(&workspace.path)) | |
| .map_err(|_| "skills_workspace_unavailable")?; | |
| if workspace::project::host_path(&path).ok() != Some(workspace.path.as_str()) | |
| || observed != workspace.directory_identity | |
| || path.starts_with(&library.private) | |
| { | |
| return Err("skills_workspace_unavailable".into()); | |
| } | |
| let published = library | |
| .projects | |
| .iter() | |
| .find(|root| root.validate(&path).is_ok()) | |
| .ok_or("skills_workspace_unavailable")?; | |
| let handle = directory::open(&path, &workspace.directory_identity) | |
| .map_err(|_| "skills_workspace_unavailable")?; | |
| if workspace::read_directory_identity( | |
| path.clone(), | |
| handle | |
| .try_clone() | |
| .map_err(|_| "skills_workspace_unavailable")?, | |
| ) | |
| .map_err(|_| "skills_workspace_unavailable")? | |
| != workspace.identity | |
| { | |
| return Err("skills_workspace_unavailable".into()); | |
| } | |
| published | |
| .validate(&path) | |
| .map_err(|_| "skills_workspace_unavailable")?; | |
| let root = ReadRoot::from_handle(handle, path.clone()) | |
| .restrict(library.policy.clone()) | |
| .map_err(|_| "skills_workspace_unavailable")?; | |
| let identity = maka_agent_facts::artifact::content_digest( | |
| &serde_json::to_vec(&workspace).map_err(|_| "skills_workspace_unavailable")?, | |
| ); | |
| let mut sources = standard(root, &path, Some(&identity[7..])); | |
| sources.append(&mut library.sources); | |
| library.sources = sources; | |
| if cancellation.is_cancelled() { | |
| return Err("skills_cancelled".into()); | |
| } | |
| Ok(library) | |
| })(); | |
| ticket.complete(Ok(())); | |
| result | |
| }) | |
| .await | |
| .map_err(|_| "skills_workspace_unavailable")? | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use crate::tests::{preferences, scope, write}; | |
| use serde_json::json; | |
| pub(crate) async fn workspace(path: &Path) -> Workspace { | |
| std::fs::create_dir_all(path).unwrap(); | |
| let (path, directory_identity) = directory::capture(path).unwrap(); | |
| let handle = directory::open(&path, &directory_identity).unwrap(); | |
| let private = tempfile::tempdir().unwrap(); | |
| let control = tempfile::tempdir().unwrap(); | |
| let identity = workspace::ensure_directory_identity( | |
| path.clone(), | |
| handle, | |
| private.path().to_owned(), | |
| control.path().to_owned(), | |
| None, | |
| ) | |
| .await | |
| .unwrap(); | |
| Workspace { | |
| worktree: None, | |
| project_id: None, | |
| path: path.to_str().unwrap().into(), | |
| directory_identity, | |
| identity, | |
| } | |
| } | |
| async fn library(base: &Path) -> Library { | |
| let home = base.join("home"); | |
| let projects = base.join("projects"); | |
| std::fs::create_dir_all(&home).unwrap(); | |
| std::fs::create_dir_all(&projects).unwrap(); | |
| Library::open( | |
| Configuration { | |
| home: Some(home.to_str().unwrap().into()), | |
| project_roots: vec![maka_host_runtime::DirectoryRootSpec { | |
| label: "Projects".into(), | |
| path: projects, | |
| }], | |
| bundled: false, | |
| manage: false, | |
| preferences: true, | |
| sources: vec![], | |
| }, | |
| base.canonicalize().unwrap().join("private"), | |
| preferences(base).await, | |
| ) | |
| .unwrap() | |
| } | |
| #[tokio::test] | |
| async fn standard_sources_keep_missing_directories_project_precedence_and_preference_identity() | |
| { | |
| let dir = tempfile::tempdir().unwrap(); | |
| let library = library(dir.path()).await; | |
| let call = scope(); | |
| let a = workspace(&dir.path().join("projects/a")).await; | |
| let b = workspace(&dir.path().join("projects/b")).await; | |
| assert!(library.snapshot(&call).await.unwrap().entries.is_empty()); | |
| // Parent handles are captured before the optional conventional directories exist. | |
| write( | |
| &dir.path().join("home/.maka/skills"), | |
| "review", | |
| "home instructions", | |
| ); | |
| write( | |
| &dir.path().join("home/.agents/skills"), | |
| "review", | |
| "home fallback", | |
| ); | |
| write( | |
| &Path::new(&a.path).join(".maka/skills"), | |
| "review", | |
| "project A", | |
| ); | |
| write( | |
| &Path::new(&a.path).join(".agents/skills"), | |
| "review", | |
| "project fallback", | |
| ); | |
| write( | |
| &Path::new(&b.path).join(".maka/skills"), | |
| "review", | |
| "project B", | |
| ); | |
| let a_library = library.for_workspace(Some(a.clone()), &call).await.unwrap(); | |
| let a_page = a_library.snapshot(&call).await.unwrap(); | |
| let reference = a_page | |
| .entries | |
| .iter() | |
| .find(|(entry, _, _)| entry.shadowed_by.is_none()) | |
| .unwrap() | |
| .0 | |
| .reference | |
| .clone(); | |
| assert_eq!(a_page.entries.len(), 4); | |
| assert!( | |
| a_page | |
| .entries | |
| .iter() | |
| .filter(|(entry, _, _)| entry.reference != reference) | |
| .all(|(entry, _, _)| entry.shadowed_by.as_deref() == Some(reference.as_str())) | |
| ); | |
| let search = |workspace: &Workspace| json!({"workspace":workspace,"tools":[],"input":{"query":"review"}}); | |
| let a_search = library.model("search", search(&a), &call).await.unwrap(); | |
| let b_search = library.model("search", search(&b), &call).await.unwrap(); | |
| assert_ne!(a_search["revision"], b_search["revision"]); | |
| assert_ne!(a_search["matches"][0]["ref"], b_search["matches"][0]["ref"]); | |
| let loaded=library.model("load",json!({"workspace":a,"tools":[],"input":{"name":"review","revision":a_search["revision"]}}),&call).await.unwrap(); | |
| assert_eq!(loaded["instructions"], "project A"); | |
| let crossed=library.model("load",json!({"workspace":b,"tools":[],"input":{"name":"review","revision":a_search["revision"]}}),&call).await.unwrap(); | |
| assert_eq!(crossed["kind"], "changed"); | |
| assert!(matches!( | |
| library | |
| .preferences | |
| .set( | |
| SetPreference { | |
| reference, | |
| revision: 0, | |
| preference: maka_skills::Preference { | |
| enabled: false, | |
| pinned: true | |
| } | |
| }, | |
| &call | |
| ) | |
| .await | |
| .unwrap(), | |
| PreferenceResult::Applied { .. } | |
| )); | |
| assert_eq!( | |
| library.model("search", search(&a), &call).await.unwrap()["totalEligible"], | |
| 0 | |
| ); | |
| assert_eq!( | |
| library.model("search", search(&b), &call).await.unwrap()["totalEligible"], | |
| 1 | |
| ); | |
| assert_eq!( | |
| library | |
| .model( | |
| "search", | |
| json!({"tools":[],"input":{"query":"review"}}), | |
| &call | |
| ) | |
| .await | |
| .unwrap()["totalEligible"], | |
| 1 | |
| ); | |
| let b_library = library.for_workspace(Some(b.clone()), &call).await.unwrap(); | |
| assert!(matches!( | |
| b_library | |
| .snapshot(&call) | |
| .await | |
| .unwrap() | |
| .read(Read { | |
| workspace: Some(b), | |
| reference: a_page.entries[0].0.reference.clone(), | |
| revision: a_page.revision, | |
| offset: 0 | |
| }) | |
| .unwrap(), | |
| Document::Changed | |
| )); | |
| std::fs::remove_dir_all(dir.path().join("home/.maka/skills")).unwrap(); | |
| assert_eq!(library.snapshot(&call).await.unwrap().entries.len(), 1); | |
| call.finish().await.unwrap(); | |
| assert!(library.for_workspace(Some(a), &call).await.is_err()); | |
| } | |
| #[tokio::test] | |
| async fn standard_sources_reject_private_aliases_replacements_and_changed_markers() { | |
| let dir = tempfile::tempdir().unwrap(); | |
| let library = library(dir.path()).await; | |
| let call = scope(); | |
| let a = workspace(&dir.path().join("projects/a")).await; | |
| let outside = workspace(&dir.path().join("outside")).await; | |
| assert!(library.for_workspace(Some(outside), &call).await.is_err()); | |
| let private = workspace(&dir.path().join("private")).await; | |
| let mut wide = library.clone(); | |
| wide.projects = vec![Arc::new(PublishedDirectory::open(dir.path()).unwrap())]; | |
| assert!( | |
| wide.for_workspace(Some(private.clone()), &call) | |
| .await | |
| .is_err() | |
| ); | |
| #[cfg(unix)] | |
| { | |
| let alias = dir.path().join("projects/alias"); | |
| std::os::unix::fs::symlink(&private.path, &alias).unwrap(); | |
| let mut observation = private; | |
| observation.path = alias.to_str().unwrap().into(); | |
| assert!( | |
| library | |
| .for_workspace(Some(observation), &call) | |
| .await | |
| .is_err() | |
| ); | |
| let escaped = dir.path().join("projects/a/.agents"); | |
| std::os::unix::fs::symlink(dir.path().join("home"), &escaped).unwrap(); | |
| write( | |
| &dir.path().join("home/skills"), | |
| "secret", | |
| "not a project skill", | |
| ); | |
| let selected = library.for_workspace(Some(a.clone()), &call).await.unwrap(); | |
| let snapshot = selected.snapshot(&call).await.unwrap(); | |
| assert!(snapshot.entries.is_empty()); | |
| assert!(!snapshot.diagnostics.is_empty()); | |
| } | |
| let mut marker = a.clone(); | |
| marker.identity = workspace(&dir.path().join("projects/b")).await.identity; | |
| assert!(library.for_workspace(Some(marker), &call).await.is_err()); | |
| std::fs::rename(&a.path, dir.path().join("projects/original")).unwrap(); | |
| let _replacement = workspace(Path::new(&a.path)).await; | |
| assert!(library.for_workspace(Some(a), &call).await.is_err()); | |
| let b = workspace(&dir.path().join("projects/c")).await; | |
| std::fs::rename(dir.path().join("projects"), dir.path().join("old-projects")).unwrap(); | |
| std::fs::create_dir_all(dir.path().join("projects")).unwrap(); | |
| assert!(library.for_workspace(Some(b), &call).await.is_err()); | |
| call.finish().await.unwrap(); | |
| } | |
| } |