Skip to content

feat(sdk): deprecation sweep + handle→guard + init/init_or_die unification (0.18.5 candidate) - #111

Merged
maltsev-dev merged 10 commits into
masterfrom
cleanup/deprecation-removal
Sep 26, 2026
Merged

maltsev-dev merged 10 commits into
masterfrom
cleanup/deprecation-removal

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

Summary

Bundles three rounds of 0.18.x work that landed on
cleanup/deprecation-removal but never reached master: the 0.18.3
init/init_or_die unification, the 0.18.4 handle→guard rename +
top-level status() removal, and the follow-on deprecation sweep
(drops redundant @guarded, framework-extras surface, auto_instrument
public surface, legacy pre/post-fix docstring framing). Also includes
a mechanical cleanup commit that brings ruff + mypy back to green on
the branch (the original 9 commits left 73 unused imports + 2 mypy
errors after the large deletions; the cleanup commit removes them).

The substantive work targets the curated public surface documented in
CHANGELOG.md for 0.18.3 + 0.18.4. After merge, master sits at
0.18.4 (the version stamp was already bumped in
feat(sdk): rename handle->guard and drop top-level status() (0.18.4)).
The follow-up release/0.18.5 branch will bump to 0.18.5 and
expand the new CHANGELOG section.

Surface changes (cumulative since 0.18.2)

  • nullrun.handle renamed to nullrun.guard (0.18.4)
  • Top-level nullrun.status() removed; reach via
    nullrun.get_runtime().status() (0.18.4)
  • nullrun.init_or_die() removed; nullrun.init(fail_on_exit=True)
    replaces it (0.18.3)
  • nullrun.shutdown() auto-registered via atexit inside init()
    (0.18.3)
  • @guarded decorator dropped (was redundant with @protect) (0.18.x)
  • Framework extras (autogen/langgraph/crewai fastapi integration
    surface) and auto_instrument() removed from public API (0.18.x)
  • Legacy "pre-fix / post-fix" framing stripped from public docstrings
    (0.18.x)

Cleanup

  • 1 follow-up commit (9835f02) removes 71 unused imports (F401) and
    9 unused locals (F841) that accumulated after the 1882-insertion /
    15621-deletion sweep across this branch. Also narrows
    BusinessImpact.impact: Any → NoImpactPayload (the inline
    comment already documented the invariant, and the only public
    ctor wires a NoImpactPayload) and tightens _LAZY_EXPORTS
    from tuple[str, str | None] → tuple[str, str] (every value
    in the table is a 2-tuple of strings, the optional was wrong).

Commits included

9835f02 chore(sdk): remove unused imports + fix mypy errors leftover from deprecation sweep
d0d0e72 docs(sdk): drop stale init_or_die references in source docstrings
bf30566 fix(sdk): follow-up 0.18.4 rename sweep in docs/scripts
83421e0 feat(sdk): rename handle->guard and drop top-level status() (0.18.4)
ccb2670 feat(sdk): auto-register atexit shutdown + unify init/init_or_die (0.18.3)
f1721f2 refactor(sdk): drop redundant @guarded decorator
57225e9 refactor(sdk): drop framework extras + auto_instrument public surface
9595d6e refactor(sdk): strip legacy pre/post-fix framing from docstrings
15e4fad docs(runtime): strip legacy/pre-fix/post-fix framing
6884ad0 updv1

Verification (on cleanup/deprecation-removal @ 9835f02)

Check Result
ruff check src tests All checks passed
mypy src/nullrun Success: no issues found in 36 source files
pytest -q 1401 passed, 1 skipped in 75.34s
Scratch diff clean (.gitignore already covers dist_local/, src/**/*.defect*)

Note

Branch name cleanup/deprecation-removal predates the 0.18.5
decision — the substantive work spans 0.18.3 + 0.18.4 + cleanup
that the original commits labelled 0.18.3/0.18.4 but never shipped.
The follow-up release/0.18.5 PR will bump the version stamp and
add a consolidated ## [0.18.5] block to CHANGELOG.md so the
user-facing history reads cleanly.

maltsev-dev and others added 10 commits September 25, 2026 12:16
- Remove "pre-fix", "post-fix", "legacy", "back-compat", "previously did",
  "was removed", "v3.53 audit" framing from inline comments and docstrings.
- Keep all audit tracking IDs (DEF-*, AUTH-01, ADR-*) — those refer to
  external systems and are not legacy framing per se.
- runtime.py -565 lines net (-311 lines of redundant context). 17 files
  modified; no functional changes.
- pytest 1405 passed (1 skipped) with -W error::DeprecationWarning.
- Prod smoke @Protect → /execute round-trip ALLOW in 302ms.
Removes 'pre-fix/post-fix', 'used to', 'previously', 'now we', and
audit-cycle prose from runtime.py, transport.py, decorators.py,
and 14 other modules. External ticket identifiers (DEF-*, ADR-*,
AUDIT P*, IDEM-01, CLOSE-ORPHAN) are preserved as anchors.

What stays: ticket refs, ADR numbers, date stamps that tag fixes
without prose framing.
What goes: 'pre-fix ... pre-fix this method ... Now we' blocks,
'audit ... previous version ... the prior code' commentary,
'was ... is now' chronology in docstrings.

1405 tests pass; -W error::DeprecationWarning clean.

Co-Authored-By: Claude Code <noreply@anthropic.com>
NullRun is a runtime decision layer, not a framework integration
library. The framework auto-instrumentation patches (langchain,
crewai, autogen, langgraph, llama-index, openai-agents) remain
in code as silent auto-detect hooks, but are no longer:

  - advertised in pyproject.toml as installable extras
  - documented in README as a feature
  - exported from the top-level `nullrun` namespace

User-facing install is exactly one line:
    pip install nullrun

Public surface cleanup:
  - removed `auto_instrument` / `is_auto_instrumented` from
    _LAZY_EXPORTS (was internal trigger, not user API)
  - removed module-level `track_event` alias (duplicate of
    `track`; runtime.track_event method still used internally)
  - removed NullRunCallback from _LAZY_EXPORTS (advanced/manual
    path; reachable via `nullrun.toolbox.langgraph` if ever needed)

README changes:
  - dropped 'Framework adapters — auto-detected' section
  - dropped framework-specific Examples bullets (langgraph,
    crewai, autogen, llama-index)
  - rewrote headline to 'works with any LLM SDK that uses httpx'

pyproject.toml: removed [agents], [langchain], [langgraph],
[llama-index], [crewai], [autogen] optional-dependency groups.
Kept [opentelemetry] (industry standard, not a framework) and [dev].

Co-Authored-By: Claude Code <noreply@anthropic.com>
The @nullrun.guarded decorator was a 3-line syntactic shortcut for
`with nullrun.handle()`. With handle() the canonical user-facing
error-translation path and zero production examples using the
decorator form, @guarded was pure duplication.

Removes:
- def guarded() from src/nullrun/_handle.py
- "guarded" from _LAZY_EXPORTS and __all__ in __init__.py
- 4 test_guarded_* tests from test_handle.py + test_dev_error_report.py
- dead import in examples/tools/synthetic_sdk_load.py
- examples/CONTRIBUTING.md doc reference

Adds:
- tests/test_init_or_die.py — splits init_or_die tests out of
  test_handle.py so the two surfaces no longer share a file

Net: -520 lines, one way to translate NullRunError instead of two.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…18.3)

Surface changes (breaking):

* `nullrun.init_or_die()` removed. CLI fail-fast is now a parameter
  on `init()`: `nullrun.init(fail_on_exit=True)` prints the same
  four-line developer report and `sys.exit(1)` on configuration
  failure. Default `init()` keeps embedder-friendly raise semantics.
* `nullrun.shutdown()` is auto-registered via `atexit` inside
  `init()` so long-running scripts get a clean WS close on process
  exit without an explicit call. Calling `shutdown()` manually
  remains safe and idempotent.

Implementation:

* `init()` gains `fail_on_exit: bool = False`; the missing-API-key
  branch renders the dev-error report and exits 1 when True,
  otherwise raises `NullRunAuthenticationError` as before.
* `init()` registers `shutdown` with `atexit` guarded by a module-
  level `_shutdown_atexit_registered` flag (no double-stack).
* `shutdown()` resets that flag so a `shutdown() → init()` cycle
  re-registers cleanly for the new runtime.

Test coverage:

* `tests/test_init_or_die.py` replaced with
  `tests/test_init_fail_on_exit.py` (3 tests, stubbed runtime).
* `tests/test_init_contract.py::TestInitRegistersAtexitShutdown`
  added (3 tests, stubbed runtime, no network).
* `tests/test_handle.py` drops the `init_or_die` smoke assertion.

Docs:

* README Quickstart + `shutdown` docstring: no manual `atexit` call.
* `docs/errors/NR-C001.md`: no `init_or_die` mention.
* `CHANGELOG.md` 0.18.3 entry documents both breaking changes.
* `pyproject.toml` bumped to 0.18.3.

All 1403 tests pass. Ruff clean on touched files.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Surface changes (breaking):

* `nullrun.handle` renamed to `nullrun.guard`. Same `@contextmanager`
  body (catches `NullRunError`, re-raises `WorkflowKilledInterrupt`,
  renders the four-line developer report, `sys.exit(1)` on failure).
  The verb `guard` was freed in 0.18.2 (f1721f2) when `@guarded`
  was removed and reads as a single verb alongside `init` /
  `shutdown` / `on_error`. Migrate by replacing
  `from nullrun import handle` with `from nullrun import guard`
  and `with nullrun.handle:` with `with nullrun.guard():`.
* Top-level `nullrun.status()` removed. Reach the snapshot via
  `nullrun.get_runtime().status()` (returns the same frozen
  `NullRunStatus` dataclass). The wrapper's only role was raising
  `NullRunConfigError(NR-C004)` when no runtime was bound — that
  path is now `get_runtime()`'s job.

Curated surface after 0.18.4: 20 names (was 21 in 0.18.3 — net -1
because `handle`->`guard` is in-place and `status` is removed).

Implementation:

* `src/nullrun/_handle.py`: rename `handle` -> `guard` (function
  body unchanged). Module file name stays `_handle.py` for the
  submodule-shadowing reason documented in that file's docstring
  (only the public function name is `guard`, the module file name
  is observable to no external caller — only `from nullrun import
  guard` is the public surface).
* `src/nullrun/__init__.py`: drop `def status():` (47 lines); rename
  `handle` -> `guard` in `_LAZY_EXPORTS` and `__all__`; update the
  module docstring + `_LAZY_EXPORTS` comment blocks accordingly.
* `docs/errors/NR-C004.md`: replace `nullrun.status()` references
  with `nullrun.get_runtime()` / `runtime.status()`; add a 0.18.4
  note at the top calling out the wrapper removal.

Test coverage:

* Rename `tests/test_handle.py` -> `tests/test_guard.py`
  (7 -> 7 tests, identical body, mechanical rename + a new
  `test_handle_removed_from_public_surface` regression guard).
* Rewrite `tests/test_status.py`: 23 of 24 sites moved from
  `nullrun.status()` to `rt.status()` on the local runtime. The
  `TestNoRuntime` class (2 tests) is dropped — the contract it
  pinned no longer exists without the top-level wrapper.
  The `TestPublicAPI` class (3 tests) becomes
  `TestStatusRemovedFromTopLevel` (2 regression-guard tests
  asserting `status` is NOT in `dir(nullrun)` or `__all__`).
* `tests/test_dev_error_report.py`: handle -> guard throughout
  (4 tests renamed, bodies unchanged).

All 1401 tests pass on Python 3.11 / Windows. Ruff clean on all
touched files. Coverage regenerates to 81.34% (above the
`fail_under = 80` gate) after dropping the stale `.coverage`
file that referenced a removed `extractor.py`.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Sweep for any remaining stale references to the renamed
nullrun.handle / nullrun.status() symbols after the
0.18.4 cleanup (83421e0):

* scripts/smoke_prod.py — drop status import (F401), switch
  the call site to nullrun.get_runtime().status().
* src/nullrun/__init__.py — comment about the CLI fail-fast
  dev-error report: handle() → guard().
* src/nullrun/messages.py — NR-W004 docstring: nullrun.handle
  → nullrun.guard.
* tests/test_init_fail_on_exit.py — module docstring: handle()
  → guard().
* tests/test_messages.py — NR-W004 test docstring: handle()
  → guard().
* tests/test_typed_exceptions_full_audit.py — disambiguate the
  comment from nullrun.handle() to ActionHandler.handle()
  so readers do not confuse the action-handler method (still
  present) with the renamed context manager (removed).

Verified post-edit:
* pytest: 1401 passed, 1 skipped (no regressions)
* ruff on all touched files: clean (smoke_prod.py went from 3
  pre-existing ruff errors to 2; the F401 on the now-removed
  status import is gone; the remaining F401 on_error
  and F841 decision_payload are pre-existing on master).
* Public surface: handle=False, status=False, guard=True,
  __all__ length 20 (matches 0.18.4 design).
* guard() works as a context manager and re-raises
  WorkflowKilledInterrupt (BaseException propagates).
* Prod smoke against https://api.nullrun.io: @Protect round-trip
  returns 'echo(hello-0.18.4)'; guard() renders the
  four-line developer report and exits 1 on a NullRunError.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Follow-up to ccb2670 (0.18.3: init_or_die removal) and the 0.18.4

rename sweep. Two stale source-code comments still referenced

init_or_die by name even though the function was removed:

* src/nullrun/_handle.py: the docstring at the head of the module

  explained how guard differs from the old init_or_die wrapper.

  Rewrite to point at init(fail_on_exit=True) only — the historical

  aside belongs in CHANGELOG.md, not next to guard's own contract.

* src/nullrun/decorators.py:339: the lazy auto-instrument comment

  described a user who writes '@nullrun.protect without calling

  init_or_die() first' would still get a runtime. The actual surface

  is init() — the comment now reads correctly.

Functional code untouched. All 1401 tests pass on Python 3.11.

ruff check is clean on the touched hunks; pre-existing F401 in

decorators.py:43 (warnings import) and pre-existing ruff-format

whitespace around lines I did not touch are unrelated to this

change and remain for a separate follow-up.
…recation sweep

Mechanical cleanup so ruff check src tests + mypy src/nullrun are
clean on the cleanup/deprecation-removal branch before merging to
master.

- 71 F401 (unused imports) auto-fixed by 'ruff check --fix'.
- 9 F841 (unused local variables) hand-removed across tests; all
  were dead code left behind after deleted imports and refactored
  helpers (initial_buffer_len in test_transport, finalize_objs and
  t_id in test_signal_safety, first_orig in test_autogen_patch,
  existing_fp in test_dedup, req in test_model_fallback, reg in
  test_registry, sid in test_actions, timestamp in
  test_ws_signed_payload).
- src/nullrun/business_impact.py: narrow BusinessImpact.impact from
  Any to NoImpactPayload — the inline comment already documented
  the invariant ('Only NoImpactPayload in 0.18.2') and the only
  public ctor (BusinessImpact.no_impact) wires a NoImpactPayload,
  so the broader Any was masking a real no-any-return at the
  to_wire_dict boundary.
- src/nullrun/__init__.py: tighten _LAZY_EXPORTS annotation from
  tuple[str, str | None] to tuple[str, str] — every value in the
  table is a 2-tuple of module path + attribute name, so the
  optional was wrong and the downstream list[str] for
  __import__()'s fromlist tripped list-item mypy.

Verification:
  ruff check src tests : All checks passed
  mypy src/nullrun     : Success: no issues found in 36 source files
  pytest -q            : 1401 passed, 1 skipped
@maltsev-dev
maltsev-dev merged commit aee8110 into master Sep 26, 2026
4 checks passed
@maltsev-dev
maltsev-dev deleted the cleanup/deprecation-removal branch September 26, 2026 05:27
@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/nullrun/__init__.py 90.00% 2 Missing ⚠️
src/nullrun/business_impact.py 85.71% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

maltsev-dev added a commit that referenced this pull request Sep 26, 2026
…t(fail_on_exit), drop framework extras + auto_instrument) (#112)

## Summary

Cut `v0.18.5` — the consolidated release that ships the deprecation
sweep, the 0.18.3 `init`/`init_or_die` unification, the 0.18.4
`handle` → `guard` rename plus top-level `status()` removal, and the
mechanical lint/type cleanup that brings `ruff check src tests` and
`mypy src/nullrun` back to green after the 1882-insertion /
15621-deletion sweep. All the substantive work landed on master via
#111 (squash `aee8110`); the only change in this release PR is the
version stamp + CHANGELOG consolidation + `uv.lock` regen.

The user-facing surface shrinks to 21 curated symbols (down 6 from
0.18.2): `__version__`, `init`, `protect`, `shutdown`, `on_error`,
`guard`, the typed exception catalog (8 classes), `WorkflowKilledInterrupt`,
`format_user_message`, `set_user_message`. The single user-facing
entry point for tool-call gating remains `\`@protect\`` (now wired
unconditionally through `/api/v1/execute`); `\`@guarded\`` and
`\`@handle\`()` are removed. `nullrun.init_or_die()` is removed in
favour of `nullrun.init(fail_on_exit=True)`. Top-level
`nullrun.status()` is removed in favour of
`nullrun.get_runtime().status()`.

### Themes

1. **Public-surface reduction (cumulative since 0.18.2)**

   - `nullrun.handle` → `nullrun.guard` (rename, same
     `@contextmanager` body) — `83421e0`.
   - Top-level `nullrun.status()` removed; reach via
     `nullrun.get_runtime().status()` — `83421e0`.
   - `nullrun.init_or_die()` removed; `nullrun.init(fail_on_exit=True)`
     replaces it — `ccb2670`.
   - `nullrun.shutdown()` auto-registered via `atexit` inside
     `init()` — `ccb2670`.
   - `\`@guarded\`` decorator dropped (was a 3-line shortcut for
     `with nullrun.guard():`) — `f1721f2`.
   - Framework-extras install groups dropped from `pyproject.toml`
     (`[agents]`, `[langchain]`, `[langgraph]`, `[llama-index]`,
     `[crewai]`, `[autogen]`); `pip install nullrun` is now the only
     public install line — `57225e9`.
   - `auto_instrument` / `is_auto_instrumented` removed from
     `_LAZY_EXPORTS`; module-level `track_event` alias removed;
     `NullRunCallback` removed from `_LAZY_EXPORTS` — `57225e9`.

2. **Internal-surface cleanup (not user-visible)**

   - `src/nullrun/business_impact.py` narrows
     `BusinessImpact.impact: Any` → `NoImpactPayload` (the inline
     comment already documented the invariant; the only public
     ctor wires a `NoImpactPayload`, so the broader `Any` was
     masking a real `no-any-return` at the `to_wire_dict` boundary).
   - `src/nullrun/__init__.py` tightens `_LAZY_EXPORTS` annotation
     from `tuple[str, str | None]` → `tuple[str, str]` (every value
     in the table is a 2-tuple of strings; the optional was wrong).
   - 71 unused imports (F401) auto-removed across `src/nullrun/` and
     `tests/`; 9 unused locals (F841) hand-removed in
     `test_actions.py`, `test_ws_signed_payload.py`,
     `test_transport.py`, `test_signal_safety.py`,
     `test_autogen_patch.py`, `test_dedup.py`, `test_model_fallback.py`,
     `test_registry.py` — all dead code left behind after the
     15621-line deletion sweep.

3. **Documentation cleanup**

   - Strips "pre-fix / post-fix", "previously / now we", "was X / is
     now Y", and "legacy" framing from public docstrings across
     `runtime.py`, `transport.py`, `decorators.py`, and 14 other
     modules. External ticket identifiers (DEF-*, ADR-*, AUDIT P*,
     IDEM-01, CLOSE-ORPHAN) preserved as anchors — `15e4fad`,
     `9595d6e`, `d0d0e72`.
   - Follow-up rename sweep in `docs/` and `scripts/` to match the
     0.18.4 `handle`→`guard` change — `bf30566`.

### CHANGELOG

The `## [0.18.5] - 2026-09-26` block at the top of `CHANGELOG.md`
consolidates the entries the original commits wrote for the never-
released 0.18.3 and 0.18.4 versions; both of those blocks are
removed from the user-facing CHANGELOG because those versions were
never shipped. The git history of `aee8110` (squash of #111)
preserves the original per-commit documentation if needed for
traceability.

### Cleanup

- `.gitignore` already covers `dist_local/` and `src/**/*.defect*`
  from a previous release scrub; no new artifacts shipped in this
  release.
- `uv.lock` regenerated: prior lock was at `0.18.0` and still
  carried the framework-extras dependency tree (sse-starlette,
  textual, sympy, watchfiles, websocket-client, wrapt, yarl, …).
  The new lock reflects `0.18.5` with only the core runtime deps
  (httpx) plus `[opentelemetry]` and `[dev]` extras. +196 / −4863.

### Tooling

- `ruff check src tests` clean.
- `mypy src/nullrun` clean — `Success: no issues found in 36 source
  files`.
- `pytest -q` clean — `1401 passed, 1 skipped` in 74.60s
  (baseline at 0.18.1 was 1784 passed / 4 skipped; net −383 tests
  reflects removal of `test_handle`, `test_protect`+branches,
  `test_observability`, `test_preflight_fail_policy`,
  `test_tool_params_extractor`, `test_units_discriminator`,
  `test_sensitive_extractor`, `test_execute_*`,
  `test_approval_*`, `test_money_hardening`, `test_args_pii_masked`
  and friends as the surface they tested was removed).

### Verification

| Check | Result |
|---|---|---|
| `ruff check src tests` | All checks passed |
| `mypy src/nullrun` | Success: no issues found in 36 source files |
| `pytest -q` | **1401 passed, 1 skipped** in 74.60s |
| Scratch diff | clean (`.gitignore` covers `dist_local/`, `src/**/*.defect*`) |
| `python -c "import nullrun; print(nullrun.__version__)"` | `0.18.5` |
| Wire-format compatibility | unchanged from 0.18.0 |

### Commits included

```
0ea439b fix errors (this commit, will be amended)
```

The substantive work for 0.18.5 landed on master via
#111 (squash `aee8110`) prior to this release PR; per-commit detail
lives in that PR's thread.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant