From 95baedce8d0d4336e83ad1716cca743d092a6325 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 15 Jul 2026 16:43:39 +0000 Subject: [PATCH 1/7] feat: add total/blocked network calls column to AI sessions table Add a "Total/blocked network calls" column to the AIBridge sessions table, rendering per-session total, blocked, and errored call counts as badges plus "No activity" and "Disabled" states. Define the NetworkCalls contract on the codersdk AIBridgeSession type; the DB aggregation and handler population are left as marked TODO touch points. Co-Authored-By: Claude Opus 4.8 --- coderd/apidoc/docs.go | 22 +++++ coderd/apidoc/swagger.json | 22 +++++ coderd/database/db2sdk/db2sdk.go | 4 + coderd/database/querier.go | 7 ++ coderd/database/queries.sql.go | 7 ++ coderd/database/queries/aibridge.sql | 7 ++ codersdk/aibridge.go | 17 +++- docs/reference/api/aigateway.md | 5 + docs/reference/api/schemas.md | 59 +++++++++--- site/src/api/typesGenerated.ts | 18 ++++ .../ListSessionsPageView.stories.tsx | 9 ++ .../ListSessionsPage/ListSessionsPageView.tsx | 3 + .../ListSessionsRow.stories.tsx | 33 +++++++ .../ListSessionsPage/ListSessionsRow.tsx | 4 + .../NetworkCallBadges.stories.tsx | 60 ++++++++++++ .../pages/AIBridgePage/NetworkCallBadges.tsx | 94 +++++++++++++++++++ site/src/testHelpers/entities.ts | 5 + 17 files changed, 359 insertions(+), 17 deletions(-) create mode 100644 site/src/pages/AIBridgePage/NetworkCallBadges.stories.tsx create mode 100644 site/src/pages/AIBridgePage/NetworkCallBadges.tsx diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 54add36baea86..9e6c00c74d6d5 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -15097,6 +15097,14 @@ const docTemplate = `{ "type": "string" } }, + "network_calls": { + "description": "NetworkCalls summarizes the tool/network calls made during the session.\nA nil value means network call monitoring was not active for the\nsession, which the UI surfaces as \"Disabled\".", + "allOf": [ + { + "$ref": "#/definitions/codersdk.AIBridgeSessionNetworkCallSummary" + } + ] + }, "providers": { "type": "array", "items": { @@ -15115,6 +15123,20 @@ const docTemplate = `{ } } }, + "codersdk.AIBridgeSessionNetworkCallSummary": { + "type": "object", + "properties": { + "blocked": { + "type": "integer" + }, + "errored": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, "codersdk.AIBridgeSessionThreadsResponse": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 5687248ce4f85..36713f5cfa89e 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -13439,6 +13439,14 @@ "type": "string" } }, + "network_calls": { + "description": "NetworkCalls summarizes the tool/network calls made during the session.\nA nil value means network call monitoring was not active for the\nsession, which the UI surfaces as \"Disabled\".", + "allOf": [ + { + "$ref": "#/definitions/codersdk.AIBridgeSessionNetworkCallSummary" + } + ] + }, "providers": { "type": "array", "items": { @@ -13457,6 +13465,20 @@ } } }, + "codersdk.AIBridgeSessionNetworkCallSummary": { + "type": "object", + "properties": { + "blocked": { + "type": "integer" + }, + "errored": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, "codersdk.AIBridgeSessionThreadsResponse": { "type": "object", "properties": { diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 4754bbbe9506b..21d62e919edb0 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -1113,6 +1113,10 @@ func AIBridgeSession(row database.ListAIBridgeSessionsRow) codersdk.AIBridgeSess CacheReadInputTokens: row.CacheReadInputTokens, CacheWriteInputTokens: row.CacheWriteInputTokens, }, + // TODO(aibridge network calls): populate session.NetworkCalls with the + // total/blocked/errored tool call counts once ListAIBridgeSessions + // aggregates them from aibridge_tool_usages. Left nil for now so the + // field serializes as omitted and the UI renders "Disabled". } // Ensure non-nil slices for JSON serialization. if session.Providers == nil { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 097e7d2ad4de0..febdcff304c5b 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1177,6 +1177,13 @@ type sqlcQuerier interface { // Pagination-first strategy: identify the page of sessions cheaply via a // single GROUP BY scan, then do expensive lateral joins (tokens, prompts, // first-interception metadata) only for the ~page-size result set. + // TODO(aibridge network calls): add a LEFT JOIN LATERAL over + // aibridge_tool_usages (keyed by sr.interception_ids, mirroring the token + // aggregation above) to compute total / blocked / errored network call counts + // per session, then expose them as columns for db2sdk.AIBridgeSession to map + // onto AIBridgeSession.NetworkCalls. The count expressions already exist in + // CalculateAIBridgeInterceptionsTelemetrySummary (injected filters and + // invocation_error IS NOT NULL). ListAIBridgeSessions(ctx context.Context, arg ListAIBridgeSessionsParams) ([]ListAIBridgeSessionsRow, error) ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeTokenUsage, error) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeToolUsage, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index dbb5c8c7a6fac..e5660c43b21be 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2198,6 +2198,13 @@ type ListAIBridgeSessionsRow struct { // Pagination-first strategy: identify the page of sessions cheaply via a // single GROUP BY scan, then do expensive lateral joins (tokens, prompts, // first-interception metadata) only for the ~page-size result set. +// TODO(aibridge network calls): add a LEFT JOIN LATERAL over +// aibridge_tool_usages (keyed by sr.interception_ids, mirroring the token +// aggregation above) to compute total / blocked / errored network call counts +// per session, then expose them as columns for db2sdk.AIBridgeSession to map +// onto AIBridgeSession.NetworkCalls. The count expressions already exist in +// CalculateAIBridgeInterceptionsTelemetrySummary (injected filters and +// invocation_error IS NOT NULL). func (q *sqlQuerier) ListAIBridgeSessions(ctx context.Context, arg ListAIBridgeSessionsParams) ([]ListAIBridgeSessionsRow, error) { rows, err := q.db.QueryContext(ctx, listAIBridgeSessions, arg.AfterSessionID, diff --git a/coderd/database/queries/aibridge.sql b/coderd/database/queries/aibridge.sql index 1c396cc69099d..019eed278f23b 100644 --- a/coderd/database/queries/aibridge.sql +++ b/coderd/database/queries/aibridge.sql @@ -515,6 +515,13 @@ LEFT JOIN LATERAL ( ORDER BY up.created_at DESC, up.id DESC LIMIT 1 ) slp ON true +-- TODO(aibridge network calls): add a LEFT JOIN LATERAL over +-- aibridge_tool_usages (keyed by sr.interception_ids, mirroring the token +-- aggregation above) to compute total / blocked / errored network call counts +-- per session, then expose them as columns for db2sdk.AIBridgeSession to map +-- onto AIBridgeSession.NetworkCalls. The count expressions already exist in +-- CalculateAIBridgeInterceptionsTelemetrySummary (injected filters and +-- invocation_error IS NOT NULL). ORDER BY sp.last_active_at DESC, sp.session_id DESC diff --git a/codersdk/aibridge.go b/codersdk/aibridge.go index 7b92638ac5993..9b6e99a703ec3 100644 --- a/codersdk/aibridge.go +++ b/codersdk/aibridge.go @@ -66,8 +66,12 @@ type AIBridgeSession struct { EndedAt *time.Time `json:"ended_at,omitempty" format:"date-time"` Threads int64 `json:"threads"` TokenUsageSummary AIBridgeSessionTokenUsageSummary `json:"token_usage_summary"` - LastPrompt *string `json:"last_prompt,omitempty"` - LastActiveAt time.Time `json:"last_active_at" format:"date-time"` + // NetworkCalls summarizes the tool/network calls made during the session. + // A nil value means network call monitoring was not active for the + // session, which the UI surfaces as "Disabled". + NetworkCalls *AIBridgeSessionNetworkCallSummary `json:"network_calls,omitempty"` + LastPrompt *string `json:"last_prompt,omitempty"` + LastActiveAt time.Time `json:"last_active_at" format:"date-time"` } type AIBridgeSessionTokenUsageSummary struct { @@ -77,6 +81,15 @@ type AIBridgeSessionTokenUsageSummary struct { CacheWriteInputTokens int64 `json:"cache_write_input_tokens"` } +// AIBridgeSessionNetworkCallSummary aggregates the tool/network calls made +// during a session. Blocked counts calls denied by policy; Errored counts +// calls that failed to complete. +type AIBridgeSessionNetworkCallSummary struct { + Total int64 `json:"total"` + Blocked int64 `json:"blocked"` + Errored int64 `json:"errored"` +} + type AIBridgeListSessionsResponse struct { Count int64 `json:"count"` Sessions []AIBridgeSession `json:"sessions"` diff --git a/docs/reference/api/aigateway.md b/docs/reference/api/aigateway.md index 09e869a1693a5..fbe2016affb8f 100644 --- a/docs/reference/api/aigateway.md +++ b/docs/reference/api/aigateway.md @@ -121,6 +121,11 @@ Alias: also available at /api/v2/aibridge/sessions for backward compatibility. "models": [ "string" ], + "network_calls": { + "blocked": 0, + "errored": 0, + "total": 0 + }, "providers": [ "string" ], diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index fcbe267ec2036..c2fcfa4c98430 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -483,6 +483,11 @@ "models": [ "string" ], + "network_calls": { + "blocked": 0, + "errored": 0, + "total": 0 + }, "providers": [ "string" ], @@ -598,6 +603,11 @@ "models": [ "string" ], + "network_calls": { + "blocked": 0, + "errored": 0, + "total": 0 + }, "providers": [ "string" ], @@ -614,21 +624,40 @@ ### Properties -| Name | Type | Required | Restrictions | Description | -|-----------------------|----------------------------------------------------------------------------------------|----------|--------------|-------------| -| `client` | string | false | | | -| `ended_at` | string | false | | | -| `id` | string | false | | | -| `initiator` | [codersdk.MinimalUser](#codersdkminimaluser) | false | | | -| `last_active_at` | string | false | | | -| `last_prompt` | string | false | | | -| `metadata` | object | false | | | -| » `[any property]` | any | false | | | -| `models` | array of string | false | | | -| `providers` | array of string | false | | | -| `started_at` | string | false | | | -| `threads` | integer | false | | | -| `token_usage_summary` | [codersdk.AIBridgeSessionTokenUsageSummary](#codersdkaibridgesessiontokenusagesummary) | false | | | +| Name | Type | Required | Restrictions | Description | +|-----------------------|------------------------------------------------------------------------------------------|----------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `client` | string | false | | | +| `ended_at` | string | false | | | +| `id` | string | false | | | +| `initiator` | [codersdk.MinimalUser](#codersdkminimaluser) | false | | | +| `last_active_at` | string | false | | | +| `last_prompt` | string | false | | | +| `metadata` | object | false | | | +| » `[any property]` | any | false | | | +| `models` | array of string | false | | | +| `network_calls` | [codersdk.AIBridgeSessionNetworkCallSummary](#codersdkaibridgesessionnetworkcallsummary) | false | | Network calls summarizes the tool/network calls made during the session. A nil value means network call monitoring was not active for the session, which the UI surfaces as "Disabled". | +| `providers` | array of string | false | | | +| `started_at` | string | false | | | +| `threads` | integer | false | | | +| `token_usage_summary` | [codersdk.AIBridgeSessionTokenUsageSummary](#codersdkaibridgesessiontokenusagesummary) | false | | | + +## codersdk.AIBridgeSessionNetworkCallSummary + +```json +{ + "blocked": 0, + "errored": 0, + "total": 0 +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-----------|---------|----------|--------------|-------------| +| `blocked` | integer | false | | | +| `errored` | integer | false | | | +| `total` | integer | false | | | ## codersdk.AIBridgeSessionThreadsResponse diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b4ee9f1ab0b3e..6e9ee7e38e41e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -140,10 +140,28 @@ export interface AIBridgeSession { readonly ended_at?: string; readonly threads: number; readonly token_usage_summary: AIBridgeSessionTokenUsageSummary; + /** + * NetworkCalls summarizes the tool/network calls made during the session. + * A nil value means network call monitoring was not active for the + * session, which the UI surfaces as "Disabled". + */ + readonly network_calls?: AIBridgeSessionNetworkCallSummary; readonly last_prompt?: string; readonly last_active_at: string; } +// From codersdk/aibridge.go +/** + * AIBridgeSessionNetworkCallSummary aggregates the tool/network calls made + * during a session. Blocked counts calls denied by policy; Errored counts + * calls that failed to complete. + */ +export interface AIBridgeSessionNetworkCallSummary { + readonly total: number; + readonly blocked: number; + readonly errored: number; +} + // From codersdk/aibridge.go /** * AIBridgeSessionThreadsResponse is the response for GET diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx index ede9692b8ef1d..dc878d972d6f5 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx @@ -106,6 +106,15 @@ export const MultipleSessions: Story = { cache_read_input_tokens: 800 * (i + 1), cache_write_input_tokens: 50 * (i + 1), }, + // Span every network call state: total/blocked, total/blocked/error, + // no activity, disabled, and total with nothing blocked. + network_calls: [ + { total: 23, blocked: 2, errored: 0 }, + { total: 23, blocked: 2, errored: 1 }, + { total: 0, blocked: 0, errored: 0 }, + undefined, + { total: 150, blocked: 0, errored: 0 }, + ][i], })), }, }; diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx index ed85496fb5f1b..dc9fc05a9f689 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx @@ -86,6 +86,9 @@ export const ListSessionsPageView: FC = ({ Provider Client In/Out Tokens + + Total/blocked network calls + Threads diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.stories.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.stories.tsx index 18db56d9385e5..b6e5658b8c3d7 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.stories.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.stories.tsx @@ -107,3 +107,36 @@ export const LargeTokenCounts: Story = { }, }, }; + +export const NetworkCallsBlocked: Story = { + args: { + session: { + ...MockSession, + network_calls: { total: 23, blocked: 2, errored: 0 }, + }, + }, +}; + +export const NetworkCallsWithErrors: Story = { + args: { + session: { + ...MockSession, + network_calls: { total: 23, blocked: 2, errored: 1 }, + }, + }, +}; + +export const NoNetworkActivity: Story = { + args: { + session: { + ...MockSession, + network_calls: { total: 0, blocked: 0, errored: 0 }, + }, + }, +}; + +export const NetworkDisabled: Story = { + args: { + session: { ...MockSession, network_calls: undefined }, + }, +}; diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.tsx index a655964575e20..82e2cf607b192 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.tsx @@ -13,6 +13,7 @@ import { import { AIBridgeClientIcon } from "#/pages/AIBridgePage/icons/AIBridgeClientIcon"; import { AIBridgeProviderIcon } from "#/pages/AIBridgePage/icons/AIBridgeProviderIcon"; import { DATE_FORMAT, formatDateTime } from "#/utils/time"; +import { NetworkCallBadges } from "../NetworkCallBadges"; import { TokenBadges } from "../TokenBadges"; import { getProviderDisplayName } from "../utils"; @@ -105,6 +106,9 @@ export const ListSessionsRow: FC = ({ /> + + + {session.threads} diff --git a/site/src/pages/AIBridgePage/NetworkCallBadges.stories.tsx b/site/src/pages/AIBridgePage/NetworkCallBadges.stories.tsx new file mode 100644 index 0000000000000..164063a52ed1a --- /dev/null +++ b/site/src/pages/AIBridgePage/NetworkCallBadges.stories.tsx @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { NetworkCallBadges } from "./NetworkCallBadges"; + +const meta: Meta = { + title: "pages/AIBridgePage/NetworkCallBadges", + component: NetworkCallBadges, +}; + +export default meta; +type Story = StoryObj; + +export const TotalAndBlocked: Story = { + args: { + summary: { total: 23, blocked: 2, errored: 0 }, + }, +}; + +export const WithErrors: Story = { + args: { + summary: { total: 23, blocked: 2, errored: 1 }, + }, +}; + +export const NoBlocked: Story = { + args: { + summary: { total: 23, blocked: 0, errored: 0 }, + }, +}; + +export const NoActivity: Story = { + args: { + summary: { total: 0, blocked: 0, errored: 0 }, + }, +}; + +export const Disabled: Story = { + args: { + summary: undefined, + }, +}; + +export const LargeCounts: Story = { + args: { + summary: { total: 12_480, blocked: 320, errored: 47 }, + }, +}; + +export const SizeXs: Story = { + args: { + size: "xs", + summary: { total: 23, blocked: 2, errored: 1 }, + }, +}; + +export const SizeMd: Story = { + args: { + size: "md", + summary: { total: 23, blocked: 2, errored: 1 }, + }, +}; diff --git a/site/src/pages/AIBridgePage/NetworkCallBadges.tsx b/site/src/pages/AIBridgePage/NetworkCallBadges.tsx new file mode 100644 index 0000000000000..2a5a415c44b76 --- /dev/null +++ b/site/src/pages/AIBridgePage/NetworkCallBadges.tsx @@ -0,0 +1,94 @@ +import { BanIcon, InfoIcon, TriangleAlertIcon } from "lucide-react"; +import type { FC } from "react"; +import type { AIBridgeSessionNetworkCallSummary } from "#/api/typesGenerated"; +import { Badge } from "#/components/Badge/Badge"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "#/components/Tooltip/Tooltip"; + +interface NetworkCallBadgesProps { + size?: "xs" | "sm" | "md"; + // summary is undefined when network call monitoring was not active for the + // session, which renders as "Disabled". + summary: AIBridgeSessionNetworkCallSummary | undefined; +} + +export const NetworkCallBadges: FC = ({ + size = "sm", + summary, +}) => { + if (!summary) { + return ( + + + + + Disabled + + + + + Network call monitoring was not active for this session. + + + + ); + } + + if (summary.total === 0) { + return ( + + No activity + + ); + } + + return ( + + + + + {summary.total.toLocaleString("en-US")} + + + {summary.blocked.toLocaleString("en-US")} + + {summary.errored > 0 && ( + + + {summary.errored.toLocaleString("en-US")} + + )} + + + +
+
+ Total calls + {summary.total.toLocaleString("en-US")} +
+
+ Blocked + {summary.blocked.toLocaleString("en-US")} +
+
+ Errored + {summary.errored.toLocaleString("en-US")} +
+
+
+
+
+ ); +}; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 92f6ac4027212..da017f0849b28 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -5523,6 +5523,11 @@ export const MockSession: TypesGen.AIBridgeSession = { cache_read_input_tokens: 980, cache_write_input_tokens: 120, }, + network_calls: { + total: 23, + blocked: 2, + errored: 0, + }, last_prompt: "But *can* I really fix it?", last_active_at: "2026-03-09T10:28:15.03152Z", }; From f857dffbb9008a7c7ce7cb3cd08f81069da1b0f1 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 16 Jul 2026 09:53:08 +0000 Subject: [PATCH 2/7] feat: populate total/blocked network calls on AI sessions table The AI Gateway sessions table shipped with the network-calls column scaffolded but unpopulated, so every session rendered as "Disabled". Aggregate Agent Firewall egress per session in ListAIBridgeSessions by correlating boundary_logs to each interception's firewall session and sequence window (seq, next_seq). The exclusive lower bound drops the interception's own LLM-provider call; matched_rule IS NULL counts blocked requests. Sessions that never passed through Agent Firewall report a nil summary so the UI keeps rendering "Disabled". Drop the errored counter for now; boundary_logs has no error signal yet. Co-Authored-By: Claude Opus 4.8 (1M context) --- coderd/apidoc/docs.go | 5 +- coderd/apidoc/swagger.json | 5 +- coderd/database/db2sdk/db2sdk.go | 13 +- coderd/database/dump.sql | 2 +- ...00544_aibridge_firewall_seq_index.down.sql | 5 + .../000544_aibridge_firewall_seq_index.up.sql | 10 ++ coderd/database/modelqueries.go | 3 + coderd/database/querier.go | 7 - coderd/database/queries.sql.go | 51 +++++-- coderd/database/queries/aibridge.sql | 45 ++++-- codersdk/aibridge.go | 14 +- docs/reference/api/aigateway.md | 1 - docs/reference/api/schemas.md | 36 ++--- enterprise/coderd/aibridge_test.go | 138 ++++++++++++++++++ site/src/api/typesGenerated.ts | 14 +- .../ListSessionsPageView.stories.tsx | 12 +- .../ListSessionsRow.stories.tsx | 13 +- .../NetworkCallBadges.stories.tsx | 18 +-- .../pages/AIBridgePage/NetworkCallBadges.tsx | 12 +- site/src/testHelpers/entities.ts | 1 - 20 files changed, 291 insertions(+), 114 deletions(-) create mode 100644 coderd/database/migrations/000544_aibridge_firewall_seq_index.down.sql create mode 100644 coderd/database/migrations/000544_aibridge_firewall_seq_index.up.sql diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 9e6c00c74d6d5..c42703e7f571b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -15098,7 +15098,7 @@ const docTemplate = `{ } }, "network_calls": { - "description": "NetworkCalls summarizes the tool/network calls made during the session.\nA nil value means network call monitoring was not active for the\nsession, which the UI surfaces as \"Disabled\".", + "description": "NetworkCalls summarizes the Agent Firewall network calls made during the\nsession. A nil value means the session did not pass through Agent\nFirewall, so network call monitoring was not active, which the UI\nsurfaces as \"Disabled\".", "allOf": [ { "$ref": "#/definitions/codersdk.AIBridgeSessionNetworkCallSummary" @@ -15129,9 +15129,6 @@ const docTemplate = `{ "blocked": { "type": "integer" }, - "errored": { - "type": "integer" - }, "total": { "type": "integer" } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 36713f5cfa89e..65f9f558b5518 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -13440,7 +13440,7 @@ } }, "network_calls": { - "description": "NetworkCalls summarizes the tool/network calls made during the session.\nA nil value means network call monitoring was not active for the\nsession, which the UI surfaces as \"Disabled\".", + "description": "NetworkCalls summarizes the Agent Firewall network calls made during the\nsession. A nil value means the session did not pass through Agent\nFirewall, so network call monitoring was not active, which the UI\nsurfaces as \"Disabled\".", "allOf": [ { "$ref": "#/definitions/codersdk.AIBridgeSessionNetworkCallSummary" @@ -13471,9 +13471,6 @@ "blocked": { "type": "integer" }, - "errored": { - "type": "integer" - }, "total": { "type": "integer" } diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 21d62e919edb0..c57d4e70cc62c 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -1113,10 +1113,15 @@ func AIBridgeSession(row database.ListAIBridgeSessionsRow) codersdk.AIBridgeSess CacheReadInputTokens: row.CacheReadInputTokens, CacheWriteInputTokens: row.CacheWriteInputTokens, }, - // TODO(aibridge network calls): populate session.NetworkCalls with the - // total/blocked/errored tool call counts once ListAIBridgeSessions - // aggregates them from aibridge_tool_usages. Left nil for now so the - // field serializes as omitted and the UI renders "Disabled". + } + // NetworkCalls is only meaningful when the session passed through Agent + // Firewall. When it did not, leave it nil so the UI renders "Disabled" + // rather than a misleading zero count. + if row.FirewallActive { + session.NetworkCalls = &codersdk.AIBridgeSessionNetworkCallSummary{ + Total: row.NetworkCallsTotal, + Blocked: row.NetworkCallsBlocked, + } } // Ensure non-nil slices for JSON serialization. if session.Providers == nil { diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 4b91dea30ea6d..a1bcef76d5deb 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -4668,7 +4668,7 @@ CREATE INDEX idx_ai_providers_enabled ON ai_providers USING btree (enabled) WHER CREATE INDEX idx_ai_user_daily_spend_effective_group_id_day ON ai_user_daily_spend USING btree (effective_group_id, day); -CREATE INDEX idx_aibridge_interceptions_agent_firewall_session_id ON aibridge_interceptions USING btree (agent_firewall_session_id) WHERE (agent_firewall_session_id IS NOT NULL); +CREATE INDEX idx_aibridge_interceptions_agent_firewall_session_seq ON aibridge_interceptions USING btree (agent_firewall_session_id, agent_firewall_sequence_number) WHERE (agent_firewall_session_id IS NOT NULL); CREATE INDEX idx_aibridge_interceptions_client ON aibridge_interceptions USING btree (client); diff --git a/coderd/database/migrations/000544_aibridge_firewall_seq_index.down.sql b/coderd/database/migrations/000544_aibridge_firewall_seq_index.down.sql new file mode 100644 index 0000000000000..2b4e022b51fd8 --- /dev/null +++ b/coderd/database/migrations/000544_aibridge_firewall_seq_index.down.sql @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS idx_aibridge_interceptions_agent_firewall_session_seq; + +CREATE INDEX idx_aibridge_interceptions_agent_firewall_session_id + ON aibridge_interceptions (agent_firewall_session_id) + WHERE agent_firewall_session_id IS NOT NULL; diff --git a/coderd/database/migrations/000544_aibridge_firewall_seq_index.up.sql b/coderd/database/migrations/000544_aibridge_firewall_seq_index.up.sql new file mode 100644 index 0000000000000..4f3f094ec7295 --- /dev/null +++ b/coderd/database/migrations/000544_aibridge_firewall_seq_index.up.sql @@ -0,0 +1,10 @@ +-- Replace the session-only index with a composite index on +-- (agent_firewall_session_id, agent_firewall_sequence_number). The sessions +-- list computes each interception's next firewall sequence number to bound the +-- boundary_logs it triggered; the composite index serves that lookup index-only +-- and still covers session-only lookups. +DROP INDEX IF EXISTS idx_aibridge_interceptions_agent_firewall_session_id; + +CREATE INDEX idx_aibridge_interceptions_agent_firewall_session_seq + ON aibridge_interceptions (agent_firewall_session_id, agent_firewall_sequence_number) + WHERE agent_firewall_session_id IS NOT NULL; diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index ea3213d8ec180..5c1205eeed36a 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -1052,6 +1052,9 @@ func (q *sqlQuerier) ListAuthorizedAIBridgeSessions(ctx context.Context, arg Lis &i.CacheWriteInputTokens, &i.LastPrompt, &i.LastActiveAt, + &i.NetworkCallsTotal, + &i.NetworkCallsBlocked, + &i.FirewallActive, ); err != nil { return nil, err } diff --git a/coderd/database/querier.go b/coderd/database/querier.go index febdcff304c5b..097e7d2ad4de0 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1177,13 +1177,6 @@ type sqlcQuerier interface { // Pagination-first strategy: identify the page of sessions cheaply via a // single GROUP BY scan, then do expensive lateral joins (tokens, prompts, // first-interception metadata) only for the ~page-size result set. - // TODO(aibridge network calls): add a LEFT JOIN LATERAL over - // aibridge_tool_usages (keyed by sr.interception_ids, mirroring the token - // aggregation above) to compute total / blocked / errored network call counts - // per session, then expose them as columns for db2sdk.AIBridgeSession to map - // onto AIBridgeSession.NetworkCalls. The count expressions already exist in - // CalculateAIBridgeInterceptionsTelemetrySummary (injected filters and - // invocation_error IS NOT NULL). ListAIBridgeSessions(ctx context.Context, arg ListAIBridgeSessionsParams) ([]ListAIBridgeSessionsRow, error) ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeTokenUsage, error) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeToolUsage, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e5660c43b21be..102e80d919a72 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2115,7 +2115,10 @@ SELECT COALESCE(st.cache_read_input_tokens, 0)::bigint AS cache_read_input_tokens, COALESCE(st.cache_write_input_tokens, 0)::bigint AS cache_write_input_tokens, COALESCE(slp.prompt, '') AS last_prompt, - sp.last_active_at AS last_active_at + sp.last_active_at AS last_active_at, + COALESCE(bnc.total, 0)::bigint AS network_calls_total, + COALESCE(bnc.blocked, 0)::bigint AS network_calls_blocked, + COALESCE(sr.firewall_active, false) AS firewall_active FROM session_page sp JOIN @@ -2126,7 +2129,11 @@ LEFT JOIN LATERAL ( (ARRAY_AGG(ai.metadata ORDER BY ai.started_at, ai.id))[1] AS metadata, ARRAY_AGG(DISTINCT ai.provider ORDER BY ai.provider) AS providers, ARRAY_AGG(DISTINCT ai.model ORDER BY ai.model) AS models, - ARRAY_AGG(ai.id) AS interception_ids + ARRAY_AGG(ai.id) AS interception_ids, + -- firewall_active reports whether any interception in the session + -- passed through Agent Firewall. When false, network call monitoring + -- was not active and db2sdk leaves NetworkCalls nil ("Disabled"). + BOOL_OR(ai.agent_firewall_session_id IS NOT NULL) AS firewall_active FROM aibridge_interceptions ai WHERE ai.session_id = sp.session_id AND ai.initiator_id = sp.initiator_id @@ -2151,6 +2158,33 @@ LEFT JOIN LATERAL ( ORDER BY up.created_at DESC, up.id DESC LIMIT 1 ) slp ON true +LEFT JOIN LATERAL ( + -- Count Agent Firewall network calls attributed to this session. Each + -- interception marks a point in its firewall session's monotonic sequence + -- stream; the boundary logs it triggered 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 + -- (logged at exactly its sequence number), leaving the agent's other + -- egress. next_seq considers all interceptions in the firewall session so + -- windows never bleed across AI sessions that share one firewall session. + SELECT + COUNT(bl.id)::bigint AS total, + COUNT(bl.id) FILTER (WHERE bl.matched_rule IS NULL)::bigint AS blocked + FROM aibridge_interceptions afi + LEFT JOIN LATERAL ( + SELECT MIN(nxt.agent_firewall_sequence_number) 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 (w.next_seq IS NULL OR bl.sequence_number < w.next_seq) + WHERE afi.id = ANY(sr.interception_ids) + AND afi.agent_firewall_session_id IS NOT NULL + AND afi.agent_firewall_sequence_number IS NOT NULL +) bnc ON true ORDER BY sp.last_active_at DESC, sp.session_id DESC @@ -2189,6 +2223,9 @@ type ListAIBridgeSessionsRow struct { CacheWriteInputTokens int64 `db:"cache_write_input_tokens" json:"cache_write_input_tokens"` LastPrompt string `db:"last_prompt" json:"last_prompt"` LastActiveAt time.Time `db:"last_active_at" json:"last_active_at"` + NetworkCallsTotal int64 `db:"network_calls_total" json:"network_calls_total"` + NetworkCallsBlocked int64 `db:"network_calls_blocked" json:"network_calls_blocked"` + FirewallActive bool `db:"firewall_active" json:"firewall_active"` } // Returns paginated sessions with aggregated metadata, token counts, and @@ -2198,13 +2235,6 @@ type ListAIBridgeSessionsRow struct { // Pagination-first strategy: identify the page of sessions cheaply via a // single GROUP BY scan, then do expensive lateral joins (tokens, prompts, // first-interception metadata) only for the ~page-size result set. -// TODO(aibridge network calls): add a LEFT JOIN LATERAL over -// aibridge_tool_usages (keyed by sr.interception_ids, mirroring the token -// aggregation above) to compute total / blocked / errored network call counts -// per session, then expose them as columns for db2sdk.AIBridgeSession to map -// onto AIBridgeSession.NetworkCalls. The count expressions already exist in -// CalculateAIBridgeInterceptionsTelemetrySummary (injected filters and -// invocation_error IS NOT NULL). func (q *sqlQuerier) ListAIBridgeSessions(ctx context.Context, arg ListAIBridgeSessionsParams) ([]ListAIBridgeSessionsRow, error) { rows, err := q.db.QueryContext(ctx, listAIBridgeSessions, arg.AfterSessionID, @@ -2245,6 +2275,9 @@ func (q *sqlQuerier) ListAIBridgeSessions(ctx context.Context, arg ListAIBridgeS &i.CacheWriteInputTokens, &i.LastPrompt, &i.LastActiveAt, + &i.NetworkCallsTotal, + &i.NetworkCallsBlocked, + &i.FirewallActive, ); err != nil { return nil, err } diff --git a/coderd/database/queries/aibridge.sql b/coderd/database/queries/aibridge.sql index 019eed278f23b..32dc4ffb7518a 100644 --- a/coderd/database/queries/aibridge.sql +++ b/coderd/database/queries/aibridge.sql @@ -479,7 +479,10 @@ SELECT COALESCE(st.cache_read_input_tokens, 0)::bigint AS cache_read_input_tokens, COALESCE(st.cache_write_input_tokens, 0)::bigint AS cache_write_input_tokens, COALESCE(slp.prompt, '') AS last_prompt, - sp.last_active_at AS last_active_at + sp.last_active_at AS last_active_at, + COALESCE(bnc.total, 0)::bigint AS network_calls_total, + COALESCE(bnc.blocked, 0)::bigint AS network_calls_blocked, + COALESCE(sr.firewall_active, false) AS firewall_active FROM session_page sp JOIN @@ -490,7 +493,11 @@ LEFT JOIN LATERAL ( (ARRAY_AGG(ai.metadata ORDER BY ai.started_at, ai.id))[1] AS metadata, ARRAY_AGG(DISTINCT ai.provider ORDER BY ai.provider) AS providers, ARRAY_AGG(DISTINCT ai.model ORDER BY ai.model) AS models, - ARRAY_AGG(ai.id) AS interception_ids + ARRAY_AGG(ai.id) AS interception_ids, + -- firewall_active reports whether any interception in the session + -- passed through Agent Firewall. When false, network call monitoring + -- was not active and db2sdk leaves NetworkCalls nil ("Disabled"). + BOOL_OR(ai.agent_firewall_session_id IS NOT NULL) AS firewall_active FROM aibridge_interceptions ai WHERE ai.session_id = sp.session_id AND ai.initiator_id = sp.initiator_id @@ -515,13 +522,33 @@ LEFT JOIN LATERAL ( ORDER BY up.created_at DESC, up.id DESC LIMIT 1 ) slp ON true --- TODO(aibridge network calls): add a LEFT JOIN LATERAL over --- aibridge_tool_usages (keyed by sr.interception_ids, mirroring the token --- aggregation above) to compute total / blocked / errored network call counts --- per session, then expose them as columns for db2sdk.AIBridgeSession to map --- onto AIBridgeSession.NetworkCalls. The count expressions already exist in --- CalculateAIBridgeInterceptionsTelemetrySummary (injected filters and --- invocation_error IS NOT NULL). +LEFT JOIN LATERAL ( + -- Count Agent Firewall network calls attributed to this session. Each + -- interception marks a point in its firewall session's monotonic sequence + -- stream; the boundary logs it triggered 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 + -- (logged at exactly its sequence number), leaving the agent's other + -- egress. next_seq considers all interceptions in the firewall session so + -- windows never bleed across AI sessions that share one firewall session. + SELECT + COUNT(bl.id)::bigint AS total, + COUNT(bl.id) FILTER (WHERE bl.matched_rule IS NULL)::bigint AS blocked + FROM aibridge_interceptions afi + LEFT JOIN LATERAL ( + SELECT MIN(nxt.agent_firewall_sequence_number) 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 (w.next_seq IS NULL OR bl.sequence_number < w.next_seq) + WHERE afi.id = ANY(sr.interception_ids) + AND afi.agent_firewall_session_id IS NOT NULL + AND afi.agent_firewall_sequence_number IS NOT NULL +) bnc ON true ORDER BY sp.last_active_at DESC, sp.session_id DESC diff --git a/codersdk/aibridge.go b/codersdk/aibridge.go index 9b6e99a703ec3..089c2a7e7abca 100644 --- a/codersdk/aibridge.go +++ b/codersdk/aibridge.go @@ -66,9 +66,10 @@ type AIBridgeSession struct { EndedAt *time.Time `json:"ended_at,omitempty" format:"date-time"` Threads int64 `json:"threads"` TokenUsageSummary AIBridgeSessionTokenUsageSummary `json:"token_usage_summary"` - // NetworkCalls summarizes the tool/network calls made during the session. - // A nil value means network call monitoring was not active for the - // session, which the UI surfaces as "Disabled". + // NetworkCalls summarizes the Agent Firewall network calls made during the + // session. A nil value means the session did not pass through Agent + // Firewall, so network call monitoring was not active, which the UI + // surfaces as "Disabled". NetworkCalls *AIBridgeSessionNetworkCallSummary `json:"network_calls,omitempty"` LastPrompt *string `json:"last_prompt,omitempty"` LastActiveAt time.Time `json:"last_active_at" format:"date-time"` @@ -81,13 +82,12 @@ type AIBridgeSessionTokenUsageSummary struct { CacheWriteInputTokens int64 `json:"cache_write_input_tokens"` } -// AIBridgeSessionNetworkCallSummary aggregates the tool/network calls made -// during a session. Blocked counts calls denied by policy; Errored counts -// calls that failed to complete. +// AIBridgeSessionNetworkCallSummary aggregates the Agent Firewall network +// calls made during a session. Blocked counts calls denied by the firewall +// allow-list. type AIBridgeSessionNetworkCallSummary struct { Total int64 `json:"total"` Blocked int64 `json:"blocked"` - Errored int64 `json:"errored"` } type AIBridgeListSessionsResponse struct { diff --git a/docs/reference/api/aigateway.md b/docs/reference/api/aigateway.md index fbe2016affb8f..3d8ed690729b8 100644 --- a/docs/reference/api/aigateway.md +++ b/docs/reference/api/aigateway.md @@ -123,7 +123,6 @@ Alias: also available at /api/v2/aibridge/sessions for backward compatibility. ], "network_calls": { "blocked": 0, - "errored": 0, "total": 0 }, "providers": [ diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index c2fcfa4c98430..62584af5e03e4 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -485,7 +485,6 @@ ], "network_calls": { "blocked": 0, - "errored": 0, "total": 0 }, "providers": [ @@ -605,7 +604,6 @@ ], "network_calls": { "blocked": 0, - "errored": 0, "total": 0 }, "providers": [ @@ -624,29 +622,28 @@ ### Properties -| Name | Type | Required | Restrictions | Description | -|-----------------------|------------------------------------------------------------------------------------------|----------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `client` | string | false | | | -| `ended_at` | string | false | | | -| `id` | string | false | | | -| `initiator` | [codersdk.MinimalUser](#codersdkminimaluser) | false | | | -| `last_active_at` | string | false | | | -| `last_prompt` | string | false | | | -| `metadata` | object | false | | | -| » `[any property]` | any | false | | | -| `models` | array of string | false | | | -| `network_calls` | [codersdk.AIBridgeSessionNetworkCallSummary](#codersdkaibridgesessionnetworkcallsummary) | false | | Network calls summarizes the tool/network calls made during the session. A nil value means network call monitoring was not active for the session, which the UI surfaces as "Disabled". | -| `providers` | array of string | false | | | -| `started_at` | string | false | | | -| `threads` | integer | false | | | -| `token_usage_summary` | [codersdk.AIBridgeSessionTokenUsageSummary](#codersdkaibridgesessiontokenusagesummary) | false | | | +| Name | Type | Required | Restrictions | Description | +|-----------------------|------------------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `client` | string | false | | | +| `ended_at` | string | false | | | +| `id` | string | false | | | +| `initiator` | [codersdk.MinimalUser](#codersdkminimaluser) | false | | | +| `last_active_at` | string | false | | | +| `last_prompt` | string | false | | | +| `metadata` | object | false | | | +| » `[any property]` | any | false | | | +| `models` | array of string | false | | | +| `network_calls` | [codersdk.AIBridgeSessionNetworkCallSummary](#codersdkaibridgesessionnetworkcallsummary) | false | | Network calls summarizes the Agent Firewall network calls made during the session. A nil value means the session did not pass through Agent Firewall, so network call monitoring was not active, which the UI surfaces as "Disabled". | +| `providers` | array of string | false | | | +| `started_at` | string | false | | | +| `threads` | integer | false | | | +| `token_usage_summary` | [codersdk.AIBridgeSessionTokenUsageSummary](#codersdkaibridgesessiontokenusagesummary) | false | | | ## codersdk.AIBridgeSessionNetworkCallSummary ```json { "blocked": 0, - "errored": 0, "total": 0 } ``` @@ -656,7 +653,6 @@ | Name | Type | Required | Restrictions | Description | |-----------|---------|----------|--------------|-------------| | `blocked` | integer | false | | | -| `errored` | integer | false | | | | `total` | integer | false | | | ## codersdk.AIBridgeSessionThreadsResponse diff --git a/enterprise/coderd/aibridge_test.go b/enterprise/coderd/aibridge_test.go index ccfe716864b57..caaa30c5a1298 100644 --- a/enterprise/coderd/aibridge_test.go +++ b/enterprise/coderd/aibridge_test.go @@ -16,6 +16,7 @@ import ( "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -254,6 +255,143 @@ func TestAIBridgeListSessions(t *testing.T) { require.ElementsMatch(t, []string{"claude-4", "gpt-4"}, s4.Models) }) + t.Run("NetworkCalls", func(t *testing.T) { + t.Parallel() + // Use the raw store for seeding: boundary logs are agent-created, so + // neither the owner nor system-restricted role can insert them through + // dbauthz. The API client still enforces authorization on reads. + db, ps := dbtestutil.NewDB(t) + opts := aibridgeOpts(t) + opts.Options.Database = db + opts.Options.Pubsub = ps + client, _, firstUser := coderdenttest.NewWithDatabase(t, opts) + ctx := testutil.Context(t, testutil.WaitLong) + + now := dbtime.Now() + + // mkIntc creates an ended interception tied to a firewall session at a + // given sequence number. A nil fw leaves the firewall keys unset. + mkIntc := func(clientSessionID string, startOffset time.Duration, fw *uuid.UUID, seq int32) { + endedAt := now.Add(startOffset + time.Minute) + params := database.InsertAIBridgeInterceptionParams{ + InitiatorID: firstUser.UserID, + StartedAt: now.Add(startOffset), + ClientSessionID: sql.NullString{String: clientSessionID, Valid: true}, + } + if fw != nil { + params.AgentFirewallSessionID = uuid.NullUUID{UUID: *fw, Valid: true} + params.AgentFirewallSequenceNumber = sql.NullInt32{Int32: seq, Valid: true} + } + dbgen.AIBridgeInterception(t, db, params, &endedAt) + } + + // insertLogs seeds boundary logs for one firewall session. allowed=false + // leaves matched_rule empty, which the insert stores as NULL (a + // denied/blocked request). Boundary logs are agent-created, so seed them + // with a system context rather than the owner-scoped dbgen helper. + type logSeed struct { + seq int32 + allowed bool + } + sysCtx := dbauthz.AsSystemRestricted(ctx) + insertLogs := func(fw uuid.UUID, seeds []logSeed) { + params := database.InsertBoundaryLogsParams{ + SessionID: fw, + OwnerID: firstUser.UserID, + } + for _, s := range seeds { + rule := "" + if s.allowed { + rule = "allow example.com" + } + params.ID = append(params.ID, uuid.New()) + params.SequenceNumber = append(params.SequenceNumber, s.seq) + params.CapturedAt = append(params.CapturedAt, now) + params.CreatedAt = append(params.CreatedAt, now) + params.Proto = append(params.Proto, "http") + params.Method = append(params.Method, "GET") + params.Detail = append(params.Detail, "https://example.com") + params.MatchedRule = append(params.MatchedRule, rule) + } + _, err := db.InsertBoundaryLogs(sysCtx, params) + require.NoError(t, err, "insert boundary logs") + } + + fw1, fw2, fw3, fw4 := uuid.New(), uuid.New(), uuid.New(), uuid.New() + + // Sessions A and B share firewall session fw1. A is marked at seq 0, B at + // seq 3, so A's window is (0,3) and B's is (3, +inf). The logs at seq 0 + // and 3 are the interceptions' own LLM-provider calls and must be + // excluded by the exclusive lower bound. + mkIntc("sess-A", -time.Minute, &fw1, 0) + mkIntc("sess-B", -2*time.Minute, &fw1, 3) + insertLogs(fw1, []logSeed{ + {0, true}, // LLM call for A, excluded + {1, true}, // A egress + {2, false}, // A egress, blocked + {3, true}, // LLM call for B, excluded + {4, true}, // B egress + {5, true}, // B egress + }) + + // Session C spans two firewall sessions (agent restarted): fw2 and fw3. + // Its counts sum across both windows. + mkIntc("sess-C", -3*time.Minute, &fw2, 0) + mkIntc("sess-C", -4*time.Minute, &fw3, 0) + insertLogs(fw2, []logSeed{ + {0, true}, // LLM call, excluded + {1, true}, + {2, true}, + }) + insertLogs(fw3, []logSeed{ + {0, true}, // LLM call, excluded + {1, false}, // blocked + }) + + // Session D never passed through the firewall: NetworkCalls stays nil. + mkIntc("sess-D", -5*time.Minute, nil, 0) + + // Session E is firewall-active but has no logs in range: counts are zero. + mkIntc("sess-E", -6*time.Minute, &fw4, 0) + + //nolint:gocritic // Owner role is irrelevant here. + res, err := client.AIBridgeListSessions(ctx, codersdk.AIBridgeListSessionsFilter{}) + require.NoError(t, err) + + byID := make(map[string]codersdk.AIBridgeSession, len(res.Sessions)) + for _, s := range res.Sessions { + byID[s.ID] = s + } + + // A: seq 1,2 in (0,3); seq 2 blocked. LLM calls at 0 and 3 excluded. + a := byID["sess-A"] + require.NotNil(t, a.NetworkCalls) + require.EqualValues(t, 2, a.NetworkCalls.Total) + require.EqualValues(t, 1, a.NetworkCalls.Blocked) + + // B: seq 4,5 in (3, +inf); none blocked. No bleed from A's window. + b := byID["sess-B"] + require.NotNil(t, b.NetworkCalls) + require.EqualValues(t, 2, b.NetworkCalls.Total) + require.EqualValues(t, 0, b.NetworkCalls.Blocked) + + // C: fw2 contributes seq 1,2; fw3 contributes seq 1 (blocked). + c := byID["sess-C"] + require.NotNil(t, c.NetworkCalls) + require.EqualValues(t, 3, c.NetworkCalls.Total) + require.EqualValues(t, 1, c.NetworkCalls.Blocked) + + // D: no firewall session, so monitoring was not active. + d := byID["sess-D"] + require.Nil(t, d.NetworkCalls) + + // E: firewall-active but no logs in range. + e := byID["sess-E"] + require.NotNil(t, e.NetworkCalls) + require.EqualValues(t, 0, e.NetworkCalls.Total) + require.EqualValues(t, 0, e.NetworkCalls.Blocked) + }) + t.Run("Pagination", func(t *testing.T) { t.Parallel() client, db, firstUser := coderdenttest.NewWithDatabase(t, aibridgeOpts(t)) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 6e9ee7e38e41e..28bcda4f94c55 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -141,9 +141,10 @@ export interface AIBridgeSession { readonly threads: number; readonly token_usage_summary: AIBridgeSessionTokenUsageSummary; /** - * NetworkCalls summarizes the tool/network calls made during the session. - * A nil value means network call monitoring was not active for the - * session, which the UI surfaces as "Disabled". + * NetworkCalls summarizes the Agent Firewall network calls made during the + * session. A nil value means the session did not pass through Agent + * Firewall, so network call monitoring was not active, which the UI + * surfaces as "Disabled". */ readonly network_calls?: AIBridgeSessionNetworkCallSummary; readonly last_prompt?: string; @@ -152,14 +153,13 @@ export interface AIBridgeSession { // From codersdk/aibridge.go /** - * AIBridgeSessionNetworkCallSummary aggregates the tool/network calls made - * during a session. Blocked counts calls denied by policy; Errored counts - * calls that failed to complete. + * AIBridgeSessionNetworkCallSummary aggregates the Agent Firewall network + * calls made during a session. Blocked counts calls denied by the firewall + * allow-list. */ export interface AIBridgeSessionNetworkCallSummary { readonly total: number; readonly blocked: number; - readonly errored: number; } // From codersdk/aibridge.go diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx index dc878d972d6f5..b5d10a5299dba 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx @@ -106,14 +106,14 @@ export const MultipleSessions: Story = { cache_read_input_tokens: 800 * (i + 1), cache_write_input_tokens: 50 * (i + 1), }, - // Span every network call state: total/blocked, total/blocked/error, - // no activity, disabled, and total with nothing blocked. + // Span every network call state: total with blocked, no activity, + // disabled, and total with nothing blocked. network_calls: [ - { total: 23, blocked: 2, errored: 0 }, - { total: 23, blocked: 2, errored: 1 }, - { total: 0, blocked: 0, errored: 0 }, + { total: 23, blocked: 2 }, + { total: 5, blocked: 1 }, + { total: 0, blocked: 0 }, undefined, - { total: 150, blocked: 0, errored: 0 }, + { total: 150, blocked: 0 }, ][i], })), }, diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.stories.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.stories.tsx index b6e5658b8c3d7..7528e7ebb122c 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.stories.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.stories.tsx @@ -112,16 +112,7 @@ export const NetworkCallsBlocked: Story = { args: { session: { ...MockSession, - network_calls: { total: 23, blocked: 2, errored: 0 }, - }, - }, -}; - -export const NetworkCallsWithErrors: Story = { - args: { - session: { - ...MockSession, - network_calls: { total: 23, blocked: 2, errored: 1 }, + network_calls: { total: 23, blocked: 2 }, }, }, }; @@ -130,7 +121,7 @@ export const NoNetworkActivity: Story = { args: { session: { ...MockSession, - network_calls: { total: 0, blocked: 0, errored: 0 }, + network_calls: { total: 0, blocked: 0 }, }, }, }; diff --git a/site/src/pages/AIBridgePage/NetworkCallBadges.stories.tsx b/site/src/pages/AIBridgePage/NetworkCallBadges.stories.tsx index 164063a52ed1a..71281dcbed4e3 100644 --- a/site/src/pages/AIBridgePage/NetworkCallBadges.stories.tsx +++ b/site/src/pages/AIBridgePage/NetworkCallBadges.stories.tsx @@ -11,25 +11,19 @@ type Story = StoryObj; export const TotalAndBlocked: Story = { args: { - summary: { total: 23, blocked: 2, errored: 0 }, - }, -}; - -export const WithErrors: Story = { - args: { - summary: { total: 23, blocked: 2, errored: 1 }, + summary: { total: 23, blocked: 2 }, }, }; export const NoBlocked: Story = { args: { - summary: { total: 23, blocked: 0, errored: 0 }, + summary: { total: 23, blocked: 0 }, }, }; export const NoActivity: Story = { args: { - summary: { total: 0, blocked: 0, errored: 0 }, + summary: { total: 0, blocked: 0 }, }, }; @@ -41,20 +35,20 @@ export const Disabled: Story = { export const LargeCounts: Story = { args: { - summary: { total: 12_480, blocked: 320, errored: 47 }, + summary: { total: 12_480, blocked: 320 }, }, }; export const SizeXs: Story = { args: { size: "xs", - summary: { total: 23, blocked: 2, errored: 1 }, + summary: { total: 23, blocked: 2 }, }, }; export const SizeMd: Story = { args: { size: "md", - summary: { total: 23, blocked: 2, errored: 1 }, + summary: { total: 23, blocked: 2 }, }, }; diff --git a/site/src/pages/AIBridgePage/NetworkCallBadges.tsx b/site/src/pages/AIBridgePage/NetworkCallBadges.tsx index 2a5a415c44b76..45df879124cd9 100644 --- a/site/src/pages/AIBridgePage/NetworkCallBadges.tsx +++ b/site/src/pages/AIBridgePage/NetworkCallBadges.tsx @@ -1,4 +1,4 @@ -import { BanIcon, InfoIcon, TriangleAlertIcon } from "lucide-react"; +import { BanIcon, InfoIcon } from "lucide-react"; import type { FC } from "react"; import type { AIBridgeSessionNetworkCallSummary } from "#/api/typesGenerated"; import { Badge } from "#/components/Badge/Badge"; @@ -60,12 +60,6 @@ export const NetworkCallBadges: FC = ({ {summary.blocked.toLocaleString("en-US")}
- {summary.errored > 0 && ( - - - {summary.errored.toLocaleString("en-US")} - - )} = ({ Blocked {summary.blocked.toLocaleString("en-US")} -
- Errored - {summary.errored.toLocaleString("en-US")} -
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index da017f0849b28..cf6674b2ed5e8 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -5526,7 +5526,6 @@ export const MockSession: TypesGen.AIBridgeSession = { network_calls: { total: 23, blocked: 2, - errored: 0, }, last_prompt: "But *can* I really fix it?", last_active_at: "2026-03-09T10:28:15.03152Z", From 6bbefbf659b5d94b671bcd8298e26a10dcd6775d Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 20 Jul 2026 10:58:30 +0000 Subject: [PATCH 3/7] Remove overly verbose AI comments --- coderd/database/queries.sql.go | 3 --- coderd/database/queries/aibridge.sql | 3 --- enterprise/coderd/aibridge_test.go | 23 ++++++------------- .../ListSessionsPageView.stories.tsx | 2 -- 4 files changed, 7 insertions(+), 24 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 102e80d919a72..678d80a37c830 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2130,9 +2130,6 @@ LEFT JOIN LATERAL ( ARRAY_AGG(DISTINCT ai.provider ORDER BY ai.provider) AS providers, ARRAY_AGG(DISTINCT ai.model ORDER BY ai.model) AS models, ARRAY_AGG(ai.id) AS interception_ids, - -- firewall_active reports whether any interception in the session - -- passed through Agent Firewall. When false, network call monitoring - -- was not active and db2sdk leaves NetworkCalls nil ("Disabled"). BOOL_OR(ai.agent_firewall_session_id IS NOT NULL) AS firewall_active FROM aibridge_interceptions ai WHERE ai.session_id = sp.session_id diff --git a/coderd/database/queries/aibridge.sql b/coderd/database/queries/aibridge.sql index 32dc4ffb7518a..365be285f0c65 100644 --- a/coderd/database/queries/aibridge.sql +++ b/coderd/database/queries/aibridge.sql @@ -494,9 +494,6 @@ LEFT JOIN LATERAL ( ARRAY_AGG(DISTINCT ai.provider ORDER BY ai.provider) AS providers, ARRAY_AGG(DISTINCT ai.model ORDER BY ai.model) AS models, ARRAY_AGG(ai.id) AS interception_ids, - -- firewall_active reports whether any interception in the session - -- passed through Agent Firewall. When false, network call monitoring - -- was not active and db2sdk leaves NetworkCalls nil ("Disabled"). BOOL_OR(ai.agent_firewall_session_id IS NOT NULL) AS firewall_active FROM aibridge_interceptions ai WHERE ai.session_id = sp.session_id diff --git a/enterprise/coderd/aibridge_test.go b/enterprise/coderd/aibridge_test.go index caaa30c5a1298..c73fa3b4217a7 100644 --- a/enterprise/coderd/aibridge_test.go +++ b/enterprise/coderd/aibridge_test.go @@ -257,9 +257,6 @@ func TestAIBridgeListSessions(t *testing.T) { t.Run("NetworkCalls", func(t *testing.T) { t.Parallel() - // Use the raw store for seeding: boundary logs are agent-created, so - // neither the owner nor system-restricted role can insert them through - // dbauthz. The API client still enforces authorization on reads. db, ps := dbtestutil.NewDB(t) opts := aibridgeOpts(t) opts.Options.Database = db @@ -269,9 +266,7 @@ func TestAIBridgeListSessions(t *testing.T) { now := dbtime.Now() - // mkIntc creates an ended interception tied to a firewall session at a - // given sequence number. A nil fw leaves the firewall keys unset. - mkIntc := func(clientSessionID string, startOffset time.Duration, fw *uuid.UUID, seq int32) { + makeInterception := func(clientSessionID string, startOffset time.Duration, fw *uuid.UUID, seq int32) { endedAt := now.Add(startOffset + time.Minute) params := database.InsertAIBridgeInterceptionParams{ InitiatorID: firstUser.UserID, @@ -285,10 +280,6 @@ func TestAIBridgeListSessions(t *testing.T) { dbgen.AIBridgeInterception(t, db, params, &endedAt) } - // insertLogs seeds boundary logs for one firewall session. allowed=false - // leaves matched_rule empty, which the insert stores as NULL (a - // denied/blocked request). Boundary logs are agent-created, so seed them - // with a system context rather than the owner-scoped dbgen helper. type logSeed struct { seq int32 allowed bool @@ -323,8 +314,8 @@ func TestAIBridgeListSessions(t *testing.T) { // seq 3, so A's window is (0,3) and B's is (3, +inf). The logs at seq 0 // and 3 are the interceptions' own LLM-provider calls and must be // excluded by the exclusive lower bound. - mkIntc("sess-A", -time.Minute, &fw1, 0) - mkIntc("sess-B", -2*time.Minute, &fw1, 3) + makeInterception("sess-A", -time.Minute, &fw1, 0) + makeInterception("sess-B", -2*time.Minute, &fw1, 3) insertLogs(fw1, []logSeed{ {0, true}, // LLM call for A, excluded {1, true}, // A egress @@ -336,8 +327,8 @@ func TestAIBridgeListSessions(t *testing.T) { // Session C spans two firewall sessions (agent restarted): fw2 and fw3. // Its counts sum across both windows. - mkIntc("sess-C", -3*time.Minute, &fw2, 0) - mkIntc("sess-C", -4*time.Minute, &fw3, 0) + makeInterception("sess-C", -3*time.Minute, &fw2, 0) + makeInterception("sess-C", -4*time.Minute, &fw3, 0) insertLogs(fw2, []logSeed{ {0, true}, // LLM call, excluded {1, true}, @@ -349,10 +340,10 @@ func TestAIBridgeListSessions(t *testing.T) { }) // Session D never passed through the firewall: NetworkCalls stays nil. - mkIntc("sess-D", -5*time.Minute, nil, 0) + makeInterception("sess-D", -5*time.Minute, nil, 0) // Session E is firewall-active but has no logs in range: counts are zero. - mkIntc("sess-E", -6*time.Minute, &fw4, 0) + makeInterception("sess-E", -6*time.Minute, &fw4, 0) //nolint:gocritic // Owner role is irrelevant here. res, err := client.AIBridgeListSessions(ctx, codersdk.AIBridgeListSessionsFilter{}) diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx index b5d10a5299dba..ff32f8230c3dd 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx @@ -106,8 +106,6 @@ export const MultipleSessions: Story = { cache_read_input_tokens: 800 * (i + 1), cache_write_input_tokens: 50 * (i + 1), }, - // Span every network call state: total with blocked, no activity, - // disabled, and total with nothing blocked. network_calls: [ { total: 23, blocked: 2 }, { total: 5, blocked: 1 }, From 6fc1f78665f300e575c9fa9f84a3638e57bad721 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 20 Jul 2026 11:47:09 +0000 Subject: [PATCH 4/7] Renumber migrations --- ...index.down.sql => 000548_aibridge_firewall_seq_index.down.sql} | 0 ...seq_index.up.sql => 000548_aibridge_firewall_seq_index.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000544_aibridge_firewall_seq_index.down.sql => 000548_aibridge_firewall_seq_index.down.sql} (100%) rename coderd/database/migrations/{000544_aibridge_firewall_seq_index.up.sql => 000548_aibridge_firewall_seq_index.up.sql} (100%) diff --git a/coderd/database/migrations/000544_aibridge_firewall_seq_index.down.sql b/coderd/database/migrations/000548_aibridge_firewall_seq_index.down.sql similarity index 100% rename from coderd/database/migrations/000544_aibridge_firewall_seq_index.down.sql rename to coderd/database/migrations/000548_aibridge_firewall_seq_index.down.sql diff --git a/coderd/database/migrations/000544_aibridge_firewall_seq_index.up.sql b/coderd/database/migrations/000548_aibridge_firewall_seq_index.up.sql similarity index 100% rename from coderd/database/migrations/000544_aibridge_firewall_seq_index.up.sql rename to coderd/database/migrations/000548_aibridge_firewall_seq_index.up.sql From 43a059547ee285341e8fe1b1481898f4edbc8482 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 20 Jul 2026 11:56:02 +0000 Subject: [PATCH 5/7] chore: remove frontend changes from backend PR Move the non-generated frontend changes for network call badges to a separate branch so this PR is backend only. The generated typesGenerated.ts additions remain, as they are produced by make gen alongside the backend changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ListSessionsPageView.stories.tsx | 7 -- .../ListSessionsPage/ListSessionsPageView.tsx | 3 - .../ListSessionsRow.stories.tsx | 24 ------ .../ListSessionsPage/ListSessionsRow.tsx | 4 - .../NetworkCallBadges.stories.tsx | 54 ------------ .../pages/AIBridgePage/NetworkCallBadges.tsx | 84 ------------------- site/src/testHelpers/entities.ts | 4 - 7 files changed, 180 deletions(-) delete mode 100644 site/src/pages/AIBridgePage/NetworkCallBadges.stories.tsx delete mode 100644 site/src/pages/AIBridgePage/NetworkCallBadges.tsx diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx index ff32f8230c3dd..ede9692b8ef1d 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx @@ -106,13 +106,6 @@ export const MultipleSessions: Story = { cache_read_input_tokens: 800 * (i + 1), cache_write_input_tokens: 50 * (i + 1), }, - network_calls: [ - { total: 23, blocked: 2 }, - { total: 5, blocked: 1 }, - { total: 0, blocked: 0 }, - undefined, - { total: 150, blocked: 0 }, - ][i], })), }, }; diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx index dc9fc05a9f689..ed85496fb5f1b 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx @@ -86,9 +86,6 @@ export const ListSessionsPageView: FC = ({ Provider Client In/Out Tokens - - Total/blocked network calls - Threads diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.stories.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.stories.tsx index 7528e7ebb122c..18db56d9385e5 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.stories.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.stories.tsx @@ -107,27 +107,3 @@ export const LargeTokenCounts: Story = { }, }, }; - -export const NetworkCallsBlocked: Story = { - args: { - session: { - ...MockSession, - network_calls: { total: 23, blocked: 2 }, - }, - }, -}; - -export const NoNetworkActivity: Story = { - args: { - session: { - ...MockSession, - network_calls: { total: 0, blocked: 0 }, - }, - }, -}; - -export const NetworkDisabled: Story = { - args: { - session: { ...MockSession, network_calls: undefined }, - }, -}; diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.tsx index 82e2cf607b192..a655964575e20 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsRow.tsx @@ -13,7 +13,6 @@ import { import { AIBridgeClientIcon } from "#/pages/AIBridgePage/icons/AIBridgeClientIcon"; import { AIBridgeProviderIcon } from "#/pages/AIBridgePage/icons/AIBridgeProviderIcon"; import { DATE_FORMAT, formatDateTime } from "#/utils/time"; -import { NetworkCallBadges } from "../NetworkCallBadges"; import { TokenBadges } from "../TokenBadges"; import { getProviderDisplayName } from "../utils"; @@ -106,9 +105,6 @@ export const ListSessionsRow: FC = ({ />
- - - {session.threads} diff --git a/site/src/pages/AIBridgePage/NetworkCallBadges.stories.tsx b/site/src/pages/AIBridgePage/NetworkCallBadges.stories.tsx deleted file mode 100644 index 71281dcbed4e3..0000000000000 --- a/site/src/pages/AIBridgePage/NetworkCallBadges.stories.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { NetworkCallBadges } from "./NetworkCallBadges"; - -const meta: Meta = { - title: "pages/AIBridgePage/NetworkCallBadges", - component: NetworkCallBadges, -}; - -export default meta; -type Story = StoryObj; - -export const TotalAndBlocked: Story = { - args: { - summary: { total: 23, blocked: 2 }, - }, -}; - -export const NoBlocked: Story = { - args: { - summary: { total: 23, blocked: 0 }, - }, -}; - -export const NoActivity: Story = { - args: { - summary: { total: 0, blocked: 0 }, - }, -}; - -export const Disabled: Story = { - args: { - summary: undefined, - }, -}; - -export const LargeCounts: Story = { - args: { - summary: { total: 12_480, blocked: 320 }, - }, -}; - -export const SizeXs: Story = { - args: { - size: "xs", - summary: { total: 23, blocked: 2 }, - }, -}; - -export const SizeMd: Story = { - args: { - size: "md", - summary: { total: 23, blocked: 2 }, - }, -}; diff --git a/site/src/pages/AIBridgePage/NetworkCallBadges.tsx b/site/src/pages/AIBridgePage/NetworkCallBadges.tsx deleted file mode 100644 index 45df879124cd9..0000000000000 --- a/site/src/pages/AIBridgePage/NetworkCallBadges.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { BanIcon, InfoIcon } from "lucide-react"; -import type { FC } from "react"; -import type { AIBridgeSessionNetworkCallSummary } from "#/api/typesGenerated"; -import { Badge } from "#/components/Badge/Badge"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "#/components/Tooltip/Tooltip"; - -interface NetworkCallBadgesProps { - size?: "xs" | "sm" | "md"; - // summary is undefined when network call monitoring was not active for the - // session, which renders as "Disabled". - summary: AIBridgeSessionNetworkCallSummary | undefined; -} - -export const NetworkCallBadges: FC = ({ - size = "sm", - summary, -}) => { - if (!summary) { - return ( - - - - - Disabled - - - - - Network call monitoring was not active for this session. - - - - ); - } - - if (summary.total === 0) { - return ( - - No activity - - ); - } - - return ( - - - - - {summary.total.toLocaleString("en-US")} - - - {summary.blocked.toLocaleString("en-US")} - - - - -
-
- Total calls - {summary.total.toLocaleString("en-US")} -
-
- Blocked - {summary.blocked.toLocaleString("en-US")} -
-
-
-
-
- ); -}; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index cf6674b2ed5e8..92f6ac4027212 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -5523,10 +5523,6 @@ export const MockSession: TypesGen.AIBridgeSession = { cache_read_input_tokens: 980, cache_write_input_tokens: 120, }, - network_calls: { - total: 23, - blocked: 2, - }, last_prompt: "But *can* I really fix it?", last_active_at: "2026-03-09T10:28:15.03152Z", }; From a0b0fcdea1b3c090de8b94d98a42b15a297045f1 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 21 Jul 2026 10:35:54 +0000 Subject: [PATCH 6/7] enhance boundary_logs session index and optimize query counts --- coderd/database/dump.sql | 2 +- .../migrations/000548_aibridge_firewall_seq_index.down.sql | 5 +++++ .../migrations/000548_aibridge_firewall_seq_index.up.sql | 5 +++++ coderd/database/queries.sql.go | 4 ++-- coderd/database/queries/aibridge.sql | 4 ++-- enterprise/coderd/aibridge_test.go | 4 +--- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index a1bcef76d5deb..d85cb1c887cd3 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -4722,7 +4722,7 @@ CREATE INDEX idx_audit_logs_time_desc ON audit_logs USING btree ("time" DESC); CREATE INDEX idx_boundary_logs_captured_at ON boundary_logs USING btree (captured_at); -CREATE INDEX idx_boundary_logs_session_seq ON boundary_logs USING btree (session_id, sequence_number); +CREATE INDEX idx_boundary_logs_session_seq ON boundary_logs USING btree (session_id, sequence_number) INCLUDE (matched_rule); CREATE INDEX idx_chat_debug_runs_chat_started ON chat_debug_runs USING btree (chat_id, started_at DESC); diff --git a/coderd/database/migrations/000548_aibridge_firewall_seq_index.down.sql b/coderd/database/migrations/000548_aibridge_firewall_seq_index.down.sql index 2b4e022b51fd8..5a01fd61ecff4 100644 --- a/coderd/database/migrations/000548_aibridge_firewall_seq_index.down.sql +++ b/coderd/database/migrations/000548_aibridge_firewall_seq_index.down.sql @@ -1,3 +1,8 @@ +DROP INDEX IF EXISTS idx_boundary_logs_session_seq; + +CREATE INDEX idx_boundary_logs_session_seq + ON boundary_logs (session_id, sequence_number); + DROP INDEX IF EXISTS idx_aibridge_interceptions_agent_firewall_session_seq; CREATE INDEX idx_aibridge_interceptions_agent_firewall_session_id diff --git a/coderd/database/migrations/000548_aibridge_firewall_seq_index.up.sql b/coderd/database/migrations/000548_aibridge_firewall_seq_index.up.sql index 4f3f094ec7295..3bbadfc36142d 100644 --- a/coderd/database/migrations/000548_aibridge_firewall_seq_index.up.sql +++ b/coderd/database/migrations/000548_aibridge_firewall_seq_index.up.sql @@ -8,3 +8,8 @@ DROP INDEX IF EXISTS idx_aibridge_interceptions_agent_firewall_session_id; CREATE INDEX idx_aibridge_interceptions_agent_firewall_session_seq ON aibridge_interceptions (agent_firewall_session_id, agent_firewall_sequence_number) WHERE agent_firewall_session_id IS NOT NULL; + +DROP INDEX IF EXISTS idx_boundary_logs_session_seq; + +CREATE INDEX idx_boundary_logs_session_seq + ON boundary_logs (session_id, sequence_number) INCLUDE (matched_rule); diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 678d80a37c830..e77fd2b599492 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2165,8 +2165,8 @@ LEFT JOIN LATERAL ( -- egress. next_seq considers all interceptions in the firewall session so -- windows never bleed across AI sessions that share one firewall session. SELECT - COUNT(bl.id)::bigint AS total, - COUNT(bl.id) FILTER (WHERE bl.matched_rule IS NULL)::bigint AS blocked + COUNT(*)::bigint AS total, + COUNT(*) FILTER (WHERE bl.matched_rule IS NULL)::bigint AS blocked FROM aibridge_interceptions afi LEFT JOIN LATERAL ( SELECT MIN(nxt.agent_firewall_sequence_number) AS next_seq diff --git a/coderd/database/queries/aibridge.sql b/coderd/database/queries/aibridge.sql index 365be285f0c65..63635b6ae22ef 100644 --- a/coderd/database/queries/aibridge.sql +++ b/coderd/database/queries/aibridge.sql @@ -529,8 +529,8 @@ LEFT JOIN LATERAL ( -- egress. next_seq considers all interceptions in the firewall session so -- windows never bleed across AI sessions that share one firewall session. SELECT - COUNT(bl.id)::bigint AS total, - COUNT(bl.id) FILTER (WHERE bl.matched_rule IS NULL)::bigint AS blocked + COUNT(*)::bigint AS total, + COUNT(*) FILTER (WHERE bl.matched_rule IS NULL)::bigint AS blocked FROM aibridge_interceptions afi LEFT JOIN LATERAL ( SELECT MIN(nxt.agent_firewall_sequence_number) AS next_seq diff --git a/enterprise/coderd/aibridge_test.go b/enterprise/coderd/aibridge_test.go index c73fa3b4217a7..2e791d675a1ce 100644 --- a/enterprise/coderd/aibridge_test.go +++ b/enterprise/coderd/aibridge_test.go @@ -311,9 +311,7 @@ func TestAIBridgeListSessions(t *testing.T) { fw1, fw2, fw3, fw4 := uuid.New(), uuid.New(), uuid.New(), uuid.New() // Sessions A and B share firewall session fw1. A is marked at seq 0, B at - // seq 3, so A's window is (0,3) and B's is (3, +inf). The logs at seq 0 - // and 3 are the interceptions' own LLM-provider calls and must be - // excluded by the exclusive lower bound. + // seq 3, so A's window is (0,3) and B's is (3, +inf). makeInterception("sess-A", -time.Minute, &fw1, 0) makeInterception("sess-B", -2*time.Minute, &fw1, 3) insertLogs(fw1, []logSeed{ From c164363d6d2c871852670f0c6b533898a4745294 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 21 Jul 2026 10:37:30 +0000 Subject: [PATCH 7/7] fix migration numbers --- ...index.down.sql => 000550_aibridge_firewall_seq_index.down.sql} | 0 ...seq_index.up.sql => 000550_aibridge_firewall_seq_index.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000548_aibridge_firewall_seq_index.down.sql => 000550_aibridge_firewall_seq_index.down.sql} (100%) rename coderd/database/migrations/{000548_aibridge_firewall_seq_index.up.sql => 000550_aibridge_firewall_seq_index.up.sql} (100%) diff --git a/coderd/database/migrations/000548_aibridge_firewall_seq_index.down.sql b/coderd/database/migrations/000550_aibridge_firewall_seq_index.down.sql similarity index 100% rename from coderd/database/migrations/000548_aibridge_firewall_seq_index.down.sql rename to coderd/database/migrations/000550_aibridge_firewall_seq_index.down.sql diff --git a/coderd/database/migrations/000548_aibridge_firewall_seq_index.up.sql b/coderd/database/migrations/000550_aibridge_firewall_seq_index.up.sql similarity index 100% rename from coderd/database/migrations/000548_aibridge_firewall_seq_index.up.sql rename to coderd/database/migrations/000550_aibridge_firewall_seq_index.up.sql