Phase 20 — Engagement freeze on deliver¶
Last updated: 2026-05-08
When a partner marks the deliverable as handed to the client, the engagement is frozen: no further finding mutations until an admin explicitly unfreezes. Chain-of-custody preservation matters for audit defensibility — if a finding is added, edited, or rejected after the deliverable went to the client, the firm needs an explicit, logged break in the chain.
State machine addition¶
The existing EngagementStatus enum already had DELIVERED and ARCHIVED — Phase 20 wires those states into the mutation gate:
| State | Mutations allowed? | How to leave |
|---|---|---|
findings_review, failed, all earlier states |
✅ | normal lifecycle progression |
delivered (frozen) |
❌ | admin POST /unfreeze |
archived (frozen) |
❌ | admin POST /unfreeze |
is_frozen is now exposed on the engagement response shape; the UI uses it to render a lock banner and toggle the action button.
Endpoints¶
deliver¶
Available to admin and partner roles. Transitions the engagement to delivered, sets delivered_at timestamp, and freezes finding mutations. Refused with 409 if an audit run is in progress.
Response:
{
"ok": true,
"engagement_id": "eng-...",
"status": "delivered",
"delivered_at": "2026-05-08T...",
"already_delivered": false
}
already_delivered: true when called on a previously-delivered engagement (idempotent — returns 200 with no state change).
unfreeze¶
Admin role only — partners cannot unfreeze even an engagement they delivered. Transitions back to findings_review. Logs the action via auditforge_engagement_unfrozen.
Response:
was_frozen: false when called on an already-unfrozen engagement (idempotent no-op).
What gets blocked¶
When is_frozen=true, the following endpoints return 423 LOCKED with detail "Engagement is delivered (frozen). Admin must POST /engagement/<id>/unfreeze first.":
POST /engagement/{id}/intake— no editing the intake post-deliveryPOST /engagement/{id}/run— no rerunning the auditPOST /finding/{id}/{accept|reject|refine}— no flipping finding statusPOST /finding/{id}/edit— no editing finding contentPOST /finding/{id}/investigate-further— no spawning new follow-upsPOST /findings/bulk-action— no bulk-flipping
Read endpoints continue to work normally — partners can still view/export deliverables from a frozen engagement.
Why partner can deliver but only admin can unfreeze¶
Asymmetric privileges: a partner ships a finalized deliverable as part of their normal workflow, but breaking the chain of custody once the client has the deliverable is a higher-stakes action. Limiting unfreeze to admin means the firm has at least one extra deliberate decision before issuing a corrected version. The unfreeze action is logged so the audit trail records when the chain-of-custody break happened and who authorized it.
UX¶
Engagement detail header gets a new button:
- Before delivery: green Mark delivered button (next to Export deliverable). Confirmation dialog: "After delivery, finding mutations are blocked (chain-of-custody)."
- After delivery: yellow Unfreeze button (admin only — partner sees the same button but the click 403s; future polish: hide entirely for non-admins).
A persistent yellow banner appears below the engagement header while the engagement is frozen:
🔒 Engagement frozen (delivered). Delivered to client; finding mutations are blocked.
Admin can Unfreeze if a correction is needed.
Files¶
app/auditforge_endpoints.py—_is_engagement_frozen+_require_unfrozenhelpers;POST /engagement/{id}/deliver+/unfreeze; mutation-site guards (intake, run, finding action, edit, investigate-further, bulk-action)frontend/src/api/auditforge.ts—is_frozenfield onAuditEngagement;deliverEngagement+unfreezeEngagementtyped clientsfrontend/src/components/EngagementDetail.tsx— Mark delivered / Unfreeze button + frozen bannertests/test_auditforge_endpoints.py— 10 endpoint tests covering deliver/unfreeze, role gating, blocked-mutations behavior
Audit trail¶
Every deliver/unfreeze writes a structured warning log:
auditforge_engagement_delivered | id=eng-abc by_user=u-xyz
auditforge_engagement_unfrozen | id=eng-abc by_user=admin-token
by_user=admin-token indicates an admin-token call (no session-bound user); a session-token call records the actual user_id. Ops can grep CloudWatch Logs for auditforge_engagement_(delivered|unfrozen) to enumerate every chain-of-custody event for compliance reviews.