nullrun 0.4.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.
nullrun/__init__.py ADDED
@@ -0,0 +1,282 @@
1
+ """
2
+ NullRun Platform SDK.
3
+
4
+ Enforcement gateway client for AI agents. Curated 6-symbol surface:
5
+ `init`, `protect`, `track_llm`, `track_tool`, `track_event`. Everything
6
+ else is reachable on demand via `from nullrun import X` but does NOT
7
+ appear in `dir(nullrun)`.
8
+
9
+ Usage:
10
+ import nullrun
11
+ nullrun.init(api_key="nr_live_...")
12
+
13
+ @nullrun.protect
14
+ def my_agent(query):
15
+ return call_llm(query)
16
+
17
+ See README.md for LangGraph, OpenAI Agents, llama-index, crewai, autogen
18
+ auto-instrumentation; CHANGELOG.md for breaking changes between versions.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import threading as _threading
24
+
25
+ # Use lazy import inside __getattr__ instead of `import importlib` at
26
+ # module top-level — keeps `dir(nullrun)` focused on the curated surface.
27
+ from nullrun.__version__ import __version__
28
+
29
+ # Module-level lock that serialises the three singleton-slot writes
30
+ # inside `init()`. See plan item B3.
31
+ _init_lock = _threading.Lock()
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Curated public surface (Phase 3.4)
35
+ # ---------------------------------------------------------------------------
36
+ # These six names are imported eagerly so they show up in `dir(nullrun)` and
37
+ # in tab-completion — that's the "track AI cost in 5 minutes" surface. All
38
+ # other names (legacy Breaker exports, instrumentation, exceptions, …) live
39
+ # in `_LAZY_EXPORTS` below and are loaded on first access via __getattr__.
40
+ from nullrun.decorators import protect # the gate decorator
41
+ from nullrun.runtime import track_event, track_llm, track_tool
42
+
43
+
44
+ def init(
45
+ api_key: str | None = None,
46
+ api_url: str | None = None,
47
+ debug: bool = False,
48
+ ):
49
+ """
50
+ Initialize the NullRun SDK. Call once at application startup.
51
+
52
+ `api_key` is **required** as of 0.3.0. The previous silent fallback to
53
+ "local mode" (a NullRunNoop stub) was removed because it hid policy
54
+ violations and bypassed every backend gate — a real safety hole. Pass
55
+ `api_key=...` explicitly or set the `NULLRUN_API_KEY` environment
56
+ variable before calling `init()`. If neither is set, `init()` raises
57
+ `NullRunAuthenticationError`.
58
+
59
+ Args:
60
+ api_key: NullRun API key (or NULLRUN_API_KEY env var). Required.
61
+ api_url: Gateway URL (or NULLRUN_API_URL env var)
62
+ debug: Enable debug logging
63
+
64
+ Note: the background control-plane listener (WebSocket + HTTP poll) is
65
+ always started on `init()`. To disable it, construct `NullRunRuntime`
66
+ directly with `polling=False` — this is an internal/test-only knob.
67
+
68
+ Returns:
69
+ NullRunRuntime singleton instance.
70
+
71
+ Raises:
72
+ NullRunAuthenticationError: if neither `api_key` nor
73
+ `NULLRUN_API_KEY` is set.
74
+
75
+ Example:
76
+ import nullrun
77
+
78
+ nullrun.init(api_key="your-key")
79
+
80
+ @nullrun.protect
81
+ def my_agent():
82
+ return agent.run()
83
+ """
84
+ import logging
85
+ import os
86
+
87
+ if debug:
88
+ logging.getLogger("nullrun").setLevel(logging.DEBUG)
89
+
90
+ # T3-S2 (0.3.0): api_key is now required. Previous versions fell back
91
+ # to a NullRunNoop stub in `local_mode`, which silently bypassed every
92
+ # backend gate (budget, policy, control plane). That was a real
93
+ # safety hole — production callers were unaware their policies were
94
+ # not being enforced. We raise instead so the misconfiguration is
95
+ # caught at startup rather than producing silent allow-all decisions.
96
+ resolved_key = api_key or os.getenv("NULLRUN_API_KEY")
97
+ if not resolved_key:
98
+ from nullrun.breaker.exceptions import NullRunAuthenticationError
99
+
100
+ raise NullRunAuthenticationError(
101
+ "nullrun.init() requires an api_key. Pass api_key='nr_live_...' "
102
+ "explicitly or set the NULLRUN_API_KEY environment variable. "
103
+ "(Silent no-op fallback was removed in 0.3.0 — see CHANGELOG.)"
104
+ )
105
+
106
+ # Imported lazily so we don't pull the runtime into the namespace
107
+ # when the user only wants the static helpers.
108
+ import threading as _threading
109
+
110
+ import nullrun.decorators as _dec_mod
111
+ import nullrun.runtime as _rt_mod
112
+ from nullrun.runtime import NullRunRuntime
113
+
114
+ # Phase 0.3.1: the three singleton slots (NullRunRuntime._instance,
115
+ # _rt_mod._runtime, _dec_mod._runtime) must all be assigned
116
+ # atomically. Without a lock, concurrent init() calls from
117
+ # multiple threads can leave the three slots pointing at two
118
+ # different runtimes. The failure mode is silent — the
119
+ # decorator's @protect wrapper reads _dec._runtime once and
120
+ # never re-resolves, so a missed assignment drops every
121
+ # span_start/span_end event for that runtime.
122
+ with _init_lock:
123
+ runtime = NullRunRuntime(
124
+ api_key=api_key,
125
+ api_url=api_url,
126
+ debug=debug,
127
+ )
128
+
129
+ # Register as the module-level singleton so `nullrun.track_llm` /
130
+ # `nullrun.track_tool` (which resolve via `get_runtime()`) and any
131
+ # other consumers reading the cached instance find *this* runtime —
132
+ # not whatever a previous test or stale env would otherwise produce.
133
+ _rt_mod._runtime = runtime
134
+ NullRunRuntime._instance = runtime
135
+
136
+ # Wire the @protect decorator's own module-level cache to this
137
+ # runtime too. The decorator short-circuits on its local `_runtime`
138
+ # slot and never re-resolves via `get_instance()`, so without this
139
+ # assignment a re-init cycle (init → shutdown → init) leaves the
140
+ # decorator pointing at the dead previous runtime and silently
141
+ # drops span_start/span_end events.
142
+ _dec_mod._runtime = runtime
143
+
144
+ # Phase D6: wire auto-instrumentation AFTER the runtime is fully
145
+ # constructed. In 0.3.0 api_key is required, so this branch is
146
+ # unconditional — we always have a remote LLM traffic source if
147
+ # auto-instrumentation libraries are installed.
148
+ from nullrun.instrumentation.auto import auto_instrument
149
+ auto_instrument(runtime)
150
+
151
+ return runtime
152
+
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # Lazy exports (PEP 562) — backward compat without bloating dir()
156
+ # ---------------------------------------------------------------------------
157
+ # Each entry maps an attribute name on `nullrun` to (module_path, attr_name)
158
+ # inside that module. They are loaded on first attribute access and cached
159
+ # in `globals()` so subsequent lookups are O(1) and not visible in
160
+ # `vars(nullrun)` until then. This is the same pattern used by pandas /
161
+ # sqlalchemy / etc. to keep the top-level namespace discoverable.
162
+ _LAZY_EXPORTS: dict[str, tuple[str, str]] = {
163
+ # Runtime + context (advanced)
164
+ "NullRunRuntime": ("nullrun.runtime", "NullRunRuntime"),
165
+ "get_runtime": ("nullrun.runtime", "get_runtime"),
166
+ "get_protected_runtime": ("nullrun.decorators", "get_protected_runtime"),
167
+ "track": ("nullrun.runtime", "track"),
168
+ "reset": ("nullrun.decorators", "reset"),
169
+ "workflow": ("nullrun.context", "workflow"),
170
+ "span": ("nullrun.context", "span"),
171
+ "agent": ("nullrun.context", "agent"),
172
+ "get_workflow_id": ("nullrun.context", "get_workflow_id"),
173
+ "get_trace_id": ("nullrun.context", "get_trace_id"),
174
+ "get_span_id": ("nullrun.context", "get_span_id"),
175
+ "get_agent_id": ("nullrun.context", "get_agent_id"),
176
+
177
+ # Instrumentation
178
+ "NullRunCallback": ("nullrun.instrumentation", "NullRunCallback"),
179
+ # NOTE (Sprint 1.2 / B11-B12): `patch_openai` and `unpatch_openai`
180
+ # were removed from `_LAZY_EXPORTS` because they pointed at
181
+ # non-existent attributes on `nullrun.instrumentation` (the actual
182
+ # function is `patch_openai_agents`, with different semantics —
183
+ # it patches `agents.Runner`, not the `openai` SDK). The pre-fix
184
+ # lazy entries caused `AttributeError` on first access, which is
185
+ # a worse failure mode than a clean `ImportError` from
186
+ # `from nullrun import patch_openai` failing because the symbol
187
+ # is no longer in the lazy table.
188
+
189
+ # Toolbox — framework-specific wrappers (Phase 1 Commit 6).
190
+ # The previous `instrument()` helper lived at
191
+ # `nullrun.instrumentation.langgraph.instrument`; it is now
192
+ # `nullrun.toolbox.langgraph.wrapper`. Reachable as
193
+ # `from nullrun import wrapper` for one-line import.
194
+ "wrapper": ("nullrun.toolbox.langgraph", "wrapper"),
195
+
196
+ # Span / trace context (Phase 2 Commit 3).
197
+ # `tracing.py` is the structured replacement for the loose `_trace_id`
198
+ # / `_span_id` contextvars in `nullrun.context`. `SpanContext` is a
199
+ # single value (parent + children derive from it); `set_span` /
200
+ # `reset_span` are the token-based API the runtime and `@protect`
201
+ # use to push/pop the active span.
202
+ "SpanContext": ("nullrun.tracing", "SpanContext"),
203
+ "get_current_span": ("nullrun.tracing", "get_current_span"),
204
+ "create_root_span": ("nullrun.tracing", "create_root_span"),
205
+ "create_child_span": ("nullrun.tracing", "create_child_span"),
206
+ "set_span": ("nullrun.tracing", "set_span"),
207
+ "reset_span": ("nullrun.tracing", "reset_span"),
208
+
209
+ # Decorators
210
+ "sensitive": ("nullrun.decorators", "sensitive"),
211
+
212
+ # Actions (Phase 3)
213
+ "ActionHandler": ("nullrun.actions", "ActionHandler"),
214
+ "ActionType": ("nullrun.actions", "ActionType"),
215
+ "ActionEvent": ("nullrun.actions", "ActionEvent"),
216
+ "WebhookConfig": ("nullrun.actions", "WebhookConfig"),
217
+ "handle_action": ("nullrun.actions", "handle_action"),
218
+ "register_action_handler": ("nullrun.actions", "register_action_handler"),
219
+ "get_action_handler": ("nullrun.actions", "get_action_handler"),
220
+
221
+ # Exceptions (Phase 3)
222
+ "NullRunBlockedException": ("nullrun.breaker.exceptions", "NullRunBlockedException"),
223
+ "NullRunAuthenticationError": ("nullrun.breaker.exceptions", "NullRunAuthenticationError"),
224
+ # Sprint 2.2: zombie exception classes removed. See the
225
+ # NOTE block in breaker/exceptions.py for the list.
226
+ "WorkflowPausedException": ("nullrun.breaker.exceptions", "WorkflowPausedException"),
227
+ "WorkflowKilledException": ("nullrun.breaker.exceptions", "WorkflowKilledException"),
228
+ "WorkflowKilledInterrupt": ("nullrun.breaker.exceptions", "WorkflowKilledInterrupt"),
229
+ }
230
+
231
+
232
+ def __getattr__(name: str):
233
+ """PEP 562 — lazy attribute access for backward-compatible symbols."""
234
+ if name in _LAZY_EXPORTS:
235
+ module_path, attr_name = _LAZY_EXPORTS[name]
236
+ module = __import__(module_path, fromlist=[attr_name])
237
+ value = getattr(module, attr_name)
238
+ # Cache on the module so subsequent lookups are O(1) and
239
+ # dir(nullrun) still reports the curated public surface until
240
+ # the legacy name is actually accessed.
241
+ globals()[name] = value
242
+ return value
243
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
244
+
245
+
246
+ def __dir__() -> list[str]:
247
+ """PEP 562 — `dir(nullrun)` only shows the curated public surface.
248
+
249
+ We deliberately ignore `globals()` here so that auto-imported
250
+ submodules (`nullrun.decorators`, `nullrun.runtime`, etc.) and any
251
+ side-effect imports do NOT leak into the public namespace. Users
252
+ who want internals can still reach them via `from nullrun import X`
253
+ (see `_LAZY_EXPORTS` in `__getattr__`) — `dir()` is for discovery,
254
+ not for reachability.
255
+ """
256
+ return sorted(__all__)
257
+
258
+
259
+ __all__ = [
260
+ # Version (single value, always public)
261
+ "__version__",
262
+
263
+ # Phase 3.4: the curated public surface — six symbols.
264
+ # Everything else stays importable as `from nullrun import X` for
265
+ # backward compatibility, but does NOT appear in `dir(nullrun)`
266
+ # until the user actually accesses it.
267
+ "init",
268
+ "protect", # gate decorator
269
+ "track_llm",
270
+ "track_tool",
271
+ "track_event",
272
+ ]
273
+
274
+ # Sprint 2.1: the SDK-side ``decision_history`` module was deleted.
275
+ # Decision history is a backend + dashboard surface only — the SDK
276
+ # does not (and cannot) replay LLM calls because NULLRUN does not
277
+ # store request/response payloads or hold client LLM keys. The
278
+ # orphan ``start_recording`` / ``stop_recording`` methods on
279
+ # ``NullRunRuntime`` are kept as no-op stubs for one minor version
280
+ # for backward compatibility; they will be removed in 0.5.0.
281
+ # Do NOT re-export ReplayManager / ReplaySession / ReplayEvent /
282
+ # EventRecorder.
nullrun/__version__.py ADDED
@@ -0,0 +1,4 @@
1
+ """NullRun Platform SDK."""
2
+
3
+ __version__ = "0.4.0"
4
+ __platform_version__ = "1.0.0"