Forensics Read Model

May 26, 2026 ยท View on GitHub

The forensics query surface is an experimental API backed by a defined read model. This page documents the ownership, schema, filter compatibility, and current performance boundary.

Scope

Applies to:

  • POST /api/forensics/query
  • POST /api/forensics/batch-export
  • GET /api/forensics/export-status/:task_id
  • GET /api/forensics/consistency/:job_id
  • GET /api/jobs/:id/evidence-graph
  • GET /api/jobs/:id/audit-log

These endpoints are exposed only when api.forensics.experimental=true.

Read Model Ownership

FieldOwnerSource
job_idJob storejob.Job.ID
agent_idJob storejob.Job.AgentID
tenant_idJob storejob.Job.TenantID
created_atJob storejob.Job.CreatedAt
statusJob storejob.Job.Status
event_countEvent storejobstore.ListEvents(job_id) length
tool_callsEvent storetool_invocation_finished.payload.tool_name
key_eventsEvent storeselected evidence event types

The job store owns tenant scoping. The event store owns evidence details. The query handler combines them only after narrowing candidates by agent_filter and tenant.

Required Query Shape

Current implementation requires agent_filter. This keeps the first read model bounded and avoids an unindexed cross-tenant scan.

{
  "agent_filter": ["agent_123"],
  "tenant_id": "tenant_a",
  "limit": 20,
  "offset": 0
}

If tenant_id is omitted, the authenticated tenant from request context is used. If neither is present, default is used.

Filter Compatibility

FilterCompatibility rule
tenant_idExact tenant scope. Must never return jobs from another tenant.
agent_filterRequired. Exact match against agent_id.
status_filterCase-insensitive match against job status string.
tool_filterExact match or trailing * prefix match, for example stripe*.
event_filterExact event type match.
time_range.startIncludes jobs created at or after the start time.
time_range.endIncludes jobs created at or before the end time.

New filters must be additive and optional while the surface remains experimental.

Pagination Compatibility

  • limit <= 0 defaults to 20.
  • limit > 200 is capped to 200.
  • offset < 0 is treated as 0.
  • page is offset / limit after limit normalization.
  • Results are sorted by created_at descending.

Performance Boundary

Current implementation is bounded by:

  • candidate jobs returned by agent_filter + tenant
  • one event-store read per candidate job
  • max response page size of 200

This is acceptable for experimental query workflows and release drills. Before removing the experimental gate, add an indexed read model or materialized summary for high-cardinality tenants and define a latency SLO.

Release Gate

Run:

./scripts/release-forensics-read-model-drill.sh

The drill covers tenant isolation, pagination cap, large event stream handling, experimental route gating, and batch export status flow.