Skip to content

Wip/working tree 2026 06 18#3

Merged
maltsev-dev merged 3 commits into
masterfrom
wip/working-tree-2026-06-18
Jun 18, 2026
Merged

Wip/working tree 2026 06 18#3
maltsev-dev merged 3 commits into
masterfrom
wip/working-tree-2026-06-18

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

What

Why

How

Test plan

  • Unit tests pass (per-repo, e.g. cd backend && cargo test, cd frontend && npm test)
  • Lint passes (per-repo, e.g. cd frontend && npm run lint)
  • Type-check passes (per-repo, e.g. cd frontend && npm run type-check)
  • Manually verified in dev / staging

Risk

Checklist

  • I have read the repo's CONTRIBUTING.md (if present)
  • My change does not introduce new lint warnings
  • I have updated the CHANGELOG (if user-visible)
  • I have considered backwards compatibility

Counterpart of NULLRUN fix(ws-control) (commit 5e2f65b). The
backend now embeds the exact bytes that were HMAC-signed in a
separate signed_payload field. The SDK:

  1. Verifies the signature against bytes.fromhex(signed_payload),
     falling back to the legacy wire-bytes path only when the
     field is absent (pre-FIX-C servers).
  2. Dispatches state changes from the parsed signed_payload
     bytes, not from the outer envelope body. This closes a
     security hole: an attacker who captured a (signed_payload,
     signature) pair from a benign 'state=Normal' event could
     otherwise splice a forged 'state=Killed' into the outer body
     and the signature would still verify, because the signature
     covers only the signed_payload bytes. Reading dispatch state
     from the trusted source keeps the captured signature
     semantically bound to its captured body.

Tests in test_ws_signed_payload.py cover:
  - round-trip, wrong-secret, tampered-payload rejection
  - malformed signed_payload does not crash
  - replay-with-spliced-body: signature still verifies, but the
    dispatched state is the captured one (not the forged one) -
    the attack is harmless
  - replays where the attacker also rewrites signed_payload are
    rejected via signature mismatch

Note: the two ACK tests are still failing because
ACKNOWLEDGED_STATES is still lowercase. That is fixed separately
by S-2 in the same release - kept as a separate commit so the
byte-mismatch/security fix is reviewable on its own.
The server's WsWorkflowState enum (NULLRUN/backend/src/proxy/http/
ws_control.rs) emits 'Killed' / 'Paused' (PascalCase). The SDK was
comparing against {'killed', 'paused'} (lowercase), so the ACK path
was dead and the server's pending-ack queue grew without ever
being drained.

This unblocks the two remaining failing tests in
test_ws_signed_payload.py:
  - test_state_change_with_signed_payload_is_dispatched (now sends
    the ACK that the server expects)
  - test_acknowledged_states_use_pascalcase (now matches server
    casing)

With byte-mismatch FIX-C in place (commits 5e2f65b + 105fb80), the
KILL/PAUSE path now works end-to-end:
  1. server signs the inner message and embeds the bytes in
     signed_payload
  2. server sends the envelope (flattened WsMessage + signature +
     timestamp + api_key_id + signed_payload)
  3. SDK verifies signature against bytes.fromhex(signed_payload)
  4. SDK dispatches from the trusted source (parsed signed_payload),
     so a captured (signed_payload, signature) pair can only
     re-trigger its captured state, never a forged one
  5. SDK sends ACK on Killed/Paused, draining server's pending-acks
The working tree contained a large uncommitted changeset that was
never pushed: 68 files, +8955/-3328 lines. Reading the diff shape
this is the 0.3.0 -> 0.4.0 production-readiness migration
(per CHANGELOG.md / audit §6.1):

  - PoolConfig / AdaptivePool removed (Transport now is a
    context manager; weakref.finalize replaces atexit.register)
  - gRPC transport removed (NULLRUN_USE_GRPC no-op; create_grpc_transport
    was a NameError)
  - signal.signal global hijack removed
  - track.proto removed
  - decision_history / flow / gate / common placeholders removed
  - six zombie exceptions removed (CostLimitExceeded,
    ApprovalRequired, BreakerTimeout, LoopDetectedException,
    RetryStormException, RateLimitExceededException)
  - _organization_id_var, _api_key_id_var removed
  - patch_openai / unpatch_openai removed
  - auto-instrumentation extended with langgraph / llama-index /
    crewai / autogen / openai-agents via safe_patch
  - SENSITIVE_ARG_KEYS expanded from 7 to 29 tokens
  - HMAC always-on for /track/batch, /gate, /evaluate, /status,
    /auth/verify + WS ACKs signed
  - 14 new test files
  - analyze.md (this session's plan)

Tracking as a wip branch so the work is preserved. This commit does
not change the byte-mismatch FIX-C landing in
fix/ws-byte-mismatch-verify-signed-payload (commits 105fb80,
73f3197) - those branches are based on 316a694 + the byte-mismatch
fixes only.
@maltsev-dev maltsev-dev merged commit 1244901 into master Jun 18, 2026
0 of 4 checks passed
@maltsev-dev maltsev-dev deleted the wip/working-tree-2026-06-18 branch June 18, 2026 15:05
maltsev-dev added a commit that referenced this pull request Jul 3, 2026
Task #3 / Task #18 (2026-07-03): wire the SDK to the backend's
v3 default. Per CLAUDE.md §24, every /check mints a
server-side uuidv7 execution_id; the SDK receives it in the
response and propagates it to /track.

This is the SDK_MIN_VERSION for the v3 rollout per CLAUDE.md
§0 pre-flip checklist.

Changes:

src/nullrun/uuid7.py (new):
- RFC 9562 §5.7 time-ordered ID generator. 48-bit unix_ts_ms
  prefix + 12-bit rand_a + 62-bit rand_b. Same layout as the
  backend's mint_execution_id() so log scrapers can sort by
  ID alone.
- Uses secrets.token_bytes(10) for cryptographically secure
  random component.
- uuid7() returns stdlib UUID; uuid7_str() returns the
  canonical 36-char string.

src/nullrun/capabilities.py (new):
- ServerCapabilities dataclass mirrors /health payload.
- is_v3_ready() returns True only when ALL three v3 caps
  (server_minted_execution_id, per_execution_reservations,
  heartbeat_time_based) are set.
- probe_capabilities(api_url) — best-effort /health fetch
  with 2s timeout. Returns None on failure (not fatal).
- validate_sdk_version(sdk_version, caps) — returns warnings
  for SDK_MIN_VERSION mismatch.
- SDK_MIN_VERSION_FOR_V3 = '0.12.0' is the gate's coordinate
  for the v3 rollout.

src/nullrun/__init__.py:
- init() now probes /health after singleton registration and
  logs a startup warning for version mismatch (does NOT fail
  init() — the gate still rejects with PROTOCOL_TOO_OLD).
- Probe is best-effort: timeout/5xx logs at INFO.

src/nullrun/__version__.py:
- Bumped 0.11.0 → 0.12.0 (the SDK_MIN_VERSION coordinate).

CHANGELOG.md:
- New 0.12.0 entry with Added/Changed sections.

tests/test_uuid7.py (new): 8 tests pin the wire contract:
- Returns stdlib UUID
- 36-char string format
- Version bits = 7
- Variant bits = 0b10
- Time-ordered (consecutive calls sort)
- 1000 unique IDs under rapid calls
- Round-trips through uuid.UUID()

tests/test_capabilities.py (new): 9 tests pin:
- v3-ready backend parses to is_v3_ready()=True
- Missing keys default to False (fail-closed)
- Partial v3 caps → not ready
- Old SDK against v3 backend → warning
- Current SDK → no warning
- Legacy backend → 'not v3-ready' warning
- Unparseable versions don't crash
- as_dict() is wire-safe (no secrets)
- SDK_MIN_VERSION_FOR_V3 = '0.12.0'

Tests: 17 new SDK tests pass. Full backend test suite still
green at 1443.
maltsev-dev added a commit that referenced this pull request Jul 3, 2026
* feat(sdk): server-minted execution_id default — uuid7 + capability probe

Task #3 / Task #18 (2026-07-03): wire the SDK to the backend's
v3 default. Per CLAUDE.md §24, every /check mints a
server-side uuidv7 execution_id; the SDK receives it in the
response and propagates it to /track.

This is the SDK_MIN_VERSION for the v3 rollout per CLAUDE.md
§0 pre-flip checklist.

Changes:

src/nullrun/uuid7.py (new):
- RFC 9562 §5.7 time-ordered ID generator. 48-bit unix_ts_ms
  prefix + 12-bit rand_a + 62-bit rand_b. Same layout as the
  backend's mint_execution_id() so log scrapers can sort by
  ID alone.
- Uses secrets.token_bytes(10) for cryptographically secure
  random component.
- uuid7() returns stdlib UUID; uuid7_str() returns the
  canonical 36-char string.

src/nullrun/capabilities.py (new):
- ServerCapabilities dataclass mirrors /health payload.
- is_v3_ready() returns True only when ALL three v3 caps
  (server_minted_execution_id, per_execution_reservations,
  heartbeat_time_based) are set.
- probe_capabilities(api_url) — best-effort /health fetch
  with 2s timeout. Returns None on failure (not fatal).
- validate_sdk_version(sdk_version, caps) — returns warnings
  for SDK_MIN_VERSION mismatch.
- SDK_MIN_VERSION_FOR_V3 = '0.12.0' is the gate's coordinate
  for the v3 rollout.

src/nullrun/__init__.py:
- init() now probes /health after singleton registration and
  logs a startup warning for version mismatch (does NOT fail
  init() — the gate still rejects with PROTOCOL_TOO_OLD).
- Probe is best-effort: timeout/5xx logs at INFO.

src/nullrun/__version__.py:
- Bumped 0.11.0 → 0.12.0 (the SDK_MIN_VERSION coordinate).

CHANGELOG.md:
- New 0.12.0 entry with Added/Changed sections.

tests/test_uuid7.py (new): 8 tests pin the wire contract:
- Returns stdlib UUID
- 36-char string format
- Version bits = 7
- Variant bits = 0b10
- Time-ordered (consecutive calls sort)
- 1000 unique IDs under rapid calls
- Round-trips through uuid.UUID()

tests/test_capabilities.py (new): 9 tests pin:
- v3-ready backend parses to is_v3_ready()=True
- Missing keys default to False (fail-closed)
- Partial v3 caps → not ready
- Old SDK against v3 backend → warning
- Current SDK → no warning
- Legacy backend → 'not v3-ready' warning
- Unparseable versions don't crash
- as_dict() is wire-safe (no secrets)
- SDK_MIN_VERSION_FOR_V3 = '0.12.0'

Tests: 17 new SDK tests pass. Full backend test suite still
green at 1443.

* fix(sdk): populate Author/Author-email via metadata hook

PEP 621 maps `authors` to PKG-INFO's `Author-email:` line but not to the
legacy single `Author:` line that `pip show` renders, and pip does
not display `Maintainer:` either. As a result every previous release
shipped with an empty `Author:` and the maintainer's name never
appeared in `pip show nullrun`.

Hatchling compounds this: its authors parser only adds an entry to
`authors_data["name"]` (which becomes `Author:`) when an
inline-table has a `name` and NO `email`. When both are present the
name is folded into `Author-email:`'s display_name and the legacy
`Author:` line is suppressed entirely.

Fix: declare `authors` and `maintainers` as dynamic fields and
populate them from a custom hatchling metadata hook
(`hatch_build.py`). The hook splits the primary author into a
name-only + email-only inline-table pair so hatchling populates both
`Author:` and `Author-email:`. Declaring at least one dynamic field
is what actually wires `MetadataHookInterface.update()` — without it
hatchling configures the hook but never invokes it.

* fix(sdk): bind logger in init() and cover capability probe paths

CI on Python 3.11 failed with `NameError: name 'logger' is not defined`
in 5 tests. The `feat(sdk)` commit (2a7886b) added new
`logger.warning/info/debug` calls in `init()` after the existing
`import logging` but never assigned the `logger` name. Master
passed only because its pre-existing `logger.warning` calls sit
inside an `if existing is not None:` branch that tests rarely
exercise; the new ones run on every `init()` call.

Also covers the 9 newly-uncovered lines Codecov flagged:
`probe_capabilities` failure paths (non-2xx / ConnectError /
malformed JSON) and the four new `init()` logging branches
(`debug=True` sets DEBUG; probe unreachable → INFO; probe raises
→ DEBUG; existing runtime shutdown raises → WARNING).

Local verification (.venv-ci, Python 3.14):
- pytest: 1154 passed (was 1129; +25 new)
- ruff: clean
- mypy: clean
- coverage: 82.02% (threshold 82.00%)

* style: reorder capability probe imports per ruff I001

Ruff's isort rule flagged the import block in `init()` — the
`from nullrun.__version__` line was placed after
`from nullrun.capabilities` but `__version__` sorts before
`capabilities` (underscore is 0x5F, letters are 0x61+), so the
correct alphabetical order is reversed.

CI `Run ruff` step was failing on this; the previous commit's
ruff output was checked against an outdated working copy.
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