Skip to content

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

POST /auditforge/engagement/{id}/deliver
POST /auditforge/engagement/{id}/unfreeze

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:

{
  "ok": true,
  "engagement_id": "eng-...",
  "status": "findings_review",
  "was_frozen": true
}

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-delivery
  • POST /engagement/{id}/run — no rerunning the audit
  • POST /finding/{id}/{accept|reject|refine} — no flipping finding status
  • POST /finding/{id}/edit — no editing finding content
  • POST /finding/{id}/investigate-further — no spawning new follow-ups
  • POST /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_unfrozen helpers; POST /engagement/{id}/deliver + /unfreeze; mutation-site guards (intake, run, finding action, edit, investigate-further, bulk-action)
  • frontend/src/api/auditforge.tsis_frozen field on AuditEngagement; deliverEngagement + unfreezeEngagement typed clients
  • frontend/src/components/EngagementDetail.tsx — Mark delivered / Unfreeze button + frozen banner
  • tests/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.