Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions docs/http-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ policy passes.

`reason` must be non-empty. `actor` defaults to `"http"`.

**Response**
**Response (policy passes)**
```json
{
"action_id": "act_def456",
Expand All @@ -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."`.

---

Expand Down
13 changes: 10 additions & 3 deletions docs/operations-and-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"]`.

---

Expand Down
17 changes: 16 additions & 1 deletion docs/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
Loading