Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ jobs:
python -m pip install --upgrade pip
# xdist ships in the dev tree already; pin it explicitly so
# a future deps churn can't drop it without breaking CI.
pip install -e ".[dev]" "pytest-xdist>=3.6"
# ``pytest-rerunfailures`` is used by Sprint 0 (coverage) on
# a single rare-flaky test under pytest-xdist on linux
# (thread-scheduling race in the approval-wait fixture);
# pin it for the same reason.
pip install -e ".[dev]" "pytest-xdist>=3.6" "pytest-rerunfailures>=14.0,<16.0"

- name: Run tests
# `-n auto` lets xdist pick a worker count from the runner's
Expand Down
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,42 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

---

## [0.14.0] - 2026-07-23


### Added

- **`InvalidMoneyPrecisionError`** and **`InvalidMoneyAmountError`** — dedicated `ValueError` subclasses with structured fields. The amount variant carries a `reason` discriminator (`"negative"` / `"overflow"` / `"non_finite"`); the precision variant carries `currency` / `allowed` / `received` / `received_digits`. Legacy `except ValueError:` blocks still catch them.
- **`BusinessImpact`** model (`dataclass(frozen=True)`) with explicit `currency` / `units` / `amount_minor` fields. `details` dict is still accepted on the legacy path.
- **`@sensitive(impact=BusinessImpact(...))`** — new decorator kwarg that emits a structured `business_impact` envelope on the `/track` event. Existing `@sensitive(details=...)` / `@sensitive(amount_minor=..., currency=...)` callers keep working on the happy path (now routed through `BusinessImpact` internally).
- **`MoneyImpactExtractor`** — new helper that normalises `Decimal` / `int` / `float` / str into `BusinessImpact` minor-units, raising `InvalidMoneyAmountError` / `InvalidMoneyPrecisionError` on the audit gaps above.

### Changed

- **Negative `amount_minor` rejected** on both unit paths. A negative value would silently fall through every `op=gt` predicate (`negative < positive` is always False) — pre-fix a $-50 refund could be wired through without the backend catching it. `0` is still accepted (legitimate $0.00 refund).
- **Sub-precision Decimal rejected** — `Decimal("1.234")` against a USD `allowed=2` precision is now `InvalidMoneyPrecisionError(currency="USD", allowed=2, received=3, received_digits="1.234")` instead of a silent round to `1.23` that drops the high-order digit the user explicitly typed. `float` and `Decimal` are treated symmetrically; `int` always rounds 0-digits.
- **`/execute` handles `require_approval` correctly** — re-checks with the `approval_id` returned by the backend (was dropping the approval handshake on round-trips).
- **Server `approval_timeout` clamped to `[1, 3600]s`** on the SDK side as defence against a malformed / overshooting backend that returns `0` or `2147483647` in the Разрыв 1c field.

### Tests

- `tests/test_money_hardening.py` — 5 Definition-of-Done scenarios (negative amount, sub-precision Decimal, overflow, non-finite, `0` accepted).
- `tests/test_business_impact.py` — `BusinessImpact` model contract + integration with the wire envelope.
- `tests/test_units_discriminator.py` — `USD` vs `USDT` collision caught at the `BusinessImpact` boundary, not on the backend at `/track` time.
- `tests/test_sensitive_extractor.py` — `@sensitive(impact=...)` round-trip + legacy `details=` backward-compat.
- `tests/test_approval_money_flow.py` — 5 contract tests covering the `MoneyImpactExtractor` path end-to-end.
- `tests/test_execute_approval_flow.py` — `/execute` round-trip with stub backend exercising the `require_approval` + `approval_id` re-check path.

### Compatibility

- **Backward compatible** on the happy path. Every existing call site keeps working; the new errors are `ValueError` subclasses; the new `BusinessImpact` decorator kwarg is optional.
- **No SDK_MIN_VERSION bump** — legacy backends without the Разрыв 1c field fall through to the env default (see 0.13.13 release notes).
- **No on-wire change** — envelope shape preserved; new fields are additive on the SDK side and ignored by older backends.

---

---

## [0.13.13] - 2026-07-21

Approval-wait SDK sync with backend commit `0ad03b9` ("\u0420\u0430\u0437\u0440\u044b\u0432 1c", gate hot-path trigger). The backend now sends `approval_timeout_seconds: Option<i64>` and `approval_expires_at: Option<String>` on every `/gate` response so a backend approval rule can set a non-default short timeout. Pre-fix, the SDK only consulted `NULLRUN_APPROVAL_TIMEOUT_SECONDS` (env default 300s), which silently desynced from a 20s backend expiry sweeper. No public API change. No SDK_MIN_VERSION bump. No on-wire change.
Expand Down
36 changes: 35 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,27 @@ name = "nullrun"
# ``_wait_for_approval_resolution`` for explicit per-call
# control. Public API backward compatible (existing callers
# unaffected). No SDK_MIN_VERSION bump. No on-wire change.
version = "0.13.13"
# 0.14.0 (2026-07-23): hardening pass on the money contract —
# closes the four review gaps from the Phase 1.1 / UX follow-up.
# (1) ``InvalidMoneyPrecisionError`` / ``InvalidMoneyAmountError``
# dedicated ``ValueError`` subclasses with structured
# discriminators (``reason="negative"|"overflow"|"non_finite"``,
# ``currency`` / ``allowed`` / ``received`` / ``received_digits``).
# (2) Negative ``amount_minor`` now rejected on both unit paths
# (was silently falling through ``op=gt`` predicates because
# ``negative < positive`` is always False).
# (3) Sub-precision Decimals rejected instead of silently
# rounding away the high-order digits a user explicitly typed.
# (4) Explicit ``units`` discriminator + ``Decimal`` support on
# sensitive-call metadata, with a new ``BusinessImpact`` /
# ``MoneyImpactExtractor`` and ``@sensitive(impact=...)`` wiring.
# The /execute handler now re-checks with ``approval_id`` and
# the server's ``approval_timeout`` is clamped to ``[1, 3600]s``
# (defence against malformed / overshooting backends). Behaviour
# adds new optional kwarg + new public class, but every existing
# call site is unchanged on the happy path. No SDK_MIN_VERSION
# bump. No on-wire change.
version = "0.14.0"
# Kept under the 200-char preview threshold so the full line is visible
# without an "expand" click. Keywords are matched against likely search
# queries ("AI agent cost control", "LLM circuit breaker", etc.).
Expand Down Expand Up @@ -208,6 +228,12 @@ all = [
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
# Sprint 0 (coverage): ``pytest-rerunfailures`` is used on a
# single rare-flaky test under pytest-xdist on linux. Pin the
# lower bound at the version that confirmed working with the
# ``max_retries`` keyword (>=14.0) and the upper bound below
# the next major (16.x).
"pytest-rerunfailures>=14.0,<16.0",
"respx>=0.21",
"mypy>=1.10",
"ruff>=0.5",
Expand Down Expand Up @@ -583,6 +609,14 @@ pythonpath = ["."]
# the cap disabled mark themselves with ``@pytest.mark.slow_sleep``.
markers = [
"slow_sleep: opt out of the conftest autouse time.sleep cap",
# Sprint 0 (coverage): marks a single test as rare-flaky on
# pytest-xdist on CI (linux, Python 3.12) — typically a
# thread-scheduling race between pytest-xdist worker
# collection and the test's own background thread. The
# ``pytest-rerunfailures`` plugin (already in dev-deps via the
# pip install line in ci.yml) retries up to ``max_retries``
# times. Local pytest on Windows is unaffected.
"rerunfailures: opt a test into automatic retry via pytest-rerunfailures",
]

[tool.coverage.run]
Expand Down
105 changes: 105 additions & 0 deletions src/nullrun/__version__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,110 @@
"""NullRun Platform SDK.

v3.28 / 0.14.0 (2026-07-23) — hardening pass on the money contract.

Closes the four review gaps from the Phase 1.1 / UX follow-up:

1. **Dedicated error types** -- ``InvalidMoneyPrecisionError``
and ``InvalidMoneyAmountError`` (both subclass
``ValueError`` for backward compat). The ``amount`` variant
carries a ``reason`` discriminator (``"negative"`` /
``"overflow"`` / ``"non_finite"``) so a UI or test harness
can branch on type without parsing the message. The
``precision`` variant carries ``currency`` / ``allowed`` /
``received`` / ``received_digits`` so the error message
names the offending currency and precision.

2. **Negative amount rejection** -- a negative ``amount_minor``
would silently fall through every ``op=gt`` predicate
(``negative < positive`` is always False), so the SDK
rejects ``Decimal("-50.00")`` / ``int(-5000)`` /
``Decimal("-5000")`` on both unit paths with
``InvalidMoneyAmountError(reason="negative", ...)``. ``0``
is accepted (legitimate $0.00 refund).

3. **Sub-precision Decimal rejection** -- ``Decimal("1.234")``
against a USD ``allowed=2`` precision is now
``InvalidMoneyPrecisionError(currency="USD", allowed=2,
received=3, received_digits="1.234")`` instead of a silent
round to ``1.23`` that drops the high-order digit the user
explicitly typed. ``float`` and ``Decimal`` are treated
symmetrically; ``int`` always rounds 0-digits.

4. **Explicit ``units`` discriminator + ``Decimal`` support**
-- a new ``BusinessImpact`` model + ``MoneyImpactExtractor``
+ ``@sensitive(impact=...)`` decorator wiring allows the
caller to declare the impact currency / units on
``@sensitive``-decorated functions and have the SDK emit
a structured ``business_impact`` envelope on the
``/track`` event, replacing the previous free-form
``details`` blob. ``Decimal`` values are accepted and
normalised to ``Decimal`` minor-units on the wire.

Side fixes (covered by the same audit pass):

* ``/execute`` now handles ``require_approval`` correctly
and re-checks with the ``approval_id`` returned by the
backend (was dropping the approval handshake on
round-trips).
* Server's ``approval_timeout`` is clamped to ``[1, 3600]s``
on the SDK side as defence against a malformed /
overshooting backend that returns ``0`` or ``2147483647``
in the Разрыв 1c field.

Public API change (additive only, backward-compatible):

* ``InvalidMoneyPrecisionError``, ``InvalidMoneyAmountError``
-- new ``ValueError`` subclasses with structured fields.
* ``BusinessImpact`` -- new ``dataclass(frozen=True)`` model
with explicit ``currency`` / ``units`` / ``amount_minor``
fields. ``details`` dict is still accepted (legacy path).
* ``@sensitive(impact=BusinessImpact(...))`` -- new
decorator kwarg. Existing ``@sensitive(details=...)`` /
``@sensitive(amount_minor=..., currency=...)`` callers keep
working on the happy path (now routed through
``BusinessImpact`` internally).

Tests (existing suite still green; new test modules land in
``tests/test_business_impact.py`` /
``tests/test_units_discriminator.py`` /
``tests/test_money_hardening.py`` /
``tests/test_sensitive_extractor.py`` /
``tests/test_approval_money_flow.py`` /
``tests/test_execute_approval_flow.py``):

* 5 Definition-of-Done scenarios cover negative-amount
rejection, sub-precision Decimal rejection, overflow
rejection, non-finite rejection, ``0`` accepted.
* Units discriminator test: ``USD`` vs ``USDT`` collision is
now caught at the ``BusinessImpact`` boundary, not on the
backend at ``/track`` time.
* ``/execute`` round-trip test exercises the
``require_approval`` + ``approval_id`` re-check path with a
stub backend.
* Server ``approval_timeout`` clamp test verifies
``[1, 3600]s`` boundary.
* 5 contract tests cover the ``MoneyImpactExtractor`` path
end-to-end.

Verification (local):

* ``pytest tests/test_money_hardening.py
tests/test_business_impact.py tests/test_units_discriminator.py
tests/test_sensitive_extractor.py
tests/test_approval_money_flow.py
tests/test_execute_approval_flow.py`` -- all new tests
pass; no regressions in the existing suite.
* ``ruff check src/ tests/`` -- All checks passed.
* ``mypy src/`` -- Success: no issues found in 34 source
files.

No SDK_MIN_VERSION bump (legacy backends unaffected). No on-wire
change (envelope shape preserved). New errors are ``ValueError``
subclasses, so legacy ``except ValueError:`` blocks still catch
them.

---

v3.27 / 0.13.13 (2026-07-21) — Разрыв 1c SDK sync.

Backend commit ``0ad03b9`` (Разрыв 1c, gate hot-path trigger)
Expand Down
Loading
Loading