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.
- openbox_core/__init__.py +59 -0
- openbox_core/adapters/__init__.py +3 -0
- openbox_core/adapters/base.py +123 -0
- openbox_core/approvals.py +106 -0
- openbox_core/client.py +298 -0
- openbox_core/config.py +260 -0
- openbox_core/conformance/__init__.py +3 -0
- openbox_core/conformance/fake_core.py +169 -0
- openbox_core/conformance/hook_preflight.py +87 -0
- openbox_core/conformance/instrumentation.py +91 -0
- openbox_core/context.py +205 -0
- openbox_core/contracts/__init__.py +3 -0
- openbox_core/contracts/context.py +79 -0
- openbox_core/contracts/events.py +401 -0
- openbox_core/contracts/otel_spans.py +325 -0
- openbox_core/contracts/results.py +287 -0
- openbox_core/errors.py +287 -0
- openbox_core/gate.py +185 -0
- openbox_core/hooks/__init__.py +3 -0
- openbox_core/hooks/events.py +64 -0
- openbox_core/hooks/preflight.py +292 -0
- openbox_core/hooks/wrappers.py +105 -0
- openbox_core/identity.py +231 -0
- openbox_core/instrumentation/__init__.py +3 -0
- openbox_core/instrumentation/db.py +689 -0
- openbox_core/instrumentation/file.py +239 -0
- openbox_core/instrumentation/function.py +121 -0
- openbox_core/instrumentation/http.py +840 -0
- openbox_core/instrumentation/llm.py +3 -0
- openbox_core/instrumentation/manager.py +135 -0
- openbox_core/instrumentation/shared.py +27 -0
- openbox_core/otel/__init__.py +3 -0
- openbox_core/otel/propagation.py +45 -0
- openbox_core/otel/provider.py +35 -0
- openbox_core/otel/setup.py +36 -0
- openbox_core/otel/span_processor.py +62 -0
- openbox_core/otel/trace_context.py +71 -0
- openbox_core/py.typed +0 -0
- openbox_core/runtime.py +138 -0
- openbox_core/sdk_version.py +79 -0
- openbox_core/serialization.py +129 -0
- openbox_core/validation/__init__.py +3 -0
- openbox_core/validation/diagnostics.py +60 -0
- openbox_core/validation/event_rules.py +164 -0
- openbox_core/validation/registry.py +31 -0
- openbox_core/validation/span_normalization.py +107 -0
- openbox_core/wire/__init__.py +3 -0
- openbox_core/wire/core_span.py +130 -0
- openbox_core/wire/evaluate_payload.py +56 -0
- openbox_sdk_python-0.2.0.dist-info/METADATA +94 -0
- openbox_sdk_python-0.2.0.dist-info/RECORD +52 -0
- 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
|