auto-code-fixer 0.3.4__tar.gz → 0.3.5__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.
Files changed (29) hide show
  1. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/PKG-INFO +16 -1
  2. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/README.md +15 -0
  3. auto_code_fixer-0.3.5/auto_code_fixer/__init__.py +1 -0
  4. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer/cli.py +143 -15
  5. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer/fixer.py +64 -0
  6. auto_code_fixer-0.3.5/auto_code_fixer/patch_protocol.py +102 -0
  7. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer.egg-info/PKG-INFO +16 -1
  8. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer.egg-info/SOURCES.txt +3 -1
  9. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/pyproject.toml +1 -1
  10. auto_code_fixer-0.3.5/tests/test_patch_protocol.py +46 -0
  11. auto_code_fixer-0.3.4/auto_code_fixer/__init__.py +0 -1
  12. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/LICENSE +0 -0
  13. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer/command_runner.py +0 -0
  14. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer/installer.py +0 -0
  15. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer/models.py +0 -0
  16. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer/patcher.py +0 -0
  17. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer/plan.py +0 -0
  18. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer/runner.py +0 -0
  19. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer/sandbox.py +0 -0
  20. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer/traceback_utils.py +0 -0
  21. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer/utils.py +0 -0
  22. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer/venv_manager.py +0 -0
  23. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer.egg-info/dependency_links.txt +0 -0
  24. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer.egg-info/entry_points.txt +0 -0
  25. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer.egg-info/requires.txt +0 -0
  26. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/auto_code_fixer.egg-info/top_level.txt +0 -0
  27. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/setup.cfg +0 -0
  28. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/tests/test_fix_imported_file.py +0 -0
  29. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.5}/tests/test_internal_imports.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: auto-code-fixer
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: Automatically fix Python code using ChatGPT
5
5
  Author-email: Arif Shah <ashah7775@gmail.com>
6
6
  License: MIT
@@ -121,6 +121,21 @@ auto-code-fixer main.py --ai-plan
121
121
  ```
122
122
  This enables a helper that can suggest which local file to edit. It is best-effort.
123
123
 
124
+ ### Optional structured patch protocol (JSON + sha256)
125
+ ```bash
126
+ auto-code-fixer main.py --patch-protocol
127
+ ```
128
+ When enabled, the model is asked to return strict JSON with `{files:[{path,new_content,sha256}]}`.
129
+ The tool verifies the SHA-256 hash of `new_content` before applying edits, and falls back to the
130
+ legacy full-text mode if parsing/validation fails.
131
+
132
+ ### Optional formatting / linting (best-effort)
133
+ ```bash
134
+ auto-code-fixer main.py --format black
135
+ auto-code-fixer main.py --lint ruff --fix
136
+ ```
137
+ These run inside the sandbox venv and are skipped if the tools are not installed.
138
+
124
139
  ---
125
140
 
126
141
  ## Environment variables
@@ -100,6 +100,21 @@ auto-code-fixer main.py --ai-plan
100
100
  ```
101
101
  This enables a helper that can suggest which local file to edit. It is best-effort.
102
102
 
103
+ ### Optional structured patch protocol (JSON + sha256)
104
+ ```bash
105
+ auto-code-fixer main.py --patch-protocol
106
+ ```
107
+ When enabled, the model is asked to return strict JSON with `{files:[{path,new_content,sha256}]}`.
108
+ The tool verifies the SHA-256 hash of `new_content` before applying edits, and falls back to the
109
+ legacy full-text mode if parsing/validation fails.
110
+
111
+ ### Optional formatting / linting (best-effort)
112
+ ```bash
113
+ auto-code-fixer main.py --format black
114
+ auto-code-fixer main.py --lint ruff --fix
115
+ ```
116
+ These run inside the sandbox venv and are skipped if the tools are not installed.
117
+
103
118
  ---
104
119
 
105
120
  ## Environment variables
@@ -0,0 +1 @@
1
+ __version__ = "0.3.5"
@@ -19,7 +19,23 @@ from auto_code_fixer import __version__
19
19
  DEFAULT_MAX_RETRIES = 8 # can be overridden via CLI
20
20
 
21
21
 
22
- def fix_file(file_path, project_root, api_key, ask, verbose, *, dry_run: bool, model: str | None, timeout_s: int, max_retries: int, run_cmd: str | None) -> bool:
22
+ def fix_file(
23
+ file_path,
24
+ project_root,
25
+ api_key,
26
+ ask,
27
+ verbose,
28
+ *,
29
+ dry_run: bool,
30
+ model: str | None,
31
+ timeout_s: int,
32
+ max_retries: int,
33
+ run_cmd: str | None,
34
+ patch_protocol: bool,
35
+ fmt: str | None,
36
+ lint: str | None,
37
+ lint_fix: bool,
38
+ ) -> bool:
23
39
  log(f"Processing entry file: {file_path}")
24
40
 
25
41
  project_root = os.path.abspath(project_root)
@@ -48,6 +64,46 @@ def fix_file(file_path, project_root, api_key, ask, verbose, *, dry_run: bool, m
48
64
 
49
65
  venv_python = create_venv(sandbox_root)
50
66
 
67
+ def _run_optional_formatters_and_linters() -> None:
68
+ # Best-effort formatting/linting (only if tools are installed in the sandbox venv).
69
+ from auto_code_fixer.command_runner import run_command
70
+
71
+ if fmt == "black":
72
+ rc, out, err = run_command(
73
+ "python -m black .",
74
+ timeout_s=max(timeout_s, 60),
75
+ python_exe=venv_python,
76
+ cwd=sandbox_root,
77
+ extra_env={"PYTHONPATH": sandbox_root},
78
+ )
79
+ if rc == 0:
80
+ log("Formatted with black", "DEBUG")
81
+ else:
82
+ # If black isn't installed, ignore.
83
+ if "No module named" in (err or "") and "black" in (err or ""):
84
+ log("black not installed in sandbox venv; skipping format", "DEBUG")
85
+ else:
86
+ log(f"black failed (rc={rc}): {err}", "DEBUG")
87
+
88
+ if lint == "ruff":
89
+ cmd = "python -m ruff check ."
90
+ if lint_fix:
91
+ cmd += " --fix"
92
+ rc, out, err = run_command(
93
+ cmd,
94
+ timeout_s=max(timeout_s, 60),
95
+ python_exe=venv_python,
96
+ cwd=sandbox_root,
97
+ extra_env={"PYTHONPATH": sandbox_root},
98
+ )
99
+ if rc == 0:
100
+ log("ruff check passed", "DEBUG")
101
+ else:
102
+ if "No module named" in (err or "") and "ruff" in (err or ""):
103
+ log("ruff not installed in sandbox venv; skipping lint", "DEBUG")
104
+ else:
105
+ log(f"ruff reported issues (rc={rc}): {err}", "DEBUG")
106
+
51
107
  changed_sandbox_files: set[str] = set()
52
108
 
53
109
  for attempt in range(max_retries):
@@ -187,24 +243,65 @@ def fix_file(file_path, project_root, api_key, ask, verbose, *, dry_run: bool, m
187
243
 
188
244
  log(f"Sending {os.path.relpath(target_file, sandbox_root)} + error to GPT 🧠", "DEBUG")
189
245
 
190
- fixed_code = fix_code_with_gpt(
191
- original_code=open(target_file, encoding="utf-8").read(),
192
- error_log=stderr,
193
- api_key=api_key,
194
- model=model,
195
- )
246
+ applied_any = False
247
+
248
+ if patch_protocol:
249
+ try:
250
+ from auto_code_fixer.fixer import fix_code_with_gpt_patch_protocol
251
+ from auto_code_fixer.patch_protocol import (
252
+ parse_patch_protocol_response,
253
+ validate_and_resolve_patch_files,
254
+ )
255
+ from auto_code_fixer.patcher import safe_read, safe_write
256
+
257
+ hint_paths = [os.path.relpath(target_file, sandbox_root)]
258
+ raw = fix_code_with_gpt_patch_protocol(
259
+ sandbox_root=sandbox_root,
260
+ error_log=stderr,
261
+ api_key=api_key,
262
+ model=model,
263
+ hint_paths=hint_paths,
264
+ )
265
+
266
+ patch_files = parse_patch_protocol_response(raw)
267
+ resolved = validate_and_resolve_patch_files(patch_files, sandbox_root=sandbox_root)
268
+
269
+ # Apply patches (only if they change content)
270
+ for abs_path, new_content in resolved:
271
+ old = ""
272
+ if os.path.exists(abs_path):
273
+ old = safe_read(abs_path)
274
+ if new_content.strip() == (old or "").strip():
275
+ continue
276
+ safe_write(abs_path, new_content)
277
+ changed_sandbox_files.add(os.path.abspath(abs_path))
278
+ applied_any = True
279
+
280
+ except Exception as e:
281
+ log(f"Patch protocol failed ({e}); falling back to full-text mode", "WARN")
282
+
283
+ if not applied_any:
284
+ fixed_code = fix_code_with_gpt(
285
+ original_code=open(target_file, encoding="utf-8").read(),
286
+ error_log=stderr,
287
+ api_key=api_key,
288
+ model=model,
289
+ )
196
290
 
197
- if fixed_code.strip() == open(target_file, encoding="utf-8").read().strip():
198
- log("GPT returned no changes. Stopping.", "WARN")
199
- break
291
+ if fixed_code.strip() == open(target_file, encoding="utf-8").read().strip():
292
+ log("GPT returned no changes. Stopping.", "WARN")
293
+ break
200
294
 
201
- with open(target_file, "w", encoding="utf-8") as f:
202
- f.write(fixed_code)
295
+ with open(target_file, "w", encoding="utf-8") as f:
296
+ f.write(fixed_code)
203
297
 
204
- changed_sandbox_files.add(os.path.abspath(target_file))
298
+ changed_sandbox_files.add(os.path.abspath(target_file))
299
+ applied_any = True
205
300
 
206
- log("Code updated by GPT ✏️")
207
- time.sleep(1)
301
+ if applied_any:
302
+ _run_optional_formatters_and_linters()
303
+ log("Code updated by GPT ✏️")
304
+ time.sleep(1)
208
305
 
209
306
  log("Failed to auto-fix file after max retries ❌", "ERROR")
210
307
  cleanup_sandbox()
@@ -248,6 +345,33 @@ def main():
248
345
  help="Optional: use AI to suggest which file to edit (AUTO_CODE_FIXER_AI_PLAN=1)",
249
346
  )
250
347
 
348
+ parser.add_argument(
349
+ "--patch-protocol",
350
+ action="store_true",
351
+ help=(
352
+ "Optional: ask the model for a strict JSON patch protocol with sha256 verification "
353
+ "(falls back to full-text mode if parsing/validation fails)"
354
+ ),
355
+ )
356
+
357
+ parser.add_argument(
358
+ "--format",
359
+ default=None,
360
+ choices=["black"],
361
+ help="Optional: run formatter in sandbox (best-effort). Currently supported: black",
362
+ )
363
+ parser.add_argument(
364
+ "--lint",
365
+ default=None,
366
+ choices=["ruff"],
367
+ help="Optional: run linter in sandbox (best-effort). Currently supported: ruff",
368
+ )
369
+ parser.add_argument(
370
+ "--fix",
371
+ action="store_true",
372
+ help="If used with --lint ruff, apply fixes (ruff --fix) (best-effort)",
373
+ )
374
+
251
375
  # ✅ Proper boolean flags
252
376
  ask_group = parser.add_mutually_exclusive_group()
253
377
  ask_group.add_argument(
@@ -302,6 +426,10 @@ def main():
302
426
  timeout_s=args.timeout,
303
427
  max_retries=args.max_retries,
304
428
  run_cmd=args.run,
429
+ patch_protocol=args.patch_protocol,
430
+ fmt=args.format,
431
+ lint=args.lint,
432
+ lint_fix=args.fix,
305
433
  )
306
434
 
307
435
  raise SystemExit(0 if ok else 2)
@@ -77,3 +77,67 @@ def fix_code_with_gpt(
77
77
  text += getattr(c, "text", "") or ""
78
78
 
79
79
  return _strip_code_fences(text)
80
+
81
+
82
+ def fix_code_with_gpt_patch_protocol(
83
+ *,
84
+ sandbox_root: str,
85
+ error_log: str,
86
+ api_key: str | None = None,
87
+ model: str | None = None,
88
+ hint_paths: list[str] | None = None,
89
+ ) -> str:
90
+ """Ask the model for structured edits (patch protocol).
91
+
92
+ Returns raw model text (expected JSON). Parsing/validation happens elsewhere.
93
+
94
+ The protocol is optional and should be enabled explicitly by the caller.
95
+ """
96
+
97
+ client = get_openai_client(api_key)
98
+ model = model or os.getenv("AUTO_CODE_FIXER_MODEL") or "gpt-4.1-mini"
99
+
100
+ schema = {
101
+ "files": [
102
+ {
103
+ "path": "relative/path/from/sandbox_root.py",
104
+ "new_content": "FULL new file contents",
105
+ "sha256": "sha256 hex of new_content encoded as utf-8",
106
+ }
107
+ ]
108
+ }
109
+
110
+ hints = ""
111
+ if hint_paths:
112
+ hints = "\n\nCANDIDATE FILES (relative paths; edit one or more if needed):\n" + "\n".join(
113
+ f"- {p}" for p in hint_paths
114
+ )
115
+
116
+ prompt = (
117
+ "You are a senior Python engineer. Fix the project so it runs without errors.\n"
118
+ "Return ONLY valid JSON that matches this schema (no markdown, no commentary):\n"
119
+ + json.dumps(schema)
120
+ + hints
121
+ + "\n\nSANDBOX ROOT:\n"
122
+ + sandbox_root
123
+ + "\n\nERROR LOG:\n"
124
+ + error_log
125
+ )
126
+
127
+ resp = client.responses.create(
128
+ model=model,
129
+ input=[
130
+ {"role": "system", "content": "You output strict JSON patches for code fixes."},
131
+ {"role": "user", "content": prompt},
132
+ ],
133
+ temperature=0.2,
134
+ max_output_tokens=3000,
135
+ )
136
+
137
+ text = ""
138
+ for item in resp.output or []:
139
+ for c in item.content or []:
140
+ if getattr(c, "type", None) in ("output_text", "text"):
141
+ text += getattr(c, "text", "") or ""
142
+
143
+ return _strip_code_fences(text)
@@ -0,0 +1,102 @@
1
+ import hashlib
2
+ import json
3
+ import os
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class PatchFile:
9
+ """A single file edit from the structured patch protocol."""
10
+
11
+ path: str
12
+ new_content: str
13
+ sha256: str
14
+
15
+
16
+ def strip_code_fences(text: str) -> str:
17
+ text = (text or "").strip()
18
+ if text.startswith("```"):
19
+ text = "\n".join(text.split("\n")[1:])
20
+ if text.endswith("```"):
21
+ text = "\n".join(text.split("\n")[:-1])
22
+ return text.strip()
23
+
24
+
25
+ def compute_sha256_utf8(content: str) -> str:
26
+ return hashlib.sha256((content or "").encode("utf-8")).hexdigest()
27
+
28
+
29
+ def parse_patch_protocol_response(text: str) -> list[PatchFile]:
30
+ """Parse model output for the patch protocol.
31
+
32
+ Expected JSON schema:
33
+ {"files": [{"path": "...", "new_content": "...", "sha256": "..."}, ...]}
34
+
35
+ Raises ValueError on invalid input.
36
+ """
37
+
38
+ cleaned = strip_code_fences(text)
39
+
40
+ try:
41
+ payload = json.loads(cleaned)
42
+ except Exception as e: # pragma: no cover
43
+ raise ValueError(f"Invalid JSON: {e}")
44
+
45
+ if not isinstance(payload, dict):
46
+ raise ValueError("Patch protocol JSON must be an object")
47
+
48
+ files = payload.get("files")
49
+ if not isinstance(files, list) or not files:
50
+ raise ValueError("Patch protocol JSON must contain non-empty 'files' list")
51
+
52
+ out: list[PatchFile] = []
53
+ for i, f in enumerate(files):
54
+ if not isinstance(f, dict):
55
+ raise ValueError(f"files[{i}] must be an object")
56
+
57
+ path = f.get("path")
58
+ new_content = f.get("new_content")
59
+ sha256 = f.get("sha256")
60
+
61
+ if not isinstance(path, str) or not path.strip():
62
+ raise ValueError(f"files[{i}].path must be a non-empty string")
63
+ if not isinstance(new_content, str):
64
+ raise ValueError(f"files[{i}].new_content must be a string")
65
+ if not isinstance(sha256, str) or len(sha256.strip()) != 64:
66
+ raise ValueError(f"files[{i}].sha256 must be a 64-char hex string")
67
+
68
+ out.append(PatchFile(path=path, new_content=new_content, sha256=sha256.strip().lower()))
69
+
70
+ return out
71
+
72
+
73
+ def validate_and_resolve_patch_files(
74
+ patch_files: list[PatchFile], *, sandbox_root: str
75
+ ) -> list[tuple[str, str]]:
76
+ """Validate patch files and return a list of (abs_path, new_content).
77
+
78
+ Safety:
79
+ - paths must be relative and remain within sandbox_root
80
+ - sha256 must match new_content (utf-8)
81
+ """
82
+
83
+ sr = os.path.realpath(os.path.abspath(sandbox_root))
84
+ resolved: list[tuple[str, str]] = []
85
+
86
+ for pf in patch_files:
87
+ if os.path.isabs(pf.path):
88
+ raise ValueError(f"Absolute path not allowed in patch protocol: {pf.path}")
89
+
90
+ # Normalize and resolve against sandbox_root
91
+ abs_path = os.path.realpath(os.path.abspath(os.path.join(sr, pf.path)))
92
+
93
+ if not (abs_path.startswith(sr + os.sep) or abs_path == sr):
94
+ raise ValueError(f"Patch path escapes sandbox root: {pf.path}")
95
+
96
+ got = compute_sha256_utf8(pf.new_content)
97
+ if got != pf.sha256:
98
+ raise ValueError(f"sha256 mismatch for {pf.path}: expected {pf.sha256}, got {got}")
99
+
100
+ resolved.append((abs_path, pf.new_content))
101
+
102
+ return resolved
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: auto-code-fixer
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: Automatically fix Python code using ChatGPT
5
5
  Author-email: Arif Shah <ashah7775@gmail.com>
6
6
  License: MIT
@@ -121,6 +121,21 @@ auto-code-fixer main.py --ai-plan
121
121
  ```
122
122
  This enables a helper that can suggest which local file to edit. It is best-effort.
123
123
 
124
+ ### Optional structured patch protocol (JSON + sha256)
125
+ ```bash
126
+ auto-code-fixer main.py --patch-protocol
127
+ ```
128
+ When enabled, the model is asked to return strict JSON with `{files:[{path,new_content,sha256}]}`.
129
+ The tool verifies the SHA-256 hash of `new_content` before applying edits, and falls back to the
130
+ legacy full-text mode if parsing/validation fails.
131
+
132
+ ### Optional formatting / linting (best-effort)
133
+ ```bash
134
+ auto-code-fixer main.py --format black
135
+ auto-code-fixer main.py --lint ruff --fix
136
+ ```
137
+ These run inside the sandbox venv and are skipped if the tools are not installed.
138
+
124
139
  ---
125
140
 
126
141
  ## Environment variables
@@ -7,6 +7,7 @@ auto_code_fixer/command_runner.py
7
7
  auto_code_fixer/fixer.py
8
8
  auto_code_fixer/installer.py
9
9
  auto_code_fixer/models.py
10
+ auto_code_fixer/patch_protocol.py
10
11
  auto_code_fixer/patcher.py
11
12
  auto_code_fixer/plan.py
12
13
  auto_code_fixer/runner.py
@@ -21,4 +22,5 @@ auto_code_fixer.egg-info/entry_points.txt
21
22
  auto_code_fixer.egg-info/requires.txt
22
23
  auto_code_fixer.egg-info/top_level.txt
23
24
  tests/test_fix_imported_file.py
24
- tests/test_internal_imports.py
25
+ tests/test_internal_imports.py
26
+ tests/test_patch_protocol.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "auto-code-fixer"
7
- version = "0.3.4"
7
+ version = "0.3.5"
8
8
  description = "Automatically fix Python code using ChatGPT"
9
9
  readme = "README.md"
10
10
 
@@ -0,0 +1,46 @@
1
+ import pytest
2
+
3
+ from auto_code_fixer.patch_protocol import (
4
+ compute_sha256_utf8,
5
+ parse_patch_protocol_response,
6
+ validate_and_resolve_patch_files,
7
+ )
8
+
9
+
10
+ def test_parse_patch_protocol_happy_path_with_code_fences(tmp_path):
11
+ import json
12
+
13
+ content = "print('hi')\n"
14
+ sha = compute_sha256_utf8(content)
15
+ payload = {"files": [{"path": "a.py", "new_content": content, "sha256": sha}]}
16
+ text = "```json\n" + json.dumps(payload) + "\n```"
17
+
18
+ files = parse_patch_protocol_response(text)
19
+ assert len(files) == 1
20
+ assert files[0].path == "a.py"
21
+ assert files[0].sha256 == sha
22
+
23
+ resolved = validate_and_resolve_patch_files(files, sandbox_root=str(tmp_path))
24
+ assert resolved[0][0].endswith("a.py")
25
+ assert resolved[0][1] == content
26
+
27
+
28
+ def test_validate_rejects_sha_mismatch(tmp_path):
29
+ text = '{"files": [{"path": "a.py", "new_content": "x", "sha256": "' + ("0" * 64) + '"}]}'
30
+ files = parse_patch_protocol_response(text)
31
+ with pytest.raises(ValueError, match="sha256 mismatch"):
32
+ validate_and_resolve_patch_files(files, sandbox_root=str(tmp_path))
33
+
34
+
35
+ def test_validate_rejects_path_traversal(tmp_path):
36
+ content = "x"
37
+ sha = compute_sha256_utf8(content)
38
+ text = '{"files": [{"path": "../evil.py", "new_content": "x", "sha256": "' + sha + '"}]}'
39
+ files = parse_patch_protocol_response(text)
40
+ with pytest.raises(ValueError, match="escapes sandbox"):
41
+ validate_and_resolve_patch_files(files, sandbox_root=str(tmp_path))
42
+
43
+
44
+ def test_parse_rejects_missing_files_list():
45
+ with pytest.raises(ValueError, match="files"):
46
+ parse_patch_protocol_response("{}")
@@ -1 +0,0 @@
1
- __version__ = "0.3.4"
File without changes