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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ name = "nullrun"
# nullrun._singleton (the metaclass-backing descriptor) and
# nullrun._registry (the runtime registry) so runtime.py stays the
# orchestrator only. See __version__.py for the full changelog.
version = "0.13.2"
version = "0.13.3"
# Long form used by PyPI page meta-description and search snippets.
# Kept under the 200-char preview threshold so the full line is visible
# without an "expand" click. Keywords are matched against likely search
Expand Down
2 changes: 1 addition & 1 deletion src/nullrun/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,5 +278,5 @@
delta is the per-file mypy table in pyproject.toml).
"""

__version__ = "0.13.2"
__version__ = "0.13.3"
__platform_version__ = "1.0.0"
34 changes: 30 additions & 4 deletions src/nullrun/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -1871,8 +1871,24 @@ def track_single(
server-side from the request auth, not supplied by the SDK.
The docstring now matches the real wire contract.
"""
headers = self._build_signed_headers()
# 2026-07-06 (bug-fix): the previous shape called
# `_build_signed_headers()` *before* `_signed_request_body()`.
# That meant the HMAC branch in `_build_signed_headers`
# (gated on `body is not None`) saw `body=None` and skipped
# the X-Signature / X-Signature-Timestamp headers. The POST
# then went out unsigned; the backend's HMAC middleware
# (`HMAC_REQUIRED_PATHS` includes `/api/v1/track`) rejected
# the request with 401, the SDK raised
# `NullRunAuthenticationError`, the route dropped the event,
# and every llm_call event disappeared — leaving the
# dashboard stuck at $0 for every execution.
#
# Fix: build the body FIRST, then pass it to
# `_build_signed_headers(body=body)` so the signature is
# computed over the exact bytes that go on the wire
# (mirrors the canonical pattern in `check()` at L1530).
body = _signed_request_body(request)
headers = self._build_signed_headers(body=body)

try:
response = self._client.post(
Expand Down Expand Up @@ -1922,8 +1938,13 @@ def cancel(
if reason:
request["reason"] = reason

headers = self._build_signed_headers()
# 2026-07-06 (bug-fix): same body-before-headers reorder as
# track_single above. /api/v1/cancel isn't in HMAC_REQUIRED_PATHS
# today, but the helper still adds X-Signature when secret_key
# is set, and we want the call to be consistent with the
# canonical pattern.
body = _signed_request_body(request)
headers = self._build_signed_headers(body=body)

try:
response = self._client.post(
Expand Down Expand Up @@ -1969,8 +1990,10 @@ def heartbeat(
"chain_id":..., "last_active": ts}``).
"""
request = {"chain_id": chain_id}
headers = self._build_signed_headers()
# 2026-07-06 (bug-fix): same body-before-headers reorder as
# track_single above.
body = _signed_request_body(request)
headers = self._build_signed_headers(body=body)

try:
response = self._client.post(
Expand Down Expand Up @@ -2034,8 +2057,11 @@ def chain_end(
# call (the server ignores it on this path).
"execution_id": uuid.uuid4().hex,
}
headers = self._build_signed_headers()
# 2026-07-06 (bug-fix): same body-before-headers reorder as
# track_single. /api/v1/gate is in HMAC_REQUIRED_PATHS so
# the unsigned POST would 401 with "missing signature headers".
body = _signed_request_body(request)
headers = self._build_signed_headers(body=body)

try:
response = self._client.post(
Expand Down
Loading