feat: add GET /api/v2/agent-firewall/sessions/{id}/logs endpoint (#24816)

Add a `GET /api/v2/agent-firewall/sessions/{id}/logs` endpoint that
returns agent firewall audit logs for a given session, sorted by
sequence number ascending.

The endpoint supports `seq_after` and `seq_before` (exclusive bounds)
and `limit` query parameters. This enables the frontend to fetch exactly
the firewall events that fall between two AI Bridge interceptions within
a thread, as described in FR 4 of the Boundary/Bridge correlation RFC.

Authorization reuses the `boundary_log` RBAC resource (owner and auditor
can read; members cannot). Returns 404 for unauthorized users to avoid
leaking existence information.

The endpoint is enterprise-only, gated behind `FeatureBoundary`
entitlement, matching the session endpoint from #24814.

Depends on #24814

> [!NOTE]
> This PR was authored by Coder Agents.
This commit is contained in:
Sas Swart
2026-06-22 13:56:29 +02:00
committed by GitHub
parent c0b8fa9418
commit 335d6bda1b
13 changed files with 829 additions and 8 deletions
+103
View File
@@ -1327,6 +1327,59 @@ const docTemplate = `{
]
}
},
"/api/v2/agent-firewall/sessions/{id}/logs": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Enterprise"
],
"summary": "Get agent firewall session logs",
"operationId": "get-agent-firewall-session-logs",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Agent firewall session ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Inclusive lower bound on sequence number",
"name": "seq_after",
"in": "query"
},
{
"type": "integer",
"description": "Exclusive upper bound on sequence number",
"name": "seq_before",
"in": "query"
},
{
"type": "integer",
"description": "Maximum number of logs to return (default 100)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.AgentFirewallSessionLogsResponse"
}
}
},
"security": [
{
"CoderSessionToken": []
}
]
}
},
"/api/v2/ai/providers": {
"get": {
"produces": [
@@ -15943,6 +15996,45 @@ const docTemplate = `{
"AgentDisplayModeAlwaysCollapsed"
]
},
"codersdk.AgentFirewallLog": {
"type": "object",
"properties": {
"allowed": {
"type": "boolean"
},
"captured_at": {
"type": "string",
"format": "date-time"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"detail": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"matched_rule": {
"type": "string"
},
"method": {
"type": "string"
},
"proto": {
"type": "string"
},
"sequence_number": {
"type": "integer"
},
"session_id": {
"type": "string",
"format": "uuid"
}
}
},
"codersdk.AgentFirewallSession": {
"type": "object",
"properties": {
@@ -15967,6 +16059,17 @@ const docTemplate = `{
}
}
},
"codersdk.AgentFirewallSessionLogsResponse": {
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AgentFirewallLog"
}
}
}
},
"codersdk.AgentScriptTiming": {
"type": "object",
"properties": {
+99
View File
@@ -1174,6 +1174,55 @@
]
}
},
"/api/v2/agent-firewall/sessions/{id}/logs": {
"get": {
"produces": ["application/json"],
"tags": ["Enterprise"],
"summary": "Get agent firewall session logs",
"operationId": "get-agent-firewall-session-logs",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Agent firewall session ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Inclusive lower bound on sequence number",
"name": "seq_after",
"in": "query"
},
{
"type": "integer",
"description": "Exclusive upper bound on sequence number",
"name": "seq_before",
"in": "query"
},
{
"type": "integer",
"description": "Maximum number of logs to return (default 100)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.AgentFirewallSessionLogsResponse"
}
}
},
"security": [
{
"CoderSessionToken": []
}
]
}
},
"/api/v2/ai/providers": {
"get": {
"produces": ["application/json"],
@@ -14278,6 +14327,45 @@
"AgentDisplayModeAlwaysCollapsed"
]
},
"codersdk.AgentFirewallLog": {
"type": "object",
"properties": {
"allowed": {
"type": "boolean"
},
"captured_at": {
"type": "string",
"format": "date-time"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"detail": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"matched_rule": {
"type": "string"
},
"method": {
"type": "string"
},
"proto": {
"type": "string"
},
"sequence_number": {
"type": "integer"
},
"session_id": {
"type": "string",
"format": "uuid"
}
}
},
"codersdk.AgentFirewallSession": {
"type": "object",
"properties": {
@@ -14302,6 +14390,17 @@
}
}
},
"codersdk.AgentFirewallSessionLogsResponse": {
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AgentFirewallLog"
}
}
}
},
"codersdk.AgentScriptTiming": {
"type": "object",
"properties": {
+1
View File
@@ -1471,6 +1471,7 @@ func New(options *Options) *API {
r.Get("/", api.auditLogs)
r.Post("/testgenerate", api.generateFakeAuditLog)
})
r.Route("/files", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
+2 -2
View File
@@ -1146,8 +1146,8 @@ type sqlcQuerier interface {
ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeUserPrompt, error)
ListAIGatewayKeys(ctx context.Context) ([]ListAIGatewayKeysRow, error)
// Lists boundary logs for a session, sorted by sequence number ascending.
// Supports optional exclusive sequence number bounds (seq_after, seq_before)
// for fetching events between two known interceptions.
// Supports an inclusive lower bound (seq_after) and an exclusive upper bound
// (seq_before) for fetching events between two known interceptions.
ListBoundaryLogsBySessionID(ctx context.Context, arg ListBoundaryLogsBySessionIDParams) ([]BoundaryLog, error)
// Lists a chat's pinned context resources, ordered deterministically by
// source.
+3 -3
View File
@@ -3892,7 +3892,7 @@ FROM boundary_logs
WHERE
session_id = $1
AND CASE
WHEN $2::int IS NOT NULL THEN sequence_number > $2
WHEN $2::int IS NOT NULL THEN sequence_number >= $2
ELSE true
END
AND CASE
@@ -3911,8 +3911,8 @@ type ListBoundaryLogsBySessionIDParams struct {
}
// Lists boundary logs for a session, sorted by sequence number ascending.
// Supports optional exclusive sequence number bounds (seq_after, seq_before)
// for fetching events between two known interceptions.
// Supports an inclusive lower bound (seq_after) and an exclusive upper bound
// (seq_before) for fetching events between two known interceptions.
func (q *sqlQuerier) ListBoundaryLogsBySessionID(ctx context.Context, arg ListBoundaryLogsBySessionIDParams) ([]BoundaryLog, error) {
rows, err := q.db.QueryContext(ctx, listBoundaryLogsBySessionID,
arg.SessionID,
+3 -3
View File
@@ -64,14 +64,14 @@ SELECT * FROM boundary_logs WHERE id = @id;
-- name: ListBoundaryLogsBySessionID :many
-- Lists boundary logs for a session, sorted by sequence number ascending.
-- Supports optional exclusive sequence number bounds (seq_after, seq_before)
-- for fetching events between two known interceptions.
-- Supports an inclusive lower bound (seq_after) and an exclusive upper bound
-- (seq_before) for fetching events between two known interceptions.
SELECT *
FROM boundary_logs
WHERE
session_id = @session_id
AND CASE
WHEN sqlc.narg('seq_after')::int IS NOT NULL THEN sequence_number > sqlc.narg('seq_after')
WHEN sqlc.narg('seq_after')::int IS NOT NULL THEN sequence_number >= sqlc.narg('seq_after')
ELSE true
END
AND CASE