Skip to content

Commit 9c24809

Browse files
committed
docs: update changelog and architecture for revocation fixes and sync row-lock correction
1 parent 8044c23 commit 9c24809

2 files changed

Lines changed: 23 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,26 @@
6060

6161
### Fixed
6262

63+
- **Sync ticket reads now hold a row lock**`SyncSqlAlchemyUnitOfWork.get_pending_ticket_for_update()`
64+
and `get_ticket_for_update()` were missing `.with_for_update()`. On PostgreSQL,
65+
concurrent sync callers (e.g. `revoke_ticket` + `approve_ticket` on the same ticket)
66+
could both read the same row without a lock, both pass the application-level status
67+
guard, and both write — last write wins silently. The async backend already locked the
68+
row; the sync backend now does too.
69+
70+
- **`revoke_ticket()` rejects revocation of already-executed proposals** — both the sync
71+
and async facades called `update_status(proposal_id, PENDING)` unconditionally. A
72+
proposal already in `EXECUTED` or `FAILED` state would be silently reset to `PENDING`,
73+
making it eligible for re-approval and re-execution of an action that had already run.
74+
Both facades now fetch the current proposal status before writing and raise `ValueError`
75+
when the proposal is in a terminal state.
76+
77+
- **Removed orphaned `ApprovalGate.revoke_approval()` engine method** — the method was
78+
never called; both facades implement revoke logic inline. The engine version also
79+
mutated the DTO in-place rather than using `model_copy`, diverging from the facade
80+
pattern. Callers using `ApprovalGate` directly should use the facade's
81+
`revoke_ticket()` instead.
82+
6383
- **`ControlPlaneFacade.run()` now enforces preconditions automatically**`run()`
6484
accepts `preconditions`, `proposal_id`, `action_id`, and `precondition_providers`
6585
kwargs. When supplied, preconditions are verified after the kill-switch check and

docs/architecture.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ Two caveats apply: (1) the risk accumulator is excluded, so the simulated tier m
225225

226226
An approved ticket can be revoked after the fact via `ApprovalGateway.revoke_ticket()` (sync, async, and resilient variants). Revocation resets the associated proposal back to `PENDING` and appends a state-bearing `APPROVAL_REVOKED` event. The ticket's status transitions to `ApprovalStatus.REVOKED`; the `revoked_by`, `revocation_reason`, and `revoked_at` fields record who revoked it and why. Callers are responsible for re-issuing a new ticket via `create_ticket()` when manual re-approval is warranted.
227227

228+
Revocation is rejected when the associated proposal is already in a terminal state (`EXECUTED` or `FAILED`) — the action has already run and resetting it to `PENDING` would allow re-execution. Both facades raise `ValueError` in this case; the ticket is not modified.
229+
228230
### Session risk accumulation
229231

230232
`SessionRiskAccumulator` watches the action chain within a session and escalates the effective `RiskLevel` when accumulated score crosses thresholds or known danger sequences are detected. It sits between `classify_risk_level()` and `classify_action_tier()` in the policy flow, so a session accumulating risky history automatically receives stricter routing on subsequent proposals.
@@ -344,6 +346,7 @@ Compatibility posture and migration guidance are documented in [compatibility.md
344346
## 10) Operational gotchas and anti-patterns
345347

346348
- Avoid calling model methods directly and bypassing engines; that breaks audit trails and recovery assumptions.
349+
- Avoid re-implementing approval write paths outside the facade — `get_ticket_for_update()` and `get_pending_ticket_for_update()` hold a `FOR UPDATE` row lock; bypassing them loses the lock and can produce silent last-write-wins races under concurrency.
347350
- Avoid sharing a single active cycle across multiple proposal streams without concurrency checks.
348351
- Avoid unbounded scoped approvals (countless session scope without expiry) unless intentionally audited.
349352
- Avoid swallowing `state_bearing=True` persistence errors; those failures must block the decision path.

0 commit comments

Comments
 (0)