From f2acd05d81c29e0034d409951793d63b3e8f19a3 Mon Sep 17 00:00:00 2001 From: Gk Date: Fri, 17 Apr 2026 16:33:42 -0700 Subject: [PATCH] feat: lift worker transport evidence-verification rejection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cueapi-worker 0.3.0 (released 2026-04-17 to PyPI) closes the worker- side evidence gap via CUEAPI_OUTCOME_FILE. The daemon reads the handler's per-run temp file after exit and merges the evidence fields into its outcome POST. All five verification modes now work on both transports. Changes: - app/services/cue_service.py: remove _check_transport_verification_combo and the two calls in create_cue + update_cue. Replace with info-level logging when a worker cue is configured with an evidence-requiring mode (breadcrumb for operators still running older cueapi-worker). - tests/test_transport_verification_combo.py: flip expected 400 → 201 on create, 400 → 200 on PATCH. Header comment documents the history. Two test classes renamed from WorkerEvidenceRejected* to WorkerEvidenceAccepted* / PatchTransitions::test_patch_worker_to_evidence_mode_accepted. - README.md: update transport-compatibility footnote to reflect the new accept-everything reality, with an upgrade hint for users on cueapi-worker < 0.3.0. - CHANGELOG: replace the "Restricted" entry (worker+evidence rejection) with a "Removed" entry describing the lift. Tests: 13/13 pass locally on the updated combo suite. Preconditions met: - cueapi-worker 0.3.0 published to PyPI (2026-04-17 22:04:39 UTC) - cueapi-core #18 merged to main (verification_mode column + rule engine in place to read verification_mode and produce outcome_state transitions) Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 4 +- README.md | 2 +- app/services/cue_service.py | 98 +++++++++------------- tests/test_transport_verification_combo.py | 45 +++++----- 4 files changed, 66 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5ccf2a..a914955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,8 @@ All notable changes to cueapi-core will be documented here. - **`POST /v1/executions/{id}/verify`** now accepts `{valid: bool, reason: str?}`. `valid=true` (default, preserving legacy behavior) transitions to `verified_success`; `valid=false` transitions to `verification_failed` and records the reason onto `evidence_summary` (truncated to 500 chars). Accepted starting states expanded to include `reported_failure`. - `OutcomeResponse` now surfaces `outcome_state` in the response body. -### Restricted -- Worker-transport cues cannot currently combine with evidence-requiring verification modes (`require_external_id`, `require_result_url`, `require_artifacts`). Attempting to create or PATCH such a combination returns `400 unsupported_verification_for_transport`. `none` and `manual` are allowed for worker cues. This restriction will be lifted once cueapi-worker 0.3.0 (evidence reporting via `CUEAPI_OUTCOME_FILE`) is on PyPI. +### Removed +- **Rejection of `(worker transport, require_*)` verification combos** has been lifted. cueapi-worker 0.3.0 (released 2026-04-17 to PyPI) closes the evidence gap via `$CUEAPI_OUTCOME_FILE`: handlers write evidence JSON to a per-run temp file; the daemon reads the file after exit and merges into the outcome POST. All five verification modes now work on both transports. Operators running older cueapi-worker versions should upgrade via `pip install --upgrade cueapi-worker` — until they do, evidence-requiring modes on worker cues will land every execution in `verification_failed`. ## [0.1.2] - 2026-03-28 diff --git a/README.md b/README.md index b9a5761..eea447e 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ curl -X POST http://localhost:8000/v1/executions/EXEC_ID/verify \ Backward-compat paths still work: `POST /outcome` with just `{success: true}` behaves identically to before, and `PATCH /v1/executions/{id}/evidence` remains available as a two-step alternative. -> Worker-transport cues can currently use `none` or `manual` only. Evidence-requiring modes (`require_external_id`, `require_result_url`, `require_artifacts`) are rejected at create/update time with `400 unsupported_verification_for_transport`. This restriction will be lifted once cueapi-worker 0.3.0 ships to PyPI with evidence reporting via `CUEAPI_OUTCOME_FILE`. +> Worker-transport cues accept every verification mode. Handlers report evidence via `$CUEAPI_OUTCOME_FILE` (cueapi-worker >= 0.3.0 on PyPI as of 2026-04-17). The daemon reads the file after the handler exits and merges the evidence into its outcome POST. If you're still on an older cueapi-worker, the evidence modes will land in `verification_failed` for every execution — `pip install --upgrade cueapi-worker` to unblock. ## Alerts diff --git a/app/services/cue_service.py b/app/services/cue_service.py index 000a77f..9fd4b4f 100644 --- a/app/services/cue_service.py +++ b/app/services/cue_service.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import logging from datetime import datetime, timedelta, timezone from typing import Optional @@ -29,47 +30,16 @@ def validate_cron(expression: str) -> bool: return False -# Verification modes that require evidence on the outcome report. -# Worker transport (today) has no path to attach evidence on the single -# outcome POST, so these modes are rejected for worker cues at create / -# update time. Ref: cueapi-worker < 0.3.0. This rejection is lifted in -# a later PR once cueapi-worker 0.3.0 (CUEAPI_OUTCOME_FILE) is on PyPI. -_EVIDENCE_REQUIRING_MODES = frozenset( - {"require_external_id", "require_result_url", "require_artifacts"} -) -_WORKER_COMPATIBLE_MODES = ("none", "manual") - - -def _check_transport_verification_combo( - transport: str, mode: Optional[str] -) -> Optional[dict]: - """Reject worker transport paired with evidence-based verification. - - Returns an error dict (matching the service-layer error shape) when - the combination is invalid, or None when it's fine. Lives here - rather than as a Pydantic validator because the existing API shape - uses structured 400 errors (``{"error": {"code": ...}}``) and - Pydantic ValueErrors surface as 422 with a different schema. - """ - if transport != "worker" or not mode or mode in _WORKER_COMPATIBLE_MODES: - return None - if mode not in _EVIDENCE_REQUIRING_MODES: - return None - return { - "error": { - "code": "unsupported_verification_for_transport", - "message": ( - "Worker transport does not yet support evidence-based " - "verification modes. Use 'none' or 'manual' for worker " - "cues, or switch to webhook transport for evidence " - "verification." - ), - "status": 400, - "transport": "worker", - "verification_mode": mode, - "supported_worker_modes": list(_WORKER_COMPATIBLE_MODES), - } - } +logger = logging.getLogger(__name__) + + +# Worker transport now supports every verification mode. Handlers +# report evidence via ``$CUEAPI_OUTCOME_FILE`` (cueapi-worker >= 0.3.0 +# on PyPI as of 2026-04-17); the daemon reads the file after the +# handler exits and merges the evidence into its outcome POST. The +# rejection function that used to gate ``(worker, require_*)`` combos +# was deleted along with this comment's predecessor. See the +# cueapi-worker README for the outcome-file schema and conflict rules. def _contains_null_byte(obj) -> bool: @@ -204,16 +174,22 @@ async def create_cue(db: AsyncSession, user: AuthenticatedUser, data: CueCreate) transport = data.transport or "webhook" warning = None - # Reject worker transport paired with evidence-based verification. - # See ``_check_transport_verification_combo`` for the rationale — - # this will be lifted once cueapi-worker 0.3.0 (evidence reporting - # via CUEAPI_OUTCOME_FILE) is on PyPI. - if data.verification is not None: - combo_err = _check_transport_verification_combo( - transport, data.verification.mode.value + # Note: previously rejected (worker, require_*) combos because + # cueapi-worker had no evidence channel. cueapi-worker 0.3.0 + # (CUEAPI_OUTCOME_FILE) closed that gap; all modes are now + # accepted on both transports. Log at INFO so operators with older + # worker versions have a breadcrumb. + if ( + transport == "worker" + and data.verification is not None + and data.verification.mode.value in ("require_external_id", "require_result_url", "require_artifacts") + ): + logger.info( + "Worker cue created with evidence-based verification mode=%s " + "(requires cueapi-worker >= 0.3.0 on the handler machine to " + "write $CUEAPI_OUTCOME_FILE).", + data.verification.mode.value, ) - if combo_err is not None: - return combo_err if transport == "webhook": is_valid, error_msg = validate_callback_url(str(data.callback.url), settings.ENV) @@ -456,16 +432,22 @@ async def update_cue(db: AsyncSession, user: AuthenticatedUser, cue_id: str, dat cue.retry_max_attempts = data.retry.max_attempts cue.retry_backoff_minutes = data.retry.backoff_minutes - # Verification policy update. Validate the *resulting* (transport, - # mode) combo — transport is effectively immutable via PATCH today, - # so the resulting transport is whatever the cue currently has. + # Verification policy update. All (transport, mode) combinations + # are now accepted — cueapi-worker 0.3.0 closed the worker-side + # evidence gap via $CUEAPI_OUTCOME_FILE. Log at INFO when a worker + # cue is switched to an evidence-based mode so operators have a + # breadcrumb in case older worker versions are still deployed. if data.verification is not None: resulting_transport = cue.callback_transport or "webhook" - combo_err = _check_transport_verification_combo( - resulting_transport, data.verification.mode.value - ) - if combo_err is not None: - return combo_err + if ( + resulting_transport == "worker" + and data.verification.mode.value in ("require_external_id", "require_result_url", "require_artifacts") + ): + logger.info( + "Cue %s updated to evidence-based verification mode=%s on " + "worker transport (requires cueapi-worker >= 0.3.0).", + cue.id, data.verification.mode.value, + ) cue.verification_mode = data.verification.mode.value if data.on_failure is not None: diff --git a/tests/test_transport_verification_combo.py b/tests/test_transport_verification_combo.py index a0e546a..b7cad3e 100644 --- a/tests/test_transport_verification_combo.py +++ b/tests/test_transport_verification_combo.py @@ -1,11 +1,18 @@ -"""Worker + evidence-based verification rejection. +"""Worker + evidence-based verification: now accepted. -This combo is rejected at cue create/update time because cueapi-worker -< 0.3.0 has no mechanism to attach evidence on the outcome report. The -rejection is lifted in a later PR once cueapi-worker 0.3.0 is on PyPI. +History: this file previously pinned the rejection of +``(worker, require_*)`` combos because cueapi-worker < 0.3.0 had no +mechanism to attach evidence on the outcome report. Those tests +asserted 400 on create and PATCH. -Eight tests: 3 evidence-requiring modes × (create, update, webhook -allowed) + 2 worker-compatible modes confirming the combo is allowed. +cueapi-worker 0.3.0 (CUEAPI_OUTCOME_FILE) shipped to PyPI 2026-04-17 +and closes that gap: the handler writes evidence to a per-run temp +file, the daemon merges it into the outcome POST. The rejection was +lifted in the PR that replaces this file's content. + +The assertions below now pin the accept behavior. Retained for +regression: if anyone reintroduces the combo-rejection they'll see +these tests fail and have to rationalize the rollback. """ from __future__ import annotations @@ -34,13 +41,13 @@ def _cue_body(*, transport="worker", mode=None, name=None): return body -class TestWorkerEvidenceRejectedAtCreate: +class TestWorkerEvidenceAcceptedAtCreate: @pytest.mark.asyncio @pytest.mark.parametrize( "mode", ["require_external_id", "require_result_url", "require_artifacts"], ) - async def test_worker_plus_evidence_mode_rejected( + async def test_worker_plus_evidence_mode_accepted( self, client: AsyncClient, auth_headers, mode ): resp = await client.post( @@ -48,13 +55,10 @@ async def test_worker_plus_evidence_mode_rejected( headers=auth_headers, json=_cue_body(transport="worker", mode=mode), ) - assert resp.status_code == 400 + assert resp.status_code == 201, resp.text body = resp.json() - err = body["detail"]["error"] if "detail" in body else body["error"] - assert err["code"] == "unsupported_verification_for_transport" - assert err["transport"] == "worker" - assert err["verification_mode"] == mode - assert err["supported_worker_modes"] == ["none", "manual"] + assert body["verification"] == {"mode": mode} + assert body["transport"] == "worker" or body["callback"]["transport"] == "worker" class TestWorkerCompatibleModesAcceptedAtCreate: @@ -102,10 +106,11 @@ async def test_webhook_any_mode_accepted( class TestPatchTransitions: @pytest.mark.asyncio - async def test_patch_worker_to_evidence_mode_rejected( + async def test_patch_worker_to_evidence_mode_accepted( self, client: AsyncClient, auth_headers ): - # Create worker cue with no verification + # Create worker cue with no verification, then PATCH to an + # evidence-requiring mode. Previously this returned 400; now 200. create = await client.post( "/v1/cues", headers=auth_headers, @@ -114,16 +119,13 @@ async def test_patch_worker_to_evidence_mode_rejected( assert create.status_code == 201 cue_id = create.json()["id"] - # Try to PATCH verification to an evidence-requiring mode resp = await client.patch( f"/v1/cues/{cue_id}", headers=auth_headers, json={"verification": {"mode": "require_external_id"}}, ) - assert resp.status_code == 400 - body = resp.json() - err = body["detail"]["error"] if "detail" in body else body["error"] - assert err["code"] == "unsupported_verification_for_transport" + assert resp.status_code == 200, resp.text + assert resp.json()["verification"] == {"mode": "require_external_id"} @pytest.mark.asyncio async def test_patch_webhook_to_evidence_mode_accepted( @@ -163,4 +165,3 @@ async def test_patch_worker_to_manual_accepted( json={"verification": {"mode": "manual"}}, ) assert resp.status_code == 200 - assert resp.json()["verification"] == {"mode": "manual"}