diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index c6d08e6928..d31e7468a7 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -15275,7 +15275,7 @@ const docTemplate = `{ "key_prefix": { "type": "string" }, - "last_used_at": { + "last_heartbeat_at": { "type": "string", "format": "date-time" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index d5d0f124fa..6c7ba2819e 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -13621,7 +13621,7 @@ "key_prefix": { "type": "string" }, - "last_used_at": { + "last_heartbeat_at": { "type": "string", "format": "date-time" }, diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 9c042ba16b..d6e5a27e77 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -7033,14 +7033,14 @@ func (q *querier) UpdateAIBridgeInterceptionEnded(ctx context.Context, params da return q.db.UpdateAIBridgeInterceptionEnded(ctx, params) } -// Records liveness for a key used in active DRPC session between coderd and standalone AI Gateway. -func (q *querier) UpdateAIGatewayKeyLastUsedAt(ctx context.Context, id uuid.UUID) (int64, error) { +// Records heartbeat liveness for a key used in active DRPC session between coderd and standalone AI Gateway. +func (q *querier) UpdateAIGatewayKeyLastHeartbeatAt(ctx context.Context, id uuid.UUID) (int64, error) { // Standalone AI Gateway has no Coder identity, so this runs under the // system actor recording connection liveness on the AI Gateway key. if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAIGatewayKey); err != nil { return 0, err } - return q.db.UpdateAIGatewayKeyLastUsedAt(ctx, id) + return q.db.UpdateAIGatewayKeyLastHeartbeatAt(ctx, id) } func (q *querier) UpdateAIProvider(ctx context.Context, arg database.UpdateAIProviderParams) (database.AIProvider, error) { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 903983486e..865d075f0b 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -6974,9 +6974,9 @@ func (s *MethodTestSuite) TestAIBridge() { dbm.EXPECT().GetAIGatewayKeyByHashedSecret(gomock.Any(), hashedSecret).Return(key, nil).AnyTimes() check.Args(hashedSecret).Asserts(rbac.ResourceAIGatewayKey, policy.ActionRead).Returns(key) })) - s.Run("UpdateAIGatewayKeyLastUsedAt", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + s.Run("UpdateAIGatewayKeyLastHeartbeatAt", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { id := uuid.New() - dbm.EXPECT().UpdateAIGatewayKeyLastUsedAt(gomock.Any(), id).Return(int64(1), nil).AnyTimes() + dbm.EXPECT().UpdateAIGatewayKeyLastHeartbeatAt(gomock.Any(), id).Return(int64(1), nil).AnyTimes() check.Args(id).Asserts(rbac.ResourceAIGatewayKey, policy.ActionUpdate).Returns(int64(1)) })) } diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 37825cd4a7..11e785f8ab 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -5050,11 +5050,11 @@ func (m queryMetricsStore) UpdateAIBridgeInterceptionEnded(ctx context.Context, return r0, r1 } -func (m queryMetricsStore) UpdateAIGatewayKeyLastUsedAt(ctx context.Context, arg uuid.UUID) (int64, error) { +func (m queryMetricsStore) UpdateAIGatewayKeyLastHeartbeatAt(ctx context.Context, id uuid.UUID) (int64, error) { start := time.Now() - r0, r1 := m.s.UpdateAIGatewayKeyLastUsedAt(ctx, arg) - m.queryLatencies.WithLabelValues("UpdateAIGatewayKeyLastUsedAt").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateAIGatewayKeyLastUsedAt").Inc() + r0, r1 := m.s.UpdateAIGatewayKeyLastHeartbeatAt(ctx, id) + m.queryLatencies.WithLabelValues("UpdateAIGatewayKeyLastHeartbeatAt").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateAIGatewayKeyLastHeartbeatAt").Inc() return r0, r1 } diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 5bee53da34..248c26d0a1 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -9518,19 +9518,19 @@ func (mr *MockStoreMockRecorder) UpdateAIBridgeInterceptionEnded(ctx, arg any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAIBridgeInterceptionEnded", reflect.TypeOf((*MockStore)(nil).UpdateAIBridgeInterceptionEnded), ctx, arg) } -// UpdateAIGatewayKeyLastUsedAt mocks base method. -func (m *MockStore) UpdateAIGatewayKeyLastUsedAt(ctx context.Context, id uuid.UUID) (int64, error) { +// UpdateAIGatewayKeyLastHeartbeatAt mocks base method. +func (m *MockStore) UpdateAIGatewayKeyLastHeartbeatAt(ctx context.Context, id uuid.UUID) (int64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateAIGatewayKeyLastUsedAt", ctx, id) + ret := m.ctrl.Call(m, "UpdateAIGatewayKeyLastHeartbeatAt", ctx, id) ret0, _ := ret[0].(int64) ret1, _ := ret[1].(error) return ret0, ret1 } -// UpdateAIGatewayKeyLastUsedAt indicates an expected call of UpdateAIGatewayKeyLastUsedAt. -func (mr *MockStoreMockRecorder) UpdateAIGatewayKeyLastUsedAt(ctx, id any) *gomock.Call { +// UpdateAIGatewayKeyLastHeartbeatAt indicates an expected call of UpdateAIGatewayKeyLastHeartbeatAt. +func (mr *MockStoreMockRecorder) UpdateAIGatewayKeyLastHeartbeatAt(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAIGatewayKeyLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateAIGatewayKeyLastUsedAt), ctx, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAIGatewayKeyLastHeartbeatAt", reflect.TypeOf((*MockStore)(nil).UpdateAIGatewayKeyLastHeartbeatAt), ctx, id) } // UpdateAIProvider mocks base method. diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index ba0fc82fa7..c9a8eb03b6 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1440,7 +1440,7 @@ CREATE TABLE ai_gateway_keys ( name text NOT NULL, secret_prefix character varying(11) NOT NULL, hashed_secret bytea NOT NULL, - last_used_at timestamp with time zone, + last_heartbeat_at timestamp with time zone, CONSTRAINT ai_gateway_keys_hashed_secret_check CHECK ((length(hashed_secret) > 0)), CONSTRAINT ai_gateway_keys_name_check CHECK (((length(name) <= 64) AND (name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'::text))), CONSTRAINT ai_gateway_keys_secret_prefix_check CHECK ((length((secret_prefix)::text) = 11)) diff --git a/coderd/database/migrations/000532_rename_ai_gateway_key_last_heartbeat_at.down.sql b/coderd/database/migrations/000532_rename_ai_gateway_key_last_heartbeat_at.down.sql new file mode 100644 index 0000000000..4257f129d1 --- /dev/null +++ b/coderd/database/migrations/000532_rename_ai_gateway_key_last_heartbeat_at.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE ai_gateway_keys + RENAME COLUMN last_heartbeat_at TO last_used_at; diff --git a/coderd/database/migrations/000532_rename_ai_gateway_key_last_heartbeat_at.up.sql b/coderd/database/migrations/000532_rename_ai_gateway_key_last_heartbeat_at.up.sql new file mode 100644 index 0000000000..a5c3cdf5c3 --- /dev/null +++ b/coderd/database/migrations/000532_rename_ai_gateway_key_last_heartbeat_at.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE ai_gateway_keys + RENAME COLUMN last_used_at TO last_heartbeat_at; diff --git a/coderd/database/models.go b/coderd/database/models.go index ff2df8eef0..0d08e74ede 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -4618,9 +4618,9 @@ type AIGatewayKey struct { CreatedAt time.Time `db:"created_at" json:"created_at"` Name string `db:"name" json:"name"` // Public token prefix for display and audit correlation. Auth uses hashed_secret. - SecretPrefix string `db:"secret_prefix" json:"secret_prefix"` - HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"` - LastUsedAt sql.NullTime `db:"last_used_at" json:"last_used_at"` + SecretPrefix string `db:"secret_prefix" json:"secret_prefix"` + HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"` + LastHeartbeatAt sql.NullTime `db:"last_heartbeat_at" json:"last_heartbeat_at"` } // Per-model token prices used by AI Bridge to compute interception cost. diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 0cbd03e639..d3b745e3f7 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1311,10 +1311,10 @@ type sqlcQuerier interface { UnpinChatByID(ctx context.Context, id uuid.UUID) error UnsetDefaultChatModelConfigs(ctx context.Context) error UpdateAIBridgeInterceptionEnded(ctx context.Context, arg UpdateAIBridgeInterceptionEndedParams) (AIBridgeInterception, error) - // Records liveness for an active Gateway DRPC session. The database sets the + // Records heartbeat liveness for an active Gateway DRPC session. The database sets the // timestamp so it stays consistent regardless of clock drift between API // replicas. - UpdateAIGatewayKeyLastUsedAt(ctx context.Context, id uuid.UUID) (int64, error) + UpdateAIGatewayKeyLastHeartbeatAt(ctx context.Context, id uuid.UUID) (int64, error) UpdateAIProvider(ctx context.Context, arg UpdateAIProviderParams) (AIProvider, error) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error UpdateChatACLByID(ctx context.Context, arg UpdateChatACLByIDParams) error diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index e3799ad592..b6f1ca0ecc 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -14938,9 +14938,9 @@ func TestAIGatewayKeysQueries(t *testing.T) { require.Len(t, keys, 2) requireAIGatewayKeysRow(t, keys[0], first, firstRow.CreatedAt) - require.False(t, keys[0].LastUsedAt.Valid) + require.False(t, keys[0].LastHeartbeatAt.Valid) requireAIGatewayKeysRow(t, keys[1], second, secondRow.CreatedAt) - require.False(t, keys[1].LastUsedAt.Valid) + require.False(t, keys[1].LastHeartbeatAt.Valid) deleted, err := db.DeleteAIGatewayKey(ctx, first.ID) require.NoError(t, err) @@ -14989,7 +14989,7 @@ func TestGetAIGatewayKeyByHashedSecret(t *testing.T) { require.Empty(t, key.ID) } -func TestUpdateAIGatewayKeyLastUsedAt(t *testing.T) { +func TestUpdateAIGatewayKeyLastHeartbeatAt(t *testing.T) { t.Parallel() db, _, sqlDB := dbtestutil.NewDBWithSQLDB(t) @@ -14999,42 +14999,42 @@ func TestUpdateAIGatewayKeyLastUsedAt(t *testing.T) { row, err := db.InsertAIGatewayKey(ctx, params) require.NoError(t, err) - // last_used_at starts NULL until a session records liveness. + // last_heartbeat_at starts NULL until a session records liveness. keys, err := db.ListAIGatewayKeys(ctx) require.NoError(t, err) require.Len(t, keys, 1) - require.False(t, keys[0].LastUsedAt.Valid) + require.False(t, keys[0].LastHeartbeatAt.Valid) - rows, err := db.UpdateAIGatewayKeyLastUsedAt(ctx, params.ID) + rows, err := db.UpdateAIGatewayKeyLastHeartbeatAt(ctx, params.ID) require.NoError(t, err) require.EqualValues(t, 1, rows) keys, err = db.ListAIGatewayKeys(ctx) require.NoError(t, err) require.Len(t, keys, 1) - require.True(t, keys[0].LastUsedAt.Valid) + require.True(t, keys[0].LastHeartbeatAt.Valid) // The database stamps the timestamp, so compare against the row's // DB-generated CreatedAt to avoid client clock skew. - require.False(t, keys[0].LastUsedAt.Time.Before(row.CreatedAt)) + require.False(t, keys[0].LastHeartbeatAt.Time.Before(row.CreatedAt)) // Updating a key that does not exist is a no-op, not an error. - rows, err = db.UpdateAIGatewayKeyLastUsedAt(ctx, uuid.New()) + rows, err = db.UpdateAIGatewayKeyLastHeartbeatAt(ctx, uuid.New()) require.NoError(t, err) require.EqualValues(t, 0, rows) - // Set last_used_at to old time to confirm the update overwrites it with a fresh timestamp. + // Set last_heartbeat_at to old time to confirm the update overwrites it with a fresh timestamp. staleTime := row.CreatedAt.Add(-time.Hour) - _, err = sqlDB.ExecContext(ctx, "UPDATE ai_gateway_keys SET last_used_at = $1 WHERE id = $2", staleTime, params.ID) + _, err = sqlDB.ExecContext(ctx, "UPDATE ai_gateway_keys SET last_heartbeat_at = $1 WHERE id = $2", staleTime, params.ID) require.NoError(t, err) - rows, err = db.UpdateAIGatewayKeyLastUsedAt(ctx, params.ID) + rows, err = db.UpdateAIGatewayKeyLastHeartbeatAt(ctx, params.ID) require.NoError(t, err) require.EqualValues(t, 1, rows) keys, err = db.ListAIGatewayKeys(ctx) require.NoError(t, err) require.Len(t, keys, 1) - require.True(t, keys[0].LastUsedAt.Time.After(staleTime)) + require.True(t, keys[0].LastHeartbeatAt.Time.After(staleTime)) } func aiGatewayKeyParams(name string, secretPrefix string) database.InsertAIGatewayKeyParams { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 860750cd4d..44c0b73b20 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -113,15 +113,15 @@ func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, arg ActivityBump const deleteAIGatewayKey = `-- name: DeleteAIGatewayKey :one DELETE FROM ai_gateway_keys WHERE id = $1 -RETURNING id, name, secret_prefix, created_at, last_used_at +RETURNING id, name, secret_prefix, created_at, last_heartbeat_at ` type DeleteAIGatewayKeyRow struct { - ID uuid.UUID `db:"id" json:"id"` - Name string `db:"name" json:"name"` - SecretPrefix string `db:"secret_prefix" json:"secret_prefix"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - LastUsedAt sql.NullTime `db:"last_used_at" json:"last_used_at"` + ID uuid.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + SecretPrefix string `db:"secret_prefix" json:"secret_prefix"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LastHeartbeatAt sql.NullTime `db:"last_heartbeat_at" json:"last_heartbeat_at"` } func (q *sqlQuerier) DeleteAIGatewayKey(ctx context.Context, id uuid.UUID) (DeleteAIGatewayKeyRow, error) { @@ -132,13 +132,13 @@ func (q *sqlQuerier) DeleteAIGatewayKey(ctx context.Context, id uuid.UUID) (Dele &i.Name, &i.SecretPrefix, &i.CreatedAt, - &i.LastUsedAt, + &i.LastHeartbeatAt, ) return i, err } const getAIGatewayKeyByHashedSecret = `-- name: GetAIGatewayKeyByHashedSecret :one -SELECT id, created_at, name, secret_prefix, hashed_secret, last_used_at +SELECT id, created_at, name, secret_prefix, hashed_secret, last_heartbeat_at FROM ai_gateway_keys WHERE hashed_secret = $1 ` @@ -155,7 +155,7 @@ func (q *sqlQuerier) GetAIGatewayKeyByHashedSecret(ctx context.Context, hashedSe &i.Name, &i.SecretPrefix, &i.HashedSecret, - &i.LastUsedAt, + &i.LastHeartbeatAt, ) return i, err } @@ -198,17 +198,17 @@ func (q *sqlQuerier) InsertAIGatewayKey(ctx context.Context, arg InsertAIGateway } const listAIGatewayKeys = `-- name: ListAIGatewayKeys :many -SELECT id, name, secret_prefix, created_at, last_used_at +SELECT id, name, secret_prefix, created_at, last_heartbeat_at FROM ai_gateway_keys ORDER BY created_at ASC ` type ListAIGatewayKeysRow struct { - ID uuid.UUID `db:"id" json:"id"` - Name string `db:"name" json:"name"` - SecretPrefix string `db:"secret_prefix" json:"secret_prefix"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - LastUsedAt sql.NullTime `db:"last_used_at" json:"last_used_at"` + ID uuid.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + SecretPrefix string `db:"secret_prefix" json:"secret_prefix"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LastHeartbeatAt sql.NullTime `db:"last_heartbeat_at" json:"last_heartbeat_at"` } func (q *sqlQuerier) ListAIGatewayKeys(ctx context.Context) ([]ListAIGatewayKeysRow, error) { @@ -225,7 +225,7 @@ func (q *sqlQuerier) ListAIGatewayKeys(ctx context.Context) ([]ListAIGatewayKeys &i.Name, &i.SecretPrefix, &i.CreatedAt, - &i.LastUsedAt, + &i.LastHeartbeatAt, ); err != nil { return nil, err } @@ -240,17 +240,17 @@ func (q *sqlQuerier) ListAIGatewayKeys(ctx context.Context) ([]ListAIGatewayKeys return items, nil } -const updateAIGatewayKeyLastUsedAt = `-- name: UpdateAIGatewayKeyLastUsedAt :execrows +const updateAIGatewayKeyLastHeartbeatAt = `-- name: UpdateAIGatewayKeyLastHeartbeatAt :execrows UPDATE ai_gateway_keys -SET last_used_at = NOW() +SET last_heartbeat_at = NOW() WHERE id = $1 ` -// Records liveness for an active Gateway DRPC session. The database sets the +// Records heartbeat liveness for an active Gateway DRPC session. The database sets the // timestamp so it stays consistent regardless of clock drift between API // replicas. -func (q *sqlQuerier) UpdateAIGatewayKeyLastUsedAt(ctx context.Context, id uuid.UUID) (int64, error) { - result, err := q.db.ExecContext(ctx, updateAIGatewayKeyLastUsedAt, id) +func (q *sqlQuerier) UpdateAIGatewayKeyLastHeartbeatAt(ctx context.Context, id uuid.UUID) (int64, error) { + result, err := q.db.ExecContext(ctx, updateAIGatewayKeyLastHeartbeatAt, id) if err != nil { return 0, err } diff --git a/coderd/database/queries/ai_gateway_keys.sql b/coderd/database/queries/ai_gateway_keys.sql index ba74d97e78..635bcdc5ce 100644 --- a/coderd/database/queries/ai_gateway_keys.sql +++ b/coderd/database/queries/ai_gateway_keys.sql @@ -4,13 +4,13 @@ VALUES ($1, @name, $2, $3, NOW()) RETURNING id, name, secret_prefix, created_at; -- name: ListAIGatewayKeys :many -SELECT id, name, secret_prefix, created_at, last_used_at +SELECT id, name, secret_prefix, created_at, last_heartbeat_at FROM ai_gateway_keys ORDER BY created_at ASC; -- name: DeleteAIGatewayKey :one DELETE FROM ai_gateway_keys WHERE id = $1 -RETURNING id, name, secret_prefix, created_at, last_used_at; +RETURNING id, name, secret_prefix, created_at, last_heartbeat_at; -- name: GetAIGatewayKeyByHashedSecret :one -- Authenticates a standalone AI Gateway replica by its hashed key secret, @@ -20,10 +20,10 @@ SELECT * FROM ai_gateway_keys WHERE hashed_secret = $1; --- name: UpdateAIGatewayKeyLastUsedAt :execrows --- Records liveness for an active Gateway DRPC session. The database sets the +-- name: UpdateAIGatewayKeyLastHeartbeatAt :execrows +-- Records heartbeat liveness for an active Gateway DRPC session. The database sets the -- timestamp so it stays consistent regardless of clock drift between API -- replicas. UPDATE ai_gateway_keys -SET last_used_at = NOW() +SET last_heartbeat_at = NOW() WHERE id = $1; diff --git a/codersdk/aigatewaykeys.go b/codersdk/aigatewaykeys.go index 92aaee48d6..3bd0d46448 100644 --- a/codersdk/aigatewaykeys.go +++ b/codersdk/aigatewaykeys.go @@ -14,11 +14,11 @@ import ( // AIGatewayKey is a shared secret used by a standalone AI Gateway // to authenticate into coderd. type AIGatewayKey struct { - ID uuid.UUID `json:"id" table:"id" format:"uuid"` - Name string `json:"name" table:"name,default_sort"` - KeyPrefix string `json:"key_prefix" table:"key prefix"` - CreatedAt time.Time `json:"created_at" table:"created at" format:"date-time"` - LastUsedAt *time.Time `json:"last_used_at,omitempty" table:"last used at" format:"date-time"` + ID uuid.UUID `json:"id" table:"id" format:"uuid"` + Name string `json:"name" table:"name,default_sort"` + KeyPrefix string `json:"key_prefix" table:"key prefix"` + CreatedAt time.Time `json:"created_at" table:"created at" format:"date-time"` + LastHeartbeatAt *time.Time `json:"last_heartbeat_at,omitempty" table:"last heartbeat at" format:"date-time"` } // CreateAIGatewayKeyRequest requests a new AI Gateway key. diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index c30b7d9413..27556bca61 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -15,7 +15,7 @@ We track the following resources: | Resource | | | |-----------------------------------------------------------------|----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| AIGatewayKey
create, delete | |
FieldTracked
created_atfalse
hashed_secrettrue
idtrue
last_used_atfalse
nametrue
secret_prefixtrue
| +| AIGatewayKey
create, delete | |
FieldTracked
created_atfalse
hashed_secrettrue
idtrue
last_heartbeat_atfalse
nametrue
secret_prefixtrue
| | AIProvider
create, write, delete | |
FieldTracked
base_urltrue
created_atfalse
deletedtrue
display_nametrue
enabledtrue
idtrue
nametrue
settingstrue
settings_key_idfalse
typetrue
updated_atfalse
| | AIProviderKey
create, delete | |
FieldTracked
api_keytrue
api_key_key_idfalse
created_atfalse
idtrue
provider_idtrue
updated_atfalse
| | AISeatState
create | |
FieldTracked
first_used_attrue
last_event_descriptiontrue
last_event_typetrue
last_used_atfalse
updated_atfalse
user_idtrue
| diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 1bf3cda5c0..f631241229 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -201,7 +201,7 @@ curl -X GET http://coder-server:8080/api/v2/ai-gateway/keys \ "created_at": "2019-08-24T14:15:22Z", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_prefix": "string", - "last_used_at": "2019-08-24T14:15:22Z", + "last_heartbeat_at": "2019-08-24T14:15:22Z", "name": "string" } ] @@ -217,14 +217,14 @@ curl -X GET http://coder-server:8080/api/v2/ai-gateway/keys \ Status Code **200** -| Name | Type | Required | Restrictions | Description | -|------------------|-------------------|----------|--------------|-------------| -| `[array item]` | array | false | | | -| `» created_at` | string(date-time) | false | | | -| `» id` | string(uuid) | false | | | -| `» key_prefix` | string | false | | | -| `» last_used_at` | string(date-time) | false | | | -| `» name` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|-----------------------|-------------------|----------|--------------|-------------| +| `[array item]` | array | false | | | +| `» created_at` | string(date-time) | false | | | +| `» id` | string(uuid) | false | | | +| `» key_prefix` | string | false | | | +| `» last_heartbeat_at` | string(date-time) | false | | | +| `» name` | string | false | | | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 451f97b0ee..d640ba77df 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -990,20 +990,20 @@ "created_at": "2019-08-24T14:15:22Z", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_prefix": "string", - "last_used_at": "2019-08-24T14:15:22Z", + "last_heartbeat_at": "2019-08-24T14:15:22Z", "name": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|----------------|--------|----------|--------------|-------------| -| `created_at` | string | false | | | -| `id` | string | false | | | -| `key_prefix` | string | false | | | -| `last_used_at` | string | false | | | -| `name` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------|--------|----------|--------------|-------------| +| `created_at` | string | false | | | +| `id` | string | false | | | +| `key_prefix` | string | false | | | +| `last_heartbeat_at` | string | false | | | +| `name` | string | false | | | ## codersdk.AIProvider diff --git a/docs/reference/cli/ai-gateway_keys_list.md b/docs/reference/cli/ai-gateway_keys_list.md index 39f1ffcebe..babe66b416 100644 --- a/docs/reference/cli/ai-gateway_keys_list.md +++ b/docs/reference/cli/ai-gateway_keys_list.md @@ -17,10 +17,10 @@ coder ai-gateway keys list [flags] ### -c, --column -| | | -|---------|---------------------------------------------------------------| -| Type | [id\|name\|key prefix\|created at\|last used at] | -| Default | id,name,key prefix,last used at,created at | +| | | +|---------|--------------------------------------------------------------------| +| Type | [id\|name\|key prefix\|created at\|last heartbeat at] | +| Default | id,name,key prefix,last heartbeat at,created at | Columns to display in table output. diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index e412a2c2eb..e197a7782b 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -417,12 +417,12 @@ var auditableResourcesTypes = map[any]map[string]Action{ "updated_at": ActionIgnore, // Changes; not useful in a diff. }, &database.AIGatewayKey{}: { - "id": ActionTrack, - "name": ActionTrack, - "secret_prefix": ActionTrack, - "hashed_secret": ActionSecret, // Bearer token hash, never expose. - "created_at": ActionIgnore, // Implicit; not useful in a diff. - "last_used_at": ActionIgnore, // Bumped on every use. + "id": ActionTrack, + "name": ActionTrack, + "secret_prefix": ActionTrack, + "hashed_secret": ActionSecret, // Bearer token hash, never expose. + "created_at": ActionIgnore, // Implicit; not useful in a diff. + "last_heartbeat_at": ActionIgnore, // Bumped on every heartbeat. }, &database.TaskTable{}: { "id": ActionTrack, diff --git a/enterprise/cli/aigateway.go b/enterprise/cli/aigateway.go index 408391456b..2844ef43dc 100644 --- a/enterprise/cli/aigateway.go +++ b/enterprise/cli/aigateway.go @@ -75,7 +75,7 @@ func (r *RootCmd) aiGatewayKeysCreate() *serpent.Command { func (r *RootCmd) aiGatewayKeysList() *serpent.Command { formatter := cliui.NewOutputFormatter( - cliui.TableFormat([]codersdk.AIGatewayKey{}, []string{"id", "name", "key prefix", "last used at", "created at"}), + cliui.TableFormat([]codersdk.AIGatewayKey{}, []string{"id", "name", "key prefix", "last heartbeat at", "created at"}), cliui.JSONFormat(), ) diff --git a/enterprise/cli/testdata/coder_ai-gateway_keys_list_--help.golden b/enterprise/cli/testdata/coder_ai-gateway_keys_list_--help.golden index 043ac660f9..f922806ded 100644 --- a/enterprise/cli/testdata/coder_ai-gateway_keys_list_--help.golden +++ b/enterprise/cli/testdata/coder_ai-gateway_keys_list_--help.golden @@ -8,7 +8,7 @@ USAGE: Aliases: ls OPTIONS: - -c, --column [id|name|key prefix|created at|last used at] (default: id,name,key prefix,last used at,created at) + -c, --column [id|name|key prefix|created at|last heartbeat at] (default: id,name,key prefix,last heartbeat at,created at) Columns to display in table output. -o, --output table|json (default: table) diff --git a/enterprise/coderd/aibridgeserve.go b/enterprise/coderd/aibridgeserve.go index 283d4fc5f2..1bc639b058 100644 --- a/enterprise/coderd/aibridgeserve.go +++ b/enterprise/coderd/aibridgeserve.go @@ -26,9 +26,9 @@ import ( "github.com/coder/websocket" ) -// aiGatewayKeyLastUsedInterval defines how often an active DRPC session refreshes -// last_used_at for its authenticating key. -const aiGatewayKeyLastUsedInterval = 60 * time.Second +// aiGatewayKeyHeartbeatInterval defines how often an active DRPC session refreshes +// last_heartbeat_at for its authenticating key. +const aiGatewayKeyHeartbeatInterval = 60 * time.Second // aiGatewayServe upgrades the connection to a WebSocket and serves the DRPC // services (Recorder, MCPConfigurator, Authorizer) to a remote standalone AI @@ -128,8 +128,8 @@ func (api *API) aiGatewayServe(rw http.ResponseWriter, r *http.Request) { return } - if _, err := aiGatewayUpdateKeyLastUsed(connCtx, api, gatewayKey.ID); err != nil { - logger.Warn(connCtx, "update ai gateway key last used", slog.Error(err)) + if _, err := aiGatewayUpdateKeyLastHeartbeat(connCtx, api, gatewayKey.ID); err != nil { + logger.Warn(connCtx, "update ai gateway key last heartbeat", slog.Error(err)) } go aiGatewayTrackKeyUsage(connCtx, keyCtxCancel, api, gatewayKey.ID, logger) @@ -183,20 +183,20 @@ func (api *API) aiGatewayServe(rw http.ResponseWriter, r *http.Request) { _ = conn.Close(websocket.StatusGoingAway, "") } -// aiGatewayUpdateKeyLastUsed records liveness for keyID and returns whether +// aiGatewayUpdateKeyLastHeartbeat records liveness for keyID and returns whether // the key is still active. On error key is assumed to not be active. -func aiGatewayUpdateKeyLastUsed(ctx context.Context, api *API, keyID uuid.UUID) (bool, error) { +func aiGatewayUpdateKeyLastHeartbeat(ctx context.Context, api *API, keyID uuid.UUID) (bool, error) { // nolint:gocritic // Recording AI Gateway key liveness is an internal system write. - rows, err := api.Database.UpdateAIGatewayKeyLastUsedAt(dbauthz.AsSystemRestricted(ctx), keyID) + rows, err := api.Database.UpdateAIGatewayKeyLastHeartbeatAt(dbauthz.AsSystemRestricted(ctx), keyID) if err != nil { return false, err } return rows > 0, nil } -// aiGatewayTrackKeyUsage refreshes last_used_at for keyID on a fixed interval until ctx is canceled. +// aiGatewayTrackKeyUsage refreshes last_heartbeat_at for keyID on a fixed interval until ctx is canceled. func aiGatewayTrackKeyUsage(ctx context.Context, ctxCancel context.CancelFunc, api *API, keyID uuid.UUID, logger slog.Logger) { - ticker, done := api.NewTicker(aiGatewayKeyLastUsedInterval) + ticker, done := api.NewTicker(aiGatewayKeyHeartbeatInterval) defer done() consecutiveFailures := 0 @@ -207,7 +207,7 @@ func aiGatewayTrackKeyUsage(ctx context.Context, ctxCancel context.CancelFunc, a case <-ticker: } - active, err := aiGatewayUpdateKeyLastUsed(ctx, api, keyID) + active, err := aiGatewayUpdateKeyLastHeartbeat(ctx, api, keyID) if err == nil && !active { logger.Info(ctx, "ai gateway key no longer exists, closing connection") ctxCancel() @@ -223,15 +223,15 @@ func aiGatewayTrackKeyUsage(ctx context.Context, ctxCancel context.CancelFunc, a // First failure logged at Debug, next failures escalate to Warn. if consecutiveFailures&(consecutiveFailures-1) == 0 { if consecutiveFailures == 1 { - logger.Debug(ctx, "update ai gateway key last used", slog.Error(err), slog.F("consecutive_failures", consecutiveFailures)) + logger.Debug(ctx, "update ai gateway key last heartbeat", slog.Error(err), slog.F("consecutive_failures", consecutiveFailures)) } else { - logger.Warn(ctx, "update ai gateway key last used", slog.Error(err), slog.F("consecutive_failures", consecutiveFailures)) + logger.Warn(ctx, "update ai gateway key last heartbeat", slog.Error(err), slog.F("consecutive_failures", consecutiveFailures)) } } continue } if consecutiveFailures > 1 { - logger.Info(ctx, "ai gateway key last used update recovered", + logger.Info(ctx, "ai gateway key last heartbeat update recovered", slog.F("consecutive_failures", consecutiveFailures)) } consecutiveFailures = 0 diff --git a/enterprise/coderd/aibridgeserve_test.go b/enterprise/coderd/aibridgeserve_test.go index 96a37ddb9d..76f3ffe178 100644 --- a/enterprise/coderd/aibridgeserve_test.go +++ b/enterprise/coderd/aibridgeserve_test.go @@ -100,7 +100,7 @@ func TestAIGatewayServeSuccess(t *testing.T) { } for _, k := range keys { if k.ID == created.ID { - return k.LastUsedAt != nil + return k.LastHeartbeatAt != nil } } return false diff --git a/enterprise/coderd/aigatewaykeys.go b/enterprise/coderd/aigatewaykeys.go index 227d0930d1..30815f9bdc 100644 --- a/enterprise/coderd/aigatewaykeys.go +++ b/enterprise/coderd/aigatewaykeys.go @@ -186,27 +186,27 @@ func (api *API) deleteAIGatewayKey(rw http.ResponseWriter, r *http.Request) { } aReq.Old = database.AIGatewayKey{ - ID: deleted.ID, - Name: deleted.Name, - SecretPrefix: deleted.SecretPrefix, - CreatedAt: deleted.CreatedAt, - LastUsedAt: deleted.LastUsedAt, + ID: deleted.ID, + Name: deleted.Name, + SecretPrefix: deleted.SecretPrefix, + CreatedAt: deleted.CreatedAt, + LastHeartbeatAt: deleted.LastHeartbeatAt, } rw.WriteHeader(http.StatusNoContent) } func convertAIGatewayKey(row database.ListAIGatewayKeysRow) codersdk.AIGatewayKey { - var lastUsed *time.Time - if row.LastUsedAt.Valid { - t := row.LastUsedAt.Time - lastUsed = &t + var lastHeartbeat *time.Time + if row.LastHeartbeatAt.Valid { + t := row.LastHeartbeatAt.Time + lastHeartbeat = &t } return codersdk.AIGatewayKey{ - ID: row.ID, - Name: row.Name, - KeyPrefix: row.SecretPrefix, - CreatedAt: row.CreatedAt, - LastUsedAt: lastUsed, + ID: row.ID, + Name: row.Name, + KeyPrefix: row.SecretPrefix, + CreatedAt: row.CreatedAt, + LastHeartbeatAt: lastHeartbeat, } } diff --git a/enterprise/coderd/aigatewaykeys_test.go b/enterprise/coderd/aigatewaykeys_test.go index cc11ed271a..e2fc326f76 100644 --- a/enterprise/coderd/aigatewaykeys_test.go +++ b/enterprise/coderd/aigatewaykeys_test.go @@ -59,7 +59,7 @@ func TestAIGatewayKeys(t *testing.T) { require.Equal(t, created.ID, keys[0].ID) require.Equal(t, created.Name, keys[0].Name) require.Equal(t, created.KeyPrefix, keys[0].KeyPrefix) - require.Nil(t, keys[0].LastUsedAt) + require.Nil(t, keys[0].LastHeartbeatAt) require.NoError(t, ownerClient.DeleteAIGatewayKey(ctx, created.ID)) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 94e4c5eaf2..f7a2c67b74 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -263,7 +263,7 @@ export interface AIGatewayKey { readonly name: string; readonly key_prefix: string; readonly created_at: string; - readonly last_used_at?: string; + readonly last_heartbeat_at?: string; } // From codersdk/client.go diff --git a/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.tsx b/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.tsx index fbc184c483..8d26234b1d 100644 --- a/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.tsx +++ b/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPageView.tsx @@ -80,7 +80,7 @@ export const GatewayKeysPageView: FC = ({ Name Key prefix - Last used + Last heartbeat Created @@ -111,9 +111,9 @@ export const GatewayKeysPageView: FC = ({ - {key.last_used_at ? ( + {key.last_heartbeat_at ? ( - {relativeTime(new Date(key.last_used_at))} + {relativeTime(new Date(key.last_heartbeat_at))} ) : ( Never diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 3559e702eb..af0eb797c2 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -5622,7 +5622,7 @@ export const MockAIGatewayKeys: TypesGen.AIGatewayKey[] = [ name: "primary-gateway", key_prefix: "a1B2c3D4e5F", created_at: "2024-05-01T14:00:00Z", - last_used_at: "2024-05-20T09:30:00Z", + last_heartbeat_at: "2024-05-20T09:30:00Z", }, { id: "2d3f7a5b-9c4e-4a2b-8d6f-3b6c9e7f1a22",