Skip to content

Session Telemetry

Every Capsem VM gets its own SQLite database (session.db) that records canonical security events, network requests, DNS queries, AI model calls, MCP tool invocations, exec activity, kernel audit events, file changes, and snapshots. The database lives in the session directory and is destroyed with the VM (ephemeral) or preserved (persistent/forked).

Each database also carries one session_identity row. That row is the durable identity envelope for the event stream: the VM id, the resolved profile id, and the local user id that launched the VM. Event rows keep their hot-path shape and join to this identity at export/status time.

erDiagram
    net_events {
        int id PK
        text domain
        text decision
        text method
        text path
        int status_code
        int bytes_sent
        int bytes_received
        int duration_ms
    }
    session_identity {
        int id PK
        text updated_at
        text vm_id
        text profile_id
        text user_id
    }
    security_events {
        int id PK
        text event_id
        text event_family
        text event_type
        text source_engine
        text final_action
        text trace_id
        text vm_id
        text profile_id
        text user_id
    }
    security_event_steps {
        int id PK
        text event_id FK
        int step_index
        text kind
        text status
        text rule_id
    }
    detection_findings {
        int id PK
        text finding_id
        text event_id FK
        text rule_id
        text pack_id
        text severity
        text confidence
    }
    detection_finding_tags {
        text finding_id FK
        int tag_index
        text tag
    }
    security_event_links {
        int id PK
        text event_id FK
        text linked_event_id
        text link_type
    }
    model_calls {
        int id PK
        text provider
        text model
        int input_tokens
        int output_tokens
        real estimated_cost_usd
        text trace_id
    }
    tool_calls {
        int id PK
        int model_call_id FK
        text call_id
        text tool_name
        text origin
    }
    tool_responses {
        int id PK
        int model_call_id FK
        text call_id
        text content_preview
    }
    mcp_calls {
        int id PK
        text server_name
        text method
        text tool_name
        text decision
        text policy_action
        text policy_rule
        int duration_ms
    }
    dns_events {
        int id PK
        text qname
        int qtype
        int rcode
        text decision
        text matched_rule
    }
    exec_events {
        int id PK
        int exec_id
        text command
        int exit_code
        int duration_ms
    }
    audit_events {
        int id PK
        int pid
        int ppid
        text exe
        text argv
    }
    fs_events {
        int id PK
        text action
        text path
        int size
    }
    snapshot_events {
        int id PK
        int slot
        text origin
        int start_fs_event_id
        int stop_fs_event_id
    }

    model_calls ||--o{ tool_calls : "has"
    model_calls ||--o{ tool_responses : "has"
    security_events ||--o{ security_event_steps : "has"
    security_events ||--o{ detection_findings : "has"
    detection_findings ||--o{ detection_finding_tags : "has"
    security_events ||--o{ security_event_links : "links"
    snapshot_events }o--o{ fs_events : "references range"

One durable identity row for the VM/session that owns this database.

ColumnTypeDescription
idINTEGER PKAlways 1
updated_atTEXTISO 8601 time when identity was last attached
vm_idTEXTCapsem VM/session id
profile_idTEXTResolved Profile V2 id pinned to the session
user_idTEXTLocal host user id recorded by the service/process boundary

The canonical journal row for a resolved Security Engine event. Domain tables remain useful projections, but this table is the normalized place to read final decisions, attribution, and cross-engine identity.

ColumnTypeDescription
idINTEGER PKAuto-increment
event_idTEXT UNIQUEStable event id
timestampTEXTISO 8601 timestamp derived from the event
timestamp_unix_msINTEGERMillisecond timestamp used by replay/tests
event_familyTEXTdns, http, mcp, model, file, process, credential, vm, profile, conversation, or snapshot
event_typeTEXTTyped event name such as http.request
source_engineTEXTEngine that emitted the event
final_actionTEXTcontinue, ask, rewrite, block, throttle, quarantine, restore, drop_connection, observe_only, or error
enforceabilityTEXTinline_blockable, observe_only, or remediation_only
attribution_scopeTEXThost, vm, profile, session, or unknown
origin_kindTEXTWhere the activity originated, for example guest_network or host_service
accounting_ownerTEXTCounter/quota owner, such as vm:<id> or host:<id>
trace_idTEXTCross-table correlation id
vm_id, session_id, profile_id, user_idTEXTDurable ownership fields
process_id, turn_id, message_id, tool_call_id, mcp_call_idTEXTOptional correlation ids
redaction_stateTEXTraw, redacted, or summary-only
label_count, mutation_count, finding_countINTEGERCompact summary counters

Ordered processing steps for a security event: preprocessors, plugin callbacks, enforcement matches, confirmation, rate-limit checks, detection matches, postprocessors, and emitter delivery.

ColumnTypeDescription
event_idTEXT FKLinked security_events.event_id
step_indexINTEGERStable order within the resolved event
kindTEXTProcessing step kind
statusTEXTapplied, matched, skipped, or error
rule_idTEXTMatching rule, when present
pack_idTEXTRule/plugin pack, when present
messageTEXTShort diagnostic

Detection findings produced by the Security Engine before telemetry/logging sinks run.

ColumnTypeDescription
finding_idTEXT UNIQUEStable finding id
event_idTEXT FKLinked security_events.event_id
rule_idTEXTDetection rule id
pack_idTEXTDetection pack id
sigma_idTEXTOptional Sigma rule id
titleTEXTFinding title
severityTEXTinfo, low, medium, high, or critical
confidenceTEXTlow, medium, or high

Finding tags live in detection_finding_tags as one row per tag so hunting and timeline filters can index them without parsing JSON.

Correlation edges between events. Examples include parent event links, trace-history links, context-history links, model-to-tool links, process-to-file links, and future snapshot/file relationships.

Every HTTP request through the MITM proxy, whether allowed or denied.

ColumnTypeDescription
idINTEGER PKAuto-increment
timestampTEXTISO 8601
domainTEXTTarget domain
portINTEGERDefault 443
decisionTEXTallowed, denied, error
process_nameTEXTGuest process that initiated the request
pidINTEGERGuest process ID
methodTEXTHTTP method
pathTEXTRequest path
queryTEXTQuery string
status_codeINTEGERUpstream response status
bytes_sentINTEGERRequest body size
bytes_receivedINTEGERResponse body size
duration_msINTEGEREnd-to-end latency
matched_ruleTEXTWhich enforcement rule matched
request_headersTEXTRequest headers (when body logging enabled)
response_headersTEXTResponse headers
request_body_previewTEXTFirst 4 KB of request body
response_body_previewTEXTFirst 4 KB of response body
conn_typeTEXTDefault https, https-mitm for proxied
policy_modeTEXTPolicy engine mode, when set
policy_actionTEXTTyped policy action (allow, ask, block, rewrite)
policy_ruleTEXTMatching enforcement rule key
policy_reasonTEXTOptional audit reason or fail-closed detail
trace_idTEXTCross-table correlation ID

AI provider API calls with parsed response metadata.

ColumnTypeDescription
idINTEGER PKAuto-increment
timestampTEXTISO 8601
providerTEXTanthropic, openai, google
modelTEXTe.g. claude-opus-4
process_nameTEXTGuest process
pidINTEGERGuest process ID
methodTEXTHTTP method (always POST)
pathTEXTAPI path (e.g. /v1/messages)
streamINTEGERBoolean: 1 if SSE streaming
system_prompt_previewTEXTFirst N chars of system prompt
messages_countINTEGERNumber of messages in request
tools_countINTEGERNumber of tools in request
request_bytesINTEGERRequest body size
request_body_previewTEXTFirst 4 KB of request body
message_idTEXTProvider message ID
status_codeINTEGERHTTP status
text_contentTEXTConcatenated text output
thinking_contentTEXTChain-of-thought output
stop_reasonTEXTend_turn, tool_use, max_tokens, content_filter
input_tokensINTEGERInput token count
output_tokensINTEGEROutput token count
duration_msINTEGERRequest duration
response_bytesINTEGERResponse body size
estimated_cost_usdREALCost estimate from pricing table
trace_idTEXTLinks multi-turn agent conversations
usage_detailsTEXTJSON: {"cache_read": 800, "thinking": 200}

Tool invocations extracted from model responses. One row per tool_use content block.

ColumnTypeDescription
idINTEGER PKAuto-increment
model_call_idINTEGER FKReferences model_calls.id
call_indexINTEGERPosition in the response
call_idTEXTProvider-assigned call ID
tool_nameTEXTTool name
argumentsTEXTJSON arguments
originTEXTnative, local, mcp_proxy
mcp_call_idINTEGEROptional FK to mcp_calls; current model traffic does not populate it
trace_idTEXTCross-table correlation ID

Tool results from subsequent requests (matched by call_id).

ColumnTypeDescription
idINTEGER PKAuto-increment
model_call_idINTEGER FKReferences model_calls.id
call_idTEXTMatches tool_calls.call_id
content_previewTEXTTruncated tool result
is_errorINTEGERBoolean: 1 if tool returned error
trace_idTEXTCross-table correlation ID

MCP JSON-RPC tool invocations through the guest MCP relay and host MITM MCP endpoint (framed vsock:5002).

ColumnTypeDescription
idINTEGER PKAuto-increment
timestampTEXTISO 8601
server_nameTEXTMCP server name (e.g. builtin, github)
methodTEXTJSON-RPC method (tools/call, tools/list, etc.)
tool_nameTEXTTool name (for tools/call)
request_idTEXTJSON-RPC request ID
request_previewTEXTTruncated request body
response_previewTEXTTruncated response body
decisionTEXTallowed, denied, error
duration_msINTEGERCall duration
error_messageTEXTError details if failed
process_nameTEXTGuest process
bytes_sentINTEGERRequest size
bytes_receivedINTEGERResponse size
policy_modeTEXTPolicy engine mode (audit_only or enforce)
policy_actionTEXTTyped policy action (allow, ask, block, rewrite)
policy_ruleTEXTMatching rule key, for example policy.mcp.block_prod_token
policy_reasonTEXTOptional audit reason
trace_idTEXTCross-table correlation ID

DNS queries handled by the host DNS proxy.

ColumnTypeDescription
idINTEGER PKAuto-increment
timestampTEXTISO 8601
qnameTEXTQueried name
qtypeINTEGERDNS record type
qclassINTEGERDNS class
rcodeINTEGERDNS response code
decisionTEXTallowed, denied, redirected, or error
matched_ruleTEXTDomain or Policy DNS rule that matched
source_protoTEXTDNS transport source
process_nameTEXTGuest process, when known
upstream_resolver_msINTEGERUpstream resolver latency
trace_idTEXTCross-table correlation ID
policy_modeTEXTPolicy engine mode, when set
policy_actionTEXTTyped policy action (allow, ask, block, rewrite)
policy_ruleTEXTMatching enforcement rule key
policy_reasonTEXTOptional audit reason or fail-closed detail

| endpoint_id | TEXT | Hook endpoint identifier |

Commands executed through Capsem service APIs and MCP tools.

ColumnTypeDescription
idINTEGER PKAuto-increment
timestampTEXTISO 8601
exec_idINTEGERPer-session exec identifier
commandTEXTCommand string
exit_codeINTEGERProcess exit code, when complete
duration_msINTEGERRuntime duration, when complete
stdout_previewTEXTTruncated stdout
stderr_previewTEXTTruncated stderr
stdout_bytesINTEGERFull stdout byte count
stderr_bytesINTEGERFull stderr byte count
sourceTEXTSource path, usually api or MCP
mcp_call_idINTEGERRelated mcp_calls.id, when known
trace_idTEXTCross-table correlation ID
process_nameTEXTGuest process name, when known
pidINTEGERGuest process ID, when known

Kernel audit execve records streamed from the guest over vsock:5006.

ColumnTypeDescription
idINTEGER PKAuto-increment
timestampTEXTISO 8601
pidINTEGERGuest process ID
ppidINTEGERGuest parent process ID
uidINTEGERGuest user ID
exeTEXTExecutable path
commTEXTKernel command name
argvTEXTReconstructed command arguments
cwdTEXTWorking directory
exit_codeINTEGERExit code, when known
session_idINTEGERKernel audit session ID
ttyTEXTTTY, when present
audit_idTEXTKernel audit event ID
exec_event_idINTEGERRelated exec_events.id, when correlated
parent_exeTEXTParent executable, when known
trace_idTEXTCross-table correlation ID

File system changes in the workspace (tracked by VirtioFS).

ColumnTypeDescription
idINTEGER PKAuto-increment
timestampTEXTISO 8601
actionTEXTcreated, modified, deleted, restored
pathTEXTFile path relative to workspace
sizeINTEGERFile size in bytes
trace_idTEXTCross-table correlation ID

Automatic and manual workspace snapshots.

ColumnTypeDescription
idINTEGER PKAuto-increment
timestampTEXTISO 8601
slotINTEGERRing buffer slot (0-11 for auto)
originTEXTauto or manual
nameTEXTOptional snapshot name
files_countINTEGERFiles in snapshot
start_fs_event_idINTEGERFirst fs_event in range
stop_fs_event_idINTEGERLast fs_event in range
trace_idTEXTCross-table correlation ID
graph LR
    subgraph "Event Sources"
        MITM["MITM Proxy<br/>(vsock:5002)"]
        MCP["MITM MCP Endpoint<br/>(framed vsock:5002)"]
        DNS["DNS Proxy"]
        EXEC["Service exec path"]
        AUDIT["Guest audit stream<br/>(vsock:5006)"]
        FS["VirtioFS<br/>(file watcher)"]
        SNAP["Snapshot scheduler"]
        HOOK["Policy Hook client"]
    end

    subgraph "Writer Pipeline"
        CH["tokio mpsc channel"]
        WT["Dedicated writer thread<br/>(capsem-db-writer)"]
        DB["session.db<br/>(SQLite WAL)"]
    end

    MITM -->|"WriteOp::NetEvent<br/>WriteOp::ModelCall"| CH
    MCP -->|"WriteOp::McpCall"| CH
    DNS -->|"WriteOp::DnsEvent"| CH
    EXEC -->|"WriteOp::ExecEvent<br/>WriteOp::ExecEventComplete"| CH
    AUDIT -->|"WriteOp::AuditEvent"| CH
    FS -->|"WriteOp::FileEvent"| CH
    SNAP -->|"WriteOp::SnapshotEvent"| CH
    CH --> WT
    WT --> DB
VariantSourceTable(s)
WriteOp::NetEventMITM proxynet_events
WriteOp::ModelCallMITM proxy (AI traffic)model_calls + tool_calls + tool_responses
WriteOp::McpCallMITM MCP endpointmcp_calls
WriteOp::ExecEvent / ExecEventCompleteService exec pathexec_events
WriteOp::AuditEventGuest audit streamaudit_events
WriteOp::FileEventVirtioFS watcherfs_events
WriteOp::SnapshotEventSnapshot schedulersnapshot_events
WriteOp::DnsEventDNS proxydns_events

Use just query-session to prove that a policy decision happened at the intended boundary and that blocked or rewritten payloads did not leak.

Terminal window
just query-session "
SELECT timestamp, tool_name, decision, policy_action, policy_rule, policy_reason, error_message
FROM mcp_calls
WHERE policy_rule IS NOT NULL
ORDER BY id DESC
LIMIT 20;"

For no-dispatch checks, pair the policy row with the expected error response:

Terminal window
just query-session "
SELECT tool_name, policy_action, policy_rule, response_preview
FROM mcp_calls
WHERE policy_action IN ('ask', 'block', 'rewrite')
ORDER BY id DESC
LIMIT 20;"

MCP Security Engine enforcement blocks use policy_action = 'block'. The coarse mcp_calls.decision field still uses denied for denied JSON-RPC outcomes.

Terminal window
just query-session "
SELECT timestamp, domain, method, path, decision, matched_rule, status_code
, policy_action, policy_rule, policy_reason
FROM net_events
WHERE matched_rule IS NOT NULL OR policy_rule IS NOT NULL
ORDER BY id DESC
LIMIT 20;"

Header-strip rules should be checked against the captured headers:

Terminal window
just query-session "
SELECT domain, request_headers, response_headers
FROM net_events
WHERE matched_rule = 'security.rules.http.strip_credentials'
ORDER BY id DESC
LIMIT 5;"

The stripped header names may appear as keys depending on capture settings, but stripped secret values must not appear in header or body preview fields.

Terminal window
just query-session "
SELECT timestamp, qname, qtype, rcode, decision, matched_rule, process_name
, policy_action, policy_rule, policy_reason
FROM dns_events
WHERE matched_rule IS NOT NULL OR policy_rule IS NOT NULL OR decision != 'allowed'
ORDER BY id DESC
LIMIT 20;"

DNS block rows prove no upstream resolution happened when upstream_resolver_ms = 0. DNS rewrite rows should carry the enforcement rule and policy_action = 'rewrite'; synthetic answer payloads are not stored in session telemetry.

Model enforcement uses the existing parsed AI rows plus enforcement rule metadata as the enforcement slice lands. Today, use these rows to prove the subject and no-leak side of model enforcement tests:

Terminal window
just query-session "
SELECT id, provider, model, path, trace_id, request_body_preview, text_content
FROM model_calls
ORDER BY id DESC
LIMIT 10;"
Terminal window
just query-session "
SELECT tc.tool_name, tc.origin, tc.arguments, tr.content_preview, tc.trace_id
FROM tool_calls tc
LEFT JOIN tool_responses tr
ON tr.call_id = tc.call_id AND tr.trace_id = tc.trace_id
ORDER BY tc.id DESC
LIMIT 20;"

Model request policy records no-leak decisions on the associated net_events row. Model response, tool-call, and tool-response enforcement use the same rule, decision, and reason vocabulary on net_events; response-side rewrites must show only the rewritten preview.

For model-extracted tool calls, tool_calls.origin uses native, local, or mcp_proxy. The tool_calls.mcp_call_id column exists for future direct correlation, but the current model telemetry path does not populate it.

decision, rule id, reason, latency, timeout/schema/transport error text, fail-closed fallback decision, audit tags, trace_id, and session_id. Hook tests should also query the downstream boundary row (mcp_calls, net_events, dns_events, or model_calls) when proving no-dispatch and no-leak behavior.

The DbWriter spawns a dedicated thread that owns the SQLite connection:

  1. Async callers send WriteOp via tx.send() (non-blocking)
  2. Writer thread blocks on rx.blocking_recv() for the first op
  3. After receiving one op, drains the rest of the queue
  4. Executes all drained ops in a single SQLite transaction
  5. Repeats

This block-then-drain pattern batches writes for efficiency while keeping the async callers non-blocking. The channel has configurable backpressure capacity.

SQLite pragmas: WAL journal mode, NORMAL synchronous. Field values are defensively capped at 256 KB.

Drop order is critical: Drop::drop() takes tx before joining the thread. Without this, the join would deadlock (thread waits for all senders to drop, but tx drops after the join).

graph TD
    A["MITM proxy receives<br/>AI provider response"] --> B["AiResponseBody wraps<br/>hyper Body"]
    B --> C["poll_frame() feeds bytes<br/>to SseParser"]
    C --> D["SseParser emits SseEvent"]
    D --> E["ProviderStreamParser<br/>(Anthropic/OpenAI/Google)"]
    E --> F["Vec&lt;LlmEvent&gt;"]
    F --> G["collect_summary()"]
    G --> H["StreamSummary<br/>(text, tools, tokens, cost)"]
    H --> I["TelemetryEmitter.emit_model_call()"]
    I --> J["WriteOp::ModelCall<br/>with ToolCallEntry + ToolResponseEntry"]

For AI provider traffic, the response body is parsed inline to extract:

  • Model name and message ID
  • Text and thinking output
  • Tool calls with arguments and origin classification
  • Token usage (input, output, cache_read, thinking breakdowns)
  • Cost estimate from embedded pricing table
  • Stop reason (end_turn, tool_use, max_tokens)
  • Trace ID for multi-turn correlation

The DbReader provides pre-built aggregate queries:

QueryReturnsUse case
session_stats()SessionStatsDashboard summary: totals for net, model, tokens, cost
provider_token_usage()Vec<ProviderTokenUsage>Per-provider breakdown: call count, tokens, cost
domain_counts()Vec<DomainCount>Per-domain request counts with allowed/denied split
time_buckets()Vec<TimeBucket>Requests over time (for charts)
tool_usage()Vec<ToolUsageCount>Most-used tools by call count
tool_usage_with_stats()Vec<ToolUsageWithStats>Tool usage with byte and duration stats
mcp_tool_usage()Vec<McpToolUsage>MCP tool usage by server and tool name
trace_summaries()Vec<TraceSummary>Per-trace: tokens, cost, duration, tool count
trace_detail(id)TraceDetailAll model calls in a trace with tool data
Access pointProtocolQuery type
capsem inspect <id> "SQL"CLI -> service HTTP /inspect/{id}Raw SQL (read-only)
capsem info <id> --statsCLI -> service HTTP /info/{id}Pre-built SessionStats
MCP capsem_inspectMCP -> service HTTP /inspect/{id}Raw SQL (read-only)
MCP capsem_inspect_schemaMCP -> service HTTPTable schemas for LLM context
Frontend dashboardGateway -> /inspect/{id}sql.js in-browser (downloads session.db)

The /inspect endpoint executes arbitrary SQL against the session database in read-only mode (query_only pragma). The reader connection uses separate pragmas from the writer.

PropertyValue
Location~/.capsem/sessions/{id}/session.db
LifetimeCreated at VM boot, destroyed with ephemeral VM or preserved with persistent VM
AccessOnly the owning capsem-process can write; service reads via IPC
VirtioFS boundarysession.db is outside the VirtioFS share; guest cannot access it
Concurrent accessWAL mode allows concurrent reader + writer
Fork behaviorcapsem fork checkpoints and copies session.db into the image
FilePurpose
capsem-logger/src/schema.rsTable DDL, pragmas, migrations
capsem-logger/src/events.rsEvent structs (NetEvent, ModelCall, McpCall, etc.)
capsem-logger/src/writer.rsDbWriter, WriteOp, block-then-drain loop
capsem-logger/src/reader.rsDbReader, aggregation queries, raw SQL
capsem-logger/src/db.rsSessionDb convenience wrapper