EvoScientist 0.0.1.dev2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- EvoScientist/EvoScientist.py +157 -0
- EvoScientist/__init__.py +24 -0
- EvoScientist/__main__.py +4 -0
- EvoScientist/backends.py +392 -0
- EvoScientist/cli.py +1553 -0
- EvoScientist/middleware.py +35 -0
- EvoScientist/prompts.py +277 -0
- EvoScientist/skills/accelerate/SKILL.md +332 -0
- EvoScientist/skills/accelerate/references/custom-plugins.md +453 -0
- EvoScientist/skills/accelerate/references/megatron-integration.md +489 -0
- EvoScientist/skills/accelerate/references/performance.md +525 -0
- EvoScientist/skills/bitsandbytes/SKILL.md +411 -0
- EvoScientist/skills/bitsandbytes/references/memory-optimization.md +521 -0
- EvoScientist/skills/bitsandbytes/references/qlora-training.md +521 -0
- EvoScientist/skills/bitsandbytes/references/quantization-formats.md +447 -0
- EvoScientist/skills/find-skills/SKILL.md +133 -0
- EvoScientist/skills/find-skills/scripts/install_skill.py +211 -0
- EvoScientist/skills/flash-attention/SKILL.md +367 -0
- EvoScientist/skills/flash-attention/references/benchmarks.md +215 -0
- EvoScientist/skills/flash-attention/references/transformers-integration.md +293 -0
- EvoScientist/skills/llama-cpp/SKILL.md +258 -0
- EvoScientist/skills/llama-cpp/references/optimization.md +89 -0
- EvoScientist/skills/llama-cpp/references/quantization.md +213 -0
- EvoScientist/skills/llama-cpp/references/server.md +125 -0
- EvoScientist/skills/lm-evaluation-harness/SKILL.md +490 -0
- EvoScientist/skills/lm-evaluation-harness/references/api-evaluation.md +490 -0
- EvoScientist/skills/lm-evaluation-harness/references/benchmark-guide.md +488 -0
- EvoScientist/skills/lm-evaluation-harness/references/custom-tasks.md +602 -0
- EvoScientist/skills/lm-evaluation-harness/references/distributed-eval.md +519 -0
- EvoScientist/skills/ml-paper-writing/SKILL.md +937 -0
- EvoScientist/skills/ml-paper-writing/references/checklists.md +361 -0
- EvoScientist/skills/ml-paper-writing/references/citation-workflow.md +562 -0
- EvoScientist/skills/ml-paper-writing/references/reviewer-guidelines.md +367 -0
- EvoScientist/skills/ml-paper-writing/references/sources.md +159 -0
- EvoScientist/skills/ml-paper-writing/references/writing-guide.md +476 -0
- EvoScientist/skills/ml-paper-writing/templates/README.md +251 -0
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/README.md +534 -0
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026-unified-template.tex +952 -0
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.bib +111 -0
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.bst +1493 -0
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.sty +315 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/README.md +50 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/acl.sty +312 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/acl_latex.tex +377 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/acl_lualatex.tex +101 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/acl_natbib.bst +1940 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/anthology.bib.txt +26 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/custom.bib +70 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/formatting.md +326 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/README.md +3 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.bib +11 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.bst +1440 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.pdf +0 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.sty +218 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.tex +305 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/fancyhdr.sty +485 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/math_commands.tex +508 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/natbib.sty +1246 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/fancyhdr.sty +485 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.bib +24 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.bst +1440 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.pdf +0 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.sty +246 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.tex +414 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/math_commands.tex +508 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/natbib.sty +1246 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/algorithm.sty +79 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/algorithmic.sty +201 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.bib +75 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.pdf +0 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.tex +662 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/fancyhdr.sty +864 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/icml2026.bst +1443 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/icml2026.sty +767 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/icml_numpapers.pdf +0 -0
- EvoScientist/skills/ml-paper-writing/templates/neurips2025/Makefile +36 -0
- EvoScientist/skills/ml-paper-writing/templates/neurips2025/extra_pkgs.tex +53 -0
- EvoScientist/skills/ml-paper-writing/templates/neurips2025/main.tex +38 -0
- EvoScientist/skills/ml-paper-writing/templates/neurips2025/neurips.sty +382 -0
- EvoScientist/skills/peft/SKILL.md +431 -0
- EvoScientist/skills/peft/references/advanced-usage.md +514 -0
- EvoScientist/skills/peft/references/troubleshooting.md +480 -0
- EvoScientist/skills/ray-data/SKILL.md +326 -0
- EvoScientist/skills/ray-data/references/integration.md +82 -0
- EvoScientist/skills/ray-data/references/transformations.md +83 -0
- EvoScientist/skills/skill-creator/LICENSE.txt +202 -0
- EvoScientist/skills/skill-creator/SKILL.md +356 -0
- EvoScientist/skills/skill-creator/references/output-patterns.md +82 -0
- EvoScientist/skills/skill-creator/references/workflows.md +28 -0
- EvoScientist/skills/skill-creator/scripts/init_skill.py +303 -0
- EvoScientist/skills/skill-creator/scripts/package_skill.py +110 -0
- EvoScientist/skills/skill-creator/scripts/quick_validate.py +95 -0
- EvoScientist/stream/__init__.py +53 -0
- EvoScientist/stream/emitter.py +94 -0
- EvoScientist/stream/formatter.py +168 -0
- EvoScientist/stream/tracker.py +115 -0
- EvoScientist/stream/utils.py +255 -0
- EvoScientist/subagent.yaml +147 -0
- EvoScientist/tools.py +135 -0
- EvoScientist/utils.py +207 -0
- evoscientist-0.0.1.dev2.dist-info/METADATA +227 -0
- evoscientist-0.0.1.dev2.dist-info/RECORD +107 -0
- evoscientist-0.0.1.dev2.dist-info/WHEEL +5 -0
- evoscientist-0.0.1.dev2.dist-info/entry_points.txt +5 -0
- evoscientist-0.0.1.dev2.dist-info/licenses/LICENSE +21 -0
- evoscientist-0.0.1.dev2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""EvoScientist Agent graph construction.
|
|
2
|
+
|
|
3
|
+
This module creates and exports the compiled agent graph.
|
|
4
|
+
Usage:
|
|
5
|
+
from EvoScientist import agent
|
|
6
|
+
|
|
7
|
+
# Notebook / programmatic usage
|
|
8
|
+
for state in agent.stream(
|
|
9
|
+
{"messages": [HumanMessage(content="your question")]},
|
|
10
|
+
config={"configurable": {"thread_id": "1"}},
|
|
11
|
+
stream_mode="values",
|
|
12
|
+
):
|
|
13
|
+
...
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from deepagents import create_deep_agent
|
|
21
|
+
from deepagents.backends import FilesystemBackend, CompositeBackend
|
|
22
|
+
from langchain.chat_models import init_chat_model
|
|
23
|
+
|
|
24
|
+
from .backends import CustomSandboxBackend, MergedReadOnlyBackend
|
|
25
|
+
from .middleware import create_skills_middleware
|
|
26
|
+
from .prompts import RESEARCHER_INSTRUCTIONS, get_system_prompt
|
|
27
|
+
from .utils import load_subagents
|
|
28
|
+
from .tools import tavily_search, think_tool
|
|
29
|
+
|
|
30
|
+
# =============================================================================
|
|
31
|
+
# Configuration
|
|
32
|
+
# =============================================================================
|
|
33
|
+
|
|
34
|
+
# Backend mode: "sandbox" (with execute) or "filesystem" (read/write only)
|
|
35
|
+
BACKEND_MODE = "sandbox"
|
|
36
|
+
|
|
37
|
+
# Research limits
|
|
38
|
+
MAX_CONCURRENT = 3 # Max parallel sub-agents
|
|
39
|
+
MAX_ITERATIONS = 3 # Max delegation rounds
|
|
40
|
+
|
|
41
|
+
# Workspace settings
|
|
42
|
+
WORKSPACE_DIR = "./workspace/"
|
|
43
|
+
SKILLS_DIR = str(Path(__file__).parent / "skills")
|
|
44
|
+
SUBAGENTS_CONFIG = Path(__file__).parent / "subagent.yaml"
|
|
45
|
+
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# Initialization
|
|
48
|
+
# =============================================================================
|
|
49
|
+
|
|
50
|
+
# Get current date
|
|
51
|
+
current_date = datetime.now().strftime("%Y-%m-%d")
|
|
52
|
+
|
|
53
|
+
# Generate system prompt with limits
|
|
54
|
+
SYSTEM_PROMPT = get_system_prompt(
|
|
55
|
+
max_concurrent=MAX_CONCURRENT,
|
|
56
|
+
max_iterations=MAX_ITERATIONS,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Initialize chat model
|
|
60
|
+
chat_model = init_chat_model(
|
|
61
|
+
model="claude-sonnet-4-5-20250929",
|
|
62
|
+
model_provider="anthropic",
|
|
63
|
+
# thinking={"type": "enabled", "budget_tokens": 2000},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Initialize workspace backend based on mode
|
|
67
|
+
if BACKEND_MODE == "sandbox":
|
|
68
|
+
_workspace_backend = CustomSandboxBackend(
|
|
69
|
+
root_dir=WORKSPACE_DIR,
|
|
70
|
+
virtual_mode=True,
|
|
71
|
+
timeout=300,
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
_workspace_backend = FilesystemBackend(
|
|
75
|
+
root_dir=WORKSPACE_DIR,
|
|
76
|
+
virtual_mode=True,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Skills backend: merge user-installed (workspace) and system (package) skills
|
|
80
|
+
_skills_backend = MergedReadOnlyBackend(
|
|
81
|
+
primary_dir=str(Path(WORKSPACE_DIR) / "skills"), # user-installed, takes priority
|
|
82
|
+
secondary_dir=SKILLS_DIR, # package built-in, fallback
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Composite backend: workspace as default, skills mounted at /skills/
|
|
86
|
+
backend = CompositeBackend(
|
|
87
|
+
default=_workspace_backend,
|
|
88
|
+
routes={"/skills/": _skills_backend},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
tool_registry = {
|
|
92
|
+
"think_tool": think_tool,
|
|
93
|
+
"tavily_search": tavily_search,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
prompt_refs = {
|
|
97
|
+
"RESEARCHER_INSTRUCTIONS": RESEARCHER_INSTRUCTIONS.format(date=current_date),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
subagents = load_subagents(
|
|
101
|
+
SUBAGENTS_CONFIG,
|
|
102
|
+
tool_registry=tool_registry,
|
|
103
|
+
prompt_refs=prompt_refs,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Shared kwargs for agent creation
|
|
107
|
+
_AGENT_KWARGS = dict(
|
|
108
|
+
name="EvoScientist",
|
|
109
|
+
model=chat_model,
|
|
110
|
+
tools=[think_tool],
|
|
111
|
+
backend=backend,
|
|
112
|
+
subagents=subagents,
|
|
113
|
+
middleware=[create_skills_middleware(SKILLS_DIR, WORKSPACE_DIR)],
|
|
114
|
+
system_prompt=SYSTEM_PROMPT,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Default agent (no checkpointer) — used by langgraph dev / LangSmith / notebooks
|
|
118
|
+
EvoScientist_agent = create_deep_agent(**_AGENT_KWARGS).with_config({"recursion_limit": 500})
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def create_cli_agent(workspace_dir: str | None = None):
|
|
122
|
+
"""Create agent with InMemorySaver checkpointer for CLI multi-turn support.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
workspace_dir: Optional per-session workspace directory. If provided,
|
|
126
|
+
creates a fresh backend rooted at this path. If None, uses the
|
|
127
|
+
module-level default backend (./workspace/).
|
|
128
|
+
"""
|
|
129
|
+
from langgraph.checkpoint.memory import InMemorySaver # type: ignore[import-untyped]
|
|
130
|
+
|
|
131
|
+
if workspace_dir:
|
|
132
|
+
ws_backend = CustomSandboxBackend(
|
|
133
|
+
root_dir=workspace_dir,
|
|
134
|
+
virtual_mode=True,
|
|
135
|
+
timeout=300,
|
|
136
|
+
)
|
|
137
|
+
sk_backend = MergedReadOnlyBackend(
|
|
138
|
+
primary_dir=str(Path(workspace_dir) / "skills"),
|
|
139
|
+
secondary_dir=SKILLS_DIR,
|
|
140
|
+
)
|
|
141
|
+
be = CompositeBackend(
|
|
142
|
+
default=ws_backend,
|
|
143
|
+
routes={"/skills/": sk_backend},
|
|
144
|
+
)
|
|
145
|
+
mw = [create_skills_middleware(SKILLS_DIR, workspace_dir)]
|
|
146
|
+
kwargs = dict(
|
|
147
|
+
_AGENT_KWARGS,
|
|
148
|
+
backend=be,
|
|
149
|
+
middleware=mw,
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
kwargs = dict(_AGENT_KWARGS)
|
|
153
|
+
|
|
154
|
+
return create_deep_agent(
|
|
155
|
+
**kwargs,
|
|
156
|
+
checkpointer=InMemorySaver(),
|
|
157
|
+
).with_config({"recursion_limit": 500})
|
EvoScientist/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""EvoScientist Agent — AI-powered research & code execution."""
|
|
2
|
+
|
|
3
|
+
from .backends import CustomSandboxBackend, ReadOnlyFilesystemBackend
|
|
4
|
+
from .middleware import create_skills_middleware
|
|
5
|
+
from .prompts import get_system_prompt, RESEARCHER_INSTRUCTIONS
|
|
6
|
+
from .tools import tavily_search, think_tool
|
|
7
|
+
from .EvoScientist import EvoScientist_agent, create_cli_agent
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
# Agent graph (main export)
|
|
11
|
+
"EvoScientist_agent",
|
|
12
|
+
"create_cli_agent",
|
|
13
|
+
# Backends
|
|
14
|
+
"CustomSandboxBackend",
|
|
15
|
+
"ReadOnlyFilesystemBackend",
|
|
16
|
+
# Middleware
|
|
17
|
+
"create_skills_middleware",
|
|
18
|
+
# Prompts
|
|
19
|
+
"get_system_prompt",
|
|
20
|
+
"RESEARCHER_INSTRUCTIONS",
|
|
21
|
+
# Tools
|
|
22
|
+
"tavily_search",
|
|
23
|
+
"think_tool",
|
|
24
|
+
]
|
EvoScientist/__main__.py
ADDED
EvoScientist/backends.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""Custom backends for EvoScientist agent."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from deepagents.backends import FilesystemBackend
|
|
9
|
+
from deepagents.backends.filesystem import WriteResult, EditResult
|
|
10
|
+
from deepagents.backends.protocol import (
|
|
11
|
+
ExecuteResponse,
|
|
12
|
+
FileDownloadResponse,
|
|
13
|
+
FileUploadResponse,
|
|
14
|
+
SandboxBackendProtocol,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# System path prefixes that should never appear in virtual paths.
|
|
18
|
+
# If the agent hallucinates an absolute system path, we block it.
|
|
19
|
+
_SYSTEM_PATH_PREFIXES = (
|
|
20
|
+
"/Users/", "/home/", "/tmp/", "/var/", "/etc/",
|
|
21
|
+
"/opt/", "/usr/", "/bin/", "/sbin/", "/dev/",
|
|
22
|
+
"/proc/", "/sys/", "/root/",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Dangerous patterns that could escape the workspace
|
|
26
|
+
BLOCKED_PATTERNS = [
|
|
27
|
+
r'\.\.', # ../ directory traversal
|
|
28
|
+
r'~/', # home directory
|
|
29
|
+
r'\bcd\s+/', # cd to absolute path
|
|
30
|
+
r'\brm\s+-rf\s+/', # rm -rf with absolute path
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# Dangerous commands that should never be executed
|
|
34
|
+
BLOCKED_COMMANDS = [
|
|
35
|
+
'sudo',
|
|
36
|
+
'chmod',
|
|
37
|
+
'chown',
|
|
38
|
+
'mkfs',
|
|
39
|
+
'dd',
|
|
40
|
+
'shutdown',
|
|
41
|
+
'reboot',
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def validate_command(command: str) -> str | None:
|
|
46
|
+
"""
|
|
47
|
+
Validate a shell command for safety.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
None if command is safe, error message string if blocked.
|
|
51
|
+
"""
|
|
52
|
+
# Check for directory traversal and dangerous patterns
|
|
53
|
+
for pattern in BLOCKED_PATTERNS:
|
|
54
|
+
if re.search(pattern, command):
|
|
55
|
+
return (
|
|
56
|
+
f"Command blocked: contains forbidden pattern '{pattern}'. "
|
|
57
|
+
f"All commands must operate within the workspace directory. "
|
|
58
|
+
f"Use relative paths (e.g., './file.py') instead."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Check for dangerous commands
|
|
62
|
+
for cmd in BLOCKED_COMMANDS:
|
|
63
|
+
if re.search(rf'\b{cmd}\b', command):
|
|
64
|
+
return (
|
|
65
|
+
f"Command blocked: '{cmd}' is not allowed in sandbox mode. "
|
|
66
|
+
f"Only standard development commands are permitted."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def convert_virtual_paths_in_command(command: str) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Convert virtual paths (starting with /) in commands to relative paths.
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
- "python /main.py" -> "python ./main.py"
|
|
78
|
+
- "cat /data/file.txt" -> "cat ./data/file.txt"
|
|
79
|
+
- "ls /" -> "ls ."
|
|
80
|
+
- "python main.py" -> "python main.py" (unchanged)
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
command: Original command
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Converted command
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def replace_virtual_path(match):
|
|
90
|
+
path = match.group(0)
|
|
91
|
+
|
|
92
|
+
# Skip content that looks like a URL
|
|
93
|
+
if '://' in command[max(0, match.start() - 10):match.end() + 10]:
|
|
94
|
+
return path
|
|
95
|
+
|
|
96
|
+
# Convert virtual path
|
|
97
|
+
if path == '/':
|
|
98
|
+
return '.'
|
|
99
|
+
else:
|
|
100
|
+
return '.' + path
|
|
101
|
+
|
|
102
|
+
# Match pattern: paths starting with / (but not URLs)
|
|
103
|
+
pattern = r'(?<=\s)/[^\s;|&<>\'"`]*|^/[^\s;|&<>\'"`]*'
|
|
104
|
+
converted = re.sub(pattern, replace_virtual_path, command)
|
|
105
|
+
|
|
106
|
+
return converted
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ReadOnlyFilesystemBackend(FilesystemBackend):
|
|
110
|
+
"""
|
|
111
|
+
Read-only filesystem backend.
|
|
112
|
+
|
|
113
|
+
Allows read, ls, grep, glob operations but blocks write and edit.
|
|
114
|
+
Used for skills directory — agent can read skill definitions but cannot
|
|
115
|
+
modify them.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def write(self, file_path: str, content: str) -> WriteResult:
|
|
119
|
+
return WriteResult(
|
|
120
|
+
error="This directory is read-only. Write operations are not permitted here."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def edit(
|
|
124
|
+
self,
|
|
125
|
+
file_path: str,
|
|
126
|
+
old_string: str,
|
|
127
|
+
new_string: str,
|
|
128
|
+
replace_all: bool = False,
|
|
129
|
+
) -> EditResult:
|
|
130
|
+
return EditResult(
|
|
131
|
+
error="This directory is read-only. Edit operations are not permitted here."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class MergedReadOnlyBackend:
|
|
136
|
+
"""Read-only backend that merges two directories.
|
|
137
|
+
|
|
138
|
+
Reads from *primary* first (user skills in workspace/skills/),
|
|
139
|
+
falls back to *secondary* (system skills in ./skills/).
|
|
140
|
+
User skills override system skills with the same name.
|
|
141
|
+
|
|
142
|
+
Both directories share the same virtual path namespace — the agent
|
|
143
|
+
sees all skills under /skills/ regardless of which backend serves them.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def __init__(self, primary_dir: str, secondary_dir: str):
|
|
147
|
+
self._primary = ReadOnlyFilesystemBackend(root_dir=primary_dir, virtual_mode=True)
|
|
148
|
+
self._secondary = ReadOnlyFilesystemBackend(root_dir=secondary_dir, virtual_mode=True)
|
|
149
|
+
|
|
150
|
+
# -- read: try primary first, fall back to secondary --
|
|
151
|
+
|
|
152
|
+
def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> str:
|
|
153
|
+
try:
|
|
154
|
+
result = self._primary.read(file_path, offset, limit)
|
|
155
|
+
if not result.startswith("Error:"):
|
|
156
|
+
return result
|
|
157
|
+
except (ValueError, FileNotFoundError, OSError):
|
|
158
|
+
pass
|
|
159
|
+
return self._secondary.read(file_path, offset, limit)
|
|
160
|
+
|
|
161
|
+
# -- ls_info: merge both, primary wins on name conflicts --
|
|
162
|
+
|
|
163
|
+
def ls_info(self, path: str = "/") -> list:
|
|
164
|
+
secondary_items = {item["path"]: item for item in self._secondary.ls_info(path)}
|
|
165
|
+
primary_items = {item["path"]: item for item in self._primary.ls_info(path)}
|
|
166
|
+
secondary_items.update(primary_items) # primary overrides
|
|
167
|
+
return sorted(secondary_items.values(), key=lambda x: x["path"])
|
|
168
|
+
|
|
169
|
+
# -- grep_raw: search both, deduplicate --
|
|
170
|
+
|
|
171
|
+
def grep_raw(self, pattern: str, path: str | None = None, glob: str | None = None) -> list:
|
|
172
|
+
results = self._secondary.grep_raw(pattern, path, glob)
|
|
173
|
+
try:
|
|
174
|
+
results += self._primary.grep_raw(pattern, path, glob)
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
return results
|
|
178
|
+
|
|
179
|
+
# -- glob_info: merge both --
|
|
180
|
+
|
|
181
|
+
def glob_info(self, pattern: str, path: str = "/") -> list:
|
|
182
|
+
secondary = {item["path"]: item for item in self._secondary.glob_info(pattern, path)}
|
|
183
|
+
try:
|
|
184
|
+
primary = {item["path"]: item for item in self._primary.glob_info(pattern, path)}
|
|
185
|
+
secondary.update(primary)
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
return sorted(secondary.values(), key=lambda x: x["path"])
|
|
189
|
+
|
|
190
|
+
# -- write / edit: blocked --
|
|
191
|
+
|
|
192
|
+
def write(self, file_path: str, content: str) -> WriteResult:
|
|
193
|
+
return WriteResult(
|
|
194
|
+
error="This directory is read-only. Write operations are not permitted here."
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def edit(
|
|
198
|
+
self,
|
|
199
|
+
file_path: str,
|
|
200
|
+
old_string: str,
|
|
201
|
+
new_string: str,
|
|
202
|
+
replace_all: bool = False,
|
|
203
|
+
) -> EditResult:
|
|
204
|
+
return EditResult(
|
|
205
|
+
error="This directory is read-only. Edit operations are not permitted here."
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# -- async variants (required by middleware) --
|
|
209
|
+
|
|
210
|
+
async def aread(self, file_path: str, offset: int = 0, limit: int = 2000) -> str:
|
|
211
|
+
return self.read(file_path, offset, limit)
|
|
212
|
+
|
|
213
|
+
async def als_info(self, path: str = "/") -> list:
|
|
214
|
+
return self.ls_info(path)
|
|
215
|
+
|
|
216
|
+
async def agrep_raw(self, pattern: str, path: str | None = None, glob: str | None = None) -> list:
|
|
217
|
+
return self.grep_raw(pattern, path, glob)
|
|
218
|
+
|
|
219
|
+
async def aglob_info(self, pattern: str, path: str = "/") -> list:
|
|
220
|
+
return self.glob_info(pattern, path)
|
|
221
|
+
|
|
222
|
+
async def awrite(self, file_path: str, content: str) -> WriteResult:
|
|
223
|
+
return self.write(file_path, content)
|
|
224
|
+
|
|
225
|
+
async def aedit(self, file_path: str, old_string: str, new_string: str, replace_all: bool = False) -> EditResult:
|
|
226
|
+
return self.edit(file_path, old_string, new_string, replace_all)
|
|
227
|
+
|
|
228
|
+
# -- download / upload (required by BackendProtocol) --
|
|
229
|
+
|
|
230
|
+
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
|
231
|
+
"""Download files, trying primary then secondary."""
|
|
232
|
+
responses: list[FileDownloadResponse] = []
|
|
233
|
+
for path in paths:
|
|
234
|
+
resp = self._primary.download_files([path])[0]
|
|
235
|
+
if resp.error is not None:
|
|
236
|
+
resp = self._secondary.download_files([path])[0]
|
|
237
|
+
responses.append(resp)
|
|
238
|
+
return responses
|
|
239
|
+
|
|
240
|
+
async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
|
241
|
+
return self.download_files(paths)
|
|
242
|
+
|
|
243
|
+
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
|
|
244
|
+
return [
|
|
245
|
+
FileUploadResponse(path=path, error="permission_denied")
|
|
246
|
+
for path, _ in files
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
async def aupload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
|
|
250
|
+
return self.upload_files(files)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class CustomSandboxBackend(FilesystemBackend, SandboxBackendProtocol):
|
|
254
|
+
"""
|
|
255
|
+
Custom sandbox backend - inherits FilesystemBackend and implements execute method.
|
|
256
|
+
|
|
257
|
+
Features:
|
|
258
|
+
- Inherits all file operations (ls, read, write, edit, grep, glob)
|
|
259
|
+
- Adds shell command execution capability
|
|
260
|
+
- Command validation prevents directory traversal and dangerous operations
|
|
261
|
+
- Runs commands in specified working directory
|
|
262
|
+
- Compatible with LangGraph checkpointer (no thread locks)
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def __init__(
|
|
266
|
+
self,
|
|
267
|
+
root_dir: str = ".",
|
|
268
|
+
virtual_mode: bool = True,
|
|
269
|
+
working_dir: str | None = None,
|
|
270
|
+
timeout: int = 300,
|
|
271
|
+
shell: str = "/bin/bash",
|
|
272
|
+
):
|
|
273
|
+
"""
|
|
274
|
+
Initialize custom sandbox backend.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
root_dir: File system root directory
|
|
278
|
+
virtual_mode: Whether to enable virtual path mode
|
|
279
|
+
working_dir: Working directory for command execution (defaults to root_dir)
|
|
280
|
+
timeout: Command execution timeout in seconds
|
|
281
|
+
shell: Shell program to use
|
|
282
|
+
"""
|
|
283
|
+
super().__init__(root_dir=root_dir, virtual_mode=virtual_mode)
|
|
284
|
+
|
|
285
|
+
self.working_dir = working_dir or root_dir
|
|
286
|
+
self.timeout = timeout
|
|
287
|
+
self.shell = shell
|
|
288
|
+
self.virtual_mode = virtual_mode
|
|
289
|
+
|
|
290
|
+
# Ensure working directory exists
|
|
291
|
+
os.makedirs(self.working_dir, exist_ok=True)
|
|
292
|
+
|
|
293
|
+
def _resolve_path(self, key: str) -> Path:
|
|
294
|
+
"""Resolve path with sanitization to prevent nested directories.
|
|
295
|
+
|
|
296
|
+
Intercepts all file operations (read, write, edit, ls, grep, glob).
|
|
297
|
+
Auto-corrects common LLM path mistakes instead of crashing:
|
|
298
|
+
1. /workspace/file.py → /file.py
|
|
299
|
+
2. /Users/name/.../workspace/f → /f (strip up to workspace/)
|
|
300
|
+
3. /Users/name/file.py → /file.py (keep basename)
|
|
301
|
+
"""
|
|
302
|
+
# Auto-strip /workspace/ prefix to prevent nesting
|
|
303
|
+
if key.startswith("/workspace/"):
|
|
304
|
+
key = key[len("/workspace"):] # "/workspace/main.py" → "/main.py"
|
|
305
|
+
elif key == "/workspace":
|
|
306
|
+
key = "/"
|
|
307
|
+
|
|
308
|
+
# Auto-correct system absolute paths
|
|
309
|
+
for prefix in _SYSTEM_PATH_PREFIXES:
|
|
310
|
+
if key.startswith(prefix):
|
|
311
|
+
# Try to extract path after "workspace/" or "workspace" at end
|
|
312
|
+
marker = "/workspace/"
|
|
313
|
+
idx = key.find(marker)
|
|
314
|
+
if idx != -1:
|
|
315
|
+
key = "/" + key[idx + len(marker):]
|
|
316
|
+
elif key.endswith("/workspace"):
|
|
317
|
+
key = "/"
|
|
318
|
+
else:
|
|
319
|
+
# Fall back to basename
|
|
320
|
+
key = "/" + Path(key).name
|
|
321
|
+
break
|
|
322
|
+
|
|
323
|
+
return super()._resolve_path(key)
|
|
324
|
+
|
|
325
|
+
def execute(self, command: str) -> ExecuteResponse:
|
|
326
|
+
"""
|
|
327
|
+
Execute shell command in sandbox environment.
|
|
328
|
+
|
|
329
|
+
Commands are validated before execution to prevent:
|
|
330
|
+
- Directory traversal (../)
|
|
331
|
+
- Access to paths outside workspace
|
|
332
|
+
- Dangerous system commands
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
command: Command string to execute
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
ExecuteResponse containing output, exit_code, and truncated flag
|
|
339
|
+
"""
|
|
340
|
+
try:
|
|
341
|
+
# Validate command safety
|
|
342
|
+
error = validate_command(command)
|
|
343
|
+
if error:
|
|
344
|
+
return ExecuteResponse(
|
|
345
|
+
output=error,
|
|
346
|
+
exit_code=1,
|
|
347
|
+
truncated=False,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# Convert virtual paths to relative paths
|
|
351
|
+
if self.virtual_mode:
|
|
352
|
+
command = convert_virtual_paths_in_command(command=command)
|
|
353
|
+
|
|
354
|
+
result = subprocess.run(
|
|
355
|
+
command,
|
|
356
|
+
shell=True,
|
|
357
|
+
executable=self.shell,
|
|
358
|
+
cwd=self.working_dir,
|
|
359
|
+
capture_output=True,
|
|
360
|
+
text=True,
|
|
361
|
+
timeout=self.timeout,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
output = ""
|
|
365
|
+
if result.stdout:
|
|
366
|
+
output += result.stdout
|
|
367
|
+
if result.stderr:
|
|
368
|
+
output += result.stderr
|
|
369
|
+
|
|
370
|
+
return ExecuteResponse(
|
|
371
|
+
output=output,
|
|
372
|
+
exit_code=result.returncode,
|
|
373
|
+
truncated=False,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
except subprocess.TimeoutExpired:
|
|
377
|
+
return ExecuteResponse(
|
|
378
|
+
output=f"Command timed out after {self.timeout} seconds",
|
|
379
|
+
exit_code=-1,
|
|
380
|
+
truncated=False,
|
|
381
|
+
)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
return ExecuteResponse(
|
|
384
|
+
output=f"Error executing command: {str(e)}",
|
|
385
|
+
exit_code=-1,
|
|
386
|
+
truncated=False,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
async def aexecute(self, command: str) -> ExecuteResponse:
|
|
390
|
+
"""Async version of execute (runs sync version in thread)."""
|
|
391
|
+
import asyncio
|
|
392
|
+
return await asyncio.to_thread(self.execute, command)
|