diff --git a/EvoScientist/EvoScientist.py b/EvoScientist/EvoScientist.py index 5bdec59..b440141 100644 --- a/EvoScientist/EvoScientist.py +++ b/EvoScientist/EvoScientist.py @@ -253,7 +253,7 @@ def _get_default_backend(): """Build the default composite backend from current paths.""" from deepagents.backends import CompositeBackend, FilesystemBackend - from .backends import CustomSandboxBackend, MergedReadOnlyBackend + from .backends import CustomSandboxBackend, MergedSkillsBackend workspace_dir = str(_paths_mod.WORKSPACE_ROOT) set_active_workspace(workspace_dir) @@ -266,7 +266,7 @@ def _get_default_backend(): virtual_mode=True, timeout=300, ) - sk_backend = MergedReadOnlyBackend( + sk_backend = MergedSkillsBackend( primary_dir=user_skills_dir, global_dir=global_skills_dir, secondary_dir=SKILLS_DIR, @@ -367,7 +367,7 @@ def create_cli_agent(workspace_dir: str | None = None, checkpointer=None, config from deepagents.backends import CompositeBackend, FilesystemBackend from . import paths as _paths - from .backends import CustomSandboxBackend, MergedReadOnlyBackend + from .backends import CustomSandboxBackend, MergedSkillsBackend from .middleware import ( ContextOverflowMapperMiddleware, ToolErrorHandlerMiddleware, @@ -407,7 +407,7 @@ def create_cli_agent(workspace_dir: str | None = None, checkpointer=None, config virtual_mode=True, timeout=300, ) - sk_backend = MergedReadOnlyBackend( + sk_backend = MergedSkillsBackend( primary_dir=_usr_skills_dir, global_dir=_global_skills_dir, secondary_dir=SKILLS_DIR, diff --git a/EvoScientist/backends.py b/EvoScientist/backends.py index 3ac623f..94dca90 100644 --- a/EvoScientist/backends.py +++ b/EvoScientist/backends.py @@ -286,16 +286,17 @@ class ReadOnlyFilesystemBackend(FilesystemBackend): ) -class MergedReadOnlyBackend(BackendProtocol): - """Read-only backend that merges up to three skill directories. +class MergedSkillsBackend(BackendProtocol): + """Skills backend that merges up to three skill directories. Priority (high → low): - 1. primary — workspace/skills/ (project-local) - 2. global — ~/.config/evoscientist/skills/ (user global, optional) - 3. secondary — EvoScientist/skills/ (built-in, PyPI) + 1. primary — workspace/skills/ (project-local, writable) + 2. global — ~/.config/evoscientist/skills/ (user global, read-only) + 3. secondary — EvoScientist/skills/ (built-in, PyPI, read-only) Higher-priority skills override lower-priority skills with the same name. All directories share the same virtual path namespace (/skills/). + Only the workspace tier (primary) allows write and edit operations. """ def __init__( @@ -304,9 +305,7 @@ class MergedReadOnlyBackend(BackendProtocol): secondary_dir: str, global_dir: str | None = None, ): - self._primary = ReadOnlyFilesystemBackend( - root_dir=primary_dir, virtual_mode=True - ) + self._primary = FilesystemBackend(root_dir=primary_dir, virtual_mode=True) self._global = ( ReadOnlyFilesystemBackend(root_dir=global_dir, virtual_mode=True) if global_dir @@ -375,12 +374,10 @@ class MergedReadOnlyBackend(BackendProtocol): pass return GlobResult(matches=sorted(merged.values(), key=lambda x: x["path"])) - # -- write / edit: blocked -- + # -- write / edit: only workspace/skills/ (primary) is writable -- def write(self, file_path: str, content: str) -> WriteResult: - return WriteResult( - error="This directory is read-only. Write operations are not permitted here." - ) + return self._primary.write(file_path, content) def edit( self, @@ -389,9 +386,7 @@ class MergedReadOnlyBackend(BackendProtocol): new_string: str, replace_all: bool = False, ) -> EditResult: - return EditResult( - error="This directory is read-only. Edit operations are not permitted here." - ) + return self._primary.edit(file_path, old_string, new_string, replace_all) # -- download / upload -- @@ -410,10 +405,7 @@ class MergedReadOnlyBackend(BackendProtocol): return responses def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]: - return [ - FileUploadResponse(path=path, error="permission_denied") - for path, _ in files - ] + return self._primary.upload_files(files) class CustomSandboxBackend(LocalShellBackend): diff --git a/EvoScientist/paths.py b/EvoScientist/paths.py index 5f033bb..d7e54eb 100644 --- a/EvoScientist/paths.py +++ b/EvoScientist/paths.py @@ -62,17 +62,15 @@ def set_workspace_root(path: str | Path) -> None: def ensure_dirs() -> None: - """Create runtime subdirectories (memory, skills) if they do not exist. + """Create runtime subdirectories if they do not exist. + + Only memory is created eagerly — skills directories are created on demand + by install_skill() when the user first installs a skill. Does NOT create the workspace root itself — it should already exist (either the user's cwd or a directory they specified). """ - for path in (MEMORY_DIR, USER_SKILLS_DIR): - path.mkdir(parents=True, exist_ok=True) - try: - GLOBAL_SKILLS_DIR.mkdir(parents=True, exist_ok=True) - except PermissionError: - pass # read-only environments — skip silently + MEMORY_DIR.mkdir(parents=True, exist_ok=True) def default_workspace_dir() -> Path: diff --git a/EvoScientist/prompts.py b/EvoScientist/prompts.py index a0b0e9d..37676db 100644 --- a/EvoScientist/prompts.py +++ b/EvoScientist/prompts.py @@ -15,7 +15,7 @@ into reproducible experiments and a paper-ready experimental report. - Never invent results. If you cannot run something, say so and propose the smallest next step. - Delegate aggressively using the `task` tool. Prefer the research sub-agent for web search. - Use local skills when they match the task. Your available skills are listed in the system prompt — read the relevant `SKILL.md` for full instructions. - All skills are available under `/skills/` (read-only). + All skills are available under `/skills/`. ## Research Lifecycle (when applicable) For end-to-end research projects, the recommended skill sequence is: diff --git a/tests/test_paths.py b/tests/test_paths.py index d20929f..d044e62 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -102,4 +102,6 @@ class TestEnsureDirsUsesUpdatedPaths: paths.ensure_dirs() assert (new_root / "memory").is_dir() - assert (new_root / "skills").is_dir() + assert not ( + new_root / "skills" + ).exists() # skills created on demand by install_skill()