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
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import marshal
|
|
3
|
+
import sys
|
|
4
|
+
import importlib.abc
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
import threading
|
|
9
|
+
|
|
10
|
+
CACHE_DIR = ".codesuture_store"
|
|
11
|
+
HEALED_FUNCTIONS = set()
|
|
12
|
+
ANNOUNCED_HEALED_FUNCTIONS = set()
|
|
13
|
+
_store_lock = threading.Lock()
|
|
14
|
+
|
|
15
|
+
def _heal_key(module_name, func_name, key_name=None):
|
|
16
|
+
|
|
17
|
+
base = f"{module_name}.{func_name}"
|
|
18
|
+
if key_name:
|
|
19
|
+
return f"{base}:{key_name}"
|
|
20
|
+
return base
|
|
21
|
+
|
|
22
|
+
def save_patch(func, new_code, spec=None, ttl_days=7):
|
|
23
|
+
module_name = getattr(func, '__module__', None)
|
|
24
|
+
if not module_name:
|
|
25
|
+
return
|
|
26
|
+
func_name = getattr(func, '__qualname__', func.__name__)
|
|
27
|
+
func_name = func_name.replace('<', '_').replace('>', '_')
|
|
28
|
+
|
|
29
|
+
os.makedirs(CACHE_DIR, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
base_name = f"{module_name}.{func_name}"
|
|
32
|
+
cache_path = os.path.join(CACHE_DIR, f"{base_name}.code")
|
|
33
|
+
json_path = os.path.join(CACHE_DIR, f"{base_name}.json")
|
|
34
|
+
|
|
35
|
+
with _store_lock:
|
|
36
|
+
with open(cache_path, "wb") as f:
|
|
37
|
+
marshal.dump(new_code, f)
|
|
38
|
+
|
|
39
|
+
if spec is not None:
|
|
40
|
+
target_name = spec.var_name
|
|
41
|
+
if spec.strategy == 'null_guard' and spec.key_name:
|
|
42
|
+
target_name = spec.key_name[-1] if isinstance(spec.key_name, tuple) else spec.key_name
|
|
43
|
+
|
|
44
|
+
metadata = {
|
|
45
|
+
"func_name": func_name,
|
|
46
|
+
"guard_type": spec.strategy,
|
|
47
|
+
"target": target_name,
|
|
48
|
+
"default_value": spec.default_value,
|
|
49
|
+
"patched_at": datetime.utcnow().isoformat(),
|
|
50
|
+
"ttl_days": ttl_days
|
|
51
|
+
}
|
|
52
|
+
metadata["thread"] = "MainThread"
|
|
53
|
+
with _store_lock:
|
|
54
|
+
with open(json_path, "w", encoding="utf-8") as f:
|
|
55
|
+
json.dump(metadata, f, indent=2)
|
|
56
|
+
|
|
57
|
+
def _announce_healed(module_name, func_name, key_name=None):
|
|
58
|
+
key = _heal_key(module_name, func_name, key_name)
|
|
59
|
+
if key in ANNOUNCED_HEALED_FUNCTIONS:
|
|
60
|
+
return
|
|
61
|
+
ANNOUNCED_HEALED_FUNCTIONS.add(key)
|
|
62
|
+
if key_name:
|
|
63
|
+
print(f"[CodeSuture] Already healed, skipping: loaded persistent patch for {module_name}.{func_name} ({key_name})")
|
|
64
|
+
else:
|
|
65
|
+
print(f"[CodeSuture] Already healed, skipping: loaded persistent patch for {module_name}.{func_name}")
|
|
66
|
+
|
|
67
|
+
def _load_cached_code(module_name, func_name):
|
|
68
|
+
func_name = func_name.replace('<', '_').replace('>', '_')
|
|
69
|
+
if not os.path.isdir(CACHE_DIR):
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
base_name = f"{module_name}.{func_name}"
|
|
73
|
+
code_path = os.path.join(CACHE_DIR, f"{base_name}.code")
|
|
74
|
+
json_path = os.path.join(CACHE_DIR, f"{base_name}.json")
|
|
75
|
+
if not os.path.isfile(code_path):
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
if os.path.isfile(json_path):
|
|
79
|
+
try:
|
|
80
|
+
with open(json_path, "r", encoding="utf-8") as f:
|
|
81
|
+
patch_data = json.load(f)
|
|
82
|
+
patched_at = datetime.fromisoformat(patch_data["patched_at"])
|
|
83
|
+
ttl_days = patch_data.get("ttl_days", 7)
|
|
84
|
+
age_days = (datetime.utcnow() - patched_at).days
|
|
85
|
+
if age_days > ttl_days:
|
|
86
|
+
print(
|
|
87
|
+
f"[CodeSuture] [WARN] Patch for '{patch_data.get('func_name', func_name)}' "
|
|
88
|
+
f"is {age_days} day(s) old (TTL={ttl_days}d). "
|
|
89
|
+
f"Verify root cause is fixed in source. "
|
|
90
|
+
f"Run 'codesuture audit' to review all patches."
|
|
91
|
+
)
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
with open(code_path, "rb") as f:
|
|
96
|
+
return marshal.load(f)
|
|
97
|
+
|
|
98
|
+
def _load_learned_code(func, func_name):
|
|
99
|
+
from codesuture.knowledge import load_learned_rules
|
|
100
|
+
|
|
101
|
+
for rule in load_learned_rules():
|
|
102
|
+
if rule["func_name"] != func_name:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
from codesuture.guard_synthesizer import synthesize_guarded_code
|
|
106
|
+
from codesuture.pattern_matcher import PatchSpec
|
|
107
|
+
|
|
108
|
+
spec = PatchSpec(
|
|
109
|
+
strategy='autonomous_rule',
|
|
110
|
+
var_name=func_name,
|
|
111
|
+
default_value=rule["new_source"],
|
|
112
|
+
)
|
|
113
|
+
new_bc = synthesize_guarded_code(func.__code__, spec)
|
|
114
|
+
return new_bc.to_code()
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def apply_persisted_patch_to_function(func, module_name=None, func_name=None, key_name=None, announce=True):
|
|
118
|
+
|
|
119
|
+
module_name = module_name or getattr(func, '__module__', None)
|
|
120
|
+
func_name = func_name or getattr(func, '__qualname__', getattr(func, '__name__', None))
|
|
121
|
+
if not module_name or not func_name:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
new_code = _load_cached_code(module_name, func_name)
|
|
125
|
+
if new_code is None:
|
|
126
|
+
try:
|
|
127
|
+
new_code = _load_learned_code(func, func_name)
|
|
128
|
+
except Exception:
|
|
129
|
+
new_code = None
|
|
130
|
+
if new_code is None:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
from codesuture.code_replacer import replace_function_code
|
|
134
|
+
|
|
135
|
+
replace_function_code(func, new_code)
|
|
136
|
+
HEALED_FUNCTIONS.add(_heal_key(module_name, func_name, key_name))
|
|
137
|
+
if announce:
|
|
138
|
+
_announce_healed(module_name, func_name, key_name)
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
def _iter_cached_function_names(module_name):
|
|
142
|
+
if not os.path.isdir(CACHE_DIR):
|
|
143
|
+
return
|
|
144
|
+
prefix = f"{module_name}."
|
|
145
|
+
for filename in os.listdir(CACHE_DIR):
|
|
146
|
+
if filename.startswith(prefix) and filename.endswith(".code"):
|
|
147
|
+
yield filename[len(prefix):-5]
|
|
148
|
+
|
|
149
|
+
def _resolve_attr(root, dotted_name):
|
|
150
|
+
obj = root
|
|
151
|
+
for part in dotted_name.split('.'):
|
|
152
|
+
obj = getattr(obj, part)
|
|
153
|
+
return obj
|
|
154
|
+
|
|
155
|
+
def apply_persisted_patch_to_value(module_name, binding_name, value):
|
|
156
|
+
if hasattr(value, '__wrapped__'):
|
|
157
|
+
value = getattr(value, '__wrapped__')
|
|
158
|
+
if inspect.isfunction(value):
|
|
159
|
+
return apply_persisted_patch_to_function(value, module_name=module_name)
|
|
160
|
+
|
|
161
|
+
applied = False
|
|
162
|
+
if inspect.isclass(value):
|
|
163
|
+
prefix = f"{binding_name}."
|
|
164
|
+
for func_name in _iter_cached_function_names(module_name) or ():
|
|
165
|
+
if not func_name.startswith(prefix):
|
|
166
|
+
continue
|
|
167
|
+
try:
|
|
168
|
+
target = _resolve_attr(value, func_name[len(prefix):])
|
|
169
|
+
if isinstance(target, property) and target.fget is not None:
|
|
170
|
+
target = target.fget
|
|
171
|
+
if inspect.ismethod(target):
|
|
172
|
+
target = target.__func__
|
|
173
|
+
if hasattr(target, '__wrapped__'):
|
|
174
|
+
target = getattr(target, '__wrapped__')
|
|
175
|
+
if inspect.isfunction(target):
|
|
176
|
+
applied = apply_persisted_patch_to_function(
|
|
177
|
+
target,
|
|
178
|
+
module_name=module_name,
|
|
179
|
+
func_name=func_name,
|
|
180
|
+
) or applied
|
|
181
|
+
except Exception:
|
|
182
|
+
continue
|
|
183
|
+
return applied
|
|
184
|
+
|
|
185
|
+
class CodeSutureGlobals(dict):
|
|
186
|
+
def __init__(self, module_name, initial=None):
|
|
187
|
+
super().__init__()
|
|
188
|
+
self._codesuture_module_name = module_name
|
|
189
|
+
if initial:
|
|
190
|
+
for key, value in initial.items():
|
|
191
|
+
dict.__setitem__(self, key, value)
|
|
192
|
+
|
|
193
|
+
def __setitem__(self, key, value):
|
|
194
|
+
dict.__setitem__(self, key, value)
|
|
195
|
+
tracer = sys.gettrace()
|
|
196
|
+
sys.settrace(None)
|
|
197
|
+
try:
|
|
198
|
+
apply_persisted_patch_to_value(self._codesuture_module_name, key, value)
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
finally:
|
|
202
|
+
sys.settrace(tracer)
|
|
203
|
+
|
|
204
|
+
def make_persisted_patch_globals(module_name, initial=None):
|
|
205
|
+
return CodeSutureGlobals(module_name, initial)
|
|
206
|
+
|
|
207
|
+
def apply_persisted_patches(module):
|
|
208
|
+
module_name = getattr(module, '__name__', None)
|
|
209
|
+
if not module_name:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
for func_name in _iter_cached_function_names(module_name) or ():
|
|
213
|
+
try:
|
|
214
|
+
obj = _resolve_attr(module, func_name)
|
|
215
|
+
if isinstance(obj, property) and obj.fget is not None:
|
|
216
|
+
obj = obj.fget
|
|
217
|
+
if inspect.ismethod(obj):
|
|
218
|
+
obj = obj.__func__
|
|
219
|
+
if hasattr(obj, '__wrapped__'):
|
|
220
|
+
obj = getattr(obj, '__wrapped__')
|
|
221
|
+
if inspect.isfunction(obj):
|
|
222
|
+
apply_persisted_patch_to_function(obj, module_name, func_name)
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
from codesuture.knowledge import load_learned_rules
|
|
227
|
+
rules = load_learned_rules()
|
|
228
|
+
for rule in rules:
|
|
229
|
+
func_name = rule["func_name"]
|
|
230
|
+
|
|
231
|
+
parts = func_name.split('.')
|
|
232
|
+
obj = module
|
|
233
|
+
try:
|
|
234
|
+
for part in parts:
|
|
235
|
+
obj = getattr(obj, part)
|
|
236
|
+
|
|
237
|
+
if _heal_key(module_name, func_name) in HEALED_FUNCTIONS:
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
if isinstance(obj, property) and obj.fget is not None:
|
|
241
|
+
obj = obj.fget
|
|
242
|
+
if inspect.ismethod(obj):
|
|
243
|
+
obj = obj.__func__
|
|
244
|
+
if hasattr(obj, '__wrapped__'):
|
|
245
|
+
obj = getattr(obj, '__wrapped__')
|
|
246
|
+
if inspect.isfunction(obj):
|
|
247
|
+
apply_persisted_patch_to_function(obj, module_name, func_name)
|
|
248
|
+
except Exception:
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
class CodeSutureLoaderWrapper(importlib.abc.Loader):
|
|
252
|
+
def __init__(self, loader):
|
|
253
|
+
self.loader = loader
|
|
254
|
+
|
|
255
|
+
def create_module(self, spec):
|
|
256
|
+
if hasattr(self.loader, 'create_module'):
|
|
257
|
+
return self.loader.create_module(spec)
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
def exec_module(self, module):
|
|
261
|
+
self.loader.exec_module(module)
|
|
262
|
+
apply_persisted_patches(module)
|
|
263
|
+
|
|
264
|
+
def __getattr__(self, name):
|
|
265
|
+
return getattr(self.loader, name)
|
|
266
|
+
|
|
267
|
+
class CodeSutureMetaFinder(importlib.abc.MetaPathFinder):
|
|
268
|
+
def find_spec(self, fullname, path, target=None):
|
|
269
|
+
if hasattr(self, '_inside'): return None
|
|
270
|
+
self._inside = True
|
|
271
|
+
try:
|
|
272
|
+
for finder in sys.meta_path:
|
|
273
|
+
if finder is self: continue
|
|
274
|
+
if hasattr(finder, 'find_spec'):
|
|
275
|
+
spec = finder.find_spec(fullname, path, target)
|
|
276
|
+
if spec is not None and getattr(spec, 'loader', None) is not None:
|
|
277
|
+
if not isinstance(spec.loader, CodeSutureLoaderWrapper):
|
|
278
|
+
spec.loader = CodeSutureLoaderWrapper(spec.loader)
|
|
279
|
+
return spec
|
|
280
|
+
finally:
|
|
281
|
+
delattr(self, '_inside')
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
def install_import_hook():
|
|
285
|
+
if not any(isinstance(f, CodeSutureMetaFinder) for f in sys.meta_path):
|
|
286
|
+
sys.meta_path.insert(0, CodeSutureMetaFinder())
|
|
287
|
+
|
|
288
|
+
for module_name, module in list(sys.modules.items()):
|
|
289
|
+
if module is not None:
|
|
290
|
+
apply_persisted_patches(module)
|
|
291
|
+
|
|
292
|
+
def patch_script_code(code_obj, module_name="__main__"):
|
|
293
|
+
if not os.path.isdir(CACHE_DIR):
|
|
294
|
+
return code_obj
|
|
295
|
+
|
|
296
|
+
new_consts = list(code_obj.co_consts)
|
|
297
|
+
changed = False
|
|
298
|
+
for i, const in enumerate(new_consts):
|
|
299
|
+
if type(const).__name__ == 'code':
|
|
300
|
+
func_name = const.co_name
|
|
301
|
+
base_name = f"{module_name}.{func_name}"
|
|
302
|
+
code_path = os.path.join(CACHE_DIR, f"{base_name}.code")
|
|
303
|
+
new_code = None
|
|
304
|
+
if os.path.isfile(code_path):
|
|
305
|
+
with open(code_path, "rb") as f:
|
|
306
|
+
new_code = marshal.load(f)
|
|
307
|
+
else:
|
|
308
|
+
|
|
309
|
+
from codesuture.knowledge import load_learned_rules
|
|
310
|
+
rules = load_learned_rules()
|
|
311
|
+
for rule in rules:
|
|
312
|
+
if rule["func_name"] == func_name:
|
|
313
|
+
from codesuture.guard_synthesizer import synthesize_guarded_code
|
|
314
|
+
from codesuture.pattern_matcher import PatchSpec
|
|
315
|
+
spec = PatchSpec(strategy='autonomous_rule', var_name=func_name, default_value=rule["new_source"])
|
|
316
|
+
try:
|
|
317
|
+
new_bc = synthesize_guarded_code(const, spec)
|
|
318
|
+
new_code = new_bc.to_code()
|
|
319
|
+
except Exception:
|
|
320
|
+
continue
|
|
321
|
+
break
|
|
322
|
+
|
|
323
|
+
if new_code:
|
|
324
|
+
new_consts[i] = new_code
|
|
325
|
+
changed = True
|
|
326
|
+
HEALED_FUNCTIONS.add(_heal_key(module_name, func_name))
|
|
327
|
+
_announce_healed(module_name, func_name)
|
|
328
|
+
if changed:
|
|
329
|
+
return code_obj.replace(co_consts=tuple(new_consts))
|
|
330
|
+
return code_obj
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
_llm = None
|
|
5
|
+
|
|
6
|
+
def get_llm():
|
|
7
|
+
global _llm
|
|
8
|
+
if _llm is not None:
|
|
9
|
+
return _llm
|
|
10
|
+
|
|
11
|
+
from llama_cpp import Llama
|
|
12
|
+
model_path = os.environ.get("CODESUTURE_MODEL_PATH")
|
|
13
|
+
if not model_path or not os.path.exists(model_path):
|
|
14
|
+
raise FileNotFoundError(f"LLM model not found at {model_path}. Please download a model and set CODESUTURE_MODEL_PATH environment variable.")
|
|
15
|
+
|
|
16
|
+
print("[CodeSuture] Loading local LLM... this may take a moment.")
|
|
17
|
+
_llm = Llama(
|
|
18
|
+
model_path=model_path,
|
|
19
|
+
n_ctx=2048,
|
|
20
|
+
verbose=False
|
|
21
|
+
)
|
|
22
|
+
return _llm
|
|
23
|
+
|
|
24
|
+
def propose_fix(traceback_text, function_source, exc_type_name, exc_value):
|
|
25
|
+
llm = get_llm()
|
|
26
|
+
|
|
27
|
+
prompt = f"""<|system|>
|
|
28
|
+
You are an expert Python developer fixing bugs autonomously.
|
|
29
|
+
You are given the source code of a function that crashed, and the exception traceback.
|
|
30
|
+
Your task is to rewrite the ENTIRE function to safely handle the exception. You MUST modify the code inside the function to fix the error. The easiest way is to wrap the failing code in a try/except block and return a fallback value (like 0 or None), or to use an if-statement to check the inputs. Do NOT return the original crashing code. Do NOT output code outside of the function.
|
|
31
|
+
</s>
|
|
32
|
+
<|user|>
|
|
33
|
+
The function crashed with this error:
|
|
34
|
+
{exc_type_name}: {exc_value}
|
|
35
|
+
|
|
36
|
+
Original Source Code:
|
|
37
|
+
```python
|
|
38
|
+
{function_source}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Rewrite the ENTIRE function to fix this error. Output ONLY the valid python code block.
|
|
42
|
+
</s>
|
|
43
|
+
<|assistant|>
|
|
44
|
+
```python
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
print("[CodeSuture] Asking LLM for a fix...")
|
|
48
|
+
response = llm(
|
|
49
|
+
prompt,
|
|
50
|
+
max_tokens=512,
|
|
51
|
+
stop=["```\n", "</s>"],
|
|
52
|
+
temperature=0.2
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
output = response['choices'][0]['text']
|
|
56
|
+
|
|
57
|
+
if "```python" in output:
|
|
58
|
+
output = output.split("```python")[1]
|
|
59
|
+
if "```" in output:
|
|
60
|
+
output = output.split("```")[0]
|
|
61
|
+
|
|
62
|
+
output = output.strip()
|
|
63
|
+
print(f"\n[CodeSuture] LLM Proposed Fix:\n{output}\n")
|
|
64
|
+
return output
|
codesuture/rewind.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codesuture/rewind.py
|
|
3
|
+
Low‑level frame manipulation using ctypes.
|
|
4
|
+
"""
|
|
5
|
+
import ctypes
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
class PyObject(ctypes.Structure):
|
|
9
|
+
_fields_ = [
|
|
10
|
+
("ob_refcnt", ctypes.c_ssize_t),
|
|
11
|
+
("ob_type", ctypes.c_void_p),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
class PyVarObject(PyObject):
|
|
15
|
+
_fields_ = [
|
|
16
|
+
("ob_size", ctypes.c_ssize_t),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
class PyFrameObject(PyVarObject):
|
|
20
|
+
|
|
21
|
+
_fields_ = [
|
|
22
|
+
("f_back", ctypes.c_void_p),
|
|
23
|
+
("f_code", ctypes.c_void_p),
|
|
24
|
+
("f_builtins", ctypes.c_void_p),
|
|
25
|
+
("f_globals", ctypes.c_void_p),
|
|
26
|
+
("f_locals", ctypes.c_void_p),
|
|
27
|
+
("f_valuestack", ctypes.c_void_p),
|
|
28
|
+
("f_stacktop", ctypes.c_void_p),
|
|
29
|
+
("f_lasti", ctypes.c_int),
|
|
30
|
+
("f_lineno", ctypes.c_int),
|
|
31
|
+
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
def _cast_frame(frame):
|
|
35
|
+
|
|
36
|
+
return ctypes.cast(id(frame), ctypes.POINTER(PyFrameObject))
|
|
37
|
+
|
|
38
|
+
def rewind_frame_to_start(frame, code):
|
|
39
|
+
|
|
40
|
+
cf = _cast_frame(frame)
|
|
41
|
+
|
|
42
|
+
cf.contents.f_lasti = -1
|
|
43
|
+
cf.contents.f_lineno = code.co_firstlineno
|
codesuture/rollback.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from codesuture.persistence import CACHE_DIR
|
|
8
|
+
|
|
9
|
+
def rollback_function(name):
|
|
10
|
+
|
|
11
|
+
if not os.path.isdir(CACHE_DIR):
|
|
12
|
+
print("[CodeSuture] Nothing to roll back.")
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
removed = 0
|
|
16
|
+
for fname in list(os.listdir(CACHE_DIR)):
|
|
17
|
+
|
|
18
|
+
base = fname.rsplit(".", 1)[0]
|
|
19
|
+
func_part = base.split(".", 1)[-1] if "." in base else base
|
|
20
|
+
|
|
21
|
+
if func_part == name or base == name or func_part.endswith(name):
|
|
22
|
+
path = os.path.join(CACHE_DIR, fname)
|
|
23
|
+
os.remove(path)
|
|
24
|
+
removed += 1
|
|
25
|
+
|
|
26
|
+
if removed > 0:
|
|
27
|
+
print(f"[CodeSuture] Rolled back patch for '{name}'. "
|
|
28
|
+
f"Run your script again to re-patch if needed.")
|
|
29
|
+
else:
|
|
30
|
+
print(f"[CodeSuture] No patch found matching '{name}'.")
|
|
31
|
+
|
|
32
|
+
def rollback_all():
|
|
33
|
+
|
|
34
|
+
count = 0
|
|
35
|
+
if os.path.isdir(CACHE_DIR):
|
|
36
|
+
count = len(os.listdir(CACHE_DIR))
|
|
37
|
+
if count == 0:
|
|
38
|
+
print("[CodeSuture] Nothing to roll back.")
|
|
39
|
+
return
|
|
40
|
+
shutil.rmtree(CACHE_DIR)
|
|
41
|
+
else:
|
|
42
|
+
print("[CodeSuture] Nothing to roll back.")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
fp = ".codesuture_fingerprints"
|
|
46
|
+
if os.path.isfile(fp):
|
|
47
|
+
os.remove(fp)
|
|
48
|
+
|
|
49
|
+
print(f"[CodeSuture] Cleared {count} patch file(s) and fingerprint registry.")
|
|
50
|
+
|
|
51
|
+
def rollback_dry_run():
|
|
52
|
+
|
|
53
|
+
if not os.path.isdir(CACHE_DIR):
|
|
54
|
+
print("[CodeSuture] Nothing to roll back. Store does not exist.")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
json_files = [f for f in os.listdir(CACHE_DIR) if f.endswith(".json")]
|
|
58
|
+
if not json_files:
|
|
59
|
+
print("[CodeSuture] Nothing to roll back. No patches found.")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
now = datetime.utcnow()
|
|
63
|
+
print()
|
|
64
|
+
print(" [CodeSuture DRY-RUN] Would remove the following patches:")
|
|
65
|
+
print()
|
|
66
|
+
for jf in json_files:
|
|
67
|
+
path = os.path.join(CACHE_DIR, jf)
|
|
68
|
+
try:
|
|
69
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
70
|
+
data = json.load(f)
|
|
71
|
+
func = data.get("func_name", "?")
|
|
72
|
+
guard = data.get("guard_type", "?")
|
|
73
|
+
age = "?"
|
|
74
|
+
if "patched_at" in data:
|
|
75
|
+
dt = datetime.fromisoformat(data["patched_at"])
|
|
76
|
+
age = f"{(now - dt).days}d"
|
|
77
|
+
print(f" - {func} guard={guard} age={age}")
|
|
78
|
+
except Exception:
|
|
79
|
+
print(f" - {jf} (could not read metadata)")
|
|
80
|
+
|
|
81
|
+
fp = ".codesuture_fingerprints"
|
|
82
|
+
if os.path.isfile(fp):
|
|
83
|
+
print(f" - .codesuture_fingerprints (fingerprint registry)")
|
|
84
|
+
print()
|
|
85
|
+
print(" Run 'codesuture rollback --all' to actually remove them.")
|
codesuture/sandbox.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
def test_fix(original_script_path, module_name, func_name, new_source, exc_type_name):
|
|
7
|
+
print(f"[CodeSuture Sandbox] Testing fix for {module_name}.{func_name}...")
|
|
8
|
+
|
|
9
|
+
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".py", encoding='utf-8') as f:
|
|
10
|
+
f.write(new_source)
|
|
11
|
+
source_path = f.name
|
|
12
|
+
|
|
13
|
+
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".py", encoding='utf-8') as f:
|
|
14
|
+
runner_code = f"""
|
|
15
|
+
import sys
|
|
16
|
+
import importlib
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
with open({repr(source_path)}, 'r', encoding='utf-8') as f:
|
|
20
|
+
new_source = f.read()
|
|
21
|
+
|
|
22
|
+
new_module_code = compile(new_source, "<sandbox>", 'exec')
|
|
23
|
+
new_func_code = None
|
|
24
|
+
for const in new_module_code.co_consts:
|
|
25
|
+
if type(const).__name__ == 'code' and const.co_name == {repr(func_name)}:
|
|
26
|
+
new_func_code = const
|
|
27
|
+
break
|
|
28
|
+
|
|
29
|
+
if not new_func_code:
|
|
30
|
+
print("SANDBOX ERROR: Could not find function in compiled new source")
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
if {repr(module_name)} != '__main__':
|
|
34
|
+
from codesuture.code_replacer import replace_function_code
|
|
35
|
+
mod = importlib.import_module({repr(module_name)})
|
|
36
|
+
# Find the function object inside the module
|
|
37
|
+
parts = {repr(func_name)}.split('.')
|
|
38
|
+
obj = mod
|
|
39
|
+
for part in parts:
|
|
40
|
+
obj = getattr(obj, part)
|
|
41
|
+
replace_function_code(obj, new_func_code)
|
|
42
|
+
|
|
43
|
+
# Now run the script
|
|
44
|
+
with open({repr(original_script_path)}, 'r', encoding='utf-8') as script_file:
|
|
45
|
+
source = script_file.read()
|
|
46
|
+
code = compile(source, {repr(original_script_path)}, 'exec')
|
|
47
|
+
globs = {{'__name__': '__main__', '__file__': {repr(original_script_path)}}}
|
|
48
|
+
try:
|
|
49
|
+
exec(code, globs)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print(f"SANDBOX EXCEPTION: {{type(e).__name__}}")
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
else:
|
|
54
|
+
# Patching __main__ script
|
|
55
|
+
with open({repr(original_script_path)}, 'r', encoding='utf-8') as script_file:
|
|
56
|
+
source = script_file.read()
|
|
57
|
+
code = compile(source, {repr(original_script_path)}, 'exec')
|
|
58
|
+
|
|
59
|
+
new_consts = list(code.co_consts)
|
|
60
|
+
for i, const in enumerate(new_consts):
|
|
61
|
+
if type(const).__name__ == 'code' and const.co_name == {repr(func_name)}:
|
|
62
|
+
new_consts[i] = new_func_code
|
|
63
|
+
break
|
|
64
|
+
code = code.replace(co_consts=tuple(new_consts))
|
|
65
|
+
|
|
66
|
+
globs = {{'__name__': '__main__', '__file__': {repr(original_script_path)}}}
|
|
67
|
+
try:
|
|
68
|
+
exec(code, globs)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
print(f"SANDBOX EXCEPTION: {{type(e).__name__}}")
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
if __name__ == '__main__':
|
|
74
|
+
main()
|
|
75
|
+
"""
|
|
76
|
+
f.write(runner_code)
|
|
77
|
+
runner_path = f.name
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
result = subprocess.run(
|
|
81
|
+
[sys.executable, runner_path],
|
|
82
|
+
capture_output=True,
|
|
83
|
+
text=True,
|
|
84
|
+
timeout=3
|
|
85
|
+
)
|
|
86
|
+
output = result.stdout + result.stderr
|
|
87
|
+
|
|
88
|
+
if f"SANDBOX EXCEPTION: {exc_type_name}" in output:
|
|
89
|
+
print("[CodeSuture Sandbox] Fix FAILED: original exception still occurs.")
|
|
90
|
+
return False
|
|
91
|
+
elif "SANDBOX EXCEPTION" in output:
|
|
92
|
+
print(f"[CodeSuture Sandbox] Fix FAILED: caused a new exception.\n{output}")
|
|
93
|
+
return False
|
|
94
|
+
elif result.returncode != 0:
|
|
95
|
+
print(f"[CodeSuture Sandbox] Fix FAILED: subprocess exited with error.\n{output}")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
print("[CodeSuture Sandbox] Fix PASSED!")
|
|
99
|
+
return True
|
|
100
|
+
except subprocess.TimeoutExpired:
|
|
101
|
+
print("[CodeSuture Sandbox] Fix FAILED: Timeout (possible infinite loop).")
|
|
102
|
+
return False
|
|
103
|
+
finally:
|
|
104
|
+
os.remove(source_path)
|
|
105
|
+
os.remove(runner_path)
|
codesuture/shadow.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
SENTINEL_VALUES = {"", 0, 0.0, None, False, (), frozenset()}
|
|
2
|
+
|
|
3
|
+
def is_sentinel(value) -> bool:
|
|
4
|
+
try:
|
|
5
|
+
if value in SENTINEL_VALUES:
|
|
6
|
+
return True
|
|
7
|
+
if value == [] or value == {}:
|
|
8
|
+
return True
|
|
9
|
+
except Exception:
|
|
10
|
+
pass
|
|
11
|
+
return False
|
|
12
|
+
|
|
13
|
+
def shadow_check(func_name: str, return_value, guard_type: str):
|
|
14
|
+
if is_sentinel(return_value):
|
|
15
|
+
print(
|
|
16
|
+
f"[CodeSuture SHADOW] âš {func_name}() returned sentinel "
|
|
17
|
+
f"value {return_value!r} after {guard_type} patch. "
|
|
18
|
+
f"Verify this default is safe for downstream consumers."
|
|
19
|
+
)
|
|
20
|
+
|