codesuture 0.5.0__tar.gz → 0.5.1__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.1/.gitignore +32 -0
- codesuture-0.5.1/CHANGELOG.md +56 -0
- codesuture-0.5.1/MANIFEST.in +14 -0
- codesuture-0.5.1/PKG-INFO +167 -0
- codesuture-0.5.1/README.md +146 -0
- codesuture-0.5.1/codesuture/__init__.py +1 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/cli.py +1 -1
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/guard_synthesizer.py +17 -1
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/pattern_matcher.py +58 -7
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/tracer.py +10 -0
- codesuture-0.5.1/codesuture.egg-info/PKG-INFO +167 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture.egg-info/SOURCES.txt +7 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/pyproject.toml +1 -1
- codesuture-0.5.1/tests/__init__.py +0 -0
- codesuture-0.5.1/tests/closure_test.py +16 -0
- codesuture-0.5.1/tests/debug_gc.py +23 -0
- codesuture-0.5.1/tests/harness3.py +46 -0
- codesuture-0.5.0/PKG-INFO +0 -106
- codesuture-0.5.0/README.md +0 -85
- codesuture-0.5.0/codesuture/__init__.py +0 -1
- codesuture-0.5.0/codesuture.egg-info/PKG-INFO +0 -106
- {codesuture-0.5.0 → codesuture-0.5.1}/LICENSE +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/__main__.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/_eval_fix.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/audit.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/code_replacer.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/codesuture_fix.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/debuggee.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/diff_guard.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/explain.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/fingerprint.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/knowledge.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/middleware.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/persistence.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/plugins/__init__.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/plugins/autonomous.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/rewind.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/rollback.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/sandbox.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/shadow.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/watcher.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture.egg-info/dependency_links.txt +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture.egg-info/entry_points.txt +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture.egg-info/requires.txt +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/codesuture.egg-info/top_level.txt +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/setup.cfg +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/setup.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_codesuture_debuggee.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_e2e.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_guard_synthesizer.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_harness.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_harness2.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_new_guards.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_pattern_matcher.py +0 -0
- {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_unknown_bug.py +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
|
|
14
|
+
# CodeSuture specific
|
|
15
|
+
.codesuture_cache/
|
|
16
|
+
.codesuture_store/
|
|
17
|
+
.codesuture_knowledge/
|
|
18
|
+
.codesuture_fingerprints
|
|
19
|
+
.models/
|
|
20
|
+
|
|
21
|
+
# Exclude internal / dev scripts
|
|
22
|
+
codesuture_verify*.py
|
|
23
|
+
livepatch_verify*.py
|
|
24
|
+
realbugg.py
|
|
25
|
+
real.py
|
|
26
|
+
*.zip
|
|
27
|
+
.livepatch_*/
|
|
28
|
+
|
|
29
|
+
# IDE / OS
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
32
|
+
.DS_Store
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.5.1] - 2026-05-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- propagate_patch: skip list/dict/set/generator comprehensions
|
|
12
|
+
instead of crashing with AttributeError on __code__
|
|
13
|
+
- key_guard, subscript_guard, chain_subscript_guard: infer
|
|
14
|
+
correct default type from downstream bytecode usage
|
|
15
|
+
(string methods -> "" default, numeric ops -> 0 default)
|
|
16
|
+
- KeyError on chained subscripts (e.g. request["headers"]["auth"].strip())
|
|
17
|
+
now produces a chain_subscript_guard instead of a simple key_guard,
|
|
18
|
+
preventing secondary TypeError from None subscript access
|
|
19
|
+
|
|
20
|
+
## [0.5.0] - 2026-05-08
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Async/await support (CO_COROUTINE frame detection) — automatic `RESUME 0` preservation for coroutine bytecode patching.
|
|
24
|
+
- Watch mode: `codesuture watch --max-restarts N` — subprocess loop with automatic crash-patch-restart cycle.
|
|
25
|
+
- Explain command: `codesuture explain [func_name]` — detailed table of active patches with safety assessment (LIKELY/RISKY/UNKNOWN).
|
|
26
|
+
- WSGI middleware: `CodeSutureMiddleware` — intercepts request handler exceptions, patches, and retries with `X-CodeSuture` response header.
|
|
27
|
+
|
|
28
|
+
## [0.4.0] - 2026-05-07
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- `codesuture rollback` command to selectively remove persisted patches (`codesuture rollback <func>`, `--all`, and `--dry-run`).
|
|
32
|
+
- Three new guard types:
|
|
33
|
+
- `type_coercion_guard` for `TypeError` and `ValueError` during type conversions.
|
|
34
|
+
- `index_guard` for `IndexError` bounds checking.
|
|
35
|
+
- `key_guard` for safe dictionary `KeyError` fallbacks.
|
|
36
|
+
- Enhanced `--dry-run` mode with confidence levels (HIGH/MEDIUM/LOW) based on fingerprint registry hits.
|
|
37
|
+
- Full PyPI packaging structure (`pyproject.toml`, complete `README.md`, `CHANGELOG.md`).
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- Migrated legacy guards `list_bound_guard` to `index_guard` and `dict_get_guard` to `key_guard` for consistency.
|
|
41
|
+
- Standardized CLI output format and improved error reporting.
|
|
42
|
+
|
|
43
|
+
## [0.3.0] - 2026-05-06
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- Dark Upgrade D1: Semantic diff safety gate to prevent runaway bytecode corruption.
|
|
47
|
+
- Dark Upgrade D2: Caller-aware patch propagation to automatically fix closures and bound methods in-memory.
|
|
48
|
+
- Dark Upgrade D3: Shadow execution mode (`--shadow`) to monitor and warn when sentinel defaults leak downstream.
|
|
49
|
+
- Dark Upgrade D4: Patch expiry TTL warnings to nudge developers toward source-level fixes.
|
|
50
|
+
- Dark Upgrade D5: Bytecode fingerprint registry for instant cache hits on known crash patterns.
|
|
51
|
+
- Dark Upgrade D6: `codesuture audit` command for viewing all active patches in a formatted table.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
- Addressed Windows `UnicodeDecodeError` and `cp1252` terminal limitations by enforcing `utf-8` encoding.
|
|
55
|
+
- Resolved a race condition where patch persistence was executing after the code object swap, preventing correct caller identification.
|
|
56
|
+
- Fixed namespace pollution during nested patching.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
include pyproject.toml
|
|
2
|
+
include README.md
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include LICENSE
|
|
5
|
+
include .gitignore
|
|
6
|
+
|
|
7
|
+
recursive-include codesuture *.py
|
|
8
|
+
recursive-include tests *.py
|
|
9
|
+
|
|
10
|
+
exclude codesuture_verify*.py
|
|
11
|
+
exclude livepatch_verify*.py
|
|
12
|
+
exclude realbugg.py
|
|
13
|
+
exclude real.py
|
|
14
|
+
exclude *.zip
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codesuture
|
|
3
|
+
Version: 0.5.1
|
|
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
|
+
---
|
|
27
|
+
|
|
28
|
+
## What it does
|
|
29
|
+
|
|
30
|
+
CodeSuture sits between your Python program and its crashes. When an exception occurs, it intercepts the failing frame, disassembles the bytecode to identify the root cause, injects a deterministic guard directly into the function's code object, and retries execution — all without touching a single source file.
|
|
31
|
+
|
|
32
|
+
The fix persists. On subsequent runs, CodeSuture loads the patched bytecode before the first function call. The crash is gone until you choose to remove the patch.
|
|
33
|
+
|
|
34
|
+
It is a surgical tool for keeping programs running when structural failures — null access, missing keys, type mismatches, out-of-bounds reads — would otherwise bring them down.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install codesuture
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Python 3.11+ required.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
codesuture run your_script.py
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
[CodeSuture] Caught AttributeError: 'NoneType' object has no attribute 'bio'
|
|
56
|
+
[CodeSuture] Applying null_guard on 'profile' ...
|
|
57
|
+
[CodeSuture] Patch applied to get_bio().
|
|
58
|
+
[CodeSuture] Re-executing after 1 patch(es)...
|
|
59
|
+
|
|
60
|
+
Session summary:
|
|
61
|
+
Patches applied: 1
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Run it again:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
[CodeSuture] Already healed, skipping: loaded persistent patch for get_bio
|
|
68
|
+
[CodeSuture] Session summary:
|
|
69
|
+
Patches applied: 0
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## How it works
|
|
75
|
+
|
|
76
|
+
1. **Catch** — A `sys.settrace` callback intercepts exceptions at the exact frame and bytecode offset where they occur.
|
|
77
|
+
2. **Analyze** — The pattern matcher disassembles the function's bytecode, walks the instruction chain, and identifies the failing variable or operation.
|
|
78
|
+
3. **Patch** — The guard synthesizer injects new bytecode instructions into the function's code object in memory. A semantic diff gate rejects patches that would change too much of the function's logic.
|
|
79
|
+
4. **Rewind** — Execution restarts from the patched function. The guard prevents the crash from recurring.
|
|
80
|
+
5. **Persist** — The patched code object is serialized to `.codesuture_store/` with JSON metadata. On subsequent runs it loads before execution begins.
|
|
81
|
+
|
|
82
|
+
No source files are modified at any point.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Supported guard types
|
|
87
|
+
|
|
88
|
+
| Guard type | Triggers on | Example |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `null_guard` | `AttributeError` on `None` | `user.profile.bio` when `profile` is `None` |
|
|
91
|
+
| `index_guard` | `IndexError` | `items[10]` when `len(items) == 2` |
|
|
92
|
+
| `key_guard` | `KeyError` | `cfg["timeout"]` when key is missing |
|
|
93
|
+
| `subscript_guard` | `TypeError` subscripting `None` | `data["key"]` when `data` is `None` |
|
|
94
|
+
| `chain_subscript_guard` | Nested subscript on `None` | `data["user"]["name"]` |
|
|
95
|
+
| `type_coercion_guard` | `TypeError` on conversion | `int("not_a_number")` |
|
|
96
|
+
| `division_guard` | `ZeroDivisionError` | `x / count` when `count == 0` |
|
|
97
|
+
| `str_coerce_guard` | `TypeError` on string concat | `"age: " + 25` |
|
|
98
|
+
| `file_guard` | `FileNotFoundError` | `open(path)` when file is missing |
|
|
99
|
+
| `callable_guard` | `TypeError` calling `None` | `func()` when `func` is `None` |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## CLI reference
|
|
104
|
+
|
|
105
|
+
| Command | Flags | What it does |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `codesuture run <script>` | | Run script with live patching enabled |
|
|
108
|
+
| `codesuture run <script>` | `--verbose` | Show patch diffs and instruction deltas |
|
|
109
|
+
| `codesuture run <script>` | `--shadow` | Warn when patched functions return sentinel values |
|
|
110
|
+
| `codesuture run <script>` | `--dry-run` | Preview what would be patched without applying |
|
|
111
|
+
| `codesuture run <script>` | `--ttl DAYS` | Set patch expiry in days (default: 7) |
|
|
112
|
+
| `codesuture run <script>` | `--retries N` | Max re-execution attempts per crash (default: 3) |
|
|
113
|
+
| `codesuture watch <script>` | `--max-restarts N` | Run continuously, restart after each patch |
|
|
114
|
+
| `codesuture audit` | | Show all active patches in a formatted table |
|
|
115
|
+
| `codesuture explain` | | Human-readable breakdown of what each patch changed |
|
|
116
|
+
| `codesuture explain <name>` | | Explain one function's patch |
|
|
117
|
+
| `codesuture rollback <name>` | | Remove one persisted patch |
|
|
118
|
+
| `codesuture rollback` | `--all` | Remove all patches and the fingerprint registry |
|
|
119
|
+
| `codesuture rollback` | `--dry-run` | Preview what would be removed |
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Runtime Intelligence
|
|
124
|
+
|
|
125
|
+
Beyond basic patching, CodeSuture includes a set of higher-order behaviors that make it safe to use in real codebases:
|
|
126
|
+
|
|
127
|
+
**Semantic diff gate** — Patches that would modify too many instructions relative to the guard type are automatically rejected. The engine will never corrupt a complex function to patch a simple crash.
|
|
128
|
+
|
|
129
|
+
**Caller-aware propagation** — After patching a function, CodeSuture uses `gc.get_referrers` to find every live reference to the original code object — closures, bound methods, partials — and updates them all. No in-memory copy of the broken function survives.
|
|
130
|
+
|
|
131
|
+
**Shadow execution mode** — `--shadow` monitors the return value of patched functions. If a sentinel default (`""`, `0`, `None`) leaks into downstream logic, CodeSuture logs a warning before it causes a second failure.
|
|
132
|
+
|
|
133
|
+
**Patch expiry** — Every persisted patch carries a configurable TTL. When a patch ages past its limit, CodeSuture logs a reminder that the underlying bug should be addressed in source code. Patches are scaffolding, not permanent fixes.
|
|
134
|
+
|
|
135
|
+
**Bytecode fingerprint registry** — Crash sites are hashed by their surrounding instruction window. When the same crash pattern appears again, CodeSuture applies the known guard directly without re-running analysis.
|
|
136
|
+
|
|
137
|
+
**Audit trail** — `codesuture audit` displays every active patch: function name, guard type, target variable, default value, age, and rollback instructions. `codesuture explain` gives a plain-language breakdown of exactly what each patch changed and whether the default value is safe for downstream consumers.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## What CodeSuture is not
|
|
142
|
+
|
|
143
|
+
**Not a logger.** It does not record exceptions and move on. It patches the function and retries.
|
|
144
|
+
|
|
145
|
+
**Not a fuzzer or static analyzer.** It operates at runtime on live bytecode, not on source.
|
|
146
|
+
|
|
147
|
+
**Not autonomous.** Patches should be reviewed via `codesuture audit` and `codesuture explain`. The goal is to keep your program running while you address the root cause — not to replace the fix.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Limitations
|
|
152
|
+
|
|
153
|
+
**Python 3.11+ only.** CodeSuture depends on bytecode structures introduced in CPython 3.11. Earlier versions are not supported.
|
|
154
|
+
|
|
155
|
+
**List and dict comprehensions are not patchable.** Comprehensions are implemented as anonymous nested code objects in CPython. CodeSuture detects crashes inside them and logs a warning, but does not attempt to patch them. Refactor the comprehension into a named function to enable patching.
|
|
156
|
+
|
|
157
|
+
**Semantic bugs are not patchable.** CodeSuture fixes structural crashes — null access, missing keys, type mismatches, bounds errors. It cannot fix logic errors where the code runs but produces wrong results.
|
|
158
|
+
|
|
159
|
+
**Single-process scope.** Patches apply per-process. Multi-process applications need a CodeSuture instance per worker. The `.codesuture_store/` directory is shared on disk, so patches load correctly on restart.
|
|
160
|
+
|
|
161
|
+
**Async support is experimental.** `async def` functions with standard `CO_COROUTINE` frames are patched. Async generators and deeply nested `await` chains may not be handled correctly in all cases.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# CodeSuture
|
|
2
|
+
|
|
3
|
+
> Runtime Python bytecode patcher. Catches crashes, synthesizes guards, rewrites functions in-memory, and persists fixes across runs.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
CodeSuture sits between your Python program and its crashes. When an exception occurs, it intercepts the failing frame, disassembles the bytecode to identify the root cause, injects a deterministic guard directly into the function's code object, and retries execution — all without touching a single source file.
|
|
10
|
+
|
|
11
|
+
The fix persists. On subsequent runs, CodeSuture loads the patched bytecode before the first function call. The crash is gone until you choose to remove the patch.
|
|
12
|
+
|
|
13
|
+
It is a surgical tool for keeping programs running when structural failures — null access, missing keys, type mismatches, out-of-bounds reads — would otherwise bring them down.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install codesuture
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Python 3.11+ required.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
codesuture run your_script.py
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
[CodeSuture] Caught AttributeError: 'NoneType' object has no attribute 'bio'
|
|
35
|
+
[CodeSuture] Applying null_guard on 'profile' ...
|
|
36
|
+
[CodeSuture] Patch applied to get_bio().
|
|
37
|
+
[CodeSuture] Re-executing after 1 patch(es)...
|
|
38
|
+
|
|
39
|
+
Session summary:
|
|
40
|
+
Patches applied: 1
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Run it again:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
[CodeSuture] Already healed, skipping: loaded persistent patch for get_bio
|
|
47
|
+
[CodeSuture] Session summary:
|
|
48
|
+
Patches applied: 0
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## How it works
|
|
54
|
+
|
|
55
|
+
1. **Catch** — A `sys.settrace` callback intercepts exceptions at the exact frame and bytecode offset where they occur.
|
|
56
|
+
2. **Analyze** — The pattern matcher disassembles the function's bytecode, walks the instruction chain, and identifies the failing variable or operation.
|
|
57
|
+
3. **Patch** — The guard synthesizer injects new bytecode instructions into the function's code object in memory. A semantic diff gate rejects patches that would change too much of the function's logic.
|
|
58
|
+
4. **Rewind** — Execution restarts from the patched function. The guard prevents the crash from recurring.
|
|
59
|
+
5. **Persist** — The patched code object is serialized to `.codesuture_store/` with JSON metadata. On subsequent runs it loads before execution begins.
|
|
60
|
+
|
|
61
|
+
No source files are modified at any point.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Supported guard types
|
|
66
|
+
|
|
67
|
+
| Guard type | Triggers on | Example |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `null_guard` | `AttributeError` on `None` | `user.profile.bio` when `profile` is `None` |
|
|
70
|
+
| `index_guard` | `IndexError` | `items[10]` when `len(items) == 2` |
|
|
71
|
+
| `key_guard` | `KeyError` | `cfg["timeout"]` when key is missing |
|
|
72
|
+
| `subscript_guard` | `TypeError` subscripting `None` | `data["key"]` when `data` is `None` |
|
|
73
|
+
| `chain_subscript_guard` | Nested subscript on `None` | `data["user"]["name"]` |
|
|
74
|
+
| `type_coercion_guard` | `TypeError` on conversion | `int("not_a_number")` |
|
|
75
|
+
| `division_guard` | `ZeroDivisionError` | `x / count` when `count == 0` |
|
|
76
|
+
| `str_coerce_guard` | `TypeError` on string concat | `"age: " + 25` |
|
|
77
|
+
| `file_guard` | `FileNotFoundError` | `open(path)` when file is missing |
|
|
78
|
+
| `callable_guard` | `TypeError` calling `None` | `func()` when `func` is `None` |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## CLI reference
|
|
83
|
+
|
|
84
|
+
| Command | Flags | What it does |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `codesuture run <script>` | | Run script with live patching enabled |
|
|
87
|
+
| `codesuture run <script>` | `--verbose` | Show patch diffs and instruction deltas |
|
|
88
|
+
| `codesuture run <script>` | `--shadow` | Warn when patched functions return sentinel values |
|
|
89
|
+
| `codesuture run <script>` | `--dry-run` | Preview what would be patched without applying |
|
|
90
|
+
| `codesuture run <script>` | `--ttl DAYS` | Set patch expiry in days (default: 7) |
|
|
91
|
+
| `codesuture run <script>` | `--retries N` | Max re-execution attempts per crash (default: 3) |
|
|
92
|
+
| `codesuture watch <script>` | `--max-restarts N` | Run continuously, restart after each patch |
|
|
93
|
+
| `codesuture audit` | | Show all active patches in a formatted table |
|
|
94
|
+
| `codesuture explain` | | Human-readable breakdown of what each patch changed |
|
|
95
|
+
| `codesuture explain <name>` | | Explain one function's patch |
|
|
96
|
+
| `codesuture rollback <name>` | | Remove one persisted patch |
|
|
97
|
+
| `codesuture rollback` | `--all` | Remove all patches and the fingerprint registry |
|
|
98
|
+
| `codesuture rollback` | `--dry-run` | Preview what would be removed |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Runtime Intelligence
|
|
103
|
+
|
|
104
|
+
Beyond basic patching, CodeSuture includes a set of higher-order behaviors that make it safe to use in real codebases:
|
|
105
|
+
|
|
106
|
+
**Semantic diff gate** — Patches that would modify too many instructions relative to the guard type are automatically rejected. The engine will never corrupt a complex function to patch a simple crash.
|
|
107
|
+
|
|
108
|
+
**Caller-aware propagation** — After patching a function, CodeSuture uses `gc.get_referrers` to find every live reference to the original code object — closures, bound methods, partials — and updates them all. No in-memory copy of the broken function survives.
|
|
109
|
+
|
|
110
|
+
**Shadow execution mode** — `--shadow` monitors the return value of patched functions. If a sentinel default (`""`, `0`, `None`) leaks into downstream logic, CodeSuture logs a warning before it causes a second failure.
|
|
111
|
+
|
|
112
|
+
**Patch expiry** — Every persisted patch carries a configurable TTL. When a patch ages past its limit, CodeSuture logs a reminder that the underlying bug should be addressed in source code. Patches are scaffolding, not permanent fixes.
|
|
113
|
+
|
|
114
|
+
**Bytecode fingerprint registry** — Crash sites are hashed by their surrounding instruction window. When the same crash pattern appears again, CodeSuture applies the known guard directly without re-running analysis.
|
|
115
|
+
|
|
116
|
+
**Audit trail** — `codesuture audit` displays every active patch: function name, guard type, target variable, default value, age, and rollback instructions. `codesuture explain` gives a plain-language breakdown of exactly what each patch changed and whether the default value is safe for downstream consumers.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## What CodeSuture is not
|
|
121
|
+
|
|
122
|
+
**Not a logger.** It does not record exceptions and move on. It patches the function and retries.
|
|
123
|
+
|
|
124
|
+
**Not a fuzzer or static analyzer.** It operates at runtime on live bytecode, not on source.
|
|
125
|
+
|
|
126
|
+
**Not autonomous.** Patches should be reviewed via `codesuture audit` and `codesuture explain`. The goal is to keep your program running while you address the root cause — not to replace the fix.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Limitations
|
|
131
|
+
|
|
132
|
+
**Python 3.11+ only.** CodeSuture depends on bytecode structures introduced in CPython 3.11. Earlier versions are not supported.
|
|
133
|
+
|
|
134
|
+
**List and dict comprehensions are not patchable.** Comprehensions are implemented as anonymous nested code objects in CPython. CodeSuture detects crashes inside them and logs a warning, but does not attempt to patch them. Refactor the comprehension into a named function to enable patching.
|
|
135
|
+
|
|
136
|
+
**Semantic bugs are not patchable.** CodeSuture fixes structural crashes — null access, missing keys, type mismatches, bounds errors. It cannot fix logic errors where the code runs but produces wrong results.
|
|
137
|
+
|
|
138
|
+
**Single-process scope.** Patches apply per-process. Multi-process applications need a CodeSuture instance per worker. The `.codesuture_store/` directory is shared on disk, so patches load correctly on restart.
|
|
139
|
+
|
|
140
|
+
**Async support is experimental.** `async def` functions with standard `CO_COROUTINE` frames are patched. Async generators and deeply nested `await` chains may not be handled correctly in all cases.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.1"
|
|
@@ -5,7 +5,7 @@ from codesuture.tracer import install, uninstall
|
|
|
5
5
|
def main():
|
|
6
6
|
parser = argparse.ArgumentParser(prog='codesuture',
|
|
7
7
|
description='Runtime Python bytecode patcher with self-healing re-execution')
|
|
8
|
-
parser.add_argument('--version', action='version', version='codesuture 0.5.
|
|
8
|
+
parser.add_argument('--version', action='version', version='codesuture 0.5.1')
|
|
9
9
|
sub = parser.add_subparsers(dest='command', required=True)
|
|
10
10
|
|
|
11
11
|
run_parser = sub.add_parser('run', help='Run a script with live patching')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
2
|
Synthesises guard + original bytecode for all deterministic strategies.
|
|
3
3
|
"""
|
|
4
4
|
from bytecode import Bytecode, Instr, Label, Compare
|
|
@@ -25,6 +25,22 @@ def validate_patch(original_code, patched_code):
|
|
|
25
25
|
|
|
26
26
|
def propagate_patch(original_func, patched_code) -> int:
|
|
27
27
|
import gc
|
|
28
|
+
import logging
|
|
29
|
+
if original_func is None:
|
|
30
|
+
return 0
|
|
31
|
+
if not hasattr(original_func, '__code__'):
|
|
32
|
+
return 0
|
|
33
|
+
name = getattr(original_func, '__qualname__', '') or \
|
|
34
|
+
getattr(original_func, '__name__', '')
|
|
35
|
+
if '<listcomp>' in name or '<genexpr>' in name or \
|
|
36
|
+
'<dictcomp>' in name or '<setcomp>' in name:
|
|
37
|
+
import logging
|
|
38
|
+
logging.getLogger(__name__).debug(
|
|
39
|
+
"[CodeSuture] Skipping %s — "
|
|
40
|
+
"comprehensions are not patchable via __code__", name
|
|
41
|
+
)
|
|
42
|
+
return 0
|
|
43
|
+
|
|
28
44
|
original_code = original_func.__code__
|
|
29
45
|
propagated = 0
|
|
30
46
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
from types import FrameType
|
|
2
2
|
from typing import Optional, NamedTuple
|
|
3
3
|
import dis
|
|
4
4
|
import re
|
|
@@ -182,6 +182,48 @@ def _infer_default(var_name, instructions=None, crash_idx=None):
|
|
|
182
182
|
return []
|
|
183
183
|
return ""
|
|
184
184
|
|
|
185
|
+
def _infer_subscript_default(instructions=None, crash_idx=None):
|
|
186
|
+
if instructions is not None and crash_idx is not None:
|
|
187
|
+
i = crash_idx + 1
|
|
188
|
+
while i < len(instructions):
|
|
189
|
+
instr = instructions[i]
|
|
190
|
+
if instr.opname in ('LOAD_CONST', 'LOAD_FAST'):
|
|
191
|
+
i += 1
|
|
192
|
+
continue
|
|
193
|
+
if instr.opname == 'BINARY_SUBSCR':
|
|
194
|
+
i += 1
|
|
195
|
+
continue
|
|
196
|
+
break
|
|
197
|
+
for j in range(i, min(i + 4, len(instructions))):
|
|
198
|
+
instr = instructions[j]
|
|
199
|
+
if instr.opname in ('LOAD_ATTR', 'LOAD_METHOD'):
|
|
200
|
+
string_methods = {
|
|
201
|
+
'capitalize', 'casefold', 'center', 'encode', 'expandtabs',
|
|
202
|
+
'format', 'format_map', 'join', 'ljust', 'lower', 'lstrip',
|
|
203
|
+
'removeprefix', 'removesuffix', 'replace', 'rjust', 'strip',
|
|
204
|
+
'swapcase', 'title', 'translate', 'upper', 'zfill',
|
|
205
|
+
'split', 'startswith', 'endswith',
|
|
206
|
+
}
|
|
207
|
+
if instr.argval in string_methods:
|
|
208
|
+
return ""
|
|
209
|
+
elif instr.opname in ('BINARY_OP', 'BINARY_ADD', 'BINARY_SUBTRACT',
|
|
210
|
+
'BINARY_MULTIPLY', 'BINARY_TRUE_DIVIDE'):
|
|
211
|
+
return 0
|
|
212
|
+
elif instr.opname == 'CALL':
|
|
213
|
+
for k in range(max(0, j - 2), j):
|
|
214
|
+
if instructions[k].opname == 'LOAD_GLOBAL' and \
|
|
215
|
+
instructions[k].argval in ('int', 'float'):
|
|
216
|
+
return 0
|
|
217
|
+
elif instr.opname in ('LIST_APPEND', 'STORE_SUBSCR'):
|
|
218
|
+
return None
|
|
219
|
+
elif instr.opname in ('RETURN_VALUE', 'STORE_FAST'):
|
|
220
|
+
break
|
|
221
|
+
elif instr.opname in ('PRECALL',):
|
|
222
|
+
continue
|
|
223
|
+
else:
|
|
224
|
+
break
|
|
225
|
+
return None
|
|
226
|
+
|
|
185
227
|
def _infer_attribute_default(attr_name, fallback_name, instructions=None, crash_idx=None):
|
|
186
228
|
string_methods = {
|
|
187
229
|
'capitalize', 'casefold', 'center', 'encode', 'expandtabs', 'format',
|
|
@@ -377,7 +419,7 @@ def _none_subscript_spec(frame):
|
|
|
377
419
|
return None
|
|
378
420
|
|
|
379
421
|
spec = PatchSpec('subscript_guard', var_name=container_var,
|
|
380
|
-
default_value=
|
|
422
|
+
default_value=_infer_subscript_default(instructions, idx),
|
|
381
423
|
key_name=final_key)
|
|
382
424
|
|
|
383
425
|
prop_origin = _check_property_origin(frame, instructions, idx)
|
|
@@ -444,7 +486,7 @@ def _try_chain_subscript(frame, instructions, failing_idx):
|
|
|
444
486
|
|
|
445
487
|
last_instr_idx = failing_idx + len(keys_fwd) * 2
|
|
446
488
|
final_key = all_keys[-1]
|
|
447
|
-
default =
|
|
489
|
+
default = _infer_subscript_default(instructions, last_instr_idx)
|
|
448
490
|
|
|
449
491
|
return PatchSpec('chain_subscript_guard', var_name=root_var,
|
|
450
492
|
default_value=default, key_name=tuple(all_keys))
|
|
@@ -470,18 +512,27 @@ def _dict_get_spec(frame, msg):
|
|
|
470
512
|
match = re.search(r"KeyError\: '(\w+)'", msg)
|
|
471
513
|
if not match:
|
|
472
514
|
match = re.search(r"'(\w+)'", msg)
|
|
473
|
-
if
|
|
474
|
-
return None
|
|
475
|
-
key = match.group(1)
|
|
515
|
+
key = match.group(1) if match else None
|
|
476
516
|
instructions = list(dis.get_instructions(frame.f_code))
|
|
477
517
|
tgt = _find_target_instr(instructions, frame.f_lasti)
|
|
478
518
|
if tgt is None or tgt.opname != 'BINARY_SUBSCR':
|
|
479
519
|
return None
|
|
480
520
|
idx = instructions.index(tgt)
|
|
521
|
+
|
|
522
|
+
if key is None:
|
|
523
|
+
if idx > 0 and instructions[idx - 1].opname == 'LOAD_CONST':
|
|
524
|
+
key = instructions[idx - 1].argval
|
|
525
|
+
else:
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
chain_spec = _try_chain_subscript(frame, instructions, idx)
|
|
529
|
+
if chain_spec is not None:
|
|
530
|
+
return chain_spec
|
|
531
|
+
|
|
481
532
|
for i in range(idx-1, -1, -1):
|
|
482
533
|
if instructions[i].opname in ('LOAD_FAST', 'LOAD_DEREF'):
|
|
483
534
|
dict_var = frame.f_code.co_varnames[instructions[i].arg]
|
|
484
|
-
return PatchSpec('key_guard', dict_var,
|
|
535
|
+
return PatchSpec('key_guard', dict_var, _infer_subscript_default(instructions, idx), key_name=key)
|
|
485
536
|
return None
|
|
486
537
|
|
|
487
538
|
def _str_concat_spec(frame, msg):
|
|
@@ -79,6 +79,16 @@ class CodeSutureTracer:
|
|
|
79
79
|
if _is_internal_frame(frame):
|
|
80
80
|
return
|
|
81
81
|
|
|
82
|
+
name = getattr(frame.f_code, 'co_qualname', '') or frame.f_code.co_name
|
|
83
|
+
if '<listcomp>' in name or '<genexpr>' in name or \
|
|
84
|
+
'<dictcomp>' in name or '<setcomp>' in name:
|
|
85
|
+
import logging
|
|
86
|
+
logging.getLogger(__name__).debug(
|
|
87
|
+
"[CodeSuture] Skipping %s — "
|
|
88
|
+
"comprehensions are not patchable via __code__", name
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
|
|
82
92
|
from codesuture.persistence import HEALED_FUNCTIONS, _heal_key
|
|
83
93
|
from codesuture.code_replacer import get_function_from_frame
|
|
84
94
|
try:
|