feat: add network calls list to AI session threads API (#27425)

The AI session threads API returned only a network call *summary*
(total/blocked counts + top domains). This adds the per-call list so the
session detail can render individual Agent Firewall network calls.

`ListAIBridgeSessionNetworkCalls` reuses the same sequence-number
windowing as the existing summary and includes all protocols. The list
is exposed as `network_call_logs` on the threads response and is capped
server-side at 100 rows. The summary (`network_calls.total`/`blocked`)
remains authoritative for whole-session totals: the list length and its
blocked count equal the summary only when a session has at most 100
calls, and are truncated beyond that.

### PR map (merge strictly bottom-up)

This change is a 4-PR stack. Each PR depends on all the ones below it,
so merge in this exact order:

1. #27417 — backend network summary
2. #27418 — frontend summary rows
3. #27425 — backend per-call list `network_call_logs`
4. #27426 — frontend network-calls panel

Refs AIGOV-464

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sas Swart
2026-08-03 11:34:27 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent fba9f0d485
commit 8886a5749a
17 changed files with 432 additions and 61 deletions
+79
View File
@@ -2024,6 +2024,85 @@ func (q *sqlQuerier) ListAIBridgeModels(ctx context.Context, arg ListAIBridgeMod
return items, nil
}
const listAIBridgeSessionNetworkCalls = `-- name: ListAIBridgeSessionNetworkCalls :many
SELECT bl.id, bl.session_id, bl.sequence_number, bl.captured_at, bl.created_at, bl.proto, bl.method, bl.detail, bl.matched_rule, bl.owner_id
FROM aibridge_interceptions afi
LEFT JOIN LATERAL (
SELECT COALESCE(MIN(nxt.agent_firewall_sequence_number), 2147483647) AS next_seq
FROM aibridge_interceptions nxt
WHERE nxt.agent_firewall_session_id = afi.agent_firewall_session_id
AND nxt.agent_firewall_sequence_number > afi.agent_firewall_sequence_number
) w ON true
JOIN boundary_logs bl
ON bl.session_id = afi.agent_firewall_session_id
AND bl.sequence_number > afi.agent_firewall_sequence_number
AND bl.sequence_number < w.next_seq
WHERE afi.session_id = $1::text
AND afi.ended_at IS NOT NULL
AND afi.agent_firewall_session_id IS NOT NULL
AND afi.agent_firewall_sequence_number IS NOT NULL
ORDER BY bl.created_at ASC, bl.sequence_number ASC, bl.id ASC
LIMIT COALESCE(NULLIF($2::integer, 0), 1000)
`
type ListAIBridgeSessionNetworkCallsParams struct {
SessionID string `db:"session_id" json:"session_id"`
Limit int32 `db:"limit_" json:"limit_"`
}
// Returns the individual Agent Firewall network calls made during an AI
// session, ordered chronologically. All protocols are included, unlike
// GetAIBridgeSessionTopDomains which considers only HTTP egress, so the list
// covers the same events the network_calls summary in ListAIBridgeSessions
// counts. The list is capped at @limit_ rows, so its length equals the summary
// total only for sessions at or below the cap. The summary stays authoritative
// for whole-session totals.
//
// Windowing mirrors that summary and GetAIBridgeSessionTopDomains: each
// interception's boundary logs fall in the open interval (this seq, next
// interception's seq) within the same firewall session. The exclusive lower
// bound drops the interception's own LLM-provider call. next_seq considers all
// interceptions in the firewall session so windows never bleed across AI
// sessions that share one firewall session, and falls back to the maximum
// sequence_number for the last interception so the window stays an
// index-satisfiable range.
// created_at leads because a session can span several firewall sessions, whose
// sequence numbers are independent streams. id breaks remaining ties so the row
// that lands on the limit boundary is stable across identical requests.
func (q *sqlQuerier) ListAIBridgeSessionNetworkCalls(ctx context.Context, arg ListAIBridgeSessionNetworkCallsParams) ([]BoundaryLog, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeSessionNetworkCalls, arg.SessionID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BoundaryLog
for rows.Next() {
var i BoundaryLog
if err := rows.Scan(
&i.ID,
&i.SessionID,
&i.SequenceNumber,
&i.CapturedAt,
&i.CreatedAt,
&i.Proto,
&i.Method,
&i.Detail,
&i.MatchedRule,
&i.OwnerID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAIBridgeSessionThreads = `-- name: ListAIBridgeSessionThreads :many
WITH paginated_threads AS (
SELECT