codesuture 0.5.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.
- codesuture-0.5.0/LICENSE +33 -0
- codesuture-0.5.0/PKG-INFO +106 -0
- codesuture-0.5.0/README.md +85 -0
- codesuture-0.5.0/codesuture/__init__.py +1 -0
- codesuture-0.5.0/codesuture/__main__.py +5 -0
- codesuture-0.5.0/codesuture/_eval_fix.py +5 -0
- codesuture-0.5.0/codesuture/audit.py +127 -0
- codesuture-0.5.0/codesuture/cli.py +127 -0
- codesuture-0.5.0/codesuture/code_replacer.py +82 -0
- codesuture-0.5.0/codesuture/codesuture_fix.py +85 -0
- codesuture-0.5.0/codesuture/debuggee.py +7 -0
- codesuture-0.5.0/codesuture/diff_guard.py +27 -0
- codesuture-0.5.0/codesuture/explain.py +147 -0
- codesuture-0.5.0/codesuture/fingerprint.py +52 -0
- codesuture-0.5.0/codesuture/guard_synthesizer.py +607 -0
- codesuture-0.5.0/codesuture/knowledge.py +35 -0
- codesuture-0.5.0/codesuture/middleware.py +86 -0
- codesuture-0.5.0/codesuture/pattern_matcher.py +555 -0
- codesuture-0.5.0/codesuture/persistence.py +330 -0
- codesuture-0.5.0/codesuture/plugins/__init__.py +0 -0
- codesuture-0.5.0/codesuture/plugins/autonomous.py +64 -0
- codesuture-0.5.0/codesuture/rewind.py +43 -0
- codesuture-0.5.0/codesuture/rollback.py +85 -0
- codesuture-0.5.0/codesuture/sandbox.py +105 -0
- codesuture-0.5.0/codesuture/shadow.py +20 -0
- codesuture-0.5.0/codesuture/tracer.py +447 -0
- codesuture-0.5.0/codesuture/watcher.py +78 -0
- codesuture-0.5.0/codesuture.egg-info/PKG-INFO +106 -0
- codesuture-0.5.0/codesuture.egg-info/SOURCES.txt +42 -0
- codesuture-0.5.0/codesuture.egg-info/dependency_links.txt +1 -0
- codesuture-0.5.0/codesuture.egg-info/entry_points.txt +2 -0
- codesuture-0.5.0/codesuture.egg-info/requires.txt +4 -0
- codesuture-0.5.0/codesuture.egg-info/top_level.txt +1 -0
- codesuture-0.5.0/pyproject.toml +37 -0
- codesuture-0.5.0/setup.cfg +4 -0
- codesuture-0.5.0/setup.py +3 -0
- codesuture-0.5.0/tests/test_codesuture_debuggee.py +17 -0
- codesuture-0.5.0/tests/test_e2e.py +51 -0
- codesuture-0.5.0/tests/test_guard_synthesizer.py +36 -0
- codesuture-0.5.0/tests/test_harness.py +20 -0
- codesuture-0.5.0/tests/test_harness2.py +16 -0
- codesuture-0.5.0/tests/test_new_guards.py +68 -0
- codesuture-0.5.0/tests/test_pattern_matcher.py +89 -0
- codesuture-0.5.0/tests/test_unknown_bug.py +9 -0
codesuture-0.5.0/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<<<<<<< HEAD
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Copyright <2026> <MUHAMMAD ABUBAKAR>
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
11
|
+
=======
|
|
12
|
+
MIT License
|
|
13
|
+
|
|
14
|
+
Copyright (c) 2026 MUHAMMAD ABUBAKAR
|
|
15
|
+
|
|
16
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
17
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
18
|
+
in the Software without restriction, including without limitation the rights
|
|
19
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
20
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
21
|
+
furnished to do so, subject to the following conditions:
|
|
22
|
+
|
|
23
|
+
The above copyright notice and this permission notice shall be included in all
|
|
24
|
+
copies or substantial portions of the Software.
|
|
25
|
+
|
|
26
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
27
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
28
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
29
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
30
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
31
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
32
|
+
SOFTWARE.
|
|
33
|
+
>>>>>>> 354c8c39be3e4e36096d5b2725b1828a0d53729f
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codesuture
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Runtime Python bytecode patcher with guard knowledge base, persistence, and self-healing re-execution
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Source, https://github.com/codesuture-py/codesuture
|
|
7
|
+
Keywords: bytecode,runtime,patching,self-healing,debugging,null-safety,resilience
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: bytecode>=0.15.1
|
|
18
|
+
Provides-Extra: autonomous
|
|
19
|
+
Requires-Dist: llama-cpp-python; extra == "autonomous"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# CodeSuture
|
|
23
|
+
|
|
24
|
+
> Runtime Python bytecode patcher. Catches crashes, synthesizes guards, rewrites functions in-memory, and persists fixes across runs.
|
|
25
|
+
|
|
26
|
+
## What it does
|
|
27
|
+
|
|
28
|
+
CodeSuture intercepts runtime exceptions in your Python program, analyzes the failing bytecode to determine the root cause, synthesizes a deterministic guard (such as a null check or bounds clamp), rewrites the function's bytecode in memory, rewinds execution to retry, and persists the fix so it loads instantly on subsequent runs. No source files are modified. It is a surgical debugging tool that turns crashes into self-healing code.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install codesuture
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
codesuture run your_buggy_script.py
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
[CodeSuture] Caught AttributeError: 'NoneType' object has no attribute 'bio'
|
|
42
|
+
[CodeSuture] Applying null_guard on 'profile' ...
|
|
43
|
+
[CodeSuture] Patch applied to get_bio().
|
|
44
|
+
[CodeSuture] Re-executing after 1 patch(es)...
|
|
45
|
+
|
|
46
|
+
Session summary:
|
|
47
|
+
Patches applied: 1
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## How it works
|
|
51
|
+
|
|
52
|
+
1. **Catch** — A `sys.settrace` callback intercepts exceptions at the exact frame and instruction offset where they occur.
|
|
53
|
+
2. **Analyze** — The pattern matcher disassembles the function's bytecode, identifies the failing variable/operation, and selects the appropriate guard type.
|
|
54
|
+
3. **Patch** — The guard synthesizer injects new bytecode instructions (null checks, bounds clamps, safe `.get()` calls) into the function's code object. A semantic diff gate rejects patches that change too many instructions.
|
|
55
|
+
4. **Rewind** — Execution restarts from the top of the patched function. The guard prevents the same crash from recurring.
|
|
56
|
+
5. **Persist** — The patched code object is serialized to `.codesuture_store/` with JSON metadata. On subsequent runs, persisted patches load before the first function call.
|
|
57
|
+
|
|
58
|
+
## Supported guard types
|
|
59
|
+
|
|
60
|
+
| Guard type | Triggers on | Example |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `null_guard` | `AttributeError` on `None` | `user.profile.bio` when `profile is None` |
|
|
63
|
+
| `index_guard` | `IndexError` (list out of range) | `items[10]` when `len(items) == 2` |
|
|
64
|
+
| `key_guard` | `KeyError` | `cfg["timeout"]` when key missing |
|
|
65
|
+
| `type_coercion_guard` | `TypeError` (conversion failure) | `int("not_a_number")` |
|
|
66
|
+
| `subscript_guard` | `TypeError` subscripting `None` | `data["key"]` when `data is None` |
|
|
67
|
+
| `chain_subscript_guard` | Nested subscript on `None` | `data["user"]["name"]` |
|
|
68
|
+
| `division_guard` | `ZeroDivisionError` | `x / count` when `count == 0` |
|
|
69
|
+
| `str_coerce_guard` | `TypeError` (str + non-str) | `"age: " + 25` |
|
|
70
|
+
| `file_guard` | `FileNotFoundError` | `open(path)` when file missing |
|
|
71
|
+
| `callable_guard` | `TypeError` calling `None` | `func()` when `func is None` |
|
|
72
|
+
|
|
73
|
+
## CLI reference
|
|
74
|
+
|
|
75
|
+
| Command | Flags | What it does |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `codesuture run <script>` | | Run script with live patching enabled |
|
|
78
|
+
| `codesuture run <script>` | `--verbose` | Show patch diffs and instruction deltas |
|
|
79
|
+
| `codesuture run <script>` | `--shadow` | Warn if patched functions return sentinel values |
|
|
80
|
+
| `codesuture run <script>` | `--dry-run` | Show what would be patched without applying |
|
|
81
|
+
| `codesuture run <script>` | `--ttl DAYS` | Set patch expiry (default: 7 days) |
|
|
82
|
+
| `codesuture run <script>` | `--retries N` | Max re-execution attempts (default: 3) |
|
|
83
|
+
| `codesuture audit` | | Show all active patches in a formatted table |
|
|
84
|
+
| `codesuture rollback <name>` | | Remove persisted patch for one function |
|
|
85
|
+
| `codesuture rollback` | `--all` | Remove ALL patches + fingerprint registry |
|
|
86
|
+
| `codesuture rollback` | `--dry-run` | List what would be removed |
|
|
87
|
+
|
|
88
|
+
## Dark upgrades
|
|
89
|
+
|
|
90
|
+
- **D1 — Semantic diff safety gate**: Rejects patches that modify too many instructions, preventing runaway bytecode corruption.
|
|
91
|
+
- **D2 — Caller-aware patch propagation**: Propagates patches to closures and bound methods via `gc.get_referrers`.
|
|
92
|
+
- **D3 — Shadow execution mode**: Monitors patched function return values and warns when sentinel defaults leak downstream.
|
|
93
|
+
- **D4 — Patch expiry TTL**: Warns when patches exceed their time-to-live, nudging developers to fix the root cause in source.
|
|
94
|
+
- **D5 — Bytecode fingerprint registry**: Caches crash patterns by bytecode hash for instant guard selection on repeated failures.
|
|
95
|
+
- **D6 — Audit command**: Displays all active patches in a formatted table with function name, guard type, age, and rollback hints.
|
|
96
|
+
|
|
97
|
+
## Limitations
|
|
98
|
+
|
|
99
|
+
- **Python 3.11+ only** — CodeSuture relies on `PUSH_NULL`, `PRECALL`, and `POP_JUMP_FORWARD_IF_*` opcodes introduced in CPython 3.11.
|
|
100
|
+
- **Async not yet supported** — `async def` functions and coroutines are not patched.
|
|
101
|
+
- **Semantic bugs not patchable** — CodeSuture fixes structural crashes (null access, missing keys, type mismatches). It cannot fix logic errors where the code runs but produces wrong results.
|
|
102
|
+
- **Single-process scope** — Patches are applied per-process. Multi-process or distributed systems need separate CodeSuture instances.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# CodeSuture
|
|
2
|
+
|
|
3
|
+
> Runtime Python bytecode patcher. Catches crashes, synthesizes guards, rewrites functions in-memory, and persists fixes across runs.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
CodeSuture intercepts runtime exceptions in your Python program, analyzes the failing bytecode to determine the root cause, synthesizes a deterministic guard (such as a null check or bounds clamp), rewrites the function's bytecode in memory, rewinds execution to retry, and persists the fix so it loads instantly on subsequent runs. No source files are modified. It is a surgical debugging tool that turns crashes into self-healing code.
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install codesuture
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
codesuture run your_buggy_script.py
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
[CodeSuture] Caught AttributeError: 'NoneType' object has no attribute 'bio'
|
|
21
|
+
[CodeSuture] Applying null_guard on 'profile' ...
|
|
22
|
+
[CodeSuture] Patch applied to get_bio().
|
|
23
|
+
[CodeSuture] Re-executing after 1 patch(es)...
|
|
24
|
+
|
|
25
|
+
Session summary:
|
|
26
|
+
Patches applied: 1
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
1. **Catch** — A `sys.settrace` callback intercepts exceptions at the exact frame and instruction offset where they occur.
|
|
32
|
+
2. **Analyze** — The pattern matcher disassembles the function's bytecode, identifies the failing variable/operation, and selects the appropriate guard type.
|
|
33
|
+
3. **Patch** — The guard synthesizer injects new bytecode instructions (null checks, bounds clamps, safe `.get()` calls) into the function's code object. A semantic diff gate rejects patches that change too many instructions.
|
|
34
|
+
4. **Rewind** — Execution restarts from the top of the patched function. The guard prevents the same crash from recurring.
|
|
35
|
+
5. **Persist** — The patched code object is serialized to `.codesuture_store/` with JSON metadata. On subsequent runs, persisted patches load before the first function call.
|
|
36
|
+
|
|
37
|
+
## Supported guard types
|
|
38
|
+
|
|
39
|
+
| Guard type | Triggers on | Example |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `null_guard` | `AttributeError` on `None` | `user.profile.bio` when `profile is None` |
|
|
42
|
+
| `index_guard` | `IndexError` (list out of range) | `items[10]` when `len(items) == 2` |
|
|
43
|
+
| `key_guard` | `KeyError` | `cfg["timeout"]` when key missing |
|
|
44
|
+
| `type_coercion_guard` | `TypeError` (conversion failure) | `int("not_a_number")` |
|
|
45
|
+
| `subscript_guard` | `TypeError` subscripting `None` | `data["key"]` when `data is None` |
|
|
46
|
+
| `chain_subscript_guard` | Nested subscript on `None` | `data["user"]["name"]` |
|
|
47
|
+
| `division_guard` | `ZeroDivisionError` | `x / count` when `count == 0` |
|
|
48
|
+
| `str_coerce_guard` | `TypeError` (str + non-str) | `"age: " + 25` |
|
|
49
|
+
| `file_guard` | `FileNotFoundError` | `open(path)` when file missing |
|
|
50
|
+
| `callable_guard` | `TypeError` calling `None` | `func()` when `func is None` |
|
|
51
|
+
|
|
52
|
+
## CLI reference
|
|
53
|
+
|
|
54
|
+
| Command | Flags | What it does |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `codesuture run <script>` | | Run script with live patching enabled |
|
|
57
|
+
| `codesuture run <script>` | `--verbose` | Show patch diffs and instruction deltas |
|
|
58
|
+
| `codesuture run <script>` | `--shadow` | Warn if patched functions return sentinel values |
|
|
59
|
+
| `codesuture run <script>` | `--dry-run` | Show what would be patched without applying |
|
|
60
|
+
| `codesuture run <script>` | `--ttl DAYS` | Set patch expiry (default: 7 days) |
|
|
61
|
+
| `codesuture run <script>` | `--retries N` | Max re-execution attempts (default: 3) |
|
|
62
|
+
| `codesuture audit` | | Show all active patches in a formatted table |
|
|
63
|
+
| `codesuture rollback <name>` | | Remove persisted patch for one function |
|
|
64
|
+
| `codesuture rollback` | `--all` | Remove ALL patches + fingerprint registry |
|
|
65
|
+
| `codesuture rollback` | `--dry-run` | List what would be removed |
|
|
66
|
+
|
|
67
|
+
## Dark upgrades
|
|
68
|
+
|
|
69
|
+
- **D1 — Semantic diff safety gate**: Rejects patches that modify too many instructions, preventing runaway bytecode corruption.
|
|
70
|
+
- **D2 — Caller-aware patch propagation**: Propagates patches to closures and bound methods via `gc.get_referrers`.
|
|
71
|
+
- **D3 — Shadow execution mode**: Monitors patched function return values and warns when sentinel defaults leak downstream.
|
|
72
|
+
- **D4 — Patch expiry TTL**: Warns when patches exceed their time-to-live, nudging developers to fix the root cause in source.
|
|
73
|
+
- **D5 — Bytecode fingerprint registry**: Caches crash patterns by bytecode hash for instant guard selection on repeated failures.
|
|
74
|
+
- **D6 — Audit command**: Displays all active patches in a formatted table with function name, guard type, age, and rollback hints.
|
|
75
|
+
|
|
76
|
+
## Limitations
|
|
77
|
+
|
|
78
|
+
- **Python 3.11+ only** — CodeSuture relies on `PUSH_NULL`, `PRECALL`, and `POP_JUMP_FORWARD_IF_*` opcodes introduced in CPython 3.11.
|
|
79
|
+
- **Async not yet supported** — `async def` functions and coroutines are not patched.
|
|
80
|
+
- **Semantic bugs not patchable** — CodeSuture fixes structural crashes (null access, missing keys, type mismatches). It cannot fix logic errors where the code runs but produces wrong results.
|
|
81
|
+
- **Single-process scope** — Patches are applied per-process. Multi-process or distributed systems need separate CodeSuture instances.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
def run_audit(patch_store_path: str = None):
|
|
7
|
+
|
|
8
|
+
candidates = [
|
|
9
|
+
".codesuture_cache", ".codesuture_store",
|
|
10
|
+
".codesuture", "codesuture_patches"
|
|
11
|
+
]
|
|
12
|
+
store = patch_store_path
|
|
13
|
+
if not store:
|
|
14
|
+
for c in candidates:
|
|
15
|
+
if os.path.exists(c):
|
|
16
|
+
store = c
|
|
17
|
+
break
|
|
18
|
+
|
|
19
|
+
if not store:
|
|
20
|
+
print("[CodeSuture] No patch store found. Nothing has been patched yet.")
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
patches = _load_all_patches(store)
|
|
24
|
+
|
|
25
|
+
if not patches:
|
|
26
|
+
print("[CodeSuture] Patch store exists but is empty.")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
now = datetime.utcnow()
|
|
30
|
+
|
|
31
|
+
col_func = max(18, max(len(p.get("func_name","?")) for p in patches) + 2)
|
|
32
|
+
col_guard = 12
|
|
33
|
+
col_target = 10
|
|
34
|
+
col_default = 10
|
|
35
|
+
col_age = 7
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
"|".encode(sys.stdout.encoding or 'ascii')
|
|
39
|
+
HAS_UNICODE = True
|
|
40
|
+
except Exception:
|
|
41
|
+
HAS_UNICODE = False
|
|
42
|
+
|
|
43
|
+
def row(f, g, t, d, a):
|
|
44
|
+
v = "|" if HAS_UNICODE else "|"
|
|
45
|
+
return (f"{v} {f:<{col_func}} {v} {g:<{col_guard}} {v} "
|
|
46
|
+
f"{t:<{col_target}} {v} {d:<{col_default}} {v} {a:<{col_age}} {v}")
|
|
47
|
+
|
|
48
|
+
if HAS_UNICODE:
|
|
49
|
+
sep = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
|
|
50
|
+
f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+")
|
|
51
|
+
top = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
|
|
52
|
+
f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+++")
|
|
53
|
+
bot = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
|
|
54
|
+
f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+")
|
|
55
|
+
else:
|
|
56
|
+
sep = (f"|-{'*'*col_func}-+-{'*'*col_guard}-+-"
|
|
57
|
+
f"{'*'*col_target}-+-{'*'*col_default}-+-{'*'*col_age}-|")
|
|
58
|
+
top = sep
|
|
59
|
+
bot = sep
|
|
60
|
+
|
|
61
|
+
print()
|
|
62
|
+
print(" CodeSuture Audit Report")
|
|
63
|
+
print()
|
|
64
|
+
print(top)
|
|
65
|
+
print(row("Function", "Guard", "Target", "Default", "Age"))
|
|
66
|
+
print(sep)
|
|
67
|
+
|
|
68
|
+
oldest_days = 0
|
|
69
|
+
expired = 0
|
|
70
|
+
for p in patches:
|
|
71
|
+
func = p.get("func_name", "?")
|
|
72
|
+
guard = p.get("guard_type", "?")
|
|
73
|
+
target = p.get("target", "?")
|
|
74
|
+
default = repr(p.get("default_value", "?"))[:col_default]
|
|
75
|
+
age_str = "?"
|
|
76
|
+
ttl_days = p.get("ttl_days", 7)
|
|
77
|
+
if "patched_at" in p:
|
|
78
|
+
try:
|
|
79
|
+
dt = datetime.fromisoformat(p["patched_at"])
|
|
80
|
+
days = (now - dt).days
|
|
81
|
+
oldest_days = max(oldest_days, days)
|
|
82
|
+
age_str = f"{days}d"
|
|
83
|
+
if days > ttl_days:
|
|
84
|
+
age_str += " [WARN]"
|
|
85
|
+
expired += 1
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
print(row(func, guard, target, default, age_str))
|
|
89
|
+
|
|
90
|
+
print(bot)
|
|
91
|
+
print()
|
|
92
|
+
print(f" Total: {len(patches)} active patch(es). "
|
|
93
|
+
f"Oldest: {oldest_days}d. "
|
|
94
|
+
f"{'[WARN] ' + str(expired) + ' expired - run codesuture rollback to clear.' if expired else 'All within TTL.'}")
|
|
95
|
+
print()
|
|
96
|
+
print(" Run 'codesuture rollback <function_name>' to remove a patch.")
|
|
97
|
+
print(" Run 'codesuture rollback --all' to clear everything.")
|
|
98
|
+
print()
|
|
99
|
+
|
|
100
|
+
def _load_all_patches(store_path: str) -> list[dict]:
|
|
101
|
+
|
|
102
|
+
patches = []
|
|
103
|
+
if os.path.isdir(store_path):
|
|
104
|
+
for root, dirs, files in os.walk(store_path):
|
|
105
|
+
for fname in files:
|
|
106
|
+
fpath = os.path.join(root, fname)
|
|
107
|
+
if fname.endswith(".json") and os.path.isfile(fpath):
|
|
108
|
+
try:
|
|
109
|
+
with open(fpath, "r", encoding="utf-8") as f:
|
|
110
|
+
data = json.load(f)
|
|
111
|
+
if isinstance(data, list):
|
|
112
|
+
patches.extend(data)
|
|
113
|
+
elif isinstance(data, dict):
|
|
114
|
+
patches.append(data)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
elif os.path.isfile(store_path):
|
|
118
|
+
try:
|
|
119
|
+
with open(store_path, "r", encoding="utf-8") as f:
|
|
120
|
+
data = json.load(f)
|
|
121
|
+
if isinstance(data, list):
|
|
122
|
+
patches = data
|
|
123
|
+
elif isinstance(data, dict):
|
|
124
|
+
patches = list(data.values())
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
return patches
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import argparse
|
|
3
|
+
from codesuture.tracer import install, uninstall
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
parser = argparse.ArgumentParser(prog='codesuture',
|
|
7
|
+
description='Runtime Python bytecode patcher with self-healing re-execution')
|
|
8
|
+
parser.add_argument('--version', action='version', version='codesuture 0.5.0')
|
|
9
|
+
sub = parser.add_subparsers(dest='command', required=True)
|
|
10
|
+
|
|
11
|
+
run_parser = sub.add_parser('run', help='Run a script with live patching')
|
|
12
|
+
run_parser.add_argument('script', help='Target script to run')
|
|
13
|
+
run_parser.add_argument('--dry-run', action='store_true', help='Show what would be patched without applying')
|
|
14
|
+
run_parser.add_argument('--log', metavar='FILE', help='Append patch events (JSON lines) to FILE')
|
|
15
|
+
run_parser.add_argument('--retries', type=int, default=3, metavar='N', help='Max patching attempts (default: 3)')
|
|
16
|
+
run_parser.add_argument('--self-test', action='store_true', help='Corrupt the engine to test self-healing')
|
|
17
|
+
run_parser.add_argument('--autonomous', action='store_true', help='Enable autonomous LLM bug-fixing')
|
|
18
|
+
run_parser.add_argument('--verbose', action='store_true', help='Show detailed debug output')
|
|
19
|
+
run_parser.add_argument('--shadow', action='store_true', help='Warn if patched functions return sentinel values')
|
|
20
|
+
run_parser.add_argument('--ttl', type=int, default=7, metavar='DAYS', help='Patch TTL in days (default: 7)')
|
|
21
|
+
|
|
22
|
+
sub.add_parser('audit', help='Show all active patches')
|
|
23
|
+
|
|
24
|
+
rb_parser = sub.add_parser('rollback', help='Remove persisted patches')
|
|
25
|
+
rb_parser.add_argument('function_name', nargs='?', default=None, help='Function name to roll back')
|
|
26
|
+
rb_parser.add_argument('--all', action='store_true', dest='rollback_all', help='Remove ALL patches + fingerprints')
|
|
27
|
+
rb_parser.add_argument('--dry-run', action='store_true', dest='rollback_dry_run', help='List what would be removed')
|
|
28
|
+
|
|
29
|
+
watch_parser = sub.add_parser('watch', help='Watch and auto-restart a script with live patching')
|
|
30
|
+
watch_parser.add_argument('script', help='Target script to watch')
|
|
31
|
+
watch_parser.add_argument('--max-restarts', type=int, default=10, metavar='N',
|
|
32
|
+
help='Maximum number of restarts (default: 10)')
|
|
33
|
+
watch_parser.add_argument('--shadow', action='store_true', help='Enable shadow mode warnings')
|
|
34
|
+
watch_parser.add_argument('--verbose', action='store_true', help='Show detailed debug output')
|
|
35
|
+
|
|
36
|
+
explain_parser = sub.add_parser('explain', help='Show detailed explanation of active patches')
|
|
37
|
+
explain_parser.add_argument('func_name', nargs='?', default=None, help='Function name to explain')
|
|
38
|
+
|
|
39
|
+
args = parser.parse_args()
|
|
40
|
+
|
|
41
|
+
if args.command == 'audit':
|
|
42
|
+
from codesuture.audit import run_audit
|
|
43
|
+
run_audit()
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
if args.command == 'rollback':
|
|
47
|
+
from codesuture.rollback import rollback_function, rollback_all, rollback_dry_run
|
|
48
|
+
if args.rollback_dry_run:
|
|
49
|
+
rollback_dry_run()
|
|
50
|
+
elif args.rollback_all:
|
|
51
|
+
rollback_all()
|
|
52
|
+
elif args.function_name:
|
|
53
|
+
rollback_function(args.function_name)
|
|
54
|
+
else:
|
|
55
|
+
print("[CodeSuture] Usage: codesuture rollback <function_name> | --all | --dry-run")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
if args.command == 'watch':
|
|
59
|
+
from codesuture.watcher import watch
|
|
60
|
+
exit_code = watch(
|
|
61
|
+
args.script,
|
|
62
|
+
max_restarts=args.max_restarts,
|
|
63
|
+
shadow=args.shadow,
|
|
64
|
+
verbose=args.verbose,
|
|
65
|
+
)
|
|
66
|
+
sys.exit(exit_code)
|
|
67
|
+
|
|
68
|
+
if args.command == 'explain':
|
|
69
|
+
from codesuture.explain import run_explain
|
|
70
|
+
run_explain(args.func_name)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
if getattr(args, 'autonomous', False):
|
|
74
|
+
try:
|
|
75
|
+
import llama_cpp
|
|
76
|
+
except ImportError:
|
|
77
|
+
print("Autonomous mode requires llama-cpp-python. Install with: pip install codesuture[autonomous]")
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
if args.command == 'run':
|
|
81
|
+
from codesuture.persistence import install_import_hook, make_persisted_patch_globals
|
|
82
|
+
|
|
83
|
+
install_import_hook()
|
|
84
|
+
|
|
85
|
+
if getattr(args, 'self_test', False):
|
|
86
|
+
import codesuture.pattern_matcher as pm
|
|
87
|
+
print("[CodeSuture] SELF-TEST: corrupting _infer_default -> None")
|
|
88
|
+
pm._infer_default = None
|
|
89
|
+
tracer = None
|
|
90
|
+
try:
|
|
91
|
+
with open(args.script, 'r', encoding='utf-8') as f:
|
|
92
|
+
source = f.read()
|
|
93
|
+
code = compile(source, args.script, 'exec')
|
|
94
|
+
|
|
95
|
+
tracer = install(dry_run=args.dry_run, log_file=args.log,
|
|
96
|
+
max_retries=args.retries,
|
|
97
|
+
autonomous=getattr(args, 'autonomous', False),
|
|
98
|
+
script_path=args.script, verbose=args.verbose,
|
|
99
|
+
shadow=args.shadow, ttl=args.ttl)
|
|
100
|
+
max_runs = args.retries + 1
|
|
101
|
+
for run in range(max_runs):
|
|
102
|
+
patched_before = tracer.stats['patched']
|
|
103
|
+
tracer._handled_exc_ids.clear()
|
|
104
|
+
try:
|
|
105
|
+
sys.settrace(tracer)
|
|
106
|
+
globs = make_persisted_patch_globals(
|
|
107
|
+
"__main__",
|
|
108
|
+
{'__name__': '__main__', '__file__': args.script},
|
|
109
|
+
)
|
|
110
|
+
exec(code, globs)
|
|
111
|
+
break
|
|
112
|
+
except Exception as e:
|
|
113
|
+
sys.settrace(None)
|
|
114
|
+
new_patches = tracer.stats['patched'] - patched_before
|
|
115
|
+
if new_patches > 0 and run < max_runs - 1:
|
|
116
|
+
print(f"[CodeSuture] Re-executing after {new_patches} patch(es)...")
|
|
117
|
+
continue
|
|
118
|
+
else:
|
|
119
|
+
print(f"[CodeSuture] Script exited with: {e}")
|
|
120
|
+
break
|
|
121
|
+
finally:
|
|
122
|
+
uninstall()
|
|
123
|
+
if tracer is not None:
|
|
124
|
+
tracer.report()
|
|
125
|
+
|
|
126
|
+
if __name__ == '__main__':
|
|
127
|
+
main()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import types
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
def replace_function_code(func, new_code):
|
|
5
|
+
|
|
6
|
+
from codesuture.guard_synthesizer import propagate_patch
|
|
7
|
+
propagate_patch(func, new_code)
|
|
8
|
+
|
|
9
|
+
def get_function_from_frame(frame):
|
|
10
|
+
name = frame.f_code.co_name
|
|
11
|
+
code = frame.f_code
|
|
12
|
+
|
|
13
|
+
if name in frame.f_locals:
|
|
14
|
+
candidate = frame.f_locals[name]
|
|
15
|
+
if hasattr(candidate, '__code__') and candidate.__code__ is code:
|
|
16
|
+
return candidate
|
|
17
|
+
|
|
18
|
+
if name in frame.f_globals:
|
|
19
|
+
candidate = frame.f_globals[name]
|
|
20
|
+
if hasattr(candidate, '__code__') and candidate.__code__ is code:
|
|
21
|
+
return candidate
|
|
22
|
+
|
|
23
|
+
if name == '<module>':
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
self_obj = frame.f_locals.get('self')
|
|
27
|
+
if self_obj is not None:
|
|
28
|
+
cls = type(self_obj)
|
|
29
|
+
method = getattr(cls, name, None)
|
|
30
|
+
if method is not None:
|
|
31
|
+
if isinstance(method, property) and method.fget is not None:
|
|
32
|
+
func = method.fget
|
|
33
|
+
else:
|
|
34
|
+
func = method
|
|
35
|
+
if hasattr(func, '__func__'):
|
|
36
|
+
func = func.__func__
|
|
37
|
+
if hasattr(func, '__code__') and func.__code__ is code:
|
|
38
|
+
return func
|
|
39
|
+
|
|
40
|
+
cls_obj = frame.f_locals.get('cls')
|
|
41
|
+
if cls_obj is not None and isinstance(cls_obj, type):
|
|
42
|
+
method = getattr(cls_obj, name, None)
|
|
43
|
+
if method is not None:
|
|
44
|
+
if isinstance(method, property) and method.fget is not None:
|
|
45
|
+
func = method.fget
|
|
46
|
+
else:
|
|
47
|
+
func = method
|
|
48
|
+
if hasattr(func, '__func__'):
|
|
49
|
+
func = func.__func__
|
|
50
|
+
if hasattr(func, '__code__') and func.__code__ is code:
|
|
51
|
+
return func
|
|
52
|
+
|
|
53
|
+
for val in frame.f_globals.values():
|
|
54
|
+
if isinstance(val, type):
|
|
55
|
+
method = getattr(val, name, None)
|
|
56
|
+
if method is not None:
|
|
57
|
+
if isinstance(method, property) and method.fget is not None:
|
|
58
|
+
func = method.fget
|
|
59
|
+
else:
|
|
60
|
+
func = method
|
|
61
|
+
if hasattr(func, '__func__'):
|
|
62
|
+
func = func.__func__
|
|
63
|
+
if hasattr(func, '__code__') and func.__code__ is code:
|
|
64
|
+
return func
|
|
65
|
+
|
|
66
|
+
for val in frame.f_globals.values():
|
|
67
|
+
if hasattr(val, '__wrapped__'):
|
|
68
|
+
inner = val.__wrapped__
|
|
69
|
+
if hasattr(inner, '__code__') and inner.__code__ is code:
|
|
70
|
+
return inner
|
|
71
|
+
if hasattr(val, '__code__') and val.__code__ is code:
|
|
72
|
+
return val
|
|
73
|
+
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def get_source_from_frame(frame):
|
|
77
|
+
|
|
78
|
+
func = get_function_from_frame(frame)
|
|
79
|
+
try:
|
|
80
|
+
return inspect.getsource(func)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
return f"# Could not get source: {e}\n"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
4
|
+
import types
|
|
5
|
+
from codesuture.pattern_matcher import analyze_exception
|
|
6
|
+
from codesuture.guard_synthesizer import synthesize_guarded_code
|
|
7
|
+
from codesuture.code_replacer import replace_function_code, get_function_from_frame
|
|
8
|
+
from codesuture.rewind import rewind_frame_to_start
|
|
9
|
+
|
|
10
|
+
def apply_fix(exc_type_name: str = None, exc_msg: str = None) -> str:
|
|
11
|
+
|
|
12
|
+
exc_info = sys.exc_info()
|
|
13
|
+
target_frame = None
|
|
14
|
+
tb = None
|
|
15
|
+
|
|
16
|
+
if exc_info[0] is not None:
|
|
17
|
+
if exc_type_name is None:
|
|
18
|
+
exc_type_name = exc_info[0].__name__
|
|
19
|
+
if exc_msg is None:
|
|
20
|
+
exc_msg = str(exc_info[1])
|
|
21
|
+
|
|
22
|
+
tb = exc_info[2]
|
|
23
|
+
curr_tb = tb
|
|
24
|
+
while curr_tb and curr_tb.tb_next:
|
|
25
|
+
curr_tb = curr_tb.tb_next
|
|
26
|
+
if curr_tb:
|
|
27
|
+
target_frame = curr_tb.tb_frame
|
|
28
|
+
|
|
29
|
+
if target_frame is None:
|
|
30
|
+
if exc_type_name is None or exc_msg is None:
|
|
31
|
+
return "ERROR: No active exception. You must provide exc_type_name and exc_msg if paused on an unhandled exception."
|
|
32
|
+
|
|
33
|
+
for fi in inspect.stack():
|
|
34
|
+
frame = fi.frame
|
|
35
|
+
filename = frame.f_code.co_filename
|
|
36
|
+
func_name = frame.f_code.co_name
|
|
37
|
+
|
|
38
|
+
if 'pydevd' in filename.lower() and func_name in ('evaluate_expression', 'new_func', '_run_with_unblock_threads', '_run_with_interrupt_thread'):
|
|
39
|
+
potential_frame = frame.f_locals.get('frame')
|
|
40
|
+
if isinstance(potential_frame, types.FrameType):
|
|
41
|
+
target_frame = potential_frame
|
|
42
|
+
break
|
|
43
|
+
|
|
44
|
+
if func_name in ('apply_fix', 'apply_fix_with_info', '<module>', 'Exec', 'exec'):
|
|
45
|
+
continue
|
|
46
|
+
if any(x in filename.lower() for x in ('pydevd', 'debugpy', 'threading', 'importlib')):
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
basename = os.path.basename(filename)
|
|
50
|
+
internal_files = (
|
|
51
|
+
'codesuture_fix.py', 'pattern_matcher.py', 'guard_synthesizer.py',
|
|
52
|
+
'code_replacer.py', 'rewind.py', 'tracer.py', 'debuggee.py'
|
|
53
|
+
)
|
|
54
|
+
if basename in internal_files:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
target_frame = frame
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
if target_frame is None:
|
|
61
|
+
return "ERROR: No paused frame found"
|
|
62
|
+
|
|
63
|
+
class FakeExc:
|
|
64
|
+
def __init__(self, msg):
|
|
65
|
+
self._msg = msg
|
|
66
|
+
def __str__(self):
|
|
67
|
+
return self._msg
|
|
68
|
+
|
|
69
|
+
exc_type = FakeExc
|
|
70
|
+
exc_type.__name__ = exc_type_name
|
|
71
|
+
exc_value = FakeExc(exc_msg)
|
|
72
|
+
|
|
73
|
+
spec = analyze_exception(target_frame, exc_type, exc_value, tb)
|
|
74
|
+
if spec is None:
|
|
75
|
+
return f"ERROR: No deterministic patch for {exc_type_name}"
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
func = get_function_from_frame(target_frame)
|
|
79
|
+
new_bc = synthesize_guarded_code(target_frame.f_code, spec)
|
|
80
|
+
new_code = new_bc.to_code()
|
|
81
|
+
replace_function_code(func, new_code)
|
|
82
|
+
rewind_frame_to_start(target_frame, target_frame.f_code)
|
|
83
|
+
return f"OK: patched {target_frame.f_code.co_name} ({spec.strategy} on {spec.var_name})"
|
|
84
|
+
except Exception as e:
|
|
85
|
+
return f"ERROR: {e}"
|