codeshield-runtime 0.1.0__tar.gz → 0.1.2__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.
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/PKG-INFO +8 -6
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/README.md +7 -5
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/pyproject.toml +1 -1
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/src/codeshield/environment.py +5 -0
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/src/codeshield/loop.py +22 -1
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/src/codeshield/tools.py +8 -2
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/tests/test_engine.py +48 -1
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/.gitignore +0 -0
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/LICENSE +0 -0
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/src/codeshield/__init__.py +0 -0
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/src/codeshield/__main__.py +0 -0
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/src/codeshield/analyzer.py +0 -0
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/src/codeshield/classifier.py +0 -0
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/src/codeshield/cli.py +0 -0
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/src/codeshield/runner.py +0 -0
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/src/codeshield/schemas.py +0 -0
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/tests/__init__.py +0 -0
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/tests/test_cli.py +0 -0
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/tests/test_coverage.py +0 -0
- {codeshield_runtime-0.1.0 → codeshield_runtime-0.1.2}/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codeshield-runtime
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A secure, isolated, and self-healing Python code execution engine powered by uv and AST analysis.
|
|
5
5
|
Project-URL: Homepage, https://github.com/AlgorithmicMind/codeshield
|
|
6
6
|
Project-URL: Repository, https://github.com/AlgorithmicMind/codeshield
|
|
@@ -158,10 +158,10 @@ CodeShield is not locked into a single LLM. Pass any Python callable as the `pat
|
|
|
158
158
|
from codeshield.loop import SelfHealingEngine
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
def
|
|
162
|
-
#
|
|
161
|
+
def custom_llm_patcher(code: str, diagnosis) -> str:
|
|
162
|
+
# Compatible with any frontier provider: GPT-5.4, Claude Sonnet 5, DeepSeek V4, Ollama
|
|
163
163
|
response = client.chat.completions.create(
|
|
164
|
-
model="gpt-
|
|
164
|
+
model="gpt-5.4-mini", # or "claude-sonnet-5", "deepseek-v4-flash"
|
|
165
165
|
messages=[
|
|
166
166
|
{
|
|
167
167
|
"role": "user",
|
|
@@ -172,7 +172,7 @@ def custom_openai_patcher(code: str, diagnosis) -> str:
|
|
|
172
172
|
return response.choices[0].message.content
|
|
173
173
|
|
|
174
174
|
|
|
175
|
-
engine = SelfHealingEngine(patch_generator=
|
|
175
|
+
engine = SelfHealingEngine(patch_generator=custom_llm_patcher)
|
|
176
176
|
```
|
|
177
177
|
|
|
178
178
|
### Zero-Config Self-Healing with Gemini Flash
|
|
@@ -234,17 +234,19 @@ The `examples/` folder contains ready-to-run recipes that have been executed and
|
|
|
234
234
|
- `02_security_gatekeeper.py`: AST rejection of unsafe code.
|
|
235
235
|
- `03_llm_healing_workflow.py`: self-healing workflow with an LLM or local fallback.
|
|
236
236
|
- `04_agent_tool_dropin.py`: end-to-end agentic tool-calling workflow with dynamic code generation.
|
|
237
|
+
- `05_custom_llm_openai_compatible.py`: model-agnostic, API-key-free self-healing with a custom `patch_generator`.
|
|
237
238
|
|
|
238
239
|
```bash
|
|
239
240
|
python examples/01_basic_sandboxing.py
|
|
240
241
|
python examples/02_security_gatekeeper.py
|
|
241
242
|
python examples/03_llm_healing_workflow.py
|
|
242
243
|
python examples/04_agent_tool_dropin.py
|
|
244
|
+
python examples/05_custom_llm_openai_compatible.py
|
|
243
245
|
```
|
|
244
246
|
|
|
245
247
|
## Running Tests & Lint
|
|
246
248
|
|
|
247
|
-
The suite currently has **
|
|
249
|
+
The suite currently has **53 tests** with **>82% code coverage** on `src/codeshield`.
|
|
248
250
|
|
|
249
251
|
```bash
|
|
250
252
|
ruff check src tests examples
|
|
@@ -122,10 +122,10 @@ CodeShield is not locked into a single LLM. Pass any Python callable as the `pat
|
|
|
122
122
|
from codeshield.loop import SelfHealingEngine
|
|
123
123
|
|
|
124
124
|
|
|
125
|
-
def
|
|
126
|
-
#
|
|
125
|
+
def custom_llm_patcher(code: str, diagnosis) -> str:
|
|
126
|
+
# Compatible with any frontier provider: GPT-5.4, Claude Sonnet 5, DeepSeek V4, Ollama
|
|
127
127
|
response = client.chat.completions.create(
|
|
128
|
-
model="gpt-
|
|
128
|
+
model="gpt-5.4-mini", # or "claude-sonnet-5", "deepseek-v4-flash"
|
|
129
129
|
messages=[
|
|
130
130
|
{
|
|
131
131
|
"role": "user",
|
|
@@ -136,7 +136,7 @@ def custom_openai_patcher(code: str, diagnosis) -> str:
|
|
|
136
136
|
return response.choices[0].message.content
|
|
137
137
|
|
|
138
138
|
|
|
139
|
-
engine = SelfHealingEngine(patch_generator=
|
|
139
|
+
engine = SelfHealingEngine(patch_generator=custom_llm_patcher)
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
### Zero-Config Self-Healing with Gemini Flash
|
|
@@ -198,17 +198,19 @@ The `examples/` folder contains ready-to-run recipes that have been executed and
|
|
|
198
198
|
- `02_security_gatekeeper.py`: AST rejection of unsafe code.
|
|
199
199
|
- `03_llm_healing_workflow.py`: self-healing workflow with an LLM or local fallback.
|
|
200
200
|
- `04_agent_tool_dropin.py`: end-to-end agentic tool-calling workflow with dynamic code generation.
|
|
201
|
+
- `05_custom_llm_openai_compatible.py`: model-agnostic, API-key-free self-healing with a custom `patch_generator`.
|
|
201
202
|
|
|
202
203
|
```bash
|
|
203
204
|
python examples/01_basic_sandboxing.py
|
|
204
205
|
python examples/02_security_gatekeeper.py
|
|
205
206
|
python examples/03_llm_healing_workflow.py
|
|
206
207
|
python examples/04_agent_tool_dropin.py
|
|
208
|
+
python examples/05_custom_llm_openai_compatible.py
|
|
207
209
|
```
|
|
208
210
|
|
|
209
211
|
## Running Tests & Lint
|
|
210
212
|
|
|
211
|
-
The suite currently has **
|
|
213
|
+
The suite currently has **53 tests** with **>82% code coverage** on `src/codeshield`.
|
|
212
214
|
|
|
213
215
|
```bash
|
|
214
216
|
ruff check src tests examples
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codeshield-runtime"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "A secure, isolated, and self-healing Python code execution engine powered by uv and AST analysis."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -63,6 +63,11 @@ class SandboxManager:
|
|
|
63
63
|
"""Return the root sandbox workspace directory."""
|
|
64
64
|
return self._workspace
|
|
65
65
|
|
|
66
|
+
@property
|
|
67
|
+
def workspace_path(self) -> Path:
|
|
68
|
+
"""Alias for the root sandbox workspace directory."""
|
|
69
|
+
return self._workspace
|
|
70
|
+
|
|
66
71
|
@property
|
|
67
72
|
def venv_path(self) -> Path:
|
|
68
73
|
"""Return the virtual environment directory."""
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# ruff: noqa: UP045
|
|
1
2
|
"""Self-healing execution loop."""
|
|
2
3
|
|
|
3
4
|
from __future__ import annotations
|
|
@@ -7,6 +8,7 @@ import os
|
|
|
7
8
|
import re
|
|
8
9
|
from collections.abc import Callable
|
|
9
10
|
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
10
12
|
|
|
11
13
|
from codeshield.analyzer import validate_syntax_and_safety
|
|
12
14
|
from codeshield.classifier import TracebackClassifier
|
|
@@ -173,7 +175,7 @@ class SelfHealingEngine:
|
|
|
173
175
|
sandbox: SandboxManager | None = None,
|
|
174
176
|
runner: SubprocessRunner | None = None,
|
|
175
177
|
classifier: TracebackClassifier | None = None,
|
|
176
|
-
patch_generator: Callable[[str, ErrorDiagnosis], str
|
|
178
|
+
patch_generator: Optional[Callable[[str, ErrorDiagnosis], Optional[str]]] = None,
|
|
177
179
|
gemini_api_key: str | None = None,
|
|
178
180
|
gemini_model: str | None = None,
|
|
179
181
|
use_llm: bool = True,
|
|
@@ -224,6 +226,9 @@ class SelfHealingEngine:
|
|
|
224
226
|
) -> tuple[ExecutionResult, ErrorDiagnosis | None]:
|
|
225
227
|
"""Run code through the AST -> sandbox -> heal loop.
|
|
226
228
|
|
|
229
|
+
The sandbox is created automatically if ``run`` is called outside a
|
|
230
|
+
``with`` context manager and is cleaned up once execution finishes.
|
|
231
|
+
|
|
227
232
|
Args:
|
|
228
233
|
request: Either a ``CodeExecutionRequest`` or a raw source string.
|
|
229
234
|
timeout: Optional execution timeout override.
|
|
@@ -238,6 +243,22 @@ class SelfHealingEngine:
|
|
|
238
243
|
if isinstance(request, str):
|
|
239
244
|
request = CodeExecutionRequest(code=request)
|
|
240
245
|
|
|
246
|
+
auto_created = not self._sandbox.python_executable.exists()
|
|
247
|
+
if auto_created:
|
|
248
|
+
self._sandbox.create()
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
return self._run_loop(request, timeout)
|
|
252
|
+
finally:
|
|
253
|
+
if auto_created:
|
|
254
|
+
self._sandbox.cleanup()
|
|
255
|
+
|
|
256
|
+
def _run_loop(
|
|
257
|
+
self,
|
|
258
|
+
request: CodeExecutionRequest,
|
|
259
|
+
timeout: float | None,
|
|
260
|
+
) -> tuple[ExecutionResult, ErrorDiagnosis | None]:
|
|
261
|
+
"""Execute the AST -> sandbox -> heal loop assuming the sandbox exists."""
|
|
241
262
|
diagnosis: ErrorDiagnosis | None = None
|
|
242
263
|
for attempt in range(1, self._max_iterations + 1):
|
|
243
264
|
logger.info("Self-healing iteration %d/%d", attempt, self._max_iterations)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# ruff: noqa: UP045
|
|
1
2
|
"""Universal agent tool wrapper for CodeShield.
|
|
2
3
|
|
|
3
4
|
The function returned by ``create_code_execution_tool`` can be registered as a
|
|
@@ -9,20 +10,25 @@ stdout or a structured error report.
|
|
|
9
10
|
from __future__ import annotations
|
|
10
11
|
|
|
11
12
|
from collections.abc import Callable
|
|
13
|
+
from typing import Optional
|
|
12
14
|
|
|
13
15
|
from codeshield.environment import SandboxError
|
|
14
16
|
from codeshield.loop import SelfHealingEngine, SelfHealingError
|
|
15
17
|
from codeshield.runner import SubprocessRunnerError
|
|
18
|
+
from codeshield.schemas import ErrorDiagnosis
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
def create_code_execution_tool(
|
|
19
|
-
engine: SelfHealingEngine
|
|
22
|
+
engine: Optional[SelfHealingEngine] = None,
|
|
23
|
+
patch_generator: Optional[Callable[[str, ErrorDiagnosis], Optional[str]]] = None,
|
|
20
24
|
) -> Callable[[str], str]:
|
|
21
25
|
"""Return a drop-in ``execute_python_code(code: str) -> str`` tool.
|
|
22
26
|
|
|
23
27
|
Args:
|
|
24
28
|
engine: Optional ``SelfHealingEngine`` instance. When ``None``, a fresh
|
|
25
29
|
engine is created for each tool call.
|
|
30
|
+
patch_generator: Optional custom patcher ``(code, diagnosis) -> patched``
|
|
31
|
+
forwarded to ``SelfHealingEngine`` when ``engine`` is not provided.
|
|
26
32
|
|
|
27
33
|
Returns:
|
|
28
34
|
A callable ready to be registered as an agent tool.
|
|
@@ -41,7 +47,7 @@ def create_code_execution_tool(
|
|
|
41
47
|
The stdout of the script if execution succeeds, or a structured
|
|
42
48
|
error report if it fails after all self-healing attempts.
|
|
43
49
|
"""
|
|
44
|
-
_engine = engine or SelfHealingEngine()
|
|
50
|
+
_engine = engine or SelfHealingEngine(patch_generator=patch_generator)
|
|
45
51
|
with _engine:
|
|
46
52
|
try:
|
|
47
53
|
result, diagnosis = _engine.run(code)
|
|
@@ -7,7 +7,7 @@ import pytest
|
|
|
7
7
|
from codeshield.analyzer import validate_syntax_and_safety
|
|
8
8
|
from codeshield.classifier import TracebackClassifier
|
|
9
9
|
from codeshield.environment import SandboxManager
|
|
10
|
-
from codeshield.loop import SelfHealingEngine
|
|
10
|
+
from codeshield.loop import SelfHealingEngine, SelfHealingError
|
|
11
11
|
from codeshield.runner import SubprocessRunner
|
|
12
12
|
from codeshield.schemas import (
|
|
13
13
|
ExecutionResult,
|
|
@@ -232,3 +232,50 @@ def test_self_healing_engine_fixes_name_error() -> None:
|
|
|
232
232
|
assert result.exit_code == 0
|
|
233
233
|
assert result.stdout.strip() == "4.0"
|
|
234
234
|
assert diagnosis is None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_self_healing_engine_run_without_context_manager() -> None:
|
|
238
|
+
"""The engine can be used without a ``with`` block and still auto-manages the sandbox."""
|
|
239
|
+
engine = SelfHealingEngine(use_llm=False)
|
|
240
|
+
|
|
241
|
+
result, diagnosis = engine.run("print('no context manager')")
|
|
242
|
+
|
|
243
|
+
assert result.exit_code == 0
|
|
244
|
+
assert result.stdout.strip() == "no context manager"
|
|
245
|
+
assert diagnosis is None
|
|
246
|
+
assert not engine._sandbox.workspace_path.exists()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_custom_patch_generator_success() -> None:
|
|
250
|
+
"""A custom patch_generator can repair code that the local fallback cannot."""
|
|
251
|
+
|
|
252
|
+
def custom_patcher(code: str, diagnosis) -> str | None:
|
|
253
|
+
if diagnosis.error_type == "TypeError":
|
|
254
|
+
return code.replace('"Result: " + 42', '"Result: " + str(42)')
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
engine = SelfHealingEngine(
|
|
258
|
+
patch_generator=custom_patcher,
|
|
259
|
+
use_llm=False,
|
|
260
|
+
)
|
|
261
|
+
with engine:
|
|
262
|
+
result, diagnosis = engine.run('print("Result: " + 42)')
|
|
263
|
+
|
|
264
|
+
assert result.exit_code == 0
|
|
265
|
+
assert result.stdout.strip() == "Result: 42"
|
|
266
|
+
assert diagnosis is None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_custom_patch_generator_ast_rejection() -> None:
|
|
270
|
+
"""A custom patch that violates the AST security gate is rejected."""
|
|
271
|
+
|
|
272
|
+
def malicious_patcher(code: str, diagnosis) -> str | None:
|
|
273
|
+
return 'import os\nos.system("echo pwned")'
|
|
274
|
+
|
|
275
|
+
engine = SelfHealingEngine(
|
|
276
|
+
patch_generator=malicious_patcher,
|
|
277
|
+
use_llm=False,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
with pytest.raises(SelfHealingError):
|
|
281
|
+
engine.run("print(undefined_value)")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|