Skip to content

Issues & Incidents. Data Model and Architecture

Agent
└── Session ← conversation (30-min inactivity = new session)
└── Turn ← single LLM/tool call (turn_index in session)
└── Event ← auditable record (hash-chained)
└── Detection[] ← findings from pipeline steps
Detections → grouped into → Issues (fingerprinted by agent + step)
Issues → escalated to → Incidents (when severity warrants action)
ConceptWhat it isLifecycleExample
SessionA conversation between user and agent. 30-min inactivity = new session.Created on first call, expires after idleSupport chat #4829
TurnOne request→pipeline→response cycle. Identified by turn_index.ImmutableTurn 3: user asks to export data
EventAn auditable thing that happened. Atomic unit of the hash chain.Immutable, hash-chainedllm_call, llm_call_blocked, config_change
DetectionA finding from a pipeline step. Lives in event.details.detections[].Immutable (part of event)detect_pii found 2 emails, action=redact
IssueA pattern of similar detections, grouped by fingerprint. Accumulates events.New → Ongoing → Resolved”PII leaking in responses”: 47 events
IncidentAn escalated situation requiring human attention. GDPR Art.33 trigger.Open → Investigating → Contained → Resolved”Data breach: PII exposed”
  • Issue = the what. A recurring problem pattern. “This agent keeps leaking PII.” Can live for weeks. Has event count, trend, first/last seen.
  • Incident = the so what. An actionable escalation. “This PII leak constitutes a breach, notify DPO within 72h.” Has containment actions, timeline, responsible party.

Not every issue becomes an incident. An issue with severity: medium may just need a config fix. An issue with severity: critical auto-creates an incident.

Issues are grouped by fingerprint: SHA-256(agent_id + ":" + detection_step)[:16]

This means all PII detections from one agent form a single issue. All injection attempts from one agent form another. Different agents get separate issues even for the same step.

ConditionSeverity
Detection action = blockCritical
Data classification = RESTRICTED or CONFIDENTIALHigh
Detection action = redact or notifyMedium
OtherwiseLow

Severity only escalates, never downgrades. If a new event is more severe, the issue severity is upgraded.

An incident is auto-created when:

  • An issue reaches severity: critical AND
  • The issue does not already have an incident

The incident includes auto-generated containment actions (e.g., “Agent calls blocked by pipeline”).

class Issue(BaseModel):
issue_id: str # "iss_..." auto-generated
fingerprint: str # SHA-256(agent_id:detection_step)[:16]
org_id: str
agent_id: str
detection_step: str # e.g. "detect_pii"
title: str # human-readable
severity: IssueSeverity # critical/high/medium/low
status: IssueStatus # new/ongoing/resolved
event_count: int
blocked_count: int
first_seen: datetime
last_seen: datetime
last_event_id: str
incident_id: str | None # linked incident if escalated
class Incident(BaseModel):
incident_id: str # "inc_..." auto-generated
org_id: str
agent_id: str
issue_ids: list[str] # linked issues
severity: IssueSeverity
lifecycle: IncidentLifecycle # open/investigating/contained/resolved
title: str
description: str
containment_actions: list[dict]
affected_categories: list[str] # e.g. ["Personal data", "Credentials"]
gdpr_notified_at: datetime | None # Art.33 tracking
detected_at: datetime
resolved_at: datetime | None

Both issues and incidents use the same JSONL + in-memory pattern as audit events:

  • data/issues.jsonl: append-only, rewritten on update
  • data/incidents.jsonl. same pattern
  • In-memory indexed by ID and fingerprint for fast lookups
MethodPathDescription
GET/issuesList issues. Params: agent_id, status, limit, offset
GET/issues/{issue_id}Get single issue
PATCH/issues/{issue_id}Update status: {"status": "resolved"}
MethodPathDescription
GET/incidents-apiList incidents. Params: agent_id, lifecycle, limit, offset
GET/incidents-api/{incident_id}Get single incident
PATCH/incidents-api/{incident_id}Update lifecycle, add containment actions, mark GDPR notified

Incident PATCH body examples:

{"lifecycle": "investigating"}
{"containment_action": "Agent API key revoked"}
{"gdpr_notified": true}

Issue creation is hooked into AuditTrail.record() in tappass/audit/trail.py. After each audit event is persisted:

  1. Check if event has detections or is llm_call_blocked
  2. For each detection step, compute fingerprint
  3. Find existing issue or create new one
  4. Bump event count, update severity, update last_seen
  5. If severity reaches critical → auto-create incident

This is best-effort (errors are logged but don’t block the audit trail).

Every Event gets hashed. The hash covers all event fields including session_id and turn_index (promoted to top-level). Issues and incidents are NOT in the hash chain: they are derived/mutable entities.

Hash input: SHA-256(canonical_json(event_fields) + prev_hash)
Fields hashed:
event_id, timestamp, org_id, event_type, agent_id, user_id, task_id,
session_id, turn_index, resource, operation, details,
source_framework, source_sdk_version, _prev_hash

What is NOT hashed: _hash itself, issue/incident IDs (derived, mutable).

The Activity tab on agent detail (frontend/src/pages/Agents.tsx) fetches from GET /issues?agent_id=... and renders a Sentry-inspired issues list with:

  • Severity color bar
  • Title + detection step
  • Event count, blocked count, last seen
  • Expandable event breadcrumbs
  • Filter tabs: All / New / Ongoing

Falls back to client-side grouping from audit events, then to mock data when no real data exists.

FilePurpose
tappass/models/_core.pyIssue, Incident, enum definitions
tappass/audit/issues.pyIssueRepo, IncidentRepo, fingerprinting
tappass/audit/trail.py_process_detections() hook
tappass/api/routes/issues.pyREST API endpoints
tappass/gateway/service.pyPasses session_id/turn_index to AuditEvent
frontend/src/types.tsApiIssue, ApiIncident TypeScript types
frontend/src/pages/Agents.tsxActivity tab UI