Skip to content

Commit 3934502

Browse files
mikemolinetclaude
andcommitted
fix(cues): idempotency_key is a body field on fire, not a header
Caught by CI on PR #33 — test_fire_with_idempotency_key failed because my SDK was sending the key as ``Idempotency-Key`` header, but the server's ``FireRequest`` schema (cueapi #683) takes it as a BODY field. Server-side inconsistency vs the messaging primitive: messages.send takes ``Idempotency-Key`` as a header (``Header(default=None, alias="Idempotency-Key")`` in app/routers/messages.py:53), but cues fire takes it as a body field on FireRequest. Same feature name, two different transports. Phase 2 spec (#683) chose body for cues; SDK has to live with it. Also fixed: ``exit_criteria`` was typed ``Dict[str, Any]`` but the server's FireRequest schema (cueapi #632) defines it as ``Optional[List[str]]`` — list of required-assertion keys for §14 work-verification-light, max 20 entries. Updated SDK type + docstring to match. Inline comment explains the server-side header-vs-body inconsistency so future SDK refactors don't "simplify" the code by moving it back to the header (which would silently 400 on the server's ``extra="forbid"``). Caught at CI not local because integration tests against staging are the only place server-side idempotency behavior is exercised. Self-noted: ALWAYS verify server schema before claiming "X is header" vs "X is body" in SDK ports — same feature can have different transports across endpoints. Coordination memo: CTO-SEC-PYTHON-33-TEST-FAIL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 935080d commit 3934502

1 file changed

Lines changed: 24 additions & 16 deletions

File tree

cueapi/resources/cues.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def fire(
226226
payload_override: Optional[Dict[str, Any]] = None,
227227
merge_strategy: Optional[str] = None,
228228
send_at: Optional[Union[str, datetime]] = None,
229-
exit_criteria: Optional[Dict[str, Any]] = None,
229+
exit_criteria: Optional[List[str]] = None,
230230
idempotency_key: Optional[str] = None,
231231
) -> Dict[str, Any]:
232232
"""Fire an existing cue, optionally overriding payload + scheduling.
@@ -250,14 +250,22 @@ def fire(
250250
send_at: Optional ISO 8601 timestamp (or ``datetime``) to
251251
delay this fire. If omitted, the execution is scheduled
252252
immediately. Per-fire scheduling landed in cueapi #618.
253-
exit_criteria: Optional per-fire termination conditions
254-
(cueapi #632). Dict shape mirrors the API contract;
255-
keys vary by criterion type.
256-
idempotency_key: Optional ``Idempotency-Key`` header
257-
(cueapi #683). Same key + same body within 24h returns
258-
the existing execution with HTTP 200 instead of creating
259-
a new fire; same key + different body returns 409
260-
``idempotency_key_conflict``.
253+
exit_criteria: Optional list of required-assertion keys for
254+
§14 work-verification-light (cueapi #632). When non-null,
255+
the receiver MUST report values for every key under
256+
``outcome.assertions``; missing keys mark the execution
257+
``verification_failed``. Empty list (``[]``) explicitly
258+
opts out of cue-level required_assertions for this fire.
259+
None = use cue-level (existing behavior). Max 20 keys.
260+
idempotency_key: Optional opaque caller-supplied dedup key
261+
(cueapi #683, ≤256 chars). Same key on the same cue
262+
within 24h returns the cached execution without firing
263+
again (matched by SHA-256 fingerprint of the canonicalized
264+
body). Same key + DIFFERENT body in the window returns
265+
409 ``idempotency_key_conflict``. Sent as a body field
266+
(NOT the ``Idempotency-Key`` header — server-side cues
267+
fire diverges from messaging-primitive convention here;
268+
Phase 2 spec puts it in the body).
261269
262270
Returns:
263271
The execution dict (id, scheduled_for, status, triggered_by,
@@ -269,6 +277,7 @@ def fire(
269277
... "cue_abc123",
270278
... payload_override={"task": "manual-trigger"},
271279
... send_at="2026-05-07T12:00:00Z",
280+
... exit_criteria=["task_completed", "result_valid"],
272281
... idempotency_key="ci-run-456",
273282
... )
274283
"""
@@ -283,12 +292,11 @@ def fire(
283292
)
284293
if exit_criteria is not None:
285294
body["exit_criteria"] = exit_criteria
286-
287-
headers = {}
295+
# idempotency_key is a body field on cues fire (server's
296+
# FireRequest schema), unlike messaging-primitive idempotency
297+
# which uses the Idempotency-Key header. Server-side
298+
# inconsistency that the SDK has to live with.
288299
if idempotency_key is not None:
289-
headers["Idempotency-Key"] = idempotency_key
300+
body["idempotency_key"] = idempotency_key
290301

291-
kwargs: Dict[str, Any] = {"json": body}
292-
if headers:
293-
kwargs["headers"] = headers
294-
return self._client._post(f"/v1/cues/{cue_id}/fire", **kwargs)
302+
return self._client._post(f"/v1/cues/{cue_id}/fire", json=body)

0 commit comments

Comments
 (0)