Le Duy Khuong (Daniel)

Series: hitl-agentic-systems · Part 6

AI / Agentic Systems

Identity and Audit Trail

Who reviewed, when; log for compliance and debug; permissions (who may review which type).

2026-03-104 min read

Post 6 in the HITL in Agentic Systems series. Post 4 had reviewed_by, reviewed_at; post 5 had the queue and channels. This post: who reviewed and when (identity + timestamp), audit trail for compliance and debug, and permissions — who may review which item type.


Opening: "Who approved that deploy at 3am?"

When an incident happens or audit asks, the usual questions are: who approved (or rejected) this item, and when? If the system does not store reviewed_by and reviewed_at, we cannot answer. Also, not every user may review every item type — production deploy might be restricted to the “deployer” role; financial items to “finance_approver”. This post clarifies identity, audit trail, and simple permissions.


1. Who reviewed, when — reviewed_by, reviewed_at

Value: Every approve/reject decision must be tied to identity (user_id) and timestamp — for compliance (“who is accountable”) and debug (“why was it approved then?”). Post 4 introduced the fields; this post goes into detail: where the user comes from, timezone, immutability.

Challenges: Where does user_id come from: web session, email token, Slack user_id? Timezone: store UTC, display local. After writing reviewed_by and reviewed_at, do not update — immutable.

Design: Schema per item: state, reviewed_by (string: user_id or identifier from SSO/Slack), reviewed_at (ISO 8601 UTC). When handling an approve/reject event from any channel (dashboard, Slack, email link): authenticate the user (session, token, Slack API), get user_id, write once when transitioning state.

Solution: Backend: when processing approve/reject, get current_user_id from auth context (middleware), set reviewed_by = current_user_id, reviewed_at = now_utc(). Do not allow updating these two fields after they are set. Timezone: store UTC; when showing the audit log convert to local if needed.

Implementation: DB: columns reviewed_by, reviewed_at; trigger or app logic ensures they are set only when state goes from pending_review to approved/rejected. Audit API: GET /approval/items/:id/history returns state transitions and metadata. Next: audit trail as an event log.


2. Audit trail — log for compliance and debug

Value: Audit trail = history of who did what when. “Who approved that deploy at 3am?” — query the log by item_id or user_id or time range. Needed for compliance (banking, healthcare) and debug (production incidents).

Challenges: Volume: many items, many events. Retention: how long to keep? PII: user_id may be PII per policy. Log only what is needed for audit, not every small action.

Design: Event log (append-only): each event has event_type (approved, rejected, timeout, created), item_id, user_id (or system), timestamp, optional comment or reason. Query: by item_id (history of one item), by user_id (what a user reviewed), by time range (all decisions in a period).

Solution: Table audit_events or append-only file log. On state transition: insert one record. No delete, no update. Export CSV/JSON for compliance when needed. Retention policy: e.g. 90 days in DB, then archive.

Implementation: Service audit_log.insert(event_type, item_id, user_id, timestamp, metadata). Call from the approval API after updating state. Dashboard “Audit” tab: filter, sort, export. Next: permissions — who may review which type.


3. Permissions — who may review which type

Value: Not every user may review every item. Production deploy might require role “deployer”; marketing email items might require “marketing_approver”. The queue only shows items the user is allowed to review; when they click approve/reject, the backend checks permission before updating.

Challenges: Simple role model (e.g. admin vs user) or more complex (many roles, many item types)? Tied to the queue: filter “only items I may review” to avoid exposing sensitive items.

Design: Items have type or category (e.g. deploy, send_email, financial). Users have role(s). Policy: role X may review types A, B. When listing the queue: filter items where the user has permission to review that item’s type. When POSTing approve/reject: check that the user may review this item; if not return 403.

Solution: Middleware or service: can_approve(user_id, item) → boolean. Implementation: permission table (role, item_type) or rule engine. Queue API: add filter by permitted types for the current user. Approval API: call can_approve before updating state.

Implementation: permission_service.can_approve(user_id, item_id): load item, load user roles, check policy. Integration: both queue list and approval action call this check. Post ends with a wrap-up: the series has covered posts 1–6 (from what agentic is to state, queue, channels, identity, audit). Posts 7 onward will cover human feedback (Modify, rating) and the feedback loop.


Previous: Review Queue and Approval Channels (post 5). Next: Human feedback — more than approve/reject (post 7).

LDK

Le Duy Khuong

AI Transformation & Digital Strategy. Writing about agentic systems, engineering leadership, and building in public.