feat: display the AI add-on column in the UI on the Users and Organization Members tables (#23291)

## Summary

Adds an entitlement-gated **AI add-on** column to both the **Users**
table and the **Organization Members** table. When
`ai_governance_user_limit` is entitled, each row shows whether the user
is consuming an AI seat.

## Background

The AI governance add-on tracks which users are consuming AI seats.
Admins need visibility into per-user seat consumption directly from the
user management tables. This change surfaces that information through
both the site-wide Users table and the per-organization Members table,
gated behind the `ai_governance_user_limit` entitlement so the column
only appears when the feature is licensed.

## Implementation

### Backend
- **New SQL query** `GetUserAISeatStates`
(`coderd/database/queries/aiseatstate.sql`) — returns user IDs consuming
an AI seat, derived from:
  - Users with entries in `aibridge_interceptions` (AI Bridge usage)
- Users who own workspaces with `has_ai_task = true` builds (AI Tasks
usage)
- **SDK types** — added `has_ai_seat: boolean` to `codersdk.User` and
`codersdk.OrganizationMemberWithUserData`
- **Handler wiring** — both the Users list endpoint (`coderd/users.go`)
and all Members endpoints (`coderd/members.go`) query AI seat state per
page of user IDs and populate the response field
- **dbauthz** — per-user `ActionRead` checks on `ResourceUserObject`

### Frontend
- **Shared `AISeatCell` component**
(`site/src/modules/users/AISeatCell.tsx`) — green `CircleCheck` for
consuming, gray `X` for non-consuming
- **`TableColumnHelpTooltip`** — extended with `ai_addon` variant with
tooltip: *"Users with access to AI features like AI Bridge, Boundary, or
Tasks who are actively consuming a seat."*
- **Column visibility** gated behind
`useFeatureVisibility().ai_governance_user_limit`

## Validation

- Backend: dbauthz full method suite (`TestMethodTestSuite`) passes
including new `GetUserAISeatStates` test
- Backend: `TestGetUsers`, `TestUsersFilter`, CLI golden file tests pass
- Frontend: 7/7 tests pass across `UsersPage.test.tsx` and
`OrganizationMembersPage.test.tsx` (column visibility gating both
directions)
- `go build ./coderd/...` compiles clean
- `pnpm --dir site run lint:types` passes
- `make gen` clean

## Risks

- **Pagination performance**: The AI seat query is scoped to the current
page's user IDs (not a full table scan), keeping it efficient for
paginated views.
- **Semantic scope**: The workspace-side AI seat derivation uses "any
build with `has_ai_task = true`" rather than "latest build only". If the
product intent is latest-build-only, this can be tightened in a
follow-up.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-6` • Thinking:
`xhigh` • Cost: `$27.25`_

<!-- mux-attribution: model=anthropic:claude-opus-4-6 thinking=xhigh
costs=27.25 -->
This commit is contained in:
Jaayden Halko
2026-03-26 10:36:40 +00:00
committed by GitHub
parent 09d2588e2a
commit 3fb7c6264f
38 changed files with 707 additions and 161 deletions
+1
View File
@@ -66,6 +66,7 @@ curl -X GET http://coder-server:8080/api/v2/audit?limit=0 \
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
+4
View File
@@ -262,6 +262,7 @@ curl -X GET http://coder-server:8080/api/v2/connectionlog?limit=0 \
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -3355,6 +3356,7 @@ curl -X PUT http://coder-server:8080/api/v2/scim/v2/Users/{id} \
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -3446,6 +3448,7 @@ curl -X PATCH http://coder-server:8080/api/v2/scim/v2/Users/{id} \
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -3842,6 +3845,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl \
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
+51 -46
View File
@@ -36,6 +36,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
"organization_id": "string"
}
],
"has_ai_seat": true,
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
@@ -68,28 +69,29 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
Status Code **200**
| Name | Type | Required | Restrictions | Description |
|------------------------|------------------------------------------------------|----------|--------------|-------------|
| `[array item]` | array | false | | |
| `» avatar_url` | string | false | | |
| `» created_at` | string(date-time) | false | | |
| `» email` | string | false | | |
| `» global_roles` | array | false | | |
| `»» display_name` | string | false | | |
| `»» name` | string | false | | |
| `»» organization_id` | string | false | | |
| `» is_service_account` | boolean | false | | |
| `» last_seen_at` | string(date-time) | false | | |
| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» roles` | array | false | | |
| `» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
| `» updated_at` | string(date-time) | false | | |
| `» user_created_at` | string(date-time) | false | | |
| `» user_id` | string(uuid) | false | | |
| `» user_updated_at` | string(date-time) | false | | |
| `» username` | string | false | | |
| Name | Type | Required | Restrictions | Description |
|------------------------|------------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» avatar_url` | string | false | | |
| `» created_at` | string(date-time) | false | | |
| `» email` | string | false | | |
| `» global_roles` | array | false | | |
| `»» display_name` | string | false | | |
| `»» name` | string | false | | |
| `»» organization_id` | string | false | | |
| `» has_ai_seat` | boolean | false | | Has ai seat intentionally omits omitempty so the API always includes the field, even when false. |
| `» is_service_account` | boolean | false | | |
| `» last_seen_at` | string(date-time) | false | | |
| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» roles` | array | false | | |
| `» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
| `» updated_at` | string(date-time) | false | | |
| `» user_created_at` | string(date-time) | false | | |
| `» user_id` | string(uuid) | false | | |
| `» user_updated_at` | string(date-time) | false | | |
| `» username` | string | false | | |
#### Enumerated Values
@@ -595,6 +597,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
"organization_id": "string"
}
],
"has_ai_seat": true,
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
@@ -802,6 +805,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/paginat
"organization_id": "string"
}
],
"has_ai_seat": true,
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
@@ -836,30 +840,31 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/paginat
Status Code **200**
| Name | Type | Required | Restrictions | Description |
|-------------------------|------------------------------------------------------|----------|--------------|-------------|
| `[array item]` | array | false | | |
| `» count` | integer | false | | |
| `» members` | array | false | | |
| `»» avatar_url` | string | false | | |
| `»» created_at` | string(date-time) | false | | |
| `»» email` | string | false | | |
| `»» global_roles` | array | false | | |
| `»»» display_name` | string | false | | |
| `»»» name` | string | false | | |
| `»»» organization_id` | string | false | | |
| `»» is_service_account` | boolean | false | | |
| `»» last_seen_at` | string(date-time) | false | | |
| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
| `»» name` | string | false | | |
| `»» organization_id` | string(uuid) | false | | |
| `»» roles` | array | false | | |
| `»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
| `»» updated_at` | string(date-time) | false | | |
| `»» user_created_at` | string(date-time) | false | | |
| `»» user_id` | string(uuid) | false | | |
| `»» user_updated_at` | string(date-time) | false | | |
| `»» username` | string | false | | |
| Name | Type | Required | Restrictions | Description |
|-------------------------|------------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» count` | integer | false | | |
| `» members` | array | false | | |
| `»» avatar_url` | string | false | | |
| `»» created_at` | string(date-time) | false | | |
| `»» email` | string | false | | |
| `»» global_roles` | array | false | | |
| `»»» display_name` | string | false | | |
| `»»» name` | string | false | | |
| `»»» organization_id` | string | false | | |
| `»» has_ai_seat` | boolean | false | | Has ai seat intentionally omits omitempty so the API always includes the field, even when false. |
| `»» is_service_account` | boolean | false | | |
| `»» last_seen_at` | string(date-time) | false | | |
| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
| `»» name` | string | false | | |
| `»» organization_id` | string(uuid) | false | | |
| `»» roles` | array | false | | |
| `»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
| `»» updated_at` | string(date-time) | false | | |
| `»» user_created_at` | string(date-time) | false | | |
| `»» user_id` | string(uuid) | false | | |
| `»» user_updated_at` | string(date-time) | false | | |
| `»» username` | string | false | | |
#### Enumerated Values
+65 -51
View File
@@ -1296,6 +1296,7 @@
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -1387,6 +1388,7 @@
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -1737,6 +1739,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -1813,6 +1816,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -1882,6 +1886,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -4524,6 +4529,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -6127,6 +6133,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
"organization_id": "string"
}
],
"has_ai_seat": true,
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
@@ -6150,24 +6157,25 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
### Properties
| Name | Type | Required | Restrictions | Description |
|----------------------|-------------------------------------------------|----------|--------------|-------------|
| `avatar_url` | string | false | | |
| `created_at` | string | false | | |
| `email` | string | false | | |
| `global_roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
| `is_service_account` | boolean | false | | |
| `last_seen_at` | string | false | | |
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
| `name` | string | false | | |
| `organization_id` | string | false | | |
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
| `updated_at` | string | false | | |
| `user_created_at` | string | false | | |
| `user_id` | string | false | | |
| `user_updated_at` | string | false | | |
| `username` | string | false | | |
| Name | Type | Required | Restrictions | Description |
|----------------------|-------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------|
| `avatar_url` | string | false | | |
| `created_at` | string | false | | |
| `email` | string | false | | |
| `global_roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
| `has_ai_seat` | boolean | false | | Has ai seat intentionally omits omitempty so the API always includes the field, even when false. |
| `is_service_account` | boolean | false | | |
| `last_seen_at` | string | false | | |
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
| `name` | string | false | | |
| `organization_id` | string | false | | |
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
| `updated_at` | string | false | | |
| `user_created_at` | string | false | | |
| `user_id` | string | false | | |
| `user_updated_at` | string | false | | |
| `username` | string | false | | |
#### Enumerated Values
@@ -6431,6 +6439,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
"organization_id": "string"
}
],
"has_ai_seat": true,
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
@@ -9070,6 +9079,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -9519,6 +9529,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -9544,23 +9555,24 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
### Properties
| Name | Type | Required | Restrictions | Description |
|----------------------|-------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------|
| `avatar_url` | string | false | | |
| `created_at` | string | true | | |
| `email` | string | true | | |
| `id` | string | true | | |
| `is_service_account` | boolean | false | | |
| `last_seen_at` | string | false | | |
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
| `name` | string | false | | |
| `organization_ids` | array of string | false | | |
| `role` | [codersdk.TemplateRole](#codersdktemplaterole) | false | | |
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
| `theme_preference` | string | false | | Deprecated: this value should be retrieved from `codersdk.UserPreferenceSettings` instead. |
| `updated_at` | string | false | | |
| `username` | string | true | | |
| Name | Type | Required | Restrictions | Description |
|----------------------|-------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------|
| `avatar_url` | string | false | | |
| `created_at` | string | true | | |
| `email` | string | true | | |
| `has_ai_seat` | boolean | false | | Has ai seat intentionally omits omitempty so the API always includes the field, even when false. |
| `id` | string | true | | |
| `is_service_account` | boolean | false | | |
| `last_seen_at` | string | false | | |
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
| `name` | string | false | | |
| `organization_ids` | array of string | false | | |
| `role` | [codersdk.TemplateRole](#codersdktemplaterole) | false | | |
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
| `theme_preference` | string | false | | Deprecated: this value should be retrieved from `codersdk.UserPreferenceSettings` instead. |
| `updated_at` | string | false | | |
| `username` | string | true | | |
#### Enumerated Values
@@ -10415,6 +10427,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -10439,22 +10452,23 @@ If the schedule is empty, the user will be updated to use the default schedule.|
### Properties
| Name | Type | Required | Restrictions | Description |
|----------------------|-------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------|
| `avatar_url` | string | false | | |
| `created_at` | string | true | | |
| `email` | string | true | | |
| `id` | string | true | | |
| `is_service_account` | boolean | false | | |
| `last_seen_at` | string | false | | |
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
| `name` | string | false | | |
| `organization_ids` | array of string | false | | |
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
| `theme_preference` | string | false | | Deprecated: this value should be retrieved from `codersdk.UserPreferenceSettings` instead. |
| `updated_at` | string | false | | |
| `username` | string | true | | |
| Name | Type | Required | Restrictions | Description |
|----------------------|-------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------|
| `avatar_url` | string | false | | |
| `created_at` | string | true | | |
| `email` | string | true | | |
| `has_ai_seat` | boolean | false | | Has ai seat intentionally omits omitempty so the API always includes the field, even when false. |
| `id` | string | true | | |
| `is_service_account` | boolean | false | | |
| `last_seen_at` | string | false | | |
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
| `name` | string | false | | |
| `organization_ids` | array of string | false | | |
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
| `theme_preference` | string | false | | Deprecated: this value should be retrieved from `codersdk.UserPreferenceSettings` instead. |
| `updated_at` | string | false | | |
| `username` | string | true | | |
#### Enumerated Values
+8
View File
@@ -34,6 +34,7 @@ curl -X GET http://coder-server:8080/api/v2/users \
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -112,6 +113,7 @@ curl -X POST http://coder-server:8080/api/v2/users \
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -455,6 +457,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user} \
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -1389,6 +1392,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/profile \
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -1447,6 +1451,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/roles \
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -1517,6 +1522,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/roles \
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -1575,6 +1581,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/activate \
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",
@@ -1633,6 +1640,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/suspend \
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"has_ai_seat": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_service_account": true,
"last_seen_at": "2019-08-24T14:15:22Z",