From 69c2c405127b38703a4f8e7096e3f993f7ec4994 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 22 Oct 2025 16:18:31 +1100 Subject: [PATCH] chore: add user details to aibridge interception list endpoint (#20397) - Adds FK from `aibridge_interceptions.initiator_id` to `users.id` - This is enforced by deleting any rows that don't have any users. Since this is an experimental feature AND coder never deletes user rows I think this is acceptable. - Adds `name` as a property on `codersdk.MinimalUser` - This matches the `visible_users` view in the database. I'm unsure why `name` wasn't already included given that `username` is. - Adds a new `initiator` field to `codersdk.AIBridgeInterception` which contains `codersdk.MinimalUser` (ID, username, name, avatar URL) - Removes `initiator_id` from `codersdk.AIBridgeInterception` - Should be fine since we're still in early access --- cli/sharing_test.go | 4 + cli/testdata/coder_users_list_--help.golden | 2 +- coderd/apidoc/docs.go | 11 ++- coderd/apidoc/swagger.json | 11 ++- coderd/database/db2sdk/db2sdk.go | 15 +++- coderd/database/dbauthz/dbauthz.go | 4 +- coderd/database/dbauthz/dbauthz_test.go | 4 +- coderd/database/dbmetrics/querymetrics.go | 4 +- coderd/database/dbmock/dbmock.go | 8 +- coderd/database/dump.sql | 3 + coderd/database/foreign_key_constraint.go | 1 + .../migrations/000385_aibridge_fks.down.sql | 0 .../migrations/000385_aibridge_fks.up.sql | 49 +++++++++++ .../testdata/fixtures/000370_aibridge.up.sql | 81 ++++++++++++++++++- coderd/database/modelqueries.go | 24 +++--- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 32 +++++--- coderd/database/queries/aibridge.sql | 5 +- coderd/templateversions.go | 1 + codersdk/aibridge.go | 2 +- codersdk/users.go | 2 +- docs/reference/api/aibridge.md | 7 +- docs/reference/api/schemas.md | 22 ++++- docs/reference/api/templates.md | 9 +++ docs/reference/api/workspaces.md | 1 + docs/reference/cli/users_list.md | 8 +- enterprise/coderd/aibridge.go | 14 +++- enterprise/coderd/aibridge_test.go | 76 ++++++++++++----- site/src/api/typesGenerated.ts | 4 +- vpn/client_test.go | 1 + 30 files changed, 329 insertions(+), 78 deletions(-) create mode 100644 coderd/database/migrations/000385_aibridge_fks.down.sql create mode 100644 coderd/database/migrations/000385_aibridge_fks.up.sql diff --git a/cli/sharing_test.go b/cli/sharing_test.go index 71e153c64e..19e1853470 100644 --- a/cli/sharing_test.go +++ b/cli/sharing_test.go @@ -54,6 +54,7 @@ func TestSharingShare(t *testing.T) { MinimalUser: codersdk.MinimalUser{ ID: toShareWithUser.ID, Username: toShareWithUser.Username, + Name: toShareWithUser.Name, AvatarURL: toShareWithUser.AvatarURL, }, Role: codersdk.WorkspaceRole("use"), @@ -103,6 +104,7 @@ func TestSharingShare(t *testing.T) { MinimalUser: codersdk.MinimalUser{ ID: toShareWithUser1.ID, Username: toShareWithUser1.Username, + Name: toShareWithUser1.Name, AvatarURL: toShareWithUser1.AvatarURL, }, Role: codersdk.WorkspaceRoleUse, @@ -111,6 +113,7 @@ func TestSharingShare(t *testing.T) { MinimalUser: codersdk.MinimalUser{ ID: toShareWithUser2.ID, Username: toShareWithUser2.Username, + Name: toShareWithUser2.Name, AvatarURL: toShareWithUser2.AvatarURL, }, Role: codersdk.WorkspaceRoleUse, @@ -155,6 +158,7 @@ func TestSharingShare(t *testing.T) { MinimalUser: codersdk.MinimalUser{ ID: toShareWithUser.ID, Username: toShareWithUser.Username, + Name: toShareWithUser.Name, AvatarURL: toShareWithUser.AvatarURL, }, Role: codersdk.WorkspaceRoleAdmin, diff --git a/cli/testdata/coder_users_list_--help.golden b/cli/testdata/coder_users_list_--help.golden index 22c1fe172f..e446d63a36 100644 --- a/cli/testdata/coder_users_list_--help.golden +++ b/cli/testdata/coder_users_list_--help.golden @@ -8,7 +8,7 @@ USAGE: Aliases: ls OPTIONS: - -c, --column [id|username|email|created at|updated at|status] (default: username,email,created at,status) + -c, --column [id|username|name|email|created at|updated at|status] (default: username,email,created at,status) Columns to display in table output. --github-user-id int diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 6c5cb1506f..585cc22710 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11687,9 +11687,8 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, - "initiator_id": { - "type": "string", - "format": "uuid" + "initiator": { + "$ref": "#/definitions/codersdk.MinimalUser" }, "metadata": { "type": "object", @@ -15071,6 +15070,9 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "name": { + "type": "string" + }, "username": { "type": "string" } @@ -20895,6 +20897,9 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "name": { + "type": "string" + }, "role": { "enum": [ "admin", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 1b492d7649..ac25626a03 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10387,9 +10387,8 @@ "type": "string", "format": "uuid" }, - "initiator_id": { - "type": "string", - "format": "uuid" + "initiator": { + "$ref": "#/definitions/codersdk.MinimalUser" }, "metadata": { "type": "object", @@ -13633,6 +13632,9 @@ "type": "string", "format": "uuid" }, + "name": { + "type": "string" + }, "username": { "type": "string" } @@ -19211,6 +19213,9 @@ "type": "string", "format": "uuid" }, + "name": { + "type": "string" + }, "role": { "enum": ["admin", "use"], "allOf": [ diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index f28fa00407..77ddcbbf4a 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -189,6 +189,16 @@ func MinimalUser(user database.User) codersdk.MinimalUser { return codersdk.MinimalUser{ ID: user.ID, Username: user.Username, + Name: user.Name, + AvatarURL: user.AvatarURL, + } +} + +func MinimalUserFromVisibleUser(user database.VisibleUser) codersdk.MinimalUser { + return codersdk.MinimalUser{ + ID: user.ID, + Username: user.Username, + Name: user.Name, AvatarURL: user.AvatarURL, } } @@ -197,7 +207,6 @@ func ReducedUser(user database.User) codersdk.ReducedUser { return codersdk.ReducedUser{ MinimalUser: MinimalUser(user), Email: user.Email, - Name: user.Name, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, LastSeenAt: user.LastSeenAt, @@ -927,7 +936,7 @@ func PreviewParameterValidation(v *previewtypes.ParameterValidation) codersdk.Pr } } -func AIBridgeInterception(interception database.AIBridgeInterception, tokenUsages []database.AIBridgeTokenUsage, userPrompts []database.AIBridgeUserPrompt, toolUsages []database.AIBridgeToolUsage) codersdk.AIBridgeInterception { +func AIBridgeInterception(interception database.AIBridgeInterception, initiator database.VisibleUser, tokenUsages []database.AIBridgeTokenUsage, userPrompts []database.AIBridgeUserPrompt, toolUsages []database.AIBridgeToolUsage) codersdk.AIBridgeInterception { sdkTokenUsages := List(tokenUsages, AIBridgeTokenUsage) sort.Slice(sdkTokenUsages, func(i, j int) bool { // created_at ASC @@ -945,7 +954,7 @@ func AIBridgeInterception(interception database.AIBridgeInterception, tokenUsage }) return codersdk.AIBridgeInterception{ ID: interception.ID, - InitiatorID: interception.InitiatorID, + Initiator: MinimalUserFromVisibleUser(initiator), Provider: interception.Provider, Model: interception.Model, Metadata: jsonOrEmptyMap(interception.Metadata), diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index ccd6151e88..f1b262047a 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -4461,7 +4461,7 @@ func (q *querier) InsertWorkspaceResourceMetadata(ctx context.Context, arg datab return q.db.InsertWorkspaceResourceMetadata(ctx, arg) } -func (q *querier) ListAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams) ([]database.AIBridgeInterception, error) { +func (q *querier) ListAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams) ([]database.ListAIBridgeInterceptionsRow, error) { prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAibridgeInterception.Type) if err != nil { return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err) @@ -5870,7 +5870,7 @@ func (q *querier) CountAuthorizedConnectionLogs(ctx context.Context, arg databas return q.CountConnectionLogs(ctx, arg) } -func (q *querier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams, _ rbac.PreparedAuthorized) ([]database.AIBridgeInterception, error) { +func (q *querier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams, _ rbac.PreparedAuthorized) ([]database.ListAIBridgeInterceptionsRow, error) { // TODO: Delete this function, all ListAIBridgeInterceptions should be authorized. For now just call ListAIBridgeInterceptions on the authz querier. // This cannot be deleted for now because it's included in the // database.Store interface, so dbauthz needs to implement it. diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 1137aa250d..feb78215c5 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4537,14 +4537,14 @@ func (s *MethodTestSuite) TestAIBridge() { s.Run("ListAIBridgeInterceptions", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { params := database.ListAIBridgeInterceptionsParams{} - db.EXPECT().ListAuthorizedAIBridgeInterceptions(gomock.Any(), params, gomock.Any()).Return([]database.AIBridgeInterception{}, nil).AnyTimes() + db.EXPECT().ListAuthorizedAIBridgeInterceptions(gomock.Any(), params, gomock.Any()).Return([]database.ListAIBridgeInterceptionsRow{}, nil).AnyTimes() // No asserts here because SQLFilter. check.Args(params).Asserts() })) s.Run("ListAuthorizedAIBridgeInterceptions", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { params := database.ListAIBridgeInterceptionsParams{} - db.EXPECT().ListAuthorizedAIBridgeInterceptions(gomock.Any(), params, gomock.Any()).Return([]database.AIBridgeInterception{}, nil).AnyTimes() + db.EXPECT().ListAuthorizedAIBridgeInterceptions(gomock.Any(), params, gomock.Any()).Return([]database.ListAIBridgeInterceptionsRow{}, nil).AnyTimes() // No asserts here because SQLFilter. check.Args(params, emptyPreparedAuthorized{}).Asserts() })) diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index cdcc675c91..9b678a8e27 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -2714,7 +2714,7 @@ func (m queryMetricsStore) InsertWorkspaceResourceMetadata(ctx context.Context, return metadata, err } -func (m queryMetricsStore) ListAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams) ([]database.AIBridgeInterception, error) { +func (m queryMetricsStore) ListAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams) ([]database.ListAIBridgeInterceptionsRow, error) { start := time.Now() r0, r1 := m.s.ListAIBridgeInterceptions(ctx, arg) m.queryLatencies.WithLabelValues("ListAIBridgeInterceptions").Observe(time.Since(start).Seconds()) @@ -3722,7 +3722,7 @@ func (m queryMetricsStore) CountAuthorizedConnectionLogs(ctx context.Context, ar return r0, r1 } -func (m queryMetricsStore) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]database.AIBridgeInterception, error) { +func (m queryMetricsStore) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]database.ListAIBridgeInterceptionsRow, error) { start := time.Now() r0, r1 := m.s.ListAuthorizedAIBridgeInterceptions(ctx, arg, prepared) m.queryLatencies.WithLabelValues("ListAuthorizedAIBridgeInterceptions").Observe(time.Since(start).Seconds()) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 89099b6e71..06b7f7ae3e 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -5804,10 +5804,10 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceResourceMetadata(ctx, arg any) * } // ListAIBridgeInterceptions mocks base method. -func (m *MockStore) ListAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams) ([]database.AIBridgeInterception, error) { +func (m *MockStore) ListAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams) ([]database.ListAIBridgeInterceptionsRow, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListAIBridgeInterceptions", ctx, arg) - ret0, _ := ret[0].([]database.AIBridgeInterception) + ret0, _ := ret[0].([]database.ListAIBridgeInterceptionsRow) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -5864,10 +5864,10 @@ func (mr *MockStoreMockRecorder) ListAIBridgeUserPromptsByInterceptionIDs(ctx, i } // ListAuthorizedAIBridgeInterceptions mocks base method. -func (m *MockStore) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]database.AIBridgeInterception, error) { +func (m *MockStore) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]database.ListAIBridgeInterceptionsRow, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListAuthorizedAIBridgeInterceptions", ctx, arg, prepared) - ret0, _ := ret[0].([]database.AIBridgeInterception) + ret0, _ := ret[0].([]database.ListAIBridgeInterceptionsRow) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 323b595c10..a01ddb8d1f 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -3511,6 +3511,9 @@ COMMENT ON TRIGGER workspace_agent_name_unique_trigger ON workspace_agents IS 'U the uniqueness requirement. A trigger allows us to enforce uniqueness going forward without requiring a migration to clean up historical data.'; +ALTER TABLE ONLY aibridge_interceptions + ADD CONSTRAINT aibridge_interceptions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id); + ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 4c629797d1..6737275dd3 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -6,6 +6,7 @@ type ForeignKeyConstraint string // ForeignKeyConstraint enums. const ( + ForeignKeyAibridgeInterceptionsInitiatorID ForeignKeyConstraint = "aibridge_interceptions_initiator_id_fkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id); ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ForeignKeyConnectionLogsOrganizationID ForeignKeyConstraint = "connection_logs_organization_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; ForeignKeyConnectionLogsWorkspaceID ForeignKeyConstraint = "connection_logs_workspace_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000385_aibridge_fks.down.sql b/coderd/database/migrations/000385_aibridge_fks.down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/coderd/database/migrations/000385_aibridge_fks.up.sql b/coderd/database/migrations/000385_aibridge_fks.up.sql new file mode 100644 index 0000000000..cc9cfbfdd9 --- /dev/null +++ b/coderd/database/migrations/000385_aibridge_fks.up.sql @@ -0,0 +1,49 @@ +-- We didn't add an FK as a premature optimization when the aibridge tables were +-- added, but for the initiator_id it's pretty annoying not having a strong +-- reference. +-- +-- Since the aibridge feature is still in early access, we're going to add the +-- FK and drop any rows that violate it (which should be none). This isn't a +-- very efficient migration, but since the feature is behind an experimental +-- flag, it shouldn't have any impact on deployments that aren't using the +-- feature. + +-- Step 1: Add FK without validating it +ALTER TABLE aibridge_interceptions + ADD CONSTRAINT aibridge_interceptions_initiator_id_fkey + FOREIGN KEY (initiator_id) + REFERENCES users(id) + -- We can't: + -- - Cascade delete because this is an auditing feature, and it also + -- wouldn't delete related aibridge rows since we don't FK them. + -- - Set null because you can't correlate to the original user ID if the + -- user somehow gets deleted. + -- + -- So we just use the default and don't do anything. This will result in a + -- deferred constraint violation error when the user is deleted. + -- + -- In Coder, we don't delete user rows ever, so this should never happen + -- unless an admin manually deletes a user with SQL. + ON DELETE NO ACTION + -- Delay validation of existing data until after we've dropped rows that + -- violate the FK. + NOT VALID; + +-- Step 2: Drop existing interceptions that violate the FK. +DELETE FROM aibridge_interceptions +WHERE initiator_id NOT IN (SELECT id FROM users); + +-- Step 3: Drop existing rows from other tables that no longer have a valid +-- interception in the database. +DELETE FROM aibridge_token_usages +WHERE interception_id NOT IN (SELECT id FROM aibridge_interceptions); + +DELETE FROM aibridge_user_prompts +WHERE interception_id NOT IN (SELECT id FROM aibridge_interceptions); + +DELETE FROM aibridge_tool_usages +WHERE interception_id NOT IN (SELECT id FROM aibridge_interceptions); + +-- Step 4: Validate the FK +ALTER TABLE aibridge_interceptions + VALIDATE CONSTRAINT aibridge_interceptions_initiator_id_fkey; diff --git a/coderd/database/migrations/testdata/fixtures/000370_aibridge.up.sql b/coderd/database/migrations/testdata/fixtures/000370_aibridge.up.sql index 0a66555eea..ef49405ae6 100644 --- a/coderd/database/migrations/testdata/fixtures/000370_aibridge.up.sql +++ b/coderd/database/migrations/testdata/fixtures/000370_aibridge.up.sql @@ -8,7 +8,7 @@ INSERT INTO ) VALUES ( 'be003e1e-b38f-43bf-847d-928074dd0aa8', - '30095c71-380b-457a-8995-97b8ee6e5307', + '30095c71-380b-457a-8995-97b8ee6e5307', -- admin@coder.com, from 000022_initial_v0.6.6.up.sql 'openai', 'gpt-5', '2025-09-15 12:45:13.921148+00' @@ -77,3 +77,82 @@ VALUES ( '{}', '2025-09-15 12:45:21.674335+00' ); + +-- For a later migration, we'll add an invalid interception without a valid +-- initiator_id. +INSERT INTO + aibridge_interceptions ( + id, + initiator_id, + provider, + model, + started_at + ) +VALUES ( + 'c6d29c6e-26a3-4137-bb2e-9dfeef3c1c26', + 'cab8d56a-8922-4999-81a9-046b43ac1312', -- user does not exist + 'openai', + 'gpt-5', + '2025-09-15 12:45:13.921148+00' + ); +INSERT INTO + aibridge_token_usages ( + id, + interception_id, + provider_response_id, + input_tokens, + output_tokens, + metadata, + created_at + ) +VALUES ( + '5650db6c-0b7c-49e3-bb26-9b2ba0107e11', + 'c6d29c6e-26a3-4137-bb2e-9dfeef3c1c26', + 'chatcmpl-CG2s28QlpKIoooUtXuLTmGbdtyS1k', + 10950, + 118, + '{}', + '2025-09-15 12:45:21.674413+00' + ); +INSERT INTO + aibridge_user_prompts ( + id, + interception_id, + provider_response_id, + prompt, + metadata, + created_at + ) +VALUES ( + '1e76cb5b-7c34-4160-b604-a4256f856169', + 'c6d29c6e-26a3-4137-bb2e-9dfeef3c1c26', + 'chatcmpl-CG2s28QlpKIoooUtXuLTmGbdtyS1k', + 'how many workspaces do i have', + '{}', + '2025-09-15 12:45:21.674335+00' + ); +INSERT INTO + aibridge_tool_usages ( + id, + interception_id, + provider_response_id, + tool, + server_url, + input, + injected, + invocation_error, + metadata, + created_at + ) +VALUES ( + '351b440f-d605-4f37-8ceb-011f0377b695', + 'c6d29c6e-26a3-4137-bb2e-9dfeef3c1c26', + 'chatcmpl-CG2s28QlpKIoooUtXuLTmGbdtyS1k', + 'coder_list_workspaces', + 'http://localhost:3000/api/experimental/mcp/http', + '{}', + true, + NULL, + '{}', + '2025-09-15 12:45:21.674413+00' + ); diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 654dfb8b37..1400893a26 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -763,11 +763,11 @@ func (q *sqlQuerier) CountAuthorizedConnectionLogs(ctx context.Context, arg Coun } type aibridgeQuerier interface { - ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]AIBridgeInterception, error) + ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]ListAIBridgeInterceptionsRow, error) CountAuthorizedAIBridgeInterceptions(ctx context.Context, arg CountAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) (int64, error) } -func (q *sqlQuerier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]AIBridgeInterception, error) { +func (q *sqlQuerier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]ListAIBridgeInterceptionsRow, error) { authorizedFilter, err := prepared.CompileToSQL(ctx, regosql.ConvertConfig{ VariableConverter: regosql.AIBridgeInterceptionConverter(), }) @@ -794,16 +794,20 @@ func (q *sqlQuerier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, ar return nil, err } defer rows.Close() - var items []AIBridgeInterception + var items []ListAIBridgeInterceptionsRow for rows.Next() { - var i AIBridgeInterception + var i ListAIBridgeInterceptionsRow if err := rows.Scan( - &i.ID, - &i.InitiatorID, - &i.Provider, - &i.Model, - &i.StartedAt, - &i.Metadata, + &i.AIBridgeInterception.ID, + &i.AIBridgeInterception.InitiatorID, + &i.AIBridgeInterception.Provider, + &i.AIBridgeInterception.Model, + &i.AIBridgeInterception.StartedAt, + &i.AIBridgeInterception.Metadata, + &i.VisibleUser.ID, + &i.VisibleUser.Username, + &i.VisibleUser.Name, + &i.VisibleUser.AvatarURL, ); err != nil { return nil, err } diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 3e2d655f44..3a83275297 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -592,7 +592,7 @@ type sqlcQuerier interface { InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) - ListAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams) ([]AIBridgeInterception, error) + ListAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams) ([]ListAIBridgeInterceptionsRow, error) ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeTokenUsage, error) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeToolUsage, error) ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeUserPrompt, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index c0fd6eb64d..5b4cf7b21e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -528,9 +528,12 @@ func (q *sqlQuerier) InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIB const listAIBridgeInterceptions = `-- name: ListAIBridgeInterceptions :many SELECT - id, initiator_id, provider, model, started_at, metadata + aibridge_interceptions.id, aibridge_interceptions.initiator_id, aibridge_interceptions.provider, aibridge_interceptions.model, aibridge_interceptions.started_at, aibridge_interceptions.metadata, + visible_users.id, visible_users.username, visible_users.name, visible_users.avatar_url FROM aibridge_interceptions +JOIN + visible_users ON visible_users.id = aibridge_interceptions.initiator_id WHERE -- Filter by time frame CASE @@ -592,7 +595,12 @@ type ListAIBridgeInterceptionsParams struct { Limit int32 `db:"limit_" json:"limit_"` } -func (q *sqlQuerier) ListAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams) ([]AIBridgeInterception, error) { +type ListAIBridgeInterceptionsRow struct { + AIBridgeInterception AIBridgeInterception `db:"aibridge_interception" json:"aibridge_interception"` + VisibleUser VisibleUser `db:"visible_user" json:"visible_user"` +} + +func (q *sqlQuerier) ListAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams) ([]ListAIBridgeInterceptionsRow, error) { rows, err := q.db.QueryContext(ctx, listAIBridgeInterceptions, arg.StartedAfter, arg.StartedBefore, @@ -607,16 +615,20 @@ func (q *sqlQuerier) ListAIBridgeInterceptions(ctx context.Context, arg ListAIBr return nil, err } defer rows.Close() - var items []AIBridgeInterception + var items []ListAIBridgeInterceptionsRow for rows.Next() { - var i AIBridgeInterception + var i ListAIBridgeInterceptionsRow if err := rows.Scan( - &i.ID, - &i.InitiatorID, - &i.Provider, - &i.Model, - &i.StartedAt, - &i.Metadata, + &i.AIBridgeInterception.ID, + &i.AIBridgeInterception.InitiatorID, + &i.AIBridgeInterception.Provider, + &i.AIBridgeInterception.Model, + &i.AIBridgeInterception.StartedAt, + &i.AIBridgeInterception.Metadata, + &i.VisibleUser.ID, + &i.VisibleUser.Username, + &i.VisibleUser.Name, + &i.VisibleUser.AvatarURL, ); err != nil { return nil, err } diff --git a/coderd/database/queries/aibridge.sql b/coderd/database/queries/aibridge.sql index 32e0246447..3a3b63b848 100644 --- a/coderd/database/queries/aibridge.sql +++ b/coderd/database/queries/aibridge.sql @@ -111,9 +111,12 @@ WHERE -- name: ListAIBridgeInterceptions :many SELECT - * + sqlc.embed(aibridge_interceptions), + sqlc.embed(visible_users) FROM aibridge_interceptions +JOIN + visible_users ON visible_users.id = aibridge_interceptions.initiator_id WHERE -- Filter by time frame CASE diff --git a/coderd/templateversions.go b/coderd/templateversions.go index 17a4d9b451..2e959702fb 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -1958,6 +1958,7 @@ func convertTemplateVersion(version database.TemplateVersion, job codersdk.Provi CreatedBy: codersdk.MinimalUser{ ID: version.CreatedBy, Username: version.CreatedByUsername, + Name: version.CreatedByName, AvatarURL: version.CreatedByAvatarURL, }, Archived: version.Archived, diff --git a/codersdk/aibridge.go b/codersdk/aibridge.go index 05f1e1a6fc..0a6e85c947 100644 --- a/codersdk/aibridge.go +++ b/codersdk/aibridge.go @@ -13,7 +13,7 @@ import ( type AIBridgeInterception struct { ID uuid.UUID `json:"id" format:"uuid"` - InitiatorID uuid.UUID `json:"initiator_id" format:"uuid"` + Initiator MinimalUser `json:"initiator"` Provider string `json:"provider"` Model string `json:"model"` Metadata map[string]any `json:"metadata"` diff --git a/codersdk/users.go b/codersdk/users.go index 266ef8c598..44464f9476 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -41,6 +41,7 @@ type UsersRequest struct { type MinimalUser struct { ID uuid.UUID `json:"id" validate:"required" table:"id" format:"uuid"` Username string `json:"username" validate:"required" table:"username,default_sort"` + Name string `json:"name,omitempty" table:"name"` AvatarURL string `json:"avatar_url,omitempty" format:"uri"` } @@ -50,7 +51,6 @@ type MinimalUser struct { // required by the frontend. type ReducedUser struct { MinimalUser `table:"m,recursive_inline"` - Name string `json:"name,omitempty"` Email string `json:"email" validate:"required" table:"email" format:"email"` CreatedAt time.Time `json:"created_at" validate:"required" table:"created at" format:"date-time"` UpdatedAt time.Time `json:"updated_at" table:"updated at" format:"date-time"` diff --git a/docs/reference/api/aibridge.md b/docs/reference/api/aibridge.md index 35af663993..54f408ec23 100644 --- a/docs/reference/api/aibridge.md +++ b/docs/reference/api/aibridge.md @@ -31,7 +31,12 @@ curl -X GET http://coder-server:8080/api/v2/api/experimental/aibridge/intercepti "results": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", + "initiator": { + "avatar_url": "http://example.com", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", + "username": "string" + }, "metadata": { "property1": null, "property2": null diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 654dc0f45b..a36199a6f6 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -436,7 +436,12 @@ ```json { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", + "initiator": { + "avatar_url": "http://example.com", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", + "username": "string" + }, "metadata": { "property1": null, "property2": null @@ -496,7 +501,7 @@ | Name | Type | Required | Restrictions | Description | |--------------------|---------------------------------------------------------------------|----------|--------------|-------------| | `id` | string | false | | | -| `initiator_id` | string | false | | | +| `initiator` | [codersdk.MinimalUser](#codersdkminimaluser) | false | | | | `metadata` | object | false | | | | » `[any property]` | any | false | | | | `model` | string | false | | | @@ -513,7 +518,12 @@ "results": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", + "initiator": { + "avatar_url": "http://example.com", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", + "username": "string" + }, "metadata": { "property1": null, "property2": null @@ -4947,6 +4957,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit| { "avatar_url": "http://example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", "username": "string" } ``` @@ -4957,6 +4968,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit| |--------------|--------|----------|--------------|-------------| | `avatar_url` | string | false | | | | `id` | string | true | | | +| `name` | string | false | | | | `username` | string | true | | | ## codersdk.NotificationMethodsResponse @@ -8558,6 +8570,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W "created_by": { "avatar_url": "http://example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", "username": "string" }, "has_external_agent": true, @@ -10129,6 +10142,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| { "avatar_url": "http://example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", "role": "admin", "username": "string" } @@ -11769,6 +11783,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| { "avatar_url": "http://example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", "role": "admin", "username": "string" } @@ -11780,6 +11795,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| |--------------|--------------------------------------------------|----------|--------------|-------------| | `avatar_url` | string | false | | | | `id` | string | true | | | +| `name` | string | false | | | | `role` | [codersdk.WorkspaceRole](#codersdkworkspacerole) | false | | | | `username` | string | true | | | diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index 9d69949ac7..2c516f4788 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -460,6 +460,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "created_by": { "avatar_url": "http://example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", "username": "string" }, "has_external_agent": true, @@ -561,6 +562,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "created_by": { "avatar_url": "http://example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", "username": "string" }, "has_external_agent": true, @@ -686,6 +688,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "created_by": { "avatar_url": "http://example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", "username": "string" }, "has_external_agent": true, @@ -1294,6 +1297,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions \ "created_by": { "avatar_url": "http://example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", "username": "string" }, "has_external_agent": true, @@ -1374,6 +1378,7 @@ Status Code **200** | `» created_by` | [codersdk.MinimalUser](schemas.md#codersdkminimaluser) | false | | | | `»» avatar_url` | string(uri) | false | | | | `»» id` | string(uuid) | true | | | +| `»» name` | string | false | | | | `»» username` | string | true | | | | `» has_external_agent` | boolean | false | | | | `» id` | string(uuid) | false | | | @@ -1579,6 +1584,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions/{templ "created_by": { "avatar_url": "http://example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", "username": "string" }, "has_external_agent": true, @@ -1659,6 +1665,7 @@ Status Code **200** | `» created_by` | [codersdk.MinimalUser](schemas.md#codersdkminimaluser) | false | | | | `»» avatar_url` | string(uri) | false | | | | `»» id` | string(uuid) | true | | | +| `»» name` | string | false | | | | `»» username` | string | true | | | | `» has_external_agent` | boolean | false | | | | `» id` | string(uuid) | false | | | @@ -1754,6 +1761,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion} \ "created_by": { "avatar_url": "http://example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", "username": "string" }, "has_external_agent": true, @@ -1864,6 +1872,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templateversions/{templateversion} "created_by": { "avatar_url": "http://example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", "username": "string" }, "has_external_agent": true, diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index e2a95613ed..86c04f79c4 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1588,6 +1588,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/acl \ { "avatar_url": "http://example.com", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", "role": "admin", "username": "string" } diff --git a/docs/reference/cli/users_list.md b/docs/reference/cli/users_list.md index 93122e7741..7217a8267b 100644 --- a/docs/reference/cli/users_list.md +++ b/docs/reference/cli/users_list.md @@ -25,10 +25,10 @@ Filter users by their GitHub user ID. ### -c, --column -| | | -|---------|--------------------------------------------------------------------| -| Type | [id\|username\|email\|created at\|updated at\|status] | -| Default | username,email,created at,status | +| | | +|---------|--------------------------------------------------------------------------| +| Type | [id\|username\|name\|email\|created at\|updated at\|status] | +| Default | username,email,created at,status | Columns to display in table output. diff --git a/enterprise/coderd/aibridge.go b/enterprise/coderd/aibridge.go index 878e8ac741..2c28745c95 100644 --- a/enterprise/coderd/aibridge.go +++ b/enterprise/coderd/aibridge.go @@ -75,7 +75,7 @@ func (api *API) aiBridgeListInterceptions(rw http.ResponseWriter, r *http.Reques var ( count int64 - rows []database.AIBridgeInterception + rows []database.ListAIBridgeInterceptionsRow ) err := api.Database.InTx(func(db database.Store) error { // Ensure the after_id interception exists and is visible to the user. @@ -132,10 +132,10 @@ func (api *API) aiBridgeListInterceptions(rw http.ResponseWriter, r *http.Reques }) } -func populatedAndConvertAIBridgeInterceptions(ctx context.Context, db database.Store, dbInterceptions []database.AIBridgeInterception) ([]codersdk.AIBridgeInterception, error) { +func populatedAndConvertAIBridgeInterceptions(ctx context.Context, db database.Store, dbInterceptions []database.ListAIBridgeInterceptionsRow) ([]codersdk.AIBridgeInterception, error) { ids := make([]uuid.UUID, len(dbInterceptions)) for i, row := range dbInterceptions { - ids[i] = row.ID + ids[i] = row.AIBridgeInterception.ID } //nolint:gocritic // This is a system function until we implement a join for aibridge interceptions. AIBridge interception subresources use the same authorization call as their parent. @@ -170,7 +170,13 @@ func populatedAndConvertAIBridgeInterceptions(ctx context.Context, db database.S items := make([]codersdk.AIBridgeInterception, len(dbInterceptions)) for i, row := range dbInterceptions { - items[i] = db2sdk.AIBridgeInterception(row, tokenUsagesMap[row.ID], userPromptsMap[row.ID], toolUsagesMap[row.ID]) + items[i] = db2sdk.AIBridgeInterception( + row.AIBridgeInterception, + row.VisibleUser, + tokenUsagesMap[row.AIBridgeInterception.ID], + userPromptsMap[row.AIBridgeInterception.ID], + toolUsagesMap[row.AIBridgeInterception.ID], + ) } return items, nil diff --git a/enterprise/coderd/aibridge_test.go b/enterprise/coderd/aibridge_test.go index 4d8cac139d..232cfd7072 100644 --- a/enterprise/coderd/aibridge_test.go +++ b/enterprise/coderd/aibridge_test.go @@ -72,7 +72,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) dv.Experiments = []string{string(codersdk.ExperimentAIBridge)} - client, db, _ := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + client, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, }, @@ -85,10 +85,28 @@ func TestAIBridgeListInterceptions(t *testing.T) { experimentalClient := codersdk.NewExperimentalClient(client) ctx := testutil.Context(t, testutil.WaitLong) + user1, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + user1Visible := database.VisibleUser{ + ID: user1.ID, + Username: user1.Username, + Name: user1.Name, + AvatarURL: user1.AvatarURL, + } + + _, user2 := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID) + user2Visible := database.VisibleUser{ + ID: user2.ID, + Username: user2.Username, + Name: user2.Name, + AvatarURL: user2.AvatarURL, + } + // Insert a bunch of test data. now := dbtime.Now() i1 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ - StartedAt: now.Add(-time.Hour), + InitiatorID: user1.ID, + StartedAt: now.Add(-time.Hour), }) i1tok1 := dbgen.AIBridgeTokenUsage(t, db, database.InsertAIBridgeTokenUsageParams{ InterceptionID: i1.ID, @@ -115,14 +133,15 @@ func TestAIBridgeListInterceptions(t *testing.T) { CreatedAt: now.Add(-time.Minute), }) i2 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ - StartedAt: now, + InitiatorID: user2.ID, + StartedAt: now, }) // Convert to SDK types for response comparison. // You may notice that the ordering of the inner arrays are ASC, this is // intentional. - i1SDK := db2sdk.AIBridgeInterception(i1, []database.AIBridgeTokenUsage{i1tok2, i1tok1}, []database.AIBridgeUserPrompt{i1up2, i1up1}, []database.AIBridgeToolUsage{i1tool2, i1tool1}) - i2SDK := db2sdk.AIBridgeInterception(i2, nil, nil, nil) + i1SDK := db2sdk.AIBridgeInterception(i1, user1Visible, []database.AIBridgeTokenUsage{i1tok2, i1tok1}, []database.AIBridgeUserPrompt{i1up2, i1up1}, []database.AIBridgeToolUsage{i1tool2, i1tool1}) + i2SDK := db2sdk.AIBridgeInterception(i2, user2Visible, nil, nil, nil) res, err := experimentalClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{}) require.NoError(t, err) @@ -158,7 +177,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { dv := coderdtest.DeploymentValues(t) dv.Experiments = []string{string(codersdk.ExperimentAIBridge)} - client, db, _ := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + client, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, }, @@ -178,8 +197,9 @@ func TestAIBridgeListInterceptions(t *testing.T) { now := dbtime.Now() for i := range 10 { interception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ - ID: uuid.UUID{byte(i)}, - StartedAt: now, + ID: uuid.UUID{byte(i)}, + InitiatorID: firstUser.UserID, + StartedAt: now, }) allInterceptionIDs = append(allInterceptionIDs, interception.ID) } @@ -190,8 +210,9 @@ func TestAIBridgeListInterceptions(t *testing.T) { require.NoError(t, err) randomOffsetDur := time.Duration(randomOffset) * time.Second interception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ - ID: uuid.UUID{byte(i + 10)}, - StartedAt: now.Add(randomOffsetDur), + ID: uuid.UUID{byte(i + 10)}, + InitiatorID: firstUser.UserID, + StartedAt: now.Add(randomOffsetDur), }) allInterceptionIDs = append(allInterceptionIDs, interception.ID) } @@ -329,27 +350,44 @@ func TestAIBridgeListInterceptions(t *testing.T) { }, }) experimentalClient := codersdk.NewExperimentalClient(client) - _, secondUser := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID) + ctx := testutil.Context(t, testutil.WaitLong) + + user1, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + user1Visible := database.VisibleUser{ + ID: user1.ID, + Username: user1.Username, + Name: user1.Name, + AvatarURL: user1.AvatarURL, + } + + _, user2 := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID) + user2Visible := database.VisibleUser{ + ID: user2.ID, + Username: user2.Username, + Name: user2.Name, + AvatarURL: user2.AvatarURL, + } // Insert a bunch of test data with varying filterable fields. now := dbtime.Now() i1 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ ID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), - InitiatorID: firstUser.UserID, + InitiatorID: user1.ID, Provider: "one", Model: "one", StartedAt: now, }) i2 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ ID: uuid.MustParse("00000000-0000-0000-0000-000000000002"), - InitiatorID: firstUser.UserID, + InitiatorID: user1.ID, Provider: "two", Model: "two", StartedAt: now.Add(-time.Hour), }) i3 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ ID: uuid.MustParse("00000000-0000-0000-0000-000000000003"), - InitiatorID: secondUser.ID, + InitiatorID: user2.ID, Provider: "three", Model: "three", StartedAt: now.Add(-2 * time.Hour), @@ -357,9 +395,9 @@ func TestAIBridgeListInterceptions(t *testing.T) { // Convert to SDK types for response comparison. We don't care about the // inner arrays for this test. - i1SDK := db2sdk.AIBridgeInterception(i1, nil, nil, nil) - i2SDK := db2sdk.AIBridgeInterception(i2, nil, nil, nil) - i3SDK := db2sdk.AIBridgeInterception(i3, nil, nil, nil) + i1SDK := db2sdk.AIBridgeInterception(i1, user1Visible, nil, nil, nil) + i2SDK := db2sdk.AIBridgeInterception(i2, user1Visible, nil, nil, nil) + i3SDK := db2sdk.AIBridgeInterception(i3, user2Visible, nil, nil, nil) cases := []struct { name string @@ -383,12 +421,12 @@ func TestAIBridgeListInterceptions(t *testing.T) { }, { name: "Initiator/UserID", - filter: codersdk.AIBridgeListInterceptionsFilter{Initiator: secondUser.ID.String()}, + filter: codersdk.AIBridgeListInterceptionsFilter{Initiator: user2.ID.String()}, want: []codersdk.AIBridgeInterception{i3SDK}, }, { name: "Initiator/Username", - filter: codersdk.AIBridgeListInterceptionsFilter{Initiator: secondUser.Username}, + filter: codersdk.AIBridgeListInterceptionsFilter{Initiator: user2.Username}, want: []codersdk.AIBridgeInterception{i3SDK}, }, { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 932f72a383..65b1d2d3e5 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -26,7 +26,7 @@ export interface AIBridgeConfig { // From codersdk/aibridge.go export interface AIBridgeInterception { readonly id: string; - readonly initiator_id: string; + readonly initiator: MinimalUser; readonly provider: string; readonly model: string; // empty interface{} type, falling back to unknown @@ -2638,6 +2638,7 @@ export interface MinimalOrganization { export interface MinimalUser { readonly id: string; readonly username: string; + readonly name?: string; readonly avatar_url?: string; } @@ -3951,7 +3952,6 @@ export interface RateLimitConfig { * required by the frontend. */ export interface ReducedUser extends MinimalUser { - readonly name?: string; readonly email: string; readonly created_at: string; readonly updated_at: string; diff --git a/vpn/client_test.go b/vpn/client_test.go index dd359afd0a..396f9b6762 100644 --- a/vpn/client_test.go +++ b/vpn/client_test.go @@ -102,6 +102,7 @@ func TestClient_WorkspaceUpdates(t *testing.T) { MinimalUser: codersdk.MinimalUser{ ID: userID, Username: "rootbeer", + Name: "Root Beer", }, }, })