openbox-sdk-python 0.2.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.
Files changed (52) hide show
  1. openbox_core/__init__.py +59 -0
  2. openbox_core/adapters/__init__.py +3 -0
  3. openbox_core/adapters/base.py +123 -0
  4. openbox_core/approvals.py +106 -0
  5. openbox_core/client.py +298 -0
  6. openbox_core/config.py +260 -0
  7. openbox_core/conformance/__init__.py +3 -0
  8. openbox_core/conformance/fake_core.py +169 -0
  9. openbox_core/conformance/hook_preflight.py +87 -0
  10. openbox_core/conformance/instrumentation.py +91 -0
  11. openbox_core/context.py +205 -0
  12. openbox_core/contracts/__init__.py +3 -0
  13. openbox_core/contracts/context.py +79 -0
  14. openbox_core/contracts/events.py +401 -0
  15. openbox_core/contracts/otel_spans.py +325 -0
  16. openbox_core/contracts/results.py +287 -0
  17. openbox_core/errors.py +287 -0
  18. openbox_core/gate.py +185 -0
  19. openbox_core/hooks/__init__.py +3 -0
  20. openbox_core/hooks/events.py +64 -0
  21. openbox_core/hooks/preflight.py +292 -0
  22. openbox_core/hooks/wrappers.py +105 -0
  23. openbox_core/identity.py +231 -0
  24. openbox_core/instrumentation/__init__.py +3 -0
  25. openbox_core/instrumentation/db.py +689 -0
  26. openbox_core/instrumentation/file.py +239 -0
  27. openbox_core/instrumentation/function.py +121 -0
  28. openbox_core/instrumentation/http.py +840 -0
  29. openbox_core/instrumentation/llm.py +3 -0
  30. openbox_core/instrumentation/manager.py +135 -0
  31. openbox_core/instrumentation/shared.py +27 -0
  32. openbox_core/otel/__init__.py +3 -0
  33. openbox_core/otel/propagation.py +45 -0
  34. openbox_core/otel/provider.py +35 -0
  35. openbox_core/otel/setup.py +36 -0
  36. openbox_core/otel/span_processor.py +62 -0
  37. openbox_core/otel/trace_context.py +71 -0
  38. openbox_core/py.typed +0 -0
  39. openbox_core/runtime.py +138 -0
  40. openbox_core/sdk_version.py +79 -0
  41. openbox_core/serialization.py +129 -0
  42. openbox_core/validation/__init__.py +3 -0
  43. openbox_core/validation/diagnostics.py +60 -0
  44. openbox_core/validation/event_rules.py +164 -0
  45. openbox_core/validation/registry.py +31 -0
  46. openbox_core/validation/span_normalization.py +107 -0
  47. openbox_core/wire/__init__.py +3 -0
  48. openbox_core/wire/core_span.py +130 -0
  49. openbox_core/wire/evaluate_payload.py +56 -0
  50. openbox_sdk_python-0.2.0.dist-info/METADATA +94 -0
  51. openbox_sdk_python-0.2.0.dist-info/RECORD +52 -0
  52. openbox_sdk_python-0.2.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,239 @@
1
+ """File wrapper — governed open() with read/write/writelines counting.
2
+
3
+ Preflight runs BEFORE the file handle is created (a BLOCK means the file is
4
+ never opened). The returned handle is proxied to count bytes/lines; closing
5
+ emits ONE completed telemetry event with the totals.
6
+
7
+ Both ``builtins.open`` and ``io.open`` are patched: ``pathlib`` file helpers
8
+ (``Path.open``/``read_text``/``write_text``) call ``io.open`` DIRECTLY and
9
+ never touch ``builtins.open``, so patching only the builtin would leave every
10
+ pathlib file access ungoverned. In CPython the two names reference the SAME
11
+ object and a single logical open resolves through exactly ONE of them
12
+ (direct ``open()`` -> builtins, pathlib -> io), so wiring both to one wrapper
13
+ governs pathlib without any double wrapping or double evaluation.
14
+
15
+ Noise control: paths under the interpreter prefix / site-packages / caches
16
+ bypass governance entirely, and (like every hook) operations with no bound
17
+ ActivityContext are skipped by the hook runtime.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import builtins
23
+ import io
24
+ import logging
25
+ import sys
26
+ import sysconfig
27
+ import threading
28
+ from typing import Any
29
+
30
+ from ..contracts.otel_spans import HookType
31
+ from ..otel.provider import get_tracer
32
+ from .shared import get_hook_runtime
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ __all__ = ["install_file_io", "uninstall_file_io", "GovernedFile"]
37
+
38
+ # The genuine opener (``builtins.open`` == ``io.open`` pre-patch) — used both
39
+ # to bypass governance and to create the handle after preflight. Non-None
40
+ # while instrumentation is installed (doubles as the idempotency guard).
41
+ _original_open: Any = None
42
+ # Whether we replaced ``io.open`` too, so uninstall restores exactly what we
43
+ # changed (skipped if a foreign wrapper already owned ``io.open`` at install).
44
+ _patched_io: bool = False
45
+
46
+ # Interpreter-owned trees (venv AND base install — they differ in venvs).
47
+ _IGNORED_PATH_PREFIXES = tuple(
48
+ {
49
+ sys.prefix,
50
+ sys.exec_prefix,
51
+ sys.base_prefix,
52
+ sys.base_exec_prefix,
53
+ sysconfig.get_paths().get("stdlib", sys.base_prefix),
54
+ sysconfig.get_paths().get("platstdlib", sys.base_prefix),
55
+ sysconfig.get_paths().get("purelib", sys.prefix),
56
+ sysconfig.get_paths().get("platlib", sys.prefix),
57
+ }
58
+ )
59
+
60
+
61
+ def _mode_operation(mode: str) -> str:
62
+ if any(c in mode for c in ("w", "a", "x", "+")):
63
+ return "write"
64
+ return "read"
65
+
66
+
67
+ def _file_span_name(mode: str) -> str:
68
+ return f"file.{_mode_operation(mode)}"
69
+
70
+
71
+ def _coerce_path(file: Any) -> str | None:
72
+ """str path for str/bytes/os.PathLike; None for fds/unknowns (pass through)."""
73
+ import os
74
+
75
+ if isinstance(file, int):
76
+ return None # file descriptor
77
+ try:
78
+ path = os.fspath(file)
79
+ except TypeError:
80
+ return None
81
+ return path.decode(errors="ignore") if isinstance(path, bytes) else path
82
+
83
+
84
+ # Re-entrancy guard: governance evaluation opens files itself (httpx/ssl,
85
+ # package-metadata scans). Governing those opens evaluates again and recurses
86
+ # until RecursionError — any open on a thread already inside file-governance
87
+ # work passes straight through.
88
+ _reentrancy = threading.local()
89
+
90
+
91
+ def _should_skip(path: str | None) -> bool:
92
+ if path is None:
93
+ return True
94
+ if "__pycache__" in path:
95
+ return True
96
+ return path.startswith(_IGNORED_PATH_PREFIXES)
97
+
98
+
99
+ class GovernedFile:
100
+ """Transparent file proxy counting reads/writes for completed telemetry."""
101
+
102
+ def __init__(self, handle: Any, span: Any, path: str, mode: str, runtime: Any):
103
+ object.__setattr__(self, "_handle", handle)
104
+ object.__setattr__(self, "_span", span)
105
+ object.__setattr__(self, "_path", path)
106
+ object.__setattr__(self, "_mode", mode)
107
+ object.__setattr__(self, "_runtime", runtime)
108
+ object.__setattr__(self, "_bytes_read", 0)
109
+ object.__setattr__(self, "_bytes_written", 0)
110
+ object.__setattr__(self, "_lines_count", 0)
111
+ object.__setattr__(self, "_closed_reported", False)
112
+
113
+ # ── counted operations ────────────────────────────────────────────────
114
+
115
+ def read(self, *args, **kwargs):
116
+ data = self._handle.read(*args, **kwargs)
117
+ object.__setattr__(self, "_bytes_read", self._bytes_read + len(data))
118
+ return data
119
+
120
+ def write(self, data):
121
+ written = self._handle.write(data)
122
+ object.__setattr__(self, "_bytes_written", self._bytes_written + len(data))
123
+ return written
124
+
125
+ def writelines(self, lines):
126
+ materialized = list(lines)
127
+ result = self._handle.writelines(materialized)
128
+ object.__setattr__(
129
+ self, "_bytes_written", self._bytes_written + sum(len(line) for line in materialized)
130
+ )
131
+ object.__setattr__(self, "_lines_count", self._lines_count + len(materialized))
132
+ return result
133
+
134
+ def close(self):
135
+ result = self._handle.close()
136
+ self._report_completed()
137
+ return result
138
+
139
+ def _report_completed(self):
140
+ if self._closed_reported:
141
+ return
142
+ object.__setattr__(self, "_closed_reported", True)
143
+ _reentrancy.active = True
144
+ try:
145
+ self._span.end()
146
+ self._runtime.completed(
147
+ self._span,
148
+ hook_type=HookType.FILE_OPERATION,
149
+ fields={
150
+ "file_path": self._path,
151
+ "file_mode": self._mode,
152
+ "file_operation": _mode_operation(self._mode),
153
+ "bytes_read": self._bytes_read or None,
154
+ "bytes_written": self._bytes_written or None,
155
+ "lines_count": self._lines_count or None,
156
+ },
157
+ )
158
+ except Exception:
159
+ logger.debug("file completed telemetry failed", exc_info=True)
160
+ finally:
161
+ _reentrancy.active = False
162
+
163
+ # ── passthrough ───────────────────────────────────────────────────────
164
+
165
+ def __enter__(self):
166
+ self._handle.__enter__()
167
+ return self
168
+
169
+ def __exit__(self, exc_type, exc_value, traceback):
170
+ result = self._handle.__exit__(exc_type, exc_value, traceback)
171
+ self._report_completed()
172
+ return result
173
+
174
+ def __iter__(self):
175
+ return iter(self._handle)
176
+
177
+ def __getattr__(self, name):
178
+ return getattr(self._handle, name)
179
+
180
+
181
+ def _governed_open(file, mode="r", *args, **kwargs):
182
+ runtime = get_hook_runtime()
183
+ path = _coerce_path(file)
184
+ if runtime is None or getattr(_reentrancy, "active", False) or _should_skip(path):
185
+ return _original_open(file, mode, *args, **kwargs)
186
+ operation = _mode_operation(mode)
187
+ span = get_tracer().start_span(_file_span_name(mode))
188
+ span.set_attribute("file.path", path)
189
+ span.set_attribute("file.mode", mode)
190
+ span.set_attribute("file.operation", operation)
191
+ _reentrancy.active = True
192
+ try:
193
+ # BLOCK/HALT raises here — the file handle is never created.
194
+ runtime.preflight(
195
+ span,
196
+ hook_type=HookType.FILE_OPERATION,
197
+ identifier=path,
198
+ fields={
199
+ "file_path": path,
200
+ "file_mode": mode,
201
+ "file_operation": operation,
202
+ },
203
+ )
204
+ finally:
205
+ _reentrancy.active = False
206
+ handle = _original_open(file, mode, *args, **kwargs)
207
+ return GovernedFile(handle, span, path, mode, runtime)
208
+
209
+
210
+ def install_file_io() -> bool:
211
+ """Idempotent open() patch across builtins AND io.
212
+
213
+ Patching ``io.open`` is what brings ``pathlib`` file helpers under
214
+ governance — they bypass ``builtins.open`` entirely.
215
+ """
216
+ global _original_open, _patched_io
217
+ if _original_open is not None:
218
+ return True
219
+ _original_open = builtins.open
220
+ builtins.open = _governed_open
221
+ # pathlib routes through io.open. Only patch it if it is still the genuine
222
+ # opener — never clobber a foreign wrapper another tool installed first
223
+ # (that would also make it the wrapper we call as "_original_open").
224
+ if io.open is _original_open:
225
+ io.open = _governed_open
226
+ _patched_io = True
227
+ return True
228
+
229
+
230
+ def uninstall_file_io() -> None:
231
+ """Restore both ``builtins.open`` and ``io.open`` to their originals."""
232
+ global _original_open, _patched_io
233
+ if _original_open is None:
234
+ return
235
+ builtins.open = _original_open
236
+ if _patched_io:
237
+ io.open = _original_open
238
+ _patched_io = False
239
+ _original_open = None
@@ -0,0 +1,121 @@
1
+ """Function decorator instrumentation — @governed for sync and async callables.
2
+
3
+ Preflight runs before the wrapped function; a BLOCK/HALT means the function
4
+ body never executes. Fast path: no active hook runtime or no bound context ⇒
5
+ zero-governance passthrough.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import functools
11
+ import inspect
12
+ import logging
13
+ from collections.abc import Callable
14
+ from typing import Any
15
+
16
+ from ..contracts.otel_spans import HookType
17
+ from ..hooks.wrappers import run_governed_async, run_governed_sync
18
+ from ..otel.provider import get_tracer
19
+ from ..serialization import to_json_safe
20
+ from .shared import get_hook_runtime
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ __all__ = ["governed"]
25
+
26
+
27
+ def _function_fields(func: Callable, args: tuple, kwargs: dict, capture_args: bool) -> dict:
28
+ fields: dict[str, Any] = {
29
+ "function": getattr(func, "__qualname__", getattr(func, "__name__", "function")),
30
+ "module": getattr(func, "__module__", None),
31
+ }
32
+ if capture_args:
33
+ try:
34
+ fields["args"] = to_json_safe({"args": list(args), "kwargs": kwargs})
35
+ except Exception:
36
+ fields["args"] = None
37
+ return fields
38
+
39
+
40
+ def governed(
41
+ func: Callable | None = None,
42
+ *,
43
+ name: str | None = None,
44
+ capture_args: bool = True,
45
+ capture_result: bool = True,
46
+ ):
47
+ """Decorate a function with started/completed hook governance.
48
+
49
+ Usage::
50
+
51
+ @governed
52
+ def charge(amount): ...
53
+
54
+ @governed(name="billing.charge", capture_args=False)
55
+ async def acharge(amount): ...
56
+ """
57
+
58
+ def decorate(target: Callable) -> Callable:
59
+ span_name = name or getattr(target, "__qualname__", "function")
60
+
61
+ if inspect.iscoroutinefunction(target):
62
+
63
+ @functools.wraps(target)
64
+ async def async_wrapper(*args, **kwargs):
65
+ runtime = get_hook_runtime()
66
+ if runtime is None:
67
+ return await target(*args, **kwargs)
68
+ span = get_tracer().start_span(f"function {span_name}")
69
+ started = _function_fields(target, args, kwargs, capture_args)
70
+ try:
71
+ return await run_governed_async(
72
+ runtime,
73
+ target,
74
+ args,
75
+ kwargs,
76
+ span=span,
77
+ hook_type=HookType.FUNCTION_CALL,
78
+ identifier=span_name,
79
+ started_fields=started,
80
+ completed_fields=(
81
+ (lambda result: {"result": to_json_safe(result)})
82
+ if capture_result
83
+ else None
84
+ ),
85
+ )
86
+ finally:
87
+ span.end()
88
+
89
+ return async_wrapper
90
+
91
+ @functools.wraps(target)
92
+ def sync_wrapper(*args, **kwargs):
93
+ runtime = get_hook_runtime()
94
+ if runtime is None:
95
+ return target(*args, **kwargs)
96
+ span = get_tracer().start_span(f"function {span_name}")
97
+ started = _function_fields(target, args, kwargs, capture_args)
98
+ try:
99
+ return run_governed_sync(
100
+ runtime,
101
+ target,
102
+ args,
103
+ kwargs,
104
+ span=span,
105
+ hook_type=HookType.FUNCTION_CALL,
106
+ identifier=span_name,
107
+ started_fields=started,
108
+ completed_fields=(
109
+ (lambda result: {"result": to_json_safe(result)})
110
+ if capture_result
111
+ else None
112
+ ),
113
+ )
114
+ finally:
115
+ span.end()
116
+
117
+ return sync_wrapper
118
+
119
+ if func is not None:
120
+ return decorate(func)
121
+ return decorate