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.
Files changed (55) hide show
  1. codesuture-0.5.1/.gitignore +32 -0
  2. codesuture-0.5.1/CHANGELOG.md +56 -0
  3. codesuture-0.5.1/MANIFEST.in +14 -0
  4. codesuture-0.5.1/PKG-INFO +167 -0
  5. codesuture-0.5.1/README.md +146 -0
  6. codesuture-0.5.1/codesuture/__init__.py +1 -0
  7. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/cli.py +1 -1
  8. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/guard_synthesizer.py +17 -1
  9. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/pattern_matcher.py +58 -7
  10. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/tracer.py +10 -0
  11. codesuture-0.5.1/codesuture.egg-info/PKG-INFO +167 -0
  12. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture.egg-info/SOURCES.txt +7 -0
  13. {codesuture-0.5.0 → codesuture-0.5.1}/pyproject.toml +1 -1
  14. codesuture-0.5.1/tests/__init__.py +0 -0
  15. codesuture-0.5.1/tests/closure_test.py +16 -0
  16. codesuture-0.5.1/tests/debug_gc.py +23 -0
  17. codesuture-0.5.1/tests/harness3.py +46 -0
  18. codesuture-0.5.0/PKG-INFO +0 -106
  19. codesuture-0.5.0/README.md +0 -85
  20. codesuture-0.5.0/codesuture/__init__.py +0 -1
  21. codesuture-0.5.0/codesuture.egg-info/PKG-INFO +0 -106
  22. {codesuture-0.5.0 → codesuture-0.5.1}/LICENSE +0 -0
  23. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/__main__.py +0 -0
  24. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/_eval_fix.py +0 -0
  25. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/audit.py +0 -0
  26. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/code_replacer.py +0 -0
  27. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/codesuture_fix.py +0 -0
  28. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/debuggee.py +0 -0
  29. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/diff_guard.py +0 -0
  30. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/explain.py +0 -0
  31. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/fingerprint.py +0 -0
  32. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/knowledge.py +0 -0
  33. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/middleware.py +0 -0
  34. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/persistence.py +0 -0
  35. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/plugins/__init__.py +0 -0
  36. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/plugins/autonomous.py +0 -0
  37. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/rewind.py +0 -0
  38. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/rollback.py +0 -0
  39. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/sandbox.py +0 -0
  40. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/shadow.py +0 -0
  41. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture/watcher.py +0 -0
  42. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture.egg-info/dependency_links.txt +0 -0
  43. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture.egg-info/entry_points.txt +0 -0
  44. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture.egg-info/requires.txt +0 -0
  45. {codesuture-0.5.0 → codesuture-0.5.1}/codesuture.egg-info/top_level.txt +0 -0
  46. {codesuture-0.5.0 → codesuture-0.5.1}/setup.cfg +0 -0
  47. {codesuture-0.5.0 → codesuture-0.5.1}/setup.py +0 -0
  48. {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_codesuture_debuggee.py +0 -0
  49. {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_e2e.py +0 -0
  50. {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_guard_synthesizer.py +0 -0
  51. {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_harness.py +0 -0
  52. {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_harness2.py +0 -0
  53. {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_new_guards.py +0 -0
  54. {codesuture-0.5.0 → codesuture-0.5.1}/tests/test_pattern_matcher.py +0 -0
  55. {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.0')
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
- from types import FrameType
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=_infer_default(final_key if isinstance(final_key, str) else '', instructions, idx),
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 = _infer_default(final_key if isinstance(final_key, str) else '', instructions, last_instr_idx)
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 not match:
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, _infer_default(key, instructions, idx), key_name=key)
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: