Repository navigation
chore(release): 0.22.1 — public exception surface + WS sync callback + doc alignment - #117
Merged
Merged
Conversation
…+ doc alignment
## Summary
Patch release, no protocol change. The SDK's public exception surface is
restored after QA pass u3h (2026-10-15) reported 24 classes the SDK RAISES
were not importable from the top-level `nullrun` namespace. The classes
existed, `_V3_ERROR_CODE_MAP` constructed them, and enforcement was never
affected — only the documented public name was missing. The defect survived
because `except NullRunError:` kept matching the instance; a narrower
`except NullRunApprovalDeniedError:` only fails when someone goes looking
for the name.
The second behavioural fix closes a discarded-coroutine class of bug on
the WS dispatch path: `on_policy_invalidated` was wired as `async def`
while the dispatcher calls it without `await`, so the callback was created
and silently discarded. The `logger.info` and the user-supplied callback
never fired — only a stderr `RuntimeWarning` that nobody reads. Same bug
class as the approval-resolved fix from 2026-07-24, missed on the sibling
callback.
The third change aligns stale docstrings with current behaviour: the
kill-vs-on_error-hook asymmetry between sync and async `@protect` was
documented as a `BaseException` bypass that never existed, and chasing the
truth exposed an actual behavioural split governed by `unify_block`, not
by base-class membership. A new regression test pins the real behaviour
so the comments cannot drift the way the old ones did.
### Fixed
- **DEF-PUBEX-01** — `nullrun.__init__._LAZY_EXPORTS` now exposes 24
exception classes the SDK RAISES (`NullRunApprovalDeniedError`,
`NullRunDeniedError`, `RateLimitError`,
`NullRunUnclassifiedRefusalError`, …). The same names added to
`__all__` so tab-completion and `from nullrun import *` reach them.
Grouped by tier (root / markers, infrastructure, transport, decision,
approval) so a reader can see the hierarchy without reading
`exceptions.py`. Regression test
`tests/test_public_exception_surface.py` reads the exception modules
via AST and asserts both directions of the bug: every raisable class
is reachable AND every exported class is raisable. (032eccb, +385/-4)
- **DEF-WS-SYNC-01** — `Transport.connect_websocket` wraps
`on_policy_invalidated` as a plain `def`, not `async def`. The WS
dispatch path calls it without `await`, and an un-awaited coroutine
raises nothing the dispatcher's `try/except` could catch. The body
never ran — only a stderr warning that nobody read. Same shape as the
approval fix from 2026-07-24, missed on the sibling callback.
`wrapped_key_rotated` stays `async def` (it awaits
`_refetch_credentials()` and the dispatcher awaits it) — the test
pins both halves. (564c39e, +170/-11)
### Documentation
- **DEF-DRIFT-01** — Aligned `@protect` / `on_error` /
`check_workflow_budget` / `messages.NR-B007` docstring blocks with
current behaviour. The `BaseException` explanation for the hook
bypass was wrong (`WorkflowKilledInterrupt -> NullRunError ->
BreakerError -> Exception`, verified); the real split is the
`unify_block` flag in `_protect_body`. Sync `@protect` re-wraps as
`NullRunBlockedException` (error code `NR-W002`), the hook fires;
async `@protect` lets the raw `WorkflowKilledInterrupt` propagate,
the hook does NOT fire. `decision == "throttle"` does not exist —
the only producer is an uncalled phase-2 schema stub. Fail-OPEN is
narrower than "any transport error": a 5xx WITH a refusal body is
HONOURED (`decision_source=GATEWAY`, blocks); only unreachable /
synthetic-source paths fail open. `NR-B007` was removed 2026-09-10
(it only ever fired on the never-sent `throttle` decision);
`WorkflowPausedException` is NOT zombie and must not be lumped in
with it. Regression test `tests/test_guard_kill_semantics.py` pins
the real behaviour. (2de9a7e, +199/-25)
### Cleanup
- Dropped `C:UsersAnatolii` (216 bytes — contents are a one-line
`fatal: ambiguous argument` from a previous shell that mis-decoded
an absolute Windows path and wrote the error output into the repo
root). `.gitignore` extended with `"/C:UsersAnatolii*"` so the shape
cannot recur; the broader class ("Windows path decoded without its
separator") produces single-byte replacement-char filenames that
don't pattern-match cleanly, so the entry is narrow on purpose.
(8e658f4, +7)
### Verification
| Check | Result |
|---|---|
| `ruff check src tests` | All checks passed |
| `mypy src/nullrun` | Success: no issues found in 39 source files |
| `pytest -q` | **1849 passed, 12 skipped** in 124.76s (vs baseline 1828 / 12 — **+21 new tests** from `test_public_exception_surface`, `test_policy_invalidated_ws_sync_callback`, `test_guard_kill_semantics`) |
| Scratch diff | clean (`dist_local/`, `*.defect*` absent) |
| `nullrun.__version__` | `0.22.1` |
| Wire-format compatibility | unchanged from 0.22.0 |
### Commits included
```
8e658f4 chore: remove accidentally-created C:UsersAnatolii scratch artefact
032eccb fix(public-surface): expose all raisable exception classes from top-level (DEF-PUBEX-01)
564c39e fix(transport): make on_policy_invalidated a sync wrapper (DEF-WS-SYNC-01)
2de9a7e chore(docs): align decorator/messages/runtime docstrings with current behaviour (DEF-DRIFT-01)
3555e5a fix errors
```
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Patch release, no protocol change. The SDK's public exception surface is
restored after QA pass u3h (2026-10-15) reported 24 classes the SDK RAISES
were not importable from the top-level
nullrunnamespace. The classesexisted,
_V3_ERROR_CODE_MAPconstructed them, and enforcement was neveraffected — only the documented public name was missing. The defect survived
because
except NullRunError:kept matching the instance; a narrowerexcept NullRunApprovalDeniedError:only fails when someone goes lookingfor the name.
The second behavioural fix closes a discarded-coroutine class of bug on
the WS dispatch path:
on_policy_invalidatedwas wired asasync defwhile the dispatcher calls it without
await, so the callback was createdand silently discarded. The
logger.infoand the user-supplied callbacknever fired — only a stderr
RuntimeWarningthat nobody reads. Same bugclass as the approval-resolved fix from 2026-07-24, missed on the sibling
callback. The third change aligns stale docstrings with current behaviour
and pins the real kill-vs-on_error-hook asymmetry between sync and async
`@protect`so the comments cannot drift the way the old ones did.Fixed
nullrun.__init__._LAZY_EXPORTSnow exposes 24exception classes the SDK RAISES (
NullRunApprovalDeniedError,NullRunDeniedError,RateLimitError,NullRunUnclassifiedRefusalError, …). The same names added to__all__so tab-completion andfrom nullrun import *reach them.Grouped by tier (root / markers, infrastructure, transport, decision,
approval) so a reader can see the hierarchy without reading
exceptions.py. Regression testtests/test_public_exception_surface.pyreads the exception modulesvia AST and asserts both directions of the bug: every raisable class
is reachable AND every exported class is raisable. (
032eccb,+385/-4)
Transport.connect_websocketwrapson_policy_invalidatedas a plaindef, notasync def. The WSdispatch path calls it without
await, and an un-awaited coroutineraises nothing the dispatcher's
try/exceptcould catch. The bodynever ran — only a stderr warning that nobody read. Same shape as the
approval fix from 2026-07-24, missed on the sibling callback.
wrapped_key_rotatedstaysasync def(it awaits_refetch_credentials()and the dispatcher awaits it) — the testpins both halves. (
564c39e, +170/-11)Cleanup
C:UsersAnatolii(216 bytes — contents are a one-linefatal: ambiguous argumentfrom a previous shell that mis-decoded anabsolute Windows path and wrote the error output into the repo root)
accidentally created in working tree and removed in
8e658f4..gitignoreextended with"/C:UsersAnatolii*"so the shape cannotrecur; the broader class ("Windows path decoded without its
separator") produces single-byte replacement-char filenames that
don't pattern-match cleanly, so the entry is narrow on purpose.
(+7)
Documentation
`@protect`/on_error/check_workflow_budget/messages.NR-B007docstring blocks withcurrent behaviour. The
BaseExceptionexplanation for the hookbypass was wrong (
WorkflowKilledInterrupt -> NullRunError -> BreakerError -> Exception, verified); the real split is theunify_blockflag in_protect_body. Sync`@protect`re-wraps as
NullRunBlockedException(error codeNR-W002), thehook fires; async
`@protect`lets the rawWorkflowKilledInterruptpropagate, the hook does NOT fire.decision == "throttle"does not exist — the only producer is anuncalled phase-2 schema stub. Fail-OPEN is narrower than "any
transport error": a 5xx WITH a refusal body is HONOURED
(
decision_source=GATEWAY, blocks); only unreachable /synthetic-source paths fail open.
NR-B007was removed 2026-09-10(it only ever fired on the never-sent
throttledecision);WorkflowPausedExceptionis NOT zombie and must not be lumped inwith it. Regression test
tests/test_guard_kill_semantics.pypinsthe real behaviour. (
2de9a7e, +199/-25)Verification
ruff check src testsmypy src/nullrunpytest -qtest_public_exception_surface,test_policy_invalidated_ws_sync_callback,test_guard_kill_semantics)dist_local/,*.defect*absent)nullrun.__version__0.22.1Commits included