From ed293f18f8ced9952c3d501c8d04e2630a470e24 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 2 May 2026 09:55:17 +0000 Subject: [PATCH] docs: document HTTP 409 response for policy-blocked promote/rollback The fix(server) commit (b4f2f6a) changed POST /v1/promote and POST /v1/rollback to return HTTP 409 Conflict when the active policy blocks the action, replacing the prior behavior of HTTP 200 with promoted_pointer_changed=false. Update three reference docs to reflect this: - docs/http-api.md: add a 'Policy-blocked response (HTTP 409)' section under POST /v1/promote with the exact JSON shape, update the error table to include 409, and mirror the equivalent note for POST /v1/rollback. - docs/operations-and-policy.md: expand the 'Promotion blocked by policy' section to call out the 409 for HTTP and the raised HTTPStatusError for the SDK, replacing the single sentence that only mentioned CLI and HTTP 200 behavior. - docs/sdk.md: add a 'Policy-blocked promote/rollback (HTTP 409)' subsection under Error handling with a code example showing how to catch and inspect the 409 detail body. Co-authored-by: Gottam Sai Bharath --- docs/http-api.md | 45 +++++++++++++++++++++++++++++------ docs/operations-and-policy.md | 13 +++++++--- docs/sdk.md | 17 ++++++++++++- 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/docs/http-api.md b/docs/http-api.md index c63fb94..da63124 100644 --- a/docs/http-api.md +++ b/docs/http-api.md @@ -292,7 +292,7 @@ policy passes. `reason` must be non-empty. `actor` defaults to `"http"`. -**Response** +**Response (policy passes)** ```json { "action_id": "act_def456", @@ -310,31 +310,62 @@ policy passes. } ``` -When `promoted_pointer_changed` is `false`, policy did not pass — the release was **not** -promoted. Check `policy.reasons` for the failure details. - **First promotion** (no prior baseline for this agent/environment): policy evaluation is skipped and the release is promoted unconditionally with reason `"first promotion: no promoted baseline for agent/environment"`. +**Policy-blocked response (HTTP 409)** + +When the active policy blocks promotion, the server returns HTTP **409 Conflict**. The +action is still written to the audit ledger; only the promoted pointer is not updated. + +```json +{ + "detail": { + "message": "Promotion blocked by policy.", + "outcome": { + "action_id": "act_def456", + "action": "promote", + "release_id": "rel_abc123", + "agent_id": "agent_support", + "environment": "production", + "baseline_release_id": "rel_prev789", + "promoted_pointer_changed": false, + "policy": { + "passed": false, + "reasons": ["candidate cost per run USD 0.006 exceeds max 0.005"], + "evaluated_at": "2026-05-01T13:00:00+00:00" + } + } + } +} +``` + +Check `detail.outcome.policy.reasons` for the specific constraints that failed. + **Errors** - HTTP 400 — unknown release ID, missing pricing table, invalid window, or empty reason. - HTTP 401 — Bearer token missing or invalid (when a token is configured). - HTTP 403 — caller is not a loopback client and no token is configured. +- HTTP 409 — action recorded in the audit ledger but blocked by the active policy. --- ## `POST /v1/rollback` Roll back to a prior release. Identical contract to `/v1/promote` but with `"action": -"rollback"` in the response. A promoted baseline must already exist; rolling back when -nothing is promoted returns HTTP 400. +"rollback"` in the response and `"message": "Rollback blocked by policy."` in the 409 +body. A promoted baseline must already exist; rolling back when nothing is promoted +returns HTTP 400. **Requires mutation access** (loopback client or Bearer token). **Request body** — same shape as `/v1/promote`. -**Response** — same shape as `/v1/promote` with `"action": "rollback"`. +**Response (policy passes)** — same shape as `/v1/promote` with `"action": "rollback"`. + +**Policy-blocked response** — same 409 shape as `/v1/promote` with `"action": "rollback"` +and `"message": "Rollback blocked by policy."`. --- diff --git a/docs/operations-and-policy.md b/docs/operations-and-policy.md index 3c8c30d..58431f7 100644 --- a/docs/operations-and-policy.md +++ b/docs/operations-and-policy.md @@ -226,9 +226,16 @@ require_high_diff_confidence: false ### Promotion blocked by policy When policy fails, the promotion/rollback attempt is **recorded in the audit ledger** -(the intent is captured) but the promoted pointer is **not** updated. The CLI exits with -a non-zero code; the HTTP API returns the full response body with `promoted_pointer_changed: -false` and `policy.passed: false`. +(the intent is captured) but the promoted pointer is **not** updated. + +- **CLI:** exits with a non-zero code and prints the policy failure reasons. +- **HTTP API:** returns **HTTP 409 Conflict** with a structured `detail` body containing + `message` and the full `outcome` object (including `promoted_pointer_changed: false` and + `policy.passed: false`). See [HTTP API reference](http-api.md#post-v1promote) for the + exact response shape. +- **SDK (`post_promote` / `post_rollback`):** raises `httpx.HTTPStatusError` with + `response.status_code == 409`. The full `detail` body is accessible via + `e.response.json()["detail"]`. --- diff --git a/docs/sdk.md b/docs/sdk.md index e7d3a37..f0cfd29 100644 --- a/docs/sdk.md +++ b/docs/sdk.md @@ -172,6 +172,12 @@ All methods call `response.raise_for_status()` before returning, so HTTP 4xx/5xx responses raise `httpx.HTTPStatusError`. Transient network failures raise `httpx.RequestError` and are retried up to `max_retries` times with exponential backoff. +**Policy-blocked promote/rollback (HTTP 409)** + +When the active policy blocks a `post_promote` or `post_rollback` call, the server returns +HTTP 409. The SDK raises `httpx.HTTPStatusError`; the full outcome — including which +policy constraints failed — is in `e.response.json()["detail"]`. + ```python import httpx @@ -183,9 +189,18 @@ try: reason="tested in staging", ) except httpx.HTTPStatusError as e: - print(e.response.status_code, e.response.json()) + if e.response.status_code == 409: + detail = e.response.json()["detail"] + # detail["message"] -> "Promotion blocked by policy." + # detail["outcome"]["policy"]["reasons"] -> list of failed constraints + print("Blocked:", detail["outcome"]["policy"]["reasons"]) + else: + raise ``` +The action is still recorded in the audit ledger even when blocked; `GET /v1/actions` +will show it with `policy_passed: false`. + ## Custom `httpx.Client` Inject a pre-configured client to set custom SSL certificates, proxies, or connection