better-dsh 0.3.0 → 0.3.1-a

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 (76) hide show
  1. package/README.md +49 -177
  2. package/cordis.patch.yml +5 -36
  3. package/docs/20_specs/kernel-provisioning/spec.md +2 -0
  4. package/docs/adr/0015-independent-bundles-agent-remote.md +40 -0
  5. package/docs/remote-tools.md +56 -0
  6. package/docs/specs/kernel-provisioning/spec.md +12 -40
  7. package/docs/specs/omp-python-repl-port/runtime-guide.md +35 -0
  8. package/docs/specs/omp-python-repl-port/spec.md +68 -0
  9. package/docs/specs/remote-tool-enhancement/spec.md +203 -0
  10. package/dsh-docs/baseline.md +13 -0
  11. package/dsh-docs/subsystems/invariants.md +3 -0
  12. package/dsh-docs/subsystems/invariants.zh.md +3 -0
  13. package/eval-description.md +7 -3
  14. package/lib/LaunchOptions-0Mr6meJc.js +4 -0
  15. package/lib/{LaunchOptions-kGXcZOe0.js → LaunchOptions-iE9M-STS.js} +1 -1
  16. package/lib/{bidi-BalpKaJc.js → bidi-CaQeBZHY.js} +1 -1
  17. package/lib/client/index.js +30 -6
  18. package/lib/failover/index.js +1 -1
  19. package/lib/fs-aware/sandbox-plugin.js +3 -3
  20. package/lib/{index-browser-CysrjMyM.js → index-browser-CJzxT9Vi.js} +4 -4
  21. package/lib/index.d.ts +16 -50
  22. package/lib/index.js +482 -1720
  23. package/lib/kernel-env-x_WkG6Fr.js +76 -0
  24. package/lib/kernel-env.d.ts +5 -69
  25. package/lib/kernel-env.js +2 -2
  26. package/lib/{lsp-server-registry-BXvUTBY7.js → lsp-server-registry-BcdkHROD.js} +1 -1
  27. package/lib/{lsp-server-registry-CH9WNFFt.js → lsp-server-registry-D4VLs1Sm.js} +1 -1
  28. package/lib/{main-BQeFrBM1.js → main-taSMz1KX.js} +1 -1
  29. package/lib/{main-CupOhMp5.js → main-uqGqYYnA.js} +7 -5
  30. package/lib/mobile/plugin.d.ts +1 -1
  31. package/lib/omp-python/DILL-LICENSE +35 -0
  32. package/lib/omp-python/OMP-LICENSE +23 -0
  33. package/lib/omp-python/better_snapshot.py +391 -0
  34. package/lib/omp-python/better_support.py +425 -0
  35. package/lib/omp-python/dill-provenance.json +55 -0
  36. package/lib/omp-python/provenance.json +13 -0
  37. package/lib/omp-python/runner.py +2499 -0
  38. package/lib/omp-python/vendor/dill/__diff.py +234 -0
  39. package/lib/omp-python/vendor/dill/__info__.py +291 -0
  40. package/lib/omp-python/vendor/dill/__init__.py +119 -0
  41. package/lib/omp-python/vendor/dill/_dill.py +2265 -0
  42. package/lib/omp-python/vendor/dill/_objects.py +545 -0
  43. package/lib/omp-python/vendor/dill/_shims.py +193 -0
  44. package/lib/omp-python/vendor/dill/detect.py +287 -0
  45. package/lib/omp-python/vendor/dill/logger.py +285 -0
  46. package/lib/omp-python/vendor/dill/objtypes.py +24 -0
  47. package/lib/omp-python/vendor/dill/pointers.py +122 -0
  48. package/lib/omp-python/vendor/dill/session.py +612 -0
  49. package/lib/omp-python/vendor/dill/settings.py +25 -0
  50. package/lib/omp-python/vendor/dill/source.py +1025 -0
  51. package/lib/omp-python/vendor/dill/temp.py +252 -0
  52. package/lib/{puppeteer-core-CSk2PtSm.js → puppeteer-core-C-4NyOpY.js} +8 -8
  53. package/lib/remote/plugin.d.ts +459 -5
  54. package/lib/remote/plugin.js +2389 -210
  55. package/lib/url-schemes/index.d.ts +11 -11
  56. package/lib/url-schemes/index.js +11 -11
  57. package/lib/web-password.d.ts +8 -8
  58. package/lib/web-password.js +5 -1
  59. package/lib/{web-trust-Dej_RI2B.d.ts → web-trust-C_Y6o8Z2.d.ts} +11 -11
  60. package/lib/web-trust.d.ts +1 -2
  61. package/lib/welcome-notice.d.ts +5 -7
  62. package/lib/welcome-notice.js +9 -48
  63. package/lib/{wrap-T4I0nfPM.js → wrap-0VA0uFx8.js} +1 -1
  64. package/package.json +7 -24
  65. package/scripts/kernel-check.mjs +5 -0
  66. package/lib/LaunchOptions-B874eBu2.js +0 -4
  67. package/lib/bundle-delivery.d.ts +0 -20
  68. package/lib/bundle-delivery.js +0 -209
  69. package/lib/hmr.d.ts +0 -32
  70. package/lib/hmr.js +0 -49
  71. package/lib/kernel-bridge.py +0 -172
  72. package/lib/kernel-env-D8UOWeji.js +0 -195
  73. package/lib/webserver.d.ts +0 -10
  74. package/lib/webserver.js +0 -19
  75. package/scripts/kernel-provision.mjs +0 -35
  76. package/scripts/register-bundle.mjs +0 -80
@@ -0,0 +1,391 @@
1
+ """Best-effort checkpoints for the owned OMP namespace; dill 0.4.1 adapter.
2
+
3
+ No cell history is stored/replayed. Pickling user objects is not a sandbox:
4
+ reducers may execute arbitrary code. Host deadlines bound helper execution.
5
+ """
6
+ import _thread
7
+ import asyncio
8
+ import dis
9
+ import functools
10
+ import hashlib
11
+ import io
12
+ import json
13
+ import os
14
+ import platform
15
+ import re
16
+ import socket
17
+ import sqlite3
18
+ import subprocess
19
+ import sys
20
+ import threading
21
+ import time
22
+ import types
23
+ import uuid
24
+
25
+ # Select the unchanged packaged implementation before ANY user cell can import dill.
26
+ _VENDOR = os.path.join(os.path.dirname(__file__), 'vendor')
27
+ sys.path.insert(0, _VENDOR)
28
+ import dill
29
+ if dill.__version__ != '0.4.1' or os.path.realpath(dill.__file__) != os.path.realpath(os.path.join(_VENDOR, 'dill', '__init__.py')):
30
+ raise RuntimeError('Better requires its packaged dill 0.4.1')
31
+
32
+ _DIRECTORY = None
33
+ _CAP = None
34
+ _TIMEOUT = 30.0
35
+ _FORMAT = 2
36
+ _RESOURCES = (io.IOBase, _thread.LockType, type(threading.RLock()), socket.socket,
37
+ subprocess.Popen, threading.Thread, sqlite3.Connection, sqlite3.Cursor,
38
+ asyncio.Future, asyncio.AbstractEventLoop, types.GeneratorType,
39
+ types.CoroutineType, types.AsyncGeneratorType)
40
+ _PRIMITIVES = (str, bytes, int, float, bool, type(None))
41
+
42
+
43
+ class UnsupportedResource(Exception):
44
+ pass
45
+
46
+
47
+ class SizeCap(Exception):
48
+ pass
49
+
50
+
51
+ class NamespaceProxy:
52
+ pass
53
+
54
+
55
+ def _proxy(ns):
56
+ proxy = NamespaceProxy()
57
+ proxy.__dict__ = ns
58
+ return proxy
59
+
60
+
61
+ def configure(directory, cap, timeout_ms):
62
+ global _DIRECTORY, _CAP, _TIMEOUT
63
+ _DIRECTORY = os.path.abspath(directory) if directory else None
64
+ _CAP = cap
65
+ _TIMEOUT = timeout_ms / 1000.0 * 0.9
66
+
67
+
68
+ def clear():
69
+ """Delete only our fixed manifest and generation files, including orphans."""
70
+ if _DIRECTORY and os.path.islink(_DIRECTORY):
71
+ raise ValueError('checkpoint directory cannot be a symlink')
72
+ if not _DIRECTORY or not os.path.isdir(_DIRECTORY):
73
+ return
74
+ for name in os.listdir(_DIRECTORY):
75
+ if name == 'manifest.json' or re.fullmatch(r'(state-[0-9a-f]{32}\.dill|\.checkpoint-[0-9a-f]{32}\.tmp)', name):
76
+ os.unlink(os.path.join(_DIRECTORY, name))
77
+
78
+
79
+ def _paths(payload_path, manifest_path):
80
+ if not _DIRECTORY or os.path.abspath(manifest_path) != os.path.join(_DIRECTORY, 'manifest.json') or os.path.abspath(payload_path) != os.path.join(_DIRECTORY, 'state.dill'):
81
+ raise ValueError('checkpoint paths are not the configured session directory')
82
+ os.makedirs(_DIRECTORY, mode=0o700, exist_ok=True)
83
+ if os.path.islink(_DIRECTORY):
84
+ raise ValueError('checkpoint directory cannot be a symlink')
85
+ if os.path.islink(manifest_path):
86
+ raise ValueError('checkpoint manifest cannot be a symlink')
87
+ os.chmod(_DIRECTORY, 0o700)
88
+
89
+
90
+ def _environment():
91
+ return dict(pythonVersion=platform.python_version(), implementation=platform.python_implementation(),
92
+ interpreter=os.path.realpath(sys.executable), venvPath=sys.prefix,
93
+ environmentHash=hashlib.sha256(json.dumps([sys.prefix, sys.base_prefix, sys.path], sort_keys=True).encode()).hexdigest(),
94
+ dillVersion=dill.__version__, protocol=4, skills=[])
95
+
96
+
97
+ def _guard(ns):
98
+ import better_support
99
+ owned = [sys.modules['__main__'], sys.modules['better_support'], sys.modules[__name__]]
100
+ owned.extend(better_support.owned_objects())
101
+ denied = {id(value) for value in owned if not isinstance(value, _PRIMITIVES)}
102
+ def guard(value):
103
+ if id(value) in denied or (isinstance(value, (types.FunctionType, type)) and getattr(value, '__module__', None) in ('better_support', 'better_snapshot')) or type(value).__module__ == 'better_support':
104
+ raise UnsupportedResource('captured Better/runner binding')
105
+ if isinstance(value, _RESOURCES):
106
+ raise UnsupportedResource('unsupported live resource ' + type(value).__name__)
107
+ return None
108
+ return guard
109
+
110
+
111
+ class CappedBuffer(io.BytesIO):
112
+ def __init__(self, cap):
113
+ super().__init__()
114
+ self.cap = cap
115
+ def write(self, value):
116
+ if self.tell() + len(value) > self.cap:
117
+ raise SizeCap('serialized graph exceeds snapshotSizeCapBytes')
118
+ return super().write(value)
119
+
120
+
121
+ def _encode(ns, values, cap, guard):
122
+ output = CappedBuffer(cap)
123
+ # Exact Pickler: subclassing disables pinned dill's main-module logic.
124
+ pickler = dill.Pickler(output, protocol=4, recurse=False, byref=False)
125
+ pickler._main = _proxy(ns)
126
+ pickler._original_main = pickler._main
127
+ pickler._main_modified = True
128
+ pickler.persistent_id = guard
129
+ pickler.dump(values)
130
+ return output.getvalue()
131
+
132
+
133
+ def _decode(ns, payload):
134
+ unpickler = dill.Unpickler(io.BytesIO(payload))
135
+ unpickler._main = _proxy(ns)
136
+ unpickler._ignore = True
137
+ return unpickler.load()
138
+
139
+
140
+ def _dependencies(root, ns):
141
+ """Bounded static global scan; dynamic eval/globals lookups remain best effort."""
142
+ found, seen = set(), set()
143
+ def walk(value):
144
+ if id(value) in seen or isinstance(value, _PRIMITIVES):
145
+ return
146
+ seen.add(id(value))
147
+ if len(seen) > 10000:
148
+ raise UnsupportedResource('dependency graph exceeds inspection budget')
149
+ if isinstance(value, types.FunctionType):
150
+ if value.__globals__ is ns:
151
+ def code_names(code):
152
+ for op in dis.get_instructions(code):
153
+ if op.opname in ('LOAD_GLOBAL', 'LOAD_NAME'):
154
+ found.add(op.argval)
155
+ for item in code.co_consts:
156
+ if isinstance(item, types.CodeType): code_names(item)
157
+ code_names(value.__code__)
158
+ walk(value.__defaults__)
159
+ walk(value.__kwdefaults__)
160
+ for cell in value.__closure__ or ():
161
+ try: walk(cell.cell_contents)
162
+ except ValueError: pass
163
+ elif isinstance(value, functools.partial):
164
+ walk(value.func); walk(value.args); walk(value.keywords); walk(vars(value))
165
+ elif isinstance(value, (staticmethod, classmethod)):
166
+ walk(value.__func__)
167
+ elif isinstance(value, property):
168
+ walk(value.fget); walk(value.fset); walk(value.fdel)
169
+ elif isinstance(value, types.MethodType):
170
+ walk(value.__func__); walk(value.__self__)
171
+ elif isinstance(value, dict):
172
+ for key, item in value.items(): walk(key); walk(item)
173
+ elif isinstance(value, (tuple, list, set, frozenset)):
174
+ for item in value: walk(item)
175
+ elif isinstance(value, type):
176
+ if value.__module__ == ns.get('__name__'):
177
+ for item in vars(value).values(): walk(item)
178
+ for base in value.__bases__: walk(base)
179
+ elif not isinstance(value, types.ModuleType) and type(value).__module__ == ns.get('__name__'):
180
+ walk(type(value))
181
+ if hasattr(value, '__dict__'): walk(vars(value))
182
+ # Scan descriptors across the MRO, including inherited/mangled slots.
183
+ for owner in type(value).__mro__:
184
+ for descriptor in vars(owner).values():
185
+ if isinstance(descriptor, types.MemberDescriptorType):
186
+ try: walk(descriptor.__get__(value, type(value)))
187
+ except AttributeError: pass # uninitialized slot
188
+ walk(root)
189
+ return found
190
+
191
+
192
+ def _write_new(path, data):
193
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
194
+ with os.fdopen(fd, 'wb') as output:
195
+ output.write(data)
196
+ output.flush()
197
+ os.fsync(output.fileno())
198
+
199
+
200
+ def _emit(sentinel, result):
201
+ print(sentinel + json.dumps(result, ensure_ascii=True))
202
+
203
+
204
+ def _retained_payloads(manifest_path):
205
+ try:
206
+ with open(manifest_path, encoding='utf-8') as source:
207
+ manifest = json.load(source)
208
+ except (FileNotFoundError, ValueError):
209
+ return set(), None
210
+ if not isinstance(manifest, dict):
211
+ return set(), None
212
+ current = manifest.get('payload')
213
+ names = (current, manifest.get('previousPayload'))
214
+ retained = {name for name in names if isinstance(name, str) and re.fullmatch(r'state-[0-9a-f]{32}\.dill', name)}
215
+ return retained, current if isinstance(current, str) and current in retained else None
216
+
217
+
218
+ def _scavenge(retained):
219
+ for name in os.listdir(_DIRECTORY):
220
+ if name not in retained and re.fullmatch(r'(state-[0-9a-f]{32}\.dill|\.checkpoint-[0-9a-f]{32}\.tmp)', name):
221
+ os.unlink(os.path.join(_DIRECTORY, name))
222
+
223
+
224
+ def save(ns, payload_path, manifest_path, spec_json, sentinel):
225
+ attempt_paths, committed, result = [], False, None
226
+ payload_name, manifest, retained = None, None, set()
227
+ try:
228
+ _paths(payload_path, manifest_path)
229
+ retained, previous = _retained_payloads(manifest_path)
230
+ _scavenge(retained) # collect killed-writer artifacts even if this attempt fails
231
+ deadline = time.monotonic() + _TIMEOUT
232
+ def check_deadline():
233
+ if time.monotonic() >= deadline:
234
+ raise TimeoutError('checkpoint deadline exceeded; previous generation retained')
235
+ spec = json.loads(spec_json)
236
+ cap = min(spec['sizeCapBytes'], _CAP)
237
+ import better_support
238
+ candidates = better_support.user_namespace(ns)
239
+ guard = _guard(ns)
240
+ kept, omitted, dependencies = {}, {}, {}
241
+ for name, value in candidates.items():
242
+ check_deadline()
243
+ try:
244
+ _encode(ns, {name: value}, cap, guard)
245
+ dependencies[name] = _dependencies(value, ns)
246
+ kept[name] = value
247
+ except SizeCap:
248
+ raise
249
+ except Exception as error:
250
+ omitted[name] = type(error).__name__ + ': ' + str(error)
251
+ changed = True
252
+ while changed:
253
+ changed = False
254
+ for name in list(kept):
255
+ missing = dependencies[name].intersection(omitted)
256
+ if missing:
257
+ omitted[name] = 'depends on omitted globals: ' + ', '.join(sorted(missing))
258
+ del kept[name]
259
+ changed = True
260
+ check_deadline()
261
+ payload = _encode(ns, kept, cap, guard)
262
+ # Verify the FINAL shared graph, never independently reconstruct values.
263
+ trial_ns = dict(ns)
264
+ trial = _decode(trial_ns, payload)
265
+ if not isinstance(trial, dict) or set(trial) != set(kept):
266
+ raise ValueError('final graph round-trip changed admitted names')
267
+ trial_ns.update(trial)
268
+ groups = {}
269
+ for name, value in kept.items():
270
+ if id(value) in groups and trial[name] is not trial[groups[id(value)]]:
271
+ raise ValueError('final graph round-trip lost shared root identity')
272
+ groups[id(value)] = name
273
+ token = uuid.uuid4().hex
274
+ payload_name = 'state-' + token + '.dill'
275
+ manifest = dict(_environment(), snapshotFormat=_FORMAT, turn=spec['turn'],
276
+ payload=payload_name, previousPayload=previous, sizeBytes=len(payload), sha256=hashlib.sha256(payload).hexdigest(),
277
+ names=sorted(kept), dependencies={name: sorted(dependencies[name]) for name in kept},
278
+ bindingNames=sorted(better_support.injected_names()), omitted=omitted, coverage='partial' if omitted else 'best-effort',
279
+ skipped=False, limitations=['Dynamic globals/eval and arbitrary extension resources cannot be exhaustively inspected.'])
280
+ payload_file = os.path.join(_DIRECTORY, payload_name)
281
+ attempt_paths.append(payload_file)
282
+ _write_new(payload_file, payload)
283
+ temp = os.path.join(_DIRECTORY, '.checkpoint-' + token + '.tmp')
284
+ attempt_paths.append(temp)
285
+ _write_new(temp, json.dumps(manifest, sort_keys=True).encode())
286
+ # Sole commit point. Cleanup must never remove this payload afterwards.
287
+ check_deadline()
288
+ os.replace(temp, manifest_path)
289
+ committed = True
290
+ result = dict(kind='ok', sizeBytes=len(payload), names=manifest['names'], omitted=omitted, turn=spec['turn'])
291
+ warnings = []
292
+ if os.name == 'posix':
293
+ try:
294
+ fd = os.open(_DIRECTORY, os.O_RDONLY)
295
+ try: os.fsync(fd)
296
+ finally: os.close(fd)
297
+ except OSError as error:
298
+ warnings.append('directory fsync failed: ' + str(error))
299
+ try:
300
+ _scavenge({payload_name, previous})
301
+ except OSError as error:
302
+ warnings.append('stale artifact cleanup failed: ' + str(error))
303
+ if warnings:
304
+ result['warning'] = 'checkpoint committed; ' + '; '.join(warnings)
305
+ except SizeCap as error:
306
+ result = dict(kind='skipped', reason=str(error), capBytes=_CAP)
307
+ except Exception as error:
308
+ reason = type(error).__name__ + ': ' + str(error)
309
+ if payload_name is not None and not committed:
310
+ try:
311
+ _, pointed = _retained_payloads(manifest_path)
312
+ committed = pointed == payload_name
313
+ except OSError:
314
+ pass
315
+ if committed:
316
+ result = dict(kind='ok', sizeBytes=len(payload), names=manifest['names'], omitted=omitted, turn=spec['turn'])
317
+ result['warning'] = 'checkpoint committed; postcommit finalization failed: ' + reason
318
+ else:
319
+ result = dict(kind='failed', reason=reason)
320
+ finally:
321
+ if not committed and attempt_paths:
322
+ # A signal/error can arrive immediately after replace, before the
323
+ # local flag changes. The pointer is authoritative for deletion.
324
+ try:
325
+ latest, _ = _retained_payloads(manifest_path)
326
+ retained.update(latest)
327
+ except OSError:
328
+ # Cannot prove non-commit: leave recognized artifacts for the
329
+ # next save's pre-write scavenger rather than risk data loss.
330
+ retained.update(os.path.basename(path) for path in attempt_paths)
331
+ for path in attempt_paths:
332
+ if os.path.basename(path) in retained:
333
+ continue
334
+ try: os.unlink(path)
335
+ except FileNotFoundError: pass
336
+ except OSError as error:
337
+ if result is not None:
338
+ result['reason'] += '; uncommitted artifact cleanup failed: ' + str(error)
339
+ if result is not None:
340
+ _emit(sentinel, result)
341
+
342
+
343
+ def restore(ns, payload_path, manifest_path, sentinel, bindings_json):
344
+ try:
345
+ _paths(payload_path, manifest_path)
346
+ if os.path.getsize(manifest_path) > 1048576:
347
+ raise ValueError('manifest exceeds metadata cap')
348
+ with open(manifest_path, encoding='utf-8') as source:
349
+ manifest = json.load(source)
350
+ if not isinstance(manifest, dict) or manifest.get('snapshotFormat') != _FORMAT:
351
+ raise ValueError('unsupported snapshot format')
352
+ for key, value in _environment().items():
353
+ if manifest.get(key) != value: raise ValueError('checkpoint environment mismatch: ' + key)
354
+ name = manifest.get('payload', '')
355
+ if not isinstance(name, str) or not re.fullmatch(r'state-[0-9a-f]{32}\.dill', name):
356
+ raise ValueError('invalid owned payload name')
357
+ names = manifest.get('names')
358
+ import better_support
359
+ if not isinstance(names, list) or any(not isinstance(n, str) or n.startswith('__omp_') or n in ('__name__', '__builtins__', '__doc__') for n in names):
360
+ raise ValueError('invalid checkpoint namespace names')
361
+ turn = manifest.get('turn')
362
+ if not isinstance(turn, int) or isinstance(turn, bool) or turn < 0:
363
+ raise ValueError('invalid checkpoint turn')
364
+ path = os.path.join(_DIRECTORY, name)
365
+ size = os.path.getsize(path)
366
+ if os.path.islink(path) or size != manifest.get('sizeBytes') or size > _CAP:
367
+ raise ValueError('payload size/cap/path validation failed')
368
+ with open(path, 'rb') as source: payload = source.read(_CAP + 1)
369
+ if hashlib.sha256(payload).hexdigest() != manifest.get('sha256'):
370
+ raise ValueError('payload integrity hash mismatch')
371
+ values = _decode(ns, payload)
372
+ if not isinstance(values, dict) or sorted(values) != sorted(names):
373
+ raise ValueError('payload namespace differs from manifest')
374
+ omitted = dict(manifest.get('omitted', {}))
375
+ unavailable = set(manifest.get('bindingNames', [])) - set(json.loads(bindings_json))
376
+ dependencies = manifest.get('dependencies', {})
377
+ changed = True
378
+ while changed:
379
+ changed = False
380
+ for root in list(values):
381
+ missing_bindings = set(dependencies.get(root, [])).intersection(unavailable)
382
+ missing_state = set(dependencies.get(root, [])).intersection(omitted)
383
+ if missing_bindings or missing_state:
384
+ omitted[root] = ('missing current host binding: ' + ', '.join(sorted(missing_bindings))) if missing_bindings else ('depends on omitted globals: ' + ', '.join(sorted(missing_state)))
385
+ del values[root]
386
+ changed = True
387
+ ns.update(values)
388
+ _emit(sentinel, dict(kind='restored', count=len(values), turn=turn, names=names,
389
+ omitted=omitted))
390
+ except Exception as error:
391
+ _emit(sentinel, dict(kind='degraded', reason=type(error).__name__ + ': ' + str(error)))