Skip to content

chore(release): 0.18.0 — close approval-row orphan via auto-consume on success + exception paths (ADR-047) - #109

Merged
maltsev-dev merged 1 commit into
masterfrom
release/0.18.0
Sep 21, 2026
Merged

maltsev-dev merged 1 commit into
masterfrom
release/0.18.0

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

Summary

Minor release — closes the structural orphan where the approvals row stayed at status='APPROVED' past expires_at because the only path that flipped it to CONSUMED was the orchestrator's Step 6 inline at backend/src/proxy/http/gate/orchestrator.rs:713, which mode="inline" tools bypass entirely. The fix wires the SDK to call a new structurally-distinct endpoint POST /api/v1/approvals/{approval_id}/consume from both the success path (after WS approval resolves to outcome=approved) and the exception path (`_safe_cancel_active_execution`). Operator-initiated /cancel on an approval envelope ALSO consumes the row in spawned Step 4e. Audit emits distinguish operator-cancel from SDK-consume via distinct matched_rule strings.

Behaviour change: outbound HTTP call from the SDK success branch (check_workflow_budget after WS approval) and exception path. Wire-format unchanged (additive endpoint). SDK_MIN_VERSION unchanged.

Fixed

  • ADR-047 — close approval-row orphan via auto-consume on success + exception paths (03cd6ed, 9 files, +584/-9). Runtime.consume_approval(approval_id, execution_id=None) + Transport.consume_approval() hit the new POST /api/v1/approvals/{approval_id}/consume endpoint (backend 2.8.0+). Sibling to /api/v1/cancel; structurally distinct from consume_approved (omits execution_id binding per ADR-046, carries organization_id filter, optionally filters by api_key_id). Three response shapes: consumed / already_consumed / not_approved — all return 200 (idempotent). check_workflow_budget auto-calls on the outcome=approved branch. _safe_cancel_active_execution ALSO calls after cancel_execution. Best-effort posture: catches all exceptions, surfaces at logger.debug, never blocks the success or exception path. The approval_expiry_sweeper at backend/src/workers/approval_expiry.rs will close any rows that slip through within ~5 min.

  • ADR-047 (audit attribution) — two distinct matched_rule strings on the audit_events table: lifecycle.consume_approved_via_sdk + consume_reason="sdk_consume_endpoint" for SDK-side, lifecycle.consume_approved_via_cancel + consume_reason="operator_cancel" for operator-initiated cancel. Same audit_kind (approval.consumed) and status (success) so the approvals lifecycle view aggregates them correctly while the audit deep-dive filters on matched_rule + consume_reason.

Cleanup

No pre-flight scrub needed — .gitignore already covered dist_local/ and src/**/*.defect* since the 0.16.7 cleanup commit; git grep -nE "dist_local|\.defect" returns only .gitignore + CHANGELOG references (no committed artifacts).

Verification

Check Result
ruff check src tests All checks passed
mypy src/nullrun Success: no issues found in 37 source files
pytest -q 1814 passed, 4 skipped in 122.74s (vs 1807 baseline at 0.17.1 — 7 new tests: 6 in TestConsumeApprovalEndpoint / TestCheckWorkflowBudgetConsumeOnApproved / TestSafeCancelCallsConsumeApproval + 1 prior)
Scratch diff vs origin/master clean (no dist_local/, no *.defect*)
nullrun.__version__ 0.18.0
Wire-format compatibility unchanged from 0.17.1 (additive consume endpoint only)

Commits included

03cd6ed chore(release): 0.18.0 — close approval-row orphan via auto-consume on success + exception paths (ADR-047)

…n success + exception paths (ADR-047)

- Add Runtime.consume_approval() + Transport.consume_approval() hitting
  the new POST /api/v1/approvals/{approval_id}/consume endpoint (backend
  2.8.0+). Best-effort: catches all exceptions, surfaces at DEBUG, never
  blocks the success path.
- Add Runtime.lookup_pending_approval_id_for_execution() +
  Runtime._mark_approval_resolved_for_execution() — RLock-guarded
  reverse-index (execution_id -> approval_id) populated by the WS push
  handler.
- check_workflow_budget on outcome=approved: auto-call consume_approval
  + pop reverse index BEFORE return. Closes the structural orphan where
  mode=inline tools left approval rows at status=APPROVED past expires_at.
- _safe_cancel_active_execution: after cancel_execution, ALSO call
  consume_approval if a pending approval_id was captured for this
  execution_id. Idempotent — if no approval was captured the lookup
  returns None and this is a no-op.
- Bump version 0.17.1 -> 0.18.0 across pyproject.toml, src/nullrun/__version__.py,
  uv.lock stamp (line 2873); CHANGELOG entry + README section
  'Closing orphan grants (v0.18+)'.
- Add 6 pin tests in tests/test_v3_wire_contract.py:
  TestConsumeApprovalEndpoint (4), TestCheckWorkflowBudgetConsumeOnApproved (1),
  TestSafeCancelCallsConsumeApproval (2).

Verified: ruff check src tests all-green, mypy src/nullrun clean (37 source
files), pytest 1814 passed / 4 skipped (was 1807 baseline + 6 new + 1 prior).
Wire-format unchanged (additive endpoint). SDK_MIN_VERSION unchanged.
@maltsev-dev
maltsev-dev merged commit 839ffee into master Sep 21, 2026
4 checks passed
@maltsev-dev
maltsev-dev deleted the release/0.18.0 branch September 21, 2026 09:08
@codecov

codecov Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.41463% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/nullrun/runtime.py 50.00% 10 Missing and 2 partials ⚠️
src/nullrun/transport.py 80.00% 2 Missing ⚠️
src/nullrun/decorators.py 83.33% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

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