devloop 0.2.2__tar.gz → 0.3.0__tar.gz
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.
- {devloop-0.2.2 → devloop-0.3.0}/PKG-INFO +41 -8
- {devloop-0.2.2 → devloop-0.3.0}/README.md +36 -7
- {devloop-0.2.2 → devloop-0.3.0}/pyproject.toml +11 -1
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/linter.py +79 -89
- devloop-0.3.0/src/devloop/agents/sandbox_helper.py +231 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/type_checker.py +24 -14
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/cli/commands/summary.py +27 -1
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/cli/main.py +120 -10
- devloop-0.3.0/src/devloop/cli/pyodide_installer.py +132 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/config.py +146 -103
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/context_store.py +1 -3
- devloop-0.3.0/src/devloop/core/operational_health.py +282 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/summary_formatter.py +13 -1
- devloop-0.3.0/src/devloop/security/__init__.py +15 -0
- devloop-0.3.0/src/devloop/security/audit_logger.py +263 -0
- devloop-0.3.0/src/devloop/security/bubblewrap_sandbox.py +352 -0
- devloop-0.3.0/src/devloop/security/cgroups_helper.py +253 -0
- devloop-0.3.0/src/devloop/security/factory.py +207 -0
- devloop-0.3.0/src/devloop/security/no_sandbox.py +146 -0
- devloop-0.3.0/src/devloop/security/package.json +19 -0
- devloop-0.3.0/src/devloop/security/pyodide_runner.js +290 -0
- devloop-0.3.0/src/devloop/security/pyodide_sandbox.py +271 -0
- devloop-0.3.0/src/devloop/security/sandbox.py +228 -0
- {devloop-0.2.2 → devloop-0.3.0}/LICENSE +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/__init__.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/__init__.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/agent_health_monitor.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/ci_monitor.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/code_rabbit.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/doc_lifecycle.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/echo.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/file_logger.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/formatter.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/git_commit_assistant.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/performance_profiler.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/security_scanner.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/snyk.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/agents/test_runner.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/cli/__init__.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/cli/commands/__init__.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/cli/commands/custom_agents.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/cli/commands/feedback.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/cli/main_v1.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/collectors/__init__.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/collectors/base.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/collectors/filesystem.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/collectors/git.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/collectors/manager.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/collectors/process.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/collectors/system.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/__init__.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/agent.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/agent_template.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/amp_integration.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/auto_fix.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/context.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/contextual_feedback.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/custom_agent.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/debug_trace.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/event.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/event_store.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/feedback.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/learning.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/manager.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/performance.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/proactive_feedback.py +0 -0
- {devloop-0.2.2 → devloop-0.3.0}/src/devloop/core/summary_generator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devloop
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Intelligent background agents for development workflow automation
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -21,6 +21,10 @@ Classifier: Topic :: Software Development :: Build Tools
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
23
|
Classifier: Topic :: Utilities
|
|
24
|
+
Provides-Extra: all-optional
|
|
25
|
+
Provides-Extra: ci-monitor
|
|
26
|
+
Provides-Extra: code-rabbit
|
|
27
|
+
Provides-Extra: snyk
|
|
24
28
|
Requires-Dist: aiofiles (>=23.2,<24.0)
|
|
25
29
|
Requires-Dist: psutil (>=5.9,<6.0)
|
|
26
30
|
Requires-Dist: pydantic (>=2.5,<3.0)
|
|
@@ -104,9 +108,31 @@ All agents run **non-intrusively in the background**, respecting your workflow.
|
|
|
104
108
|
#### Option 1: From PyPI (Recommended)
|
|
105
109
|
|
|
106
110
|
```bash
|
|
111
|
+
# Basic installation (all default agents)
|
|
107
112
|
pip install devloop
|
|
113
|
+
|
|
114
|
+
# With optional agents (Snyk security scanning)
|
|
115
|
+
pip install devloop[snyk]
|
|
116
|
+
|
|
117
|
+
# With multiple optional agents
|
|
118
|
+
pip install devloop[snyk,code-rabbit]
|
|
119
|
+
|
|
120
|
+
# With all optional agents
|
|
121
|
+
pip install devloop[all-optional]
|
|
108
122
|
```
|
|
109
123
|
|
|
124
|
+
**Available extras:**
|
|
125
|
+
- `snyk` — Dependency vulnerability scanning via Snyk CLI
|
|
126
|
+
- `code-rabbit` — AI-powered code analysis
|
|
127
|
+
- `ci-monitor` — CI/CD pipeline monitoring
|
|
128
|
+
- `all-optional` — All of the above
|
|
129
|
+
|
|
130
|
+
**Optional sandbox enhancements:**
|
|
131
|
+
- **Pyodide WASM Sandbox** (cross-platform Python sandboxing)
|
|
132
|
+
- Requires: Node.js 18+ (system dependency)
|
|
133
|
+
- Install: See [Pyodide Installation Guide](./docs/PYODIDE_INSTALLATION.md)
|
|
134
|
+
- Works in POC mode without installation for testing
|
|
135
|
+
|
|
110
136
|
#### Option 2: From Source
|
|
111
137
|
|
|
112
138
|
```bash
|
|
@@ -127,17 +153,24 @@ poetry shell
|
|
|
127
153
|
### Initialize & Run (Fully Automated)
|
|
128
154
|
|
|
129
155
|
```bash
|
|
130
|
-
# 1. Initialize in your project (
|
|
156
|
+
# 1. Initialize in your project (interactive setup)
|
|
131
157
|
devloop init /path/to/your/project
|
|
132
158
|
```
|
|
133
159
|
|
|
134
|
-
The `init` command
|
|
135
|
-
- ✅
|
|
136
|
-
- ✅
|
|
137
|
-
-
|
|
160
|
+
The `init` command will:
|
|
161
|
+
- ✅ Set up .devloop directory with default agents
|
|
162
|
+
- ✅ Ask which optional agents you want to enable:
|
|
163
|
+
- **Snyk** — Scan dependencies for vulnerabilities
|
|
164
|
+
- **Code Rabbit** — AI-powered code analysis
|
|
165
|
+
- **CI Monitor** — Track CI/CD pipeline status
|
|
166
|
+
- ✅ Create configuration file with your selections
|
|
167
|
+
- ✅ Set up git hooks (if git repo)
|
|
138
168
|
- ✅ Registers Amp integration (if in Amp)
|
|
139
|
-
|
|
140
|
-
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# 1a. Alternative: Non-interactive setup (skip optional agent prompts)
|
|
172
|
+
devloop init /path/to/your/project --non-interactive
|
|
173
|
+
```
|
|
141
174
|
|
|
142
175
|
Then just:
|
|
143
176
|
```bash
|
|
@@ -70,9 +70,31 @@ All agents run **non-intrusively in the background**, respecting your workflow.
|
|
|
70
70
|
#### Option 1: From PyPI (Recommended)
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
|
+
# Basic installation (all default agents)
|
|
73
74
|
pip install devloop
|
|
75
|
+
|
|
76
|
+
# With optional agents (Snyk security scanning)
|
|
77
|
+
pip install devloop[snyk]
|
|
78
|
+
|
|
79
|
+
# With multiple optional agents
|
|
80
|
+
pip install devloop[snyk,code-rabbit]
|
|
81
|
+
|
|
82
|
+
# With all optional agents
|
|
83
|
+
pip install devloop[all-optional]
|
|
74
84
|
```
|
|
75
85
|
|
|
86
|
+
**Available extras:**
|
|
87
|
+
- `snyk` — Dependency vulnerability scanning via Snyk CLI
|
|
88
|
+
- `code-rabbit` — AI-powered code analysis
|
|
89
|
+
- `ci-monitor` — CI/CD pipeline monitoring
|
|
90
|
+
- `all-optional` — All of the above
|
|
91
|
+
|
|
92
|
+
**Optional sandbox enhancements:**
|
|
93
|
+
- **Pyodide WASM Sandbox** (cross-platform Python sandboxing)
|
|
94
|
+
- Requires: Node.js 18+ (system dependency)
|
|
95
|
+
- Install: See [Pyodide Installation Guide](./docs/PYODIDE_INSTALLATION.md)
|
|
96
|
+
- Works in POC mode without installation for testing
|
|
97
|
+
|
|
76
98
|
#### Option 2: From Source
|
|
77
99
|
|
|
78
100
|
```bash
|
|
@@ -93,17 +115,24 @@ poetry shell
|
|
|
93
115
|
### Initialize & Run (Fully Automated)
|
|
94
116
|
|
|
95
117
|
```bash
|
|
96
|
-
# 1. Initialize in your project (
|
|
118
|
+
# 1. Initialize in your project (interactive setup)
|
|
97
119
|
devloop init /path/to/your/project
|
|
98
120
|
```
|
|
99
121
|
|
|
100
|
-
The `init` command
|
|
101
|
-
- ✅
|
|
102
|
-
- ✅
|
|
103
|
-
-
|
|
122
|
+
The `init` command will:
|
|
123
|
+
- ✅ Set up .devloop directory with default agents
|
|
124
|
+
- ✅ Ask which optional agents you want to enable:
|
|
125
|
+
- **Snyk** — Scan dependencies for vulnerabilities
|
|
126
|
+
- **Code Rabbit** — AI-powered code analysis
|
|
127
|
+
- **CI Monitor** — Track CI/CD pipeline status
|
|
128
|
+
- ✅ Create configuration file with your selections
|
|
129
|
+
- ✅ Set up git hooks (if git repo)
|
|
104
130
|
- ✅ Registers Amp integration (if in Amp)
|
|
105
|
-
|
|
106
|
-
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# 1a. Alternative: Non-interactive setup (skip optional agent prompts)
|
|
134
|
+
devloop init /path/to/your/project --non-interactive
|
|
135
|
+
```
|
|
107
136
|
|
|
108
137
|
Then just:
|
|
109
138
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "devloop"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Intelligent background agents for development workflow automation"
|
|
5
5
|
authors = ["DevLoop Contributors <devloop@example.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -33,6 +33,10 @@ classifiers = [
|
|
|
33
33
|
"Topic :: Utilities"
|
|
34
34
|
]
|
|
35
35
|
packages = [{include = "devloop", from = "src"}]
|
|
36
|
+
include = [
|
|
37
|
+
"src/devloop/security/package.json",
|
|
38
|
+
"src/devloop/security/pyodide_runner.js",
|
|
39
|
+
]
|
|
36
40
|
|
|
37
41
|
[tool.poetry.dependencies]
|
|
38
42
|
python = "^3.11"
|
|
@@ -53,6 +57,12 @@ bandit = "^1.7"
|
|
|
53
57
|
radon = "^6.0"
|
|
54
58
|
types-aiofiles = "^23.2"
|
|
55
59
|
|
|
60
|
+
[tool.poetry.extras]
|
|
61
|
+
snyk = []
|
|
62
|
+
code-rabbit = []
|
|
63
|
+
ci-monitor = []
|
|
64
|
+
all-optional = []
|
|
65
|
+
|
|
56
66
|
[tool.poetry.scripts]
|
|
57
67
|
devloop = "devloop.cli.main:app"
|
|
58
68
|
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"""Linter agent - runs linters on file changes."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import json
|
|
5
4
|
from datetime import datetime
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import Any, Dict, List, Optional
|
|
8
7
|
|
|
8
|
+
from devloop.agents.sandbox_helper import create_agent_sandbox_helper
|
|
9
9
|
from devloop.core.agent import Agent, AgentResult
|
|
10
10
|
from devloop.core.context_store import Finding, Severity
|
|
11
11
|
from devloop.core.event import Event
|
|
12
|
+
from devloop.security.sandbox import CommandNotAllowedError, SandboxTimeoutError
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class LinterConfig:
|
|
@@ -70,6 +71,11 @@ class LinterAgent(Agent):
|
|
|
70
71
|
)
|
|
71
72
|
self.config = LinterConfig(config or {})
|
|
72
73
|
self._last_run: Dict[str, float] = {} # path -> timestamp for debouncing
|
|
74
|
+
# Initialize sandbox helper for secure command execution
|
|
75
|
+
self.sandbox = create_agent_sandbox_helper(
|
|
76
|
+
agent_name=name,
|
|
77
|
+
agent_type="linter",
|
|
78
|
+
)
|
|
73
79
|
|
|
74
80
|
async def handle(self, event: Event) -> AgentResult:
|
|
75
81
|
"""Handle file change event by running linter."""
|
|
@@ -198,45 +204,32 @@ class LinterAgent(Agent):
|
|
|
198
204
|
async def _run_ruff(self, path: Path) -> LinterResult:
|
|
199
205
|
"""Run ruff on a Python file."""
|
|
200
206
|
try:
|
|
201
|
-
# Get
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
env = os.environ.copy()
|
|
205
|
-
venv_bin = Path(__file__).parent.parent.parent.parent / ".venv" / "bin"
|
|
206
|
-
if venv_bin.exists():
|
|
207
|
-
env["PATH"] = f"{venv_bin}:{env.get('PATH', '')}"
|
|
208
|
-
|
|
209
|
-
# Check if ruff is installed
|
|
210
|
-
check = await asyncio.create_subprocess_exec(
|
|
211
|
-
"ruff",
|
|
212
|
-
"--version",
|
|
213
|
-
stdout=asyncio.subprocess.PIPE,
|
|
214
|
-
stderr=asyncio.subprocess.PIPE,
|
|
215
|
-
env=env,
|
|
216
|
-
)
|
|
217
|
-
await check.communicate()
|
|
218
|
-
|
|
219
|
-
if check.returncode != 0:
|
|
220
|
-
return LinterResult(success=False, error="ruff not installed")
|
|
221
|
-
|
|
222
|
-
# Run ruff with JSON output
|
|
223
|
-
proc = await asyncio.create_subprocess_exec(
|
|
224
|
-
"ruff",
|
|
225
|
-
"check",
|
|
226
|
-
"--output-format",
|
|
227
|
-
"json",
|
|
228
|
-
str(path),
|
|
229
|
-
stdout=asyncio.subprocess.PIPE,
|
|
230
|
-
stderr=asyncio.subprocess.PIPE,
|
|
231
|
-
env=env,
|
|
232
|
-
)
|
|
207
|
+
# Get venv path
|
|
208
|
+
venv_path = Path(__file__).parent.parent.parent.parent / ".venv"
|
|
233
209
|
|
|
234
|
-
|
|
210
|
+
# Check if ruff is available in the sandbox
|
|
211
|
+
if not await self.sandbox.check_tool_available("ruff"):
|
|
212
|
+
return LinterResult(
|
|
213
|
+
success=False, error="ruff not installed or not allowed"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Run ruff with JSON output in sandbox
|
|
217
|
+
if venv_path.exists():
|
|
218
|
+
result = await self.sandbox.run_sandboxed_with_venv(
|
|
219
|
+
["ruff", "check", "--output-format", "json", str(path)],
|
|
220
|
+
venv_path=venv_path,
|
|
221
|
+
cwd=path.parent,
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
result = await self.sandbox.run_sandboxed(
|
|
225
|
+
["ruff", "check", "--output-format", "json", str(path)],
|
|
226
|
+
cwd=path.parent,
|
|
227
|
+
)
|
|
235
228
|
|
|
236
229
|
# ruff returns non-zero if issues found, but that's expected
|
|
237
|
-
if stdout:
|
|
230
|
+
if result.stdout:
|
|
238
231
|
try:
|
|
239
|
-
issues = json.loads(stdout
|
|
232
|
+
issues = json.loads(result.stdout)
|
|
240
233
|
return LinterResult(success=True, issues=issues)
|
|
241
234
|
except json.JSONDecodeError:
|
|
242
235
|
# No issues found or invalid JSON
|
|
@@ -245,39 +238,33 @@ class LinterAgent(Agent):
|
|
|
245
238
|
# No output = no issues
|
|
246
239
|
return LinterResult(success=True, issues=[])
|
|
247
240
|
|
|
248
|
-
except
|
|
249
|
-
|
|
241
|
+
except CommandNotAllowedError as e:
|
|
242
|
+
self.logger.error(f"ruff command not allowed in sandbox: {e}")
|
|
243
|
+
return LinterResult(success=False, error="ruff command not allowed")
|
|
244
|
+
except SandboxTimeoutError:
|
|
245
|
+
return LinterResult(success=False, error="ruff execution timeout")
|
|
246
|
+
except Exception as e:
|
|
247
|
+
self.logger.error(f"Error running ruff in sandbox: {e}")
|
|
248
|
+
return LinterResult(success=False, error=str(e))
|
|
250
249
|
|
|
251
250
|
async def _run_eslint(self, path: Path) -> LinterResult:
|
|
252
251
|
"""Run eslint on a JavaScript/TypeScript file."""
|
|
253
252
|
try:
|
|
254
|
-
# Check if eslint is
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
stderr=asyncio.subprocess.PIPE,
|
|
260
|
-
)
|
|
261
|
-
await check.communicate()
|
|
262
|
-
|
|
263
|
-
if check.returncode != 0:
|
|
264
|
-
return LinterResult(success=False, error="eslint not installed")
|
|
265
|
-
|
|
266
|
-
# Run eslint with JSON output
|
|
267
|
-
proc = await asyncio.create_subprocess_exec(
|
|
268
|
-
"eslint",
|
|
269
|
-
"--format",
|
|
270
|
-
"json",
|
|
271
|
-
str(path),
|
|
272
|
-
stdout=asyncio.subprocess.PIPE,
|
|
273
|
-
stderr=asyncio.subprocess.PIPE,
|
|
274
|
-
)
|
|
253
|
+
# Check if eslint is available in the sandbox
|
|
254
|
+
if not await self.sandbox.check_tool_available("eslint"):
|
|
255
|
+
return LinterResult(
|
|
256
|
+
success=False, error="eslint not installed or not allowed"
|
|
257
|
+
)
|
|
275
258
|
|
|
276
|
-
|
|
259
|
+
# Run eslint with JSON output in sandbox
|
|
260
|
+
result = await self.sandbox.run_sandboxed(
|
|
261
|
+
["eslint", "--format", "json", str(path)],
|
|
262
|
+
cwd=path.parent,
|
|
263
|
+
)
|
|
277
264
|
|
|
278
|
-
if stdout:
|
|
265
|
+
if result.stdout:
|
|
279
266
|
try:
|
|
280
|
-
results = json.loads(stdout
|
|
267
|
+
results = json.loads(result.stdout)
|
|
281
268
|
# ESLint returns array of file results
|
|
282
269
|
if results and len(results) > 0:
|
|
283
270
|
issues = results[0].get("messages", [])
|
|
@@ -287,48 +274,51 @@ class LinterAgent(Agent):
|
|
|
287
274
|
|
|
288
275
|
return LinterResult(success=True, issues=[])
|
|
289
276
|
|
|
290
|
-
except
|
|
291
|
-
|
|
277
|
+
except CommandNotAllowedError as e:
|
|
278
|
+
self.logger.error(f"eslint command not allowed in sandbox: {e}")
|
|
279
|
+
return LinterResult(success=False, error="eslint command not allowed")
|
|
280
|
+
except SandboxTimeoutError:
|
|
281
|
+
return LinterResult(success=False, error="eslint execution timeout")
|
|
282
|
+
except Exception as e:
|
|
283
|
+
self.logger.error(f"Error running eslint in sandbox: {e}")
|
|
284
|
+
return LinterResult(success=False, error=str(e))
|
|
292
285
|
|
|
293
286
|
async def _auto_fix(self, linter: str, path: Path) -> LinterResult:
|
|
294
287
|
"""Attempt to auto-fix issues."""
|
|
295
288
|
try:
|
|
296
|
-
# Get
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
env = os.environ.copy()
|
|
300
|
-
venv_bin = Path(__file__).parent.parent.parent.parent / ".venv" / "bin"
|
|
301
|
-
if venv_bin.exists():
|
|
302
|
-
env["PATH"] = f"{venv_bin}:{env.get('PATH', '')}"
|
|
289
|
+
# Get venv path
|
|
290
|
+
venv_path = Path(__file__).parent.parent.parent.parent / ".venv"
|
|
303
291
|
|
|
304
292
|
if linter == "ruff":
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
293
|
+
if venv_path.exists():
|
|
294
|
+
await self.sandbox.run_sandboxed_with_venv(
|
|
295
|
+
["ruff", "check", "--fix", str(path)],
|
|
296
|
+
venv_path=venv_path,
|
|
297
|
+
cwd=path.parent,
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
await self.sandbox.run_sandboxed(
|
|
301
|
+
["ruff", "check", "--fix", str(path)],
|
|
302
|
+
cwd=path.parent,
|
|
303
|
+
)
|
|
315
304
|
return LinterResult(success=True)
|
|
316
305
|
|
|
317
306
|
elif linter == "eslint":
|
|
318
|
-
|
|
319
|
-
"eslint",
|
|
320
|
-
|
|
321
|
-
str(path),
|
|
322
|
-
stdout=asyncio.subprocess.PIPE,
|
|
323
|
-
stderr=asyncio.subprocess.PIPE,
|
|
324
|
-
env=env,
|
|
307
|
+
await self.sandbox.run_sandboxed(
|
|
308
|
+
["eslint", "--fix", str(path)],
|
|
309
|
+
cwd=path.parent,
|
|
325
310
|
)
|
|
326
|
-
await proc.communicate()
|
|
327
311
|
return LinterResult(success=True)
|
|
328
312
|
|
|
329
313
|
return LinterResult(success=False, error="Auto-fix not supported")
|
|
330
314
|
|
|
315
|
+
except CommandNotAllowedError as e:
|
|
316
|
+
self.logger.error(f"Auto-fix command not allowed in sandbox: {e}")
|
|
317
|
+
return LinterResult(success=False, error="Auto-fix command not allowed")
|
|
318
|
+
except SandboxTimeoutError:
|
|
319
|
+
return LinterResult(success=False, error="Auto-fix execution timeout")
|
|
331
320
|
except Exception as e:
|
|
321
|
+
self.logger.error(f"Error during auto-fix in sandbox: {e}")
|
|
332
322
|
return LinterResult(success=False, error=str(e))
|
|
333
323
|
|
|
334
324
|
async def _write_findings_to_context(
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Helper utilities for agents to use sandbox execution.
|
|
2
|
+
|
|
3
|
+
This module provides a simple API for agents to execute commands in a sandbox
|
|
4
|
+
without needing to understand the details of sandbox configuration and selection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from devloop.security.factory import create_sandbox
|
|
15
|
+
from devloop.security.sandbox import (
|
|
16
|
+
CommandNotAllowedError,
|
|
17
|
+
SandboxConfig,
|
|
18
|
+
SandboxExecutor,
|
|
19
|
+
SandboxResult,
|
|
20
|
+
SandboxTimeoutError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AgentSandboxHelper:
|
|
27
|
+
"""Helper class for agents to use sandbox execution.
|
|
28
|
+
|
|
29
|
+
This provides a simple interface for agents to run commands in a sandbox
|
|
30
|
+
without needing to manage sandbox lifecycle or configuration details.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
agent_name: str,
|
|
36
|
+
agent_type: str,
|
|
37
|
+
config: Optional[SandboxConfig] = None,
|
|
38
|
+
):
|
|
39
|
+
"""Initialize sandbox helper for an agent.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
agent_name: Name of the agent (for logging)
|
|
43
|
+
agent_type: Type of agent (for sandbox selection, e.g., "linter", "formatter")
|
|
44
|
+
config: Optional sandbox configuration (uses defaults if not provided)
|
|
45
|
+
"""
|
|
46
|
+
self.agent_name = agent_name
|
|
47
|
+
self.agent_type = agent_type
|
|
48
|
+
self.config = config or SandboxConfig()
|
|
49
|
+
self._sandbox: Optional[SandboxExecutor] = None
|
|
50
|
+
self._sandbox_initialized = False
|
|
51
|
+
|
|
52
|
+
async def _get_sandbox(self) -> SandboxExecutor:
|
|
53
|
+
"""Get or create sandbox executor.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
SandboxExecutor instance
|
|
57
|
+
"""
|
|
58
|
+
if self._sandbox is None or not self._sandbox_initialized:
|
|
59
|
+
self._sandbox = await create_sandbox(self.config, self.agent_type)
|
|
60
|
+
self._sandbox_initialized = True
|
|
61
|
+
logger.debug(
|
|
62
|
+
f"Initialized sandbox for {self.agent_name} ({self.agent_type})"
|
|
63
|
+
)
|
|
64
|
+
return self._sandbox
|
|
65
|
+
|
|
66
|
+
async def run_sandboxed(
|
|
67
|
+
self,
|
|
68
|
+
cmd: List[str],
|
|
69
|
+
cwd: Optional[Path] = None,
|
|
70
|
+
env: Optional[Dict[str, str]] = None,
|
|
71
|
+
timeout: Optional[int] = None,
|
|
72
|
+
) -> SandboxResult:
|
|
73
|
+
"""Run a command in the sandbox.
|
|
74
|
+
|
|
75
|
+
This is the main method agents should use to execute commands safely.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
cmd: Command and arguments to execute
|
|
79
|
+
cwd: Working directory (defaults to current directory)
|
|
80
|
+
env: Environment variables (will be filtered to allowed list)
|
|
81
|
+
timeout: Optional timeout override (uses config default if not provided)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
SandboxResult with stdout, stderr, exit_code, and metrics
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
CommandNotAllowedError: If command is not in whitelist
|
|
88
|
+
SandboxTimeoutError: If execution exceeds timeout
|
|
89
|
+
RuntimeError: If sandbox execution fails
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
>>> helper = AgentSandboxHelper("linter", "linter")
|
|
93
|
+
>>> result = await helper.run_sandboxed(["ruff", "check", "file.py"])
|
|
94
|
+
>>> if result.exit_code == 0:
|
|
95
|
+
... print(result.stdout)
|
|
96
|
+
"""
|
|
97
|
+
sandbox = await self._get_sandbox()
|
|
98
|
+
|
|
99
|
+
# Use provided cwd or default to current directory
|
|
100
|
+
if cwd is None:
|
|
101
|
+
cwd = Path.cwd()
|
|
102
|
+
|
|
103
|
+
# Merge environment with current environment if provided
|
|
104
|
+
if env is not None:
|
|
105
|
+
merged_env = os.environ.copy()
|
|
106
|
+
merged_env.update(env)
|
|
107
|
+
env = merged_env
|
|
108
|
+
|
|
109
|
+
# Override timeout if provided
|
|
110
|
+
if timeout is not None:
|
|
111
|
+
original_timeout = self.config.timeout_seconds
|
|
112
|
+
self.config.timeout_seconds = timeout
|
|
113
|
+
try:
|
|
114
|
+
result = await sandbox.execute(cmd, cwd, env)
|
|
115
|
+
finally:
|
|
116
|
+
self.config.timeout_seconds = original_timeout
|
|
117
|
+
else:
|
|
118
|
+
result = await sandbox.execute(cmd, cwd, env)
|
|
119
|
+
|
|
120
|
+
logger.debug(
|
|
121
|
+
f"{self.agent_name}: Executed {cmd[0]} in {result.duration_ms}ms "
|
|
122
|
+
f"(exit={result.exit_code})"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
async def check_tool_available(self, tool_name: str) -> bool:
|
|
128
|
+
"""Check if a tool is available and allowed in the sandbox.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
tool_name: Name of the tool to check (e.g., "ruff", "black")
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
True if tool is available and allowed, False otherwise
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
>>> helper = AgentSandboxHelper("linter", "linter")
|
|
138
|
+
>>> if await helper.check_tool_available("ruff"):
|
|
139
|
+
... result = await helper.run_sandboxed(["ruff", "check", "file.py"])
|
|
140
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
result = await self.run_sandboxed(
|
|
143
|
+
[tool_name, "--version"],
|
|
144
|
+
timeout=5, # Quick check
|
|
145
|
+
)
|
|
146
|
+
return result.exit_code == 0
|
|
147
|
+
except (CommandNotAllowedError, RuntimeError):
|
|
148
|
+
return False
|
|
149
|
+
except SandboxTimeoutError:
|
|
150
|
+
# If it times out on --version, something is wrong
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
async def run_sandboxed_with_venv(
|
|
154
|
+
self,
|
|
155
|
+
cmd: List[str],
|
|
156
|
+
venv_path: Path,
|
|
157
|
+
cwd: Optional[Path] = None,
|
|
158
|
+
env: Optional[Dict[str, str]] = None,
|
|
159
|
+
) -> SandboxResult:
|
|
160
|
+
"""Run a command with a Python virtual environment in PATH.
|
|
161
|
+
|
|
162
|
+
This is a convenience method for agents that need to run Python tools
|
|
163
|
+
installed in a virtual environment.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
cmd: Command and arguments to execute
|
|
167
|
+
venv_path: Path to virtual environment root
|
|
168
|
+
cwd: Working directory
|
|
169
|
+
env: Additional environment variables
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
SandboxResult from execution
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
>>> helper = AgentSandboxHelper("linter", "linter")
|
|
176
|
+
>>> venv = Path(__file__).parent.parent.parent.parent / ".venv"
|
|
177
|
+
>>> result = await helper.run_sandboxed_with_venv(
|
|
178
|
+
... ["ruff", "check", "file.py"],
|
|
179
|
+
... venv_path=venv
|
|
180
|
+
... )
|
|
181
|
+
"""
|
|
182
|
+
# Add venv bin to PATH
|
|
183
|
+
venv_bin = venv_path / "bin"
|
|
184
|
+
if not venv_bin.exists():
|
|
185
|
+
raise ValueError(f"Virtual environment bin not found: {venv_bin}")
|
|
186
|
+
|
|
187
|
+
# Prepare environment with venv in PATH
|
|
188
|
+
env = env or {}
|
|
189
|
+
env["PATH"] = f"{venv_bin}:{os.environ.get('PATH', '')}"
|
|
190
|
+
|
|
191
|
+
return await self.run_sandboxed(cmd, cwd, env)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def create_agent_sandbox_helper(
|
|
195
|
+
agent_name: str,
|
|
196
|
+
agent_type: str,
|
|
197
|
+
config: Optional[Dict] = None,
|
|
198
|
+
) -> AgentSandboxHelper:
|
|
199
|
+
"""Factory function to create a sandbox helper for an agent.
|
|
200
|
+
|
|
201
|
+
This is the recommended way for agents to create their sandbox helper.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
agent_name: Name of the agent
|
|
205
|
+
agent_type: Type identifier (e.g., "linter", "formatter", "type_checker")
|
|
206
|
+
config: Optional configuration dict (will be converted to SandboxConfig)
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
AgentSandboxHelper instance ready to use
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
>>> # In an agent's __init__:
|
|
213
|
+
>>> from devloop.agents.sandbox_helper import create_agent_sandbox_helper
|
|
214
|
+
>>> self.sandbox = create_agent_sandbox_helper(
|
|
215
|
+
... agent_name=self.name,
|
|
216
|
+
... agent_type="linter"
|
|
217
|
+
... )
|
|
218
|
+
>>>
|
|
219
|
+
>>> # In the agent's handle method:
|
|
220
|
+
>>> result = await self.sandbox.run_sandboxed(["ruff", "check", str(path)])
|
|
221
|
+
"""
|
|
222
|
+
sandbox_config = None
|
|
223
|
+
if config:
|
|
224
|
+
# Convert config dict to SandboxConfig if provided
|
|
225
|
+
sandbox_config = SandboxConfig(**config)
|
|
226
|
+
|
|
227
|
+
return AgentSandboxHelper(
|
|
228
|
+
agent_name=agent_name,
|
|
229
|
+
agent_type=agent_type,
|
|
230
|
+
config=sandbox_config,
|
|
231
|
+
)
|