codesuture 0.5.0__py3-none-any.whl
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/__init__.py +1 -0
- codesuture/__main__.py +5 -0
- codesuture/_eval_fix.py +5 -0
- codesuture/audit.py +127 -0
- codesuture/cli.py +127 -0
- codesuture/code_replacer.py +82 -0
- codesuture/codesuture_fix.py +85 -0
- codesuture/debuggee.py +7 -0
- codesuture/diff_guard.py +27 -0
- codesuture/explain.py +147 -0
- codesuture/fingerprint.py +52 -0
- codesuture/guard_synthesizer.py +607 -0
- codesuture/knowledge.py +35 -0
- codesuture/middleware.py +86 -0
- codesuture/pattern_matcher.py +555 -0
- codesuture/persistence.py +330 -0
- codesuture/plugins/__init__.py +0 -0
- codesuture/plugins/autonomous.py +64 -0
- codesuture/rewind.py +43 -0
- codesuture/rollback.py +85 -0
- codesuture/sandbox.py +105 -0
- codesuture/shadow.py +20 -0
- codesuture/tracer.py +447 -0
- codesuture/watcher.py +78 -0
- codesuture-0.5.0.dist-info/METADATA +106 -0
- codesuture-0.5.0.dist-info/RECORD +30 -0
- codesuture-0.5.0.dist-info/WHEEL +5 -0
- codesuture-0.5.0.dist-info/entry_points.txt +2 -0
- codesuture-0.5.0.dist-info/licenses/LICENSE +33 -0
- codesuture-0.5.0.dist-info/top_level.txt +1 -0
codesuture/tracer.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
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
|
+
_PYTHON_PREFIX = os.path.normcase(os.path.abspath(sys.prefix)) + os.sep
|
|
11
|
+
_PYTHON_BASE_PREFIX = os.path.normcase(os.path.abspath(sys.base_prefix)) + os.sep
|
|
12
|
+
|
|
13
|
+
def _is_internal_frame(frame):
|
|
14
|
+
|
|
15
|
+
co_filename = frame.f_code.co_filename
|
|
16
|
+
|
|
17
|
+
if co_filename.startswith('<'):
|
|
18
|
+
return True
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
norm = os.path.normcase(os.path.abspath(co_filename))
|
|
22
|
+
if norm.startswith(_PYTHON_PREFIX) or norm.startswith(_PYTHON_BASE_PREFIX):
|
|
23
|
+
return True
|
|
24
|
+
except (ValueError, OSError):
|
|
25
|
+
pass
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
class CodeSutureTracer:
|
|
29
|
+
def __init__(self, dry_run=False, log_file=None, max_retries=3, autonomous=False, script_path=None, verbose=False, shadow=False, ttl=7):
|
|
30
|
+
self.dry_run = dry_run
|
|
31
|
+
self.log_file = log_file
|
|
32
|
+
self.max_retries = max_retries
|
|
33
|
+
self.autonomous = autonomous
|
|
34
|
+
self.script_path = script_path
|
|
35
|
+
self.verbose = verbose
|
|
36
|
+
self.shadow_mode = shadow
|
|
37
|
+
self.ttl = ttl
|
|
38
|
+
self._patched_codes = {}
|
|
39
|
+
self.attempts = {}
|
|
40
|
+
self.stats = {
|
|
41
|
+
"patched": 0,
|
|
42
|
+
"dry_run_suggestions": 0,
|
|
43
|
+
"self_healed": 0
|
|
44
|
+
}
|
|
45
|
+
self.patched_signatures = {}
|
|
46
|
+
self._handled_exc_ids = set()
|
|
47
|
+
|
|
48
|
+
def __call__(self, frame, event, arg):
|
|
49
|
+
if event == 'return' and self.shadow_mode and frame.f_code in self._patched_codes:
|
|
50
|
+
from codesuture.shadow import shadow_check
|
|
51
|
+
func_name = frame.f_code.co_name
|
|
52
|
+
guard_type = self._patched_codes[frame.f_code]
|
|
53
|
+
shadow_check(func_name, arg, guard_type)
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
if event == 'exception':
|
|
57
|
+
exc_type, exc_value, exc_tb = arg
|
|
58
|
+
self._handle_exception(frame, exc_type, exc_value, exc_tb)
|
|
59
|
+
return self
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def _extract_crash_key(self, exc_type, exc_value):
|
|
63
|
+
|
|
64
|
+
import re
|
|
65
|
+
if exc_type.__name__ == 'KeyError':
|
|
66
|
+
return str(exc_value).strip("'\"")
|
|
67
|
+
elif exc_type.__name__ == 'AttributeError':
|
|
68
|
+
m = re.search(r"has no attribute '(\w+)'", str(exc_value))
|
|
69
|
+
if m:
|
|
70
|
+
return m.group(1)
|
|
71
|
+
elif exc_type.__name__ == 'TypeError':
|
|
72
|
+
m = re.search(r"'NoneType' object is not subscriptable", str(exc_value))
|
|
73
|
+
if m:
|
|
74
|
+
return '__subscript__'
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def _handle_exception(self, frame, exc_type, exc_value, exc_tb, thread=None):
|
|
78
|
+
|
|
79
|
+
if _is_internal_frame(frame):
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
from codesuture.persistence import HEALED_FUNCTIONS, _heal_key
|
|
83
|
+
from codesuture.code_replacer import get_function_from_frame
|
|
84
|
+
try:
|
|
85
|
+
func = get_function_from_frame(frame)
|
|
86
|
+
if func is not None:
|
|
87
|
+
func_name = getattr(func, '__qualname__', func.__name__)
|
|
88
|
+
module_name = getattr(func, '__module__', '__main__')
|
|
89
|
+
crash_key = self._extract_crash_key(exc_type, exc_value)
|
|
90
|
+
if _heal_key(module_name, func_name, crash_key) in HEALED_FUNCTIONS:
|
|
91
|
+
return
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
exc_id = id(exc_value)
|
|
96
|
+
if exc_id in self._handled_exc_ids:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
spec = None
|
|
100
|
+
from codesuture.fingerprint import compute_fingerprint, lookup, record
|
|
101
|
+
fp = compute_fingerprint(frame.f_code, frame.f_lasti, exc_type.__name__)
|
|
102
|
+
cached = lookup(fp)
|
|
103
|
+
if cached:
|
|
104
|
+
print(f"[CodeSuture] Known crash pattern #{fp[:8]} -- "
|
|
105
|
+
f"applying cached {cached['guard_type']} guard directly.")
|
|
106
|
+
from codesuture.pattern_matcher import PatchSpec
|
|
107
|
+
|
|
108
|
+
spec = PatchSpec(
|
|
109
|
+
strategy=cached['guard_type'],
|
|
110
|
+
var_name=cached['target'],
|
|
111
|
+
default_value=cached.get('default_value', None),
|
|
112
|
+
key_name=tuple(cached.get('key_name')) if isinstance(cached.get('key_name'), list) else cached.get('key_name', None)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if spec is None:
|
|
116
|
+
try:
|
|
117
|
+
spec = analyze_exception(frame, exc_type, exc_value, exc_tb)
|
|
118
|
+
except Exception as internal_exc:
|
|
119
|
+
|
|
120
|
+
spec = self._self_heal(internal_exc)
|
|
121
|
+
if spec is None:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
spec = analyze_exception(frame, exc_type, exc_value, exc_tb)
|
|
126
|
+
except Exception:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
if spec is None:
|
|
130
|
+
|
|
131
|
+
from codesuture.pattern_matcher import check_learned_rules
|
|
132
|
+
func = get_function_from_frame(frame)
|
|
133
|
+
if func is not None:
|
|
134
|
+
func_name = getattr(func, '__qualname__', func.__name__)
|
|
135
|
+
spec = check_learned_rules(func_name, exc_type.__name__, str(exc_value))
|
|
136
|
+
|
|
137
|
+
if spec is None and self.autonomous and func is not None:
|
|
138
|
+
|
|
139
|
+
print(f"[CodeSuture] Autonomous mode activated for unknown error: {exc_type.__name__}")
|
|
140
|
+
import traceback
|
|
141
|
+
from codesuture.code_replacer import get_source_from_frame
|
|
142
|
+
from codesuture.plugins.autonomous import propose_fix
|
|
143
|
+
from codesuture.sandbox import test_fix
|
|
144
|
+
from codesuture.knowledge import save_learned_rule
|
|
145
|
+
from codesuture.pattern_matcher import PatchSpec
|
|
146
|
+
|
|
147
|
+
tb_text = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
|
148
|
+
function_source = get_source_from_frame(frame)
|
|
149
|
+
|
|
150
|
+
new_source = propose_fix(tb_text, function_source, exc_type.__name__, str(exc_value))
|
|
151
|
+
|
|
152
|
+
module_name = getattr(func, '__module__', '__main__')
|
|
153
|
+
|
|
154
|
+
if test_fix(self.script_path, module_name, func_name, new_source, exc_type.__name__):
|
|
155
|
+
print(f"[CodeSuture] LLM fix PASSED sandbox. Learning rule for {func_name}.")
|
|
156
|
+
save_learned_rule(exc_type.__name__, str(exc_value), func_name, new_source)
|
|
157
|
+
spec = PatchSpec(
|
|
158
|
+
strategy='autonomous_rule',
|
|
159
|
+
var_name=func_name,
|
|
160
|
+
default_value=new_source
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
print("[CodeSuture] LLM fix FAILED sandbox. Skipping autonomous patch.")
|
|
164
|
+
|
|
165
|
+
if spec is None:
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
key = (id(frame.f_code), frame.f_lasti)
|
|
169
|
+
tries = self.attempts.get(key, 0)
|
|
170
|
+
if tries >= self.max_retries:
|
|
171
|
+
print(f"[CodeSuture] Max retries ({self.max_retries}) reached at "
|
|
172
|
+
f"{frame.f_code.co_name}:{frame.f_lineno}, giving up.")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
self.attempts[key] = tries + 1
|
|
176
|
+
|
|
177
|
+
_thread_name = thread.name if thread is not None else None
|
|
178
|
+
|
|
179
|
+
entry = {
|
|
180
|
+
"timestamp": datetime.now().isoformat(),
|
|
181
|
+
"function": frame.f_code.co_name,
|
|
182
|
+
"filename": frame.f_code.co_filename,
|
|
183
|
+
"lineno": frame.f_lineno,
|
|
184
|
+
"exception": f"{exc_type.__name__}: {exc_value}",
|
|
185
|
+
"strategy": spec.strategy,
|
|
186
|
+
"var_name": spec.var_name,
|
|
187
|
+
"default": repr(spec.default_value),
|
|
188
|
+
}
|
|
189
|
+
if _thread_name is not None:
|
|
190
|
+
entry["thread"] = _thread_name
|
|
191
|
+
|
|
192
|
+
display_name = spec.var_name
|
|
193
|
+
if spec.key_name:
|
|
194
|
+
display_name = spec.key_name[-1] if isinstance(spec.key_name, tuple) else spec.key_name
|
|
195
|
+
elif spec.strategy == 'null_guard' and exc_type.__name__ == 'AttributeError':
|
|
196
|
+
import re
|
|
197
|
+
m = re.search(r"has no attribute '(\w+)'", str(exc_value))
|
|
198
|
+
if m:
|
|
199
|
+
display_name = m.group(1)
|
|
200
|
+
|
|
201
|
+
if self.dry_run:
|
|
202
|
+
entry["action"] = "dry_run"
|
|
203
|
+
from codesuture.fingerprint import lookup as fp_lookup
|
|
204
|
+
fp_hit = fp_lookup(fp) if fp else None
|
|
205
|
+
if fp_hit:
|
|
206
|
+
try:
|
|
207
|
+
import os as _os
|
|
208
|
+
fp_file = ".codesuture_fingerprints"
|
|
209
|
+
if _os.path.isfile(fp_file):
|
|
210
|
+
with open(fp_file, "r", encoding="utf-8") as fpf:
|
|
211
|
+
fp_data = json.load(fpf)
|
|
212
|
+
count = fp_data.get(fp, {}).get("count", 1) if isinstance(fp_data.get(fp), dict) else 1
|
|
213
|
+
else:
|
|
214
|
+
count = 0
|
|
215
|
+
except Exception:
|
|
216
|
+
count = 0
|
|
217
|
+
else:
|
|
218
|
+
count = 0
|
|
219
|
+
if count >= 3:
|
|
220
|
+
confidence = "HIGH"
|
|
221
|
+
elif count >= 1:
|
|
222
|
+
confidence = "MEDIUM"
|
|
223
|
+
else:
|
|
224
|
+
confidence = "LOW"
|
|
225
|
+
confidence_detail = (f"pattern seen {count}x in fingerprint registry" if count > 0
|
|
226
|
+
else "new pattern, not in fingerprint registry")
|
|
227
|
+
print(f"[CodeSuture DRY-RUN] Would apply {spec.strategy} on '{display_name}' in {frame.f_code.co_name}()")
|
|
228
|
+
print(f"[CodeSuture DRY-RUN] Confidence: {confidence} ({confidence_detail})")
|
|
229
|
+
print(f" Default value: {repr(spec.default_value)}")
|
|
230
|
+
print(f" Guard type: {spec.strategy}")
|
|
231
|
+
self._log(entry)
|
|
232
|
+
self.stats["dry_run_suggestions"] += 1
|
|
233
|
+
return
|
|
234
|
+
else:
|
|
235
|
+
print(f"[CodeSuture] Caught {exc_type.__name__}: {exc_value}")
|
|
236
|
+
|
|
237
|
+
sig = (spec.var_name, spec.key_name, spec.strategy, exc_type.__name__)
|
|
238
|
+
is_reuse = sig in self.patched_signatures
|
|
239
|
+
|
|
240
|
+
if is_reuse:
|
|
241
|
+
print(f"[CodeSuture] Reusing existing patch for '{display_name}' in {frame.f_code.co_name}()")
|
|
242
|
+
spec = self.patched_signatures[sig]
|
|
243
|
+
|
|
244
|
+
if not cached:
|
|
245
|
+
print(f"[CodeSuture] Applying {spec.strategy} on '{display_name}' ...")
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
if getattr(spec, 'target_func', None):
|
|
249
|
+
func = spec.target_func
|
|
250
|
+
old_code = getattr(func, '__code__', frame.f_code)
|
|
251
|
+
else:
|
|
252
|
+
func = get_function_from_frame(frame)
|
|
253
|
+
old_code = frame.f_code
|
|
254
|
+
|
|
255
|
+
new_bc = synthesize_guarded_code(old_code, spec)
|
|
256
|
+
new_code = new_bc.to_code()
|
|
257
|
+
self._persist_patch(frame, old_code, new_code, func)
|
|
258
|
+
|
|
259
|
+
replace_function_code(func, new_code)
|
|
260
|
+
|
|
261
|
+
if getattr(spec, 'target_func', None):
|
|
262
|
+
assert spec.target_func.__code__ is new_code, "Property fget code replacement failed"
|
|
263
|
+
|
|
264
|
+
from codesuture.persistence import save_patch
|
|
265
|
+
save_patch(func, new_code, spec, self.ttl)
|
|
266
|
+
|
|
267
|
+
if self.shadow_mode:
|
|
268
|
+
self._patched_codes[new_code] = spec.strategy
|
|
269
|
+
|
|
270
|
+
if self.verbose:
|
|
271
|
+
from codesuture.diff_guard import semantic_diff
|
|
272
|
+
diff = semantic_diff(old_code, new_code, spec.strategy)
|
|
273
|
+
print(f"[CodeSuture DEBUG] Diff: +{diff.added} -{diff.removed} instructions (allowed <= {diff.allowed})")
|
|
274
|
+
|
|
275
|
+
if not is_reuse:
|
|
276
|
+
self.patched_signatures[sig] = spec
|
|
277
|
+
|
|
278
|
+
if not cached:
|
|
279
|
+
record(fp, spec.strategy, spec.var_name, getattr(func, '__name__', 'unknown'), exc_type.__name__, spec.default_value, spec.key_name)
|
|
280
|
+
|
|
281
|
+
entry["action"] = "applied"
|
|
282
|
+
self._log(entry)
|
|
283
|
+
self.stats["patched"] += 1
|
|
284
|
+
self._handled_exc_ids.add(exc_id)
|
|
285
|
+
print(f"[CodeSuture] Patch applied to {getattr(func, '__name__', 'unknown')}().")
|
|
286
|
+
return
|
|
287
|
+
except Exception as e:
|
|
288
|
+
from codesuture.guard_synthesizer import PatchValidationError, PatchRejectedError
|
|
289
|
+
if isinstance(e, PatchValidationError):
|
|
290
|
+
print(f"[CodeSuture] {e}")
|
|
291
|
+
entry["action"] = "rejected"
|
|
292
|
+
elif isinstance(e, PatchRejectedError):
|
|
293
|
+
entry["action"] = "rejected"
|
|
294
|
+
elif isinstance(e, RuntimeError) and old_code.co_flags & 0x100:
|
|
295
|
+
print(f"[CodeSuture] WARNING: async patch for {old_code.co_name}() "
|
|
296
|
+
f"raised RuntimeError: {e} -- aborting patch, not persisting.")
|
|
297
|
+
entry["action"] = "aborted"
|
|
298
|
+
else:
|
|
299
|
+
import traceback as _tb
|
|
300
|
+
_tb.print_exc()
|
|
301
|
+
print(f"[CodeSuture] Patch failed: {e}")
|
|
302
|
+
entry["action"] = "failed"
|
|
303
|
+
|
|
304
|
+
entry["error"] = str(e)
|
|
305
|
+
self._log(entry)
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
def _self_heal(self, internal_exc):
|
|
309
|
+
|
|
310
|
+
import traceback as tb_mod
|
|
311
|
+
internal_tb = sys.exc_info()[2]
|
|
312
|
+
if internal_tb is None:
|
|
313
|
+
return None
|
|
314
|
+
curr = internal_tb
|
|
315
|
+
while curr.tb_next:
|
|
316
|
+
curr = curr.tb_next
|
|
317
|
+
internal_frame = curr.tb_frame
|
|
318
|
+
|
|
319
|
+
print(f"[CodeSuture] ENGINE SELF-HEAL: caught internal {type(internal_exc).__name__}: {internal_exc}")
|
|
320
|
+
print(f"[CodeSuture] in {internal_frame.f_code.co_name}() at {internal_frame.f_code.co_filename}:{internal_frame.f_lineno}")
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
spec = analyze_exception(
|
|
324
|
+
internal_frame, type(internal_exc), internal_exc, internal_tb
|
|
325
|
+
)
|
|
326
|
+
except Exception:
|
|
327
|
+
print("[CodeSuture] self-heal analysis failed")
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
if spec is None:
|
|
331
|
+
print("[CodeSuture] no deterministic patch found for internal error")
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
print(f"[CodeSuture] Applying {spec.strategy} on '{spec.var_name}' …")
|
|
335
|
+
try:
|
|
336
|
+
func = get_function_from_frame(internal_frame)
|
|
337
|
+
new_bc = synthesize_guarded_code(internal_frame.f_code, spec)
|
|
338
|
+
new_code = new_bc.to_code()
|
|
339
|
+
replace_function_code(func, new_code)
|
|
340
|
+
self.stats["patched"] += 1
|
|
341
|
+
print(f"[CodeSuture] Self-healed {func.__name__}().")
|
|
342
|
+
|
|
343
|
+
from codesuture.persistence import save_patch
|
|
344
|
+
save_patch(func, new_code)
|
|
345
|
+
|
|
346
|
+
return spec
|
|
347
|
+
except Exception as e:
|
|
348
|
+
print(f"[CodeSuture] self-heal patch failed: {e}")
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
def _persist_patch(self, frame, old_code, new_code, func=None):
|
|
352
|
+
import gc
|
|
353
|
+
import ctypes
|
|
354
|
+
replaced = False
|
|
355
|
+
propagated_count = 0
|
|
356
|
+
|
|
357
|
+
refs = gc.get_referrers(old_code)
|
|
358
|
+
for ref in refs:
|
|
359
|
+
if hasattr(ref, "__code__") and getattr(ref, "__code__", None) is old_code:
|
|
360
|
+
try:
|
|
361
|
+
ref.__code__ = new_code
|
|
362
|
+
replaced = True
|
|
363
|
+
propagated_count += 1
|
|
364
|
+
except Exception:
|
|
365
|
+
pass
|
|
366
|
+
elif hasattr(ref, "__func__"):
|
|
367
|
+
fn = getattr(ref, "__func__", None)
|
|
368
|
+
if hasattr(fn, "__code__") and getattr(fn, "__code__", None) is old_code:
|
|
369
|
+
try:
|
|
370
|
+
fn.__code__ = new_code
|
|
371
|
+
replaced = True
|
|
372
|
+
propagated_count += 1
|
|
373
|
+
except Exception:
|
|
374
|
+
pass
|
|
375
|
+
elif isinstance(ref, tuple):
|
|
376
|
+
for i, c in enumerate(ref):
|
|
377
|
+
if c is old_code:
|
|
378
|
+
try:
|
|
379
|
+
addr = id(ref) + 24 + i * 8
|
|
380
|
+
ctypes.c_void_p.from_address(addr).value = id(new_code)
|
|
381
|
+
ctypes.pythonapi.Py_IncRef(ctypes.py_object(new_code))
|
|
382
|
+
replaced = True
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
if propagated_count > 0:
|
|
387
|
+
print(f"[CodeSuture] Propagated patch to {propagated_count} additional live reference(s) of {frame.f_code.co_name}.")
|
|
388
|
+
elif replaced:
|
|
389
|
+
print(f"[CodeSuture] In-memory propagated patch applied to {frame.f_code.co_name}.")
|
|
390
|
+
else:
|
|
391
|
+
if func is None:
|
|
392
|
+
func_name = frame.f_code.co_name
|
|
393
|
+
func = frame.f_globals.get(func_name)
|
|
394
|
+
|
|
395
|
+
if func and hasattr(func, "__code__") and getattr(func, "__code__", None) is old_code:
|
|
396
|
+
func.__code__ = new_code
|
|
397
|
+
print(f"[CodeSuture] In-memory propagated patch applied to {func.__name__}().")
|
|
398
|
+
else:
|
|
399
|
+
print("[CodeSuture] Could not find code object in memory to persist.")
|
|
400
|
+
|
|
401
|
+
def _log(self, entry):
|
|
402
|
+
if self.log_file:
|
|
403
|
+
with open(self.log_file, 'a', encoding='utf-8') as f:
|
|
404
|
+
json.dump(entry, f, default=str)
|
|
405
|
+
f.write('\n')
|
|
406
|
+
|
|
407
|
+
def report(self):
|
|
408
|
+
print("\n[CodeSuture] Session summary:")
|
|
409
|
+
print(f" Patches applied: {len(self.patched_signatures)}")
|
|
410
|
+
if self.dry_run:
|
|
411
|
+
print(f" Dry-run suggestions: {self.stats['dry_run_suggestions']}")
|
|
412
|
+
print(f"[CodeSuture DRY-RUN] No patches applied. Run without --dry-run to apply.")
|
|
413
|
+
|
|
414
|
+
_original_excepthook = None
|
|
415
|
+
|
|
416
|
+
def _codesuture_excepthook(tracer, exc_type, exc_value, exc_tb):
|
|
417
|
+
import threading
|
|
418
|
+
if exc_tb:
|
|
419
|
+
tracer._handle_exception(exc_tb.tb_frame, exc_type, exc_value, exc_tb, thread=threading.current_thread())
|
|
420
|
+
|
|
421
|
+
if _original_excepthook:
|
|
422
|
+
_original_excepthook(exc_type, exc_value, exc_tb)
|
|
423
|
+
else:
|
|
424
|
+
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
|
425
|
+
|
|
426
|
+
def install(dry_run=False, log_file=None, max_retries=3, autonomous=False, script_path=None, verbose=False, shadow=False, ttl=7):
|
|
427
|
+
global _original_excepthook
|
|
428
|
+
import threading
|
|
429
|
+
tracer = CodeSutureTracer(dry_run, log_file, max_retries, autonomous, script_path, verbose, shadow, ttl)
|
|
430
|
+
sys.settrace(tracer)
|
|
431
|
+
threading.settrace(tracer)
|
|
432
|
+
|
|
433
|
+
if getattr(threading, 'excepthook', None) is not None:
|
|
434
|
+
if threading.excepthook != getattr(threading, '__excepthook__', None):
|
|
435
|
+
_original_excepthook = threading.excepthook
|
|
436
|
+
threading.excepthook = lambda args: _codesuture_excepthook(tracer, args.exc_type, args.exc_value, args.exc_traceback)
|
|
437
|
+
|
|
438
|
+
return tracer
|
|
439
|
+
|
|
440
|
+
def uninstall():
|
|
441
|
+
global _original_excepthook
|
|
442
|
+
sys.settrace(None)
|
|
443
|
+
import threading
|
|
444
|
+
threading.settrace(None)
|
|
445
|
+
if getattr(threading, 'excepthook', None) is not None:
|
|
446
|
+
threading.excepthook = _original_excepthook or getattr(threading, '__excepthook__', sys.__excepthook__)
|
|
447
|
+
_original_excepthook = None
|
codesuture/watcher.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
def watch(script, max_restarts=10, shadow=False, verbose=False):
|
|
7
|
+
|
|
8
|
+
restarts = 0
|
|
9
|
+
last_exception = None
|
|
10
|
+
same_exception_count = 0
|
|
11
|
+
|
|
12
|
+
print(f"[CodeSuture WATCH] Starting watch on {script} (max-restarts={max_restarts})")
|
|
13
|
+
|
|
14
|
+
while restarts <= max_restarts:
|
|
15
|
+
cmd = [sys.executable, "-m", "codesuture", "run"]
|
|
16
|
+
if shadow:
|
|
17
|
+
cmd.append("--shadow")
|
|
18
|
+
if verbose:
|
|
19
|
+
cmd.append("--verbose")
|
|
20
|
+
cmd.append(script)
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
result = subprocess.run(
|
|
24
|
+
cmd,
|
|
25
|
+
capture_output=True,
|
|
26
|
+
text=True,
|
|
27
|
+
encoding="utf-8",
|
|
28
|
+
errors="replace",
|
|
29
|
+
timeout=60,
|
|
30
|
+
)
|
|
31
|
+
except subprocess.TimeoutExpired:
|
|
32
|
+
print("[CodeSuture WATCH] Script timed out.")
|
|
33
|
+
restarts += 1
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
output = (result.stdout or "") + (result.stderr or "")
|
|
37
|
+
print(output, end="")
|
|
38
|
+
|
|
39
|
+
if result.returncode == 0:
|
|
40
|
+
print("[CodeSuture WATCH] Script exited cleanly.")
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
patches_applied = output.lower().count("patch applied")
|
|
44
|
+
|
|
45
|
+
current_exception = _extract_exception(output)
|
|
46
|
+
if current_exception and current_exception == last_exception and patches_applied == 0:
|
|
47
|
+
same_exception_count += 1
|
|
48
|
+
else:
|
|
49
|
+
same_exception_count = 0
|
|
50
|
+
last_exception = current_exception
|
|
51
|
+
|
|
52
|
+
if same_exception_count >= 2:
|
|
53
|
+
print("[CodeSuture WATCH] Unrecoverable: same exception fired 3x with 0 new patches.")
|
|
54
|
+
return 1
|
|
55
|
+
|
|
56
|
+
restarts += 1
|
|
57
|
+
|
|
58
|
+
if restarts > max_restarts:
|
|
59
|
+
break
|
|
60
|
+
|
|
61
|
+
print(f"[CodeSuture WATCH] Restarting ({restarts}/{max_restarts})...")
|
|
62
|
+
time.sleep(0.5)
|
|
63
|
+
|
|
64
|
+
print(f"[CodeSuture WATCH] Max restarts ({max_restarts}) reached.")
|
|
65
|
+
return 1
|
|
66
|
+
|
|
67
|
+
def _extract_exception(output):
|
|
68
|
+
|
|
69
|
+
import re
|
|
70
|
+
|
|
71
|
+
matches = re.findall(r"(?:Caught |Script exited with: )(\w+Error[:\s].*?)(?:\n|$)", output)
|
|
72
|
+
if matches:
|
|
73
|
+
return matches[-1].strip()
|
|
74
|
+
|
|
75
|
+
matches = re.findall(r"(\w+Error: .+?)(?:\n|$)", output)
|
|
76
|
+
if matches:
|
|
77
|
+
return matches[-1].strip()
|
|
78
|
+
return None
|
|
@@ -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,30 @@
|
|
|
1
|
+
codesuture/__init__.py,sha256=LBK46heutvn3KmsCrKIYu8RQikbfnjZaj2xFrXaeCzQ,22
|
|
2
|
+
codesuture/__main__.py,sha256=BbUA6Fvc9zApaO4IGe9F8meluGvNQNMOr_Cp_aq4y6k,75
|
|
3
|
+
codesuture/_eval_fix.py,sha256=b7Qnx3FySkEwTjRSJmyOxPfQd5ihe5hO4KqFUa7MiW0,121
|
|
4
|
+
codesuture/audit.py,sha256=tUitXsNc3LqYvcB4iDqL7IFeGUkyTrj3LZPxCW6uWdo,4376
|
|
5
|
+
codesuture/cli.py,sha256=vcvQi83K1GDHqrEnDLFcIGQZ8L6nWZWaEfPt_fEwF54,6068
|
|
6
|
+
codesuture/code_replacer.py,sha256=UddSbpQ-5UQIWxrES6G8WUXQkpQmECRVlvh5BXK5mZM,2797
|
|
7
|
+
codesuture/codesuture_fix.py,sha256=u9-dYj8h_HsNKggRn6r0AB8wu9mofdAx-pbNLCEhUG0,3154
|
|
8
|
+
codesuture/debuggee.py,sha256=gavL8h28_olmUvhJrBC8jRbxmcnNz2cu8SbA-Vxz7bo,232
|
|
9
|
+
codesuture/diff_guard.py,sha256=1CWw-dIXajU4riN8PWkq5g-NEfHx03W8QmAtuDqHJ4U,972
|
|
10
|
+
codesuture/explain.py,sha256=7hg-S6xhc5k55kEkAjGNOC_C3_Ik33IpIAj0n2sJTXY,5066
|
|
11
|
+
codesuture/fingerprint.py,sha256=65wUsBsJ6iPMekIVkjZVDLeFVWx9uFG9cA92Y3ufIrM,1823
|
|
12
|
+
codesuture/guard_synthesizer.py,sha256=xNhc9U56sv2IlbRPG48QfttVv6KC-yglVAorU-c-slQ,21295
|
|
13
|
+
codesuture/knowledge.py,sha256=brZ8KWBXUq67KJUj7XE5toTB640rRhtzm9MurzK5xDo,1067
|
|
14
|
+
codesuture/middleware.py,sha256=RNmHt2mayMqLzGa174IQHJpF5TV4tF7_7BHTwD74-so,2633
|
|
15
|
+
codesuture/pattern_matcher.py,sha256=B1CxqiP7jE-yfiRsP80SpR7ywlTbLimAbuctfad8lvY,22105
|
|
16
|
+
codesuture/persistence.py,sha256=mIJgIF1ozcI-R9-ChqDTU2UxxK61tTbNa4UOzSHQOJY,12303
|
|
17
|
+
codesuture/rewind.py,sha256=U5YoGYwR2GSLQNX7j_uq-BCORazC7_E55VFrxZM6McE,1024
|
|
18
|
+
codesuture/rollback.py,sha256=B2CSdZnzflxHprnl97KY-DVCoTU5xIR07_vmAtKW5mE,2658
|
|
19
|
+
codesuture/sandbox.py,sha256=xuXcljY-dvRrhTPrK066B1w3ZkIt0Or-1nWWkK1Rk9M,3885
|
|
20
|
+
codesuture/shadow.py,sha256=hdNj5AoINEfbvhwjaA-cp-LW2WscKvqHKOK4P915qaE,637
|
|
21
|
+
codesuture/tracer.py,sha256=AfhamJV3F8PxGWWE87_eAOeWcNbOpCq3yXWP-BuG5b0,18923
|
|
22
|
+
codesuture/watcher.py,sha256=UStuV2jeNW4THXPbbxEzeW-RvI2W0GPn5OHKFcl6E-c,2293
|
|
23
|
+
codesuture/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
codesuture/plugins/autonomous.py,sha256=i9RFUYgYwP_oeXpvJXzxRVAjICkQEpN138vvcnBNUEo,2034
|
|
25
|
+
codesuture-0.5.0.dist-info/licenses/LICENSE,sha256=vIM1B2ElIJEtcTeUSgi1zL_ACNMZTHa73OfFGcy_6A0,2248
|
|
26
|
+
codesuture-0.5.0.dist-info/METADATA,sha256=KzErH1nZmwxmFzoZnQHcw7kVnL7FbcSpe1dqRX4HWyM,5967
|
|
27
|
+
codesuture-0.5.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
28
|
+
codesuture-0.5.0.dist-info/entry_points.txt,sha256=0qxyKDStiJLxP_0RmZhmxHoH-8mISXvulCP2cSyMbY4,51
|
|
29
|
+
codesuture-0.5.0.dist-info/top_level.txt,sha256=Hd9qfnlIMfoRq-xtdVRBBigFvJBX3Z9TY1QdD609uGw,11
|
|
30
|
+
codesuture-0.5.0.dist-info/RECORD,,
|