feat: make database.Chat auditable (#24485)

Wire database.Chat into the audit system so chat lifecycle events
(creation, patches, etc.) produce audit log entries.

Part of CODAGT-200.

> 🤖
This commit is contained in:
Cian Johnston
2026-04-21 11:11:56 +01:00
committed by GitHub
parent 5f3effd839
commit c968a1f3a3
18 changed files with 271 additions and 28 deletions
+4 -2
View File
@@ -19757,7 +19757,8 @@ const docTemplate = `{
"workspace_agent",
"workspace_app",
"task",
"ai_seat"
"ai_seat",
"chat"
],
"x-enum-varnames": [
"ResourceTypeTemplate",
@@ -19786,7 +19787,8 @@ const docTemplate = `{
"ResourceTypeWorkspaceAgent",
"ResourceTypeWorkspaceApp",
"ResourceTypeTask",
"ResourceTypeAISeat"
"ResourceTypeAISeat",
"ResourceTypeChat"
]
},
"codersdk.Response": {
+4 -2
View File
@@ -18089,7 +18089,8 @@
"workspace_agent",
"workspace_app",
"task",
"ai_seat"
"ai_seat",
"chat"
],
"x-enum-varnames": [
"ResourceTypeTemplate",
@@ -18118,7 +18119,8 @@
"ResourceTypeWorkspaceAgent",
"ResourceTypeWorkspaceApp",
"ResourceTypeTask",
"ResourceTypeAISeat"
"ResourceTypeAISeat",
"ResourceTypeChat"
]
},
"codersdk.Response": {
+14
View File
@@ -435,6 +435,16 @@ func (api *API) auditLogIsResourceDeleted(ctx context.Context, alog database.Get
api.Logger.Error(ctx, "unable to fetch task", slog.Error(err))
}
return task.DeletedAt.Valid && task.DeletedAt.Time.Before(time.Now())
case database.ResourceTypeChat:
// Chats are hard-deleted, so a 404 means deleted.
_, err := api.Database.GetChatByID(ctx, alog.AuditLog.ResourceID)
if xerrors.Is(err, sql.ErrNoRows) {
return true
}
if err != nil {
api.Logger.Error(ctx, "unable to fetch chat", slog.Error(err))
}
return false
default:
return false
}
@@ -522,6 +532,10 @@ func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAudit
}
return fmt.Sprintf("/tasks/%s/%s", user.Username, task.ID)
case database.ResourceTypeChat:
// Chats are surfaced at /agents/{id}. They are owner-scoped but
// not username-scoped in the URL like workspaces or tasks.
return fmt.Sprintf("/agents/%s", alog.AuditLog.ResourceID)
default:
return ""
}
+2 -1
View File
@@ -33,7 +33,8 @@ type Auditable interface {
idpsync.GroupSyncSettings |
idpsync.RoleSyncSettings |
database.TaskTable |
database.AiSeatState
database.AiSeatState |
database.Chat
}
// Map is a map of changed fields in an audited resource. It maps field names to
+1 -1
View File
@@ -25,7 +25,7 @@ func BackgroundTaskFieldsBytes(ctx context.Context, logger slog.Logger, subsyste
wriBytes, err := json.Marshal(af)
if err != nil {
logger.Error(ctx, "marshal additional fields for dormancy audit", slog.Error(err))
logger.Error(ctx, "marshal additional fields for background audit", slog.Error(err))
return []byte("{}")
}
+10
View File
@@ -134,6 +134,8 @@ func ResourceTarget[T Auditable](tgt T) string {
return typed.Name
case database.AiSeatState:
return "AI Seat"
case database.Chat:
return typed.Title
default:
panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt))
}
@@ -200,6 +202,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
return typed.ID
case database.AiSeatState:
return typed.UserID
case database.Chat:
return typed.ID
default:
panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt))
}
@@ -257,6 +261,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
return database.ResourceTypeTask
case database.AiSeatState:
return database.ResourceTypeAiSeat
case database.Chat:
return database.ResourceTypeChat
default:
panic(fmt.Sprintf("unknown resource %T for ResourceType", typed))
}
@@ -317,6 +323,10 @@ func ResourceRequiresOrgID[T Auditable]() bool {
return true
case database.AiSeatState:
return false
case database.Chat:
// Chats always have a non-null organization_id (since
// migration 000467).
return true
default:
panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt))
}
+41
View File
@@ -1,13 +1,54 @@
package coderd
import (
"context"
"database/sql"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbmock"
)
func TestAuditLogIsResourceDeleted(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
err error
wantDeleted bool
}{
{name: "AnError", err: assert.AnError, wantDeleted: false},
{name: "NotAuthorized", err: dbauthz.NotAuthorizedError{}, wantDeleted: false},
{name: "NoError", err: nil, wantDeleted: false},
{name: "NoRows", err: sql.ErrNoRows, wantDeleted: true},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
db := dbmock.NewMockStore(ctrl)
chatID := uuid.New()
db.EXPECT().GetChatByID(gomock.Any(), chatID).Return(database.Chat{}, tc.err)
api := &API{
Options: &Options{Database: db, Logger: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})},
}
deleted := api.auditLogIsResourceDeleted(context.Background(), database.GetAuditLogsOffsetRow{
AuditLog: database.AuditLog{ResourceType: database.ResourceTypeChat, ResourceID: chatID},
})
require.Equal(t, tc.wantDeleted, deleted)
})
}
}
func TestAuditLogDescription(t *testing.T) {
t.Parallel()
testCases := []struct {
+2 -1
View File
@@ -525,7 +525,8 @@ CREATE TYPE resource_type AS ENUM (
'workspace_app',
'prebuilds_settings',
'task',
'ai_seat'
'ai_seat',
'chat'
);
CREATE TYPE shareable_workspace_owners AS ENUM (
@@ -0,0 +1,3 @@
-- Postgres does not support removing enum values, so down is a
-- no-op. Rolling back past this migration is not reversible at
-- the schema level.
@@ -0,0 +1 @@
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'chat';
+4 -1
View File
@@ -3205,6 +3205,7 @@ const (
ResourceTypePrebuildsSettings ResourceType = "prebuilds_settings"
ResourceTypeTask ResourceType = "task"
ResourceTypeAiSeat ResourceType = "ai_seat"
ResourceTypeChat ResourceType = "chat"
)
func (e *ResourceType) Scan(src interface{}) error {
@@ -3270,7 +3271,8 @@ func (e ResourceType) Valid() bool {
ResourceTypeWorkspaceApp,
ResourceTypePrebuildsSettings,
ResourceTypeTask,
ResourceTypeAiSeat:
ResourceTypeAiSeat,
ResourceTypeChat:
return true
}
return false
@@ -3305,6 +3307,7 @@ func AllResourceTypeValues() []ResourceType {
ResourceTypePrebuildsSettings,
ResourceTypeTask,
ResourceTypeAiSeat,
ResourceTypeChat,
}
}
+39
View File
@@ -538,6 +538,15 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
return
}
aReq, commitAudit := audit.InitRequest[database.Chat](rw, &audit.RequestParams{
Audit: *api.Auditor.Load(),
Log: api.Logger,
Request: r,
Action: database.AuditActionCreate,
OrganizationID: req.OrganizationID,
})
defer commitAudit()
// Validate organization membership.
if req.OrganizationID == uuid.Nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -720,6 +729,8 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
MCPServerIDs: mcpServerIDs,
Labels: labels,
DynamicTools: dynamicToolsJSON,
// IMPORTANT: users can only create root chats at the time of writing.
ParentChatID: uuid.NullUUID{},
})
if err != nil {
if maybeWriteLimitErr(ctx, rw, err) {
@@ -747,6 +758,16 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
return
}
aReq.New = chat
if chat.ParentChatID.Valid {
// Should not be possible. If we get here, something is very wrong. Bail.
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Developer error: ParentChatID got set somehow in api.postChats. This should never happen.",
})
return
}
// Link any user-uploaded files referenced in the initial
// message to this newly created chat (best-effort; cap
// enforced in SQL).
@@ -762,6 +783,7 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
})
return
}
aReq.New = chat
chatFiles := api.fetchChatFileMetadata(ctx, chat.ID)
response := db2sdk.Chat(chat, nil, chatFiles)
@@ -2040,6 +2062,16 @@ func (api *API) patchChat(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
chat := httpmw.ChatParam(r)
aReq, commitAudit := audit.InitRequest[database.Chat](rw, &audit.RequestParams{
Audit: *api.Auditor.Load(),
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
})
defer commitAudit()
aReq.Old = chat
aReq.UpdateOrganizationID(chat.OrganizationID)
var req codersdk.UpdateChatRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
@@ -2263,6 +2295,13 @@ func (api *API) patchChat(rw http.ResponseWriter, r *http.Request) {
chat = updatedChat
}
if refreshed, err := api.Database.GetChatByID(ctx, chat.ID); err == nil {
aReq.New = refreshed
} else {
aReq.New = chat // fallback
api.Logger.Error(ctx, "failed to refresh chat for audit", slog.F("chat_id", chat.ID), slog.Error(err))
}
rw.WriteHeader(http.StatusNoContent)
}
+107 -17
View File
@@ -23,6 +23,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
@@ -55,12 +56,16 @@ func chatDeploymentValues(t testing.TB) *codersdk.DeploymentValues {
return values
}
func newChatClient(t testing.TB) *codersdk.ExperimentalClient {
func newChatClient(t testing.TB, overrides ...func(*coderdtest.Options)) *codersdk.ExperimentalClient {
t.Helper()
client := coderdtest.New(t, &coderdtest.Options{
opts := &coderdtest.Options{
DeploymentValues: chatDeploymentValues(t),
})
}
for _, override := range overrides {
override(opts)
}
client := coderdtest.New(t, opts)
return codersdk.NewExperimentalClient(client)
}
@@ -76,12 +81,16 @@ func newChatClientWithDeploymentValues(
return codersdk.NewExperimentalClient(client)
}
func newChatClientWithDatabase(t testing.TB) (*codersdk.ExperimentalClient, database.Store) {
func newChatClientWithDatabase(t testing.TB, overrides ...func(*coderdtest.Options)) (*codersdk.ExperimentalClient, database.Store) {
t.Helper()
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
opts := &coderdtest.Options{
DeploymentValues: chatDeploymentValues(t),
})
}
for _, override := range overrides {
override(opts)
}
client, db := coderdtest.NewWithDatabase(t, opts)
return codersdk.NewExperimentalClient(client), db
}
@@ -210,7 +219,10 @@ func TestPostChats(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client := newChatClient(t)
mAudit := audit.NewMock()
client := newChatClient(t, func(opts *coderdtest.Options) {
opts.Auditor = mAudit
})
firstUser := coderdtest.CreateFirstUser(t, client.Client)
modelConfig := createChatModelConfig(t, client)
@@ -259,6 +271,12 @@ func TestPostChats(t *testing.T) {
}
}
require.True(t, foundUserMessage)
require.True(t, mAudit.Contains(t, database.AuditLog{
Action: database.AuditActionCreate,
ResourceType: database.ResourceTypeChat,
ResourceID: chat.ID,
UserID: member.ID,
}))
})
t.Run("MemberWithoutAgentsAccess", func(t *testing.T) {
@@ -3961,7 +3979,10 @@ func TestPatchChat(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client := newChatClient(t)
mAudit := audit.NewMock()
client := newChatClient(t, func(opts *coderdtest.Options) {
opts.Auditor = mAudit
})
firstUser := coderdtest.CreateFirstUser(t, client.Client)
_ = createChatModelConfig(t, client)
@@ -3973,13 +3994,22 @@ func TestPatchChat(t *testing.T) {
updated := getChat(ctx, t, client, chat.ID)
require.Equal(t, codersdk.ChatPlanModePlan, updated.PlanMode)
require.True(t, mAudit.Contains(t, database.AuditLog{
Action: database.AuditActionWrite,
ResourceType: database.ResourceTypeChat,
ResourceID: chat.ID,
UserID: firstUser.UserID,
}))
})
t.Run("Clear", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client := newChatClient(t)
mAudit := audit.NewMock()
client := newChatClient(t, func(opts *coderdtest.Options) {
opts.Auditor = mAudit
})
firstUser := coderdtest.CreateFirstUser(t, client.Client)
_ = createChatModelConfig(t, client)
@@ -3996,23 +4026,37 @@ func TestPatchChat(t *testing.T) {
updated := getChat(ctx, t, client, chat.ID)
require.Empty(t, updated.PlanMode)
require.True(t, mAudit.Contains(t, database.AuditLog{
Action: database.AuditActionWrite,
ResourceType: database.ResourceTypeChat,
ResourceID: chat.ID,
UserID: firstUser.UserID,
}))
})
t.Run("RejectsInvalidValue", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client := newChatClient(t)
mAudit := audit.NewMock()
client := newChatClient(t, func(opts *coderdtest.Options) {
opts.Auditor = mAudit
})
firstUser := coderdtest.CreateFirstUser(t, client.Client)
_ = createChatModelConfig(t, client)
chat := createChat(ctx, t, client, firstUser.OrganizationID, "invalid plan mode")
invalidPlanMode := codersdk.ChatPlanMode("invalid")
err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
PlanMode: &invalidPlanMode,
PlanMode: ptr.Ref(codersdk.ChatPlanMode("invalid")),
})
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
require.Equal(t, "Invalid plan_mode value.", sdkErr.Message)
require.True(t, mAudit.Contains(t, database.AuditLog{
Action: database.AuditActionWrite,
ResourceType: database.ResourceTypeChat,
ResourceID: chat.ID,
UserID: firstUser.UserID,
}))
})
})
@@ -4023,7 +4067,10 @@ func TestPatchChat(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, db := newChatClientWithDatabase(t)
mAudit := audit.NewMock()
client, db := newChatClientWithDatabase(t, func(opts *coderdtest.Options) {
opts.Auditor = mAudit
})
firstUser := coderdtest.CreateFirstUser(t, client.Client)
modelConfig := createChatModelConfig(t, client)
@@ -4049,13 +4096,22 @@ func TestPatchChat(t *testing.T) {
updated := getChat(ctx, t, client, chat.ID)
require.NotNil(t, updated.WorkspaceID)
require.Equal(t, workspaceBuild.Workspace.ID, *updated.WorkspaceID)
require.True(t, mAudit.Contains(t, database.AuditLog{
Action: database.AuditActionWrite,
ResourceType: database.ResourceTypeChat,
ResourceID: chat.ID,
UserID: firstUser.UserID,
}))
})
t.Run("WorkspaceNotFound", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, db := newChatClientWithDatabase(t)
mAudit := audit.NewMock()
client, db := newChatClientWithDatabase(t, func(opts *coderdtest.Options) {
opts.Auditor = mAudit
})
firstUser := coderdtest.CreateFirstUser(t, client.Client)
modelConfig := createChatModelConfig(t, client)
@@ -4074,13 +4130,22 @@ func TestPatchChat(t *testing.T) {
})
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
require.Equal(t, "Workspace not found or you do not have access to this resource", sdkErr.Message)
require.True(t, mAudit.Contains(t, database.AuditLog{
Action: database.AuditActionWrite,
ResourceType: database.ResourceTypeChat,
ResourceID: chat.ID,
UserID: firstUser.UserID,
}))
})
t.Run("RejectsCrossOrgWorkspaceBinding", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, db := newChatClientWithDatabase(t)
mAudit := audit.NewMock()
client, db := newChatClientWithDatabase(t, func(opts *coderdtest.Options) {
opts.Auditor = mAudit
})
firstUser := coderdtest.CreateFirstUser(t, client.Client)
modelConfig := createChatModelConfig(t, client)
@@ -4108,13 +4173,22 @@ func TestPatchChat(t *testing.T) {
})
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
require.Equal(t, "Workspace does not belong to this chat's organization.", sdkErr.Message)
require.True(t, mAudit.Contains(t, database.AuditLog{
Action: database.AuditActionWrite,
ResourceType: database.ResourceTypeChat,
ResourceID: chat.ID,
UserID: firstUser.UserID,
}))
})
t.Run("ClearWorkspaceBinding", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, db := newChatClientWithDatabase(t)
mAudit := audit.NewMock()
client, db := newChatClientWithDatabase(t, func(opts *coderdtest.Options) {
opts.Auditor = mAudit
})
firstUser := coderdtest.CreateFirstUser(t, client.Client)
modelConfig := createChatModelConfig(t, client)
@@ -4147,6 +4221,12 @@ func TestPatchChat(t *testing.T) {
require.Nil(t, updated.WorkspaceID)
require.Nil(t, updated.BuildID)
require.Nil(t, updated.AgentID)
require.True(t, mAudit.Contains(t, database.AuditLog{
Action: database.AuditActionWrite,
ResourceType: database.ResourceTypeChat,
ResourceID: chat.ID,
UserID: firstUser.UserID,
}))
})
})
@@ -4423,7 +4503,10 @@ func TestArchiveChat(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client := newChatClient(t)
mAudit := audit.NewMock()
client := newChatClient(t, func(o *coderdtest.Options) {
o.Auditor = mAudit
})
firstUser := coderdtest.CreateFirstUser(t, client.Client)
_ = createChatModelConfig(t, client)
@@ -4479,6 +4562,13 @@ func TestArchiveChat(t *testing.T) {
require.Len(t, archivedChats, 1)
require.Equal(t, chatToArchive.ID, archivedChats[0].ID)
require.True(t, archivedChats[0].Archived)
require.True(t, mAudit.Contains(t, database.AuditLog{
Action: database.AuditActionWrite,
ResourceType: database.ResourceTypeChat,
ResourceID: chatToArchive.ID,
UserID: firstUser.UserID,
}))
})
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
+3
View File
@@ -46,6 +46,7 @@ const (
ResourceTypeWorkspaceApp ResourceType = "workspace_app"
ResourceTypeTask ResourceType = "task"
ResourceTypeAISeat ResourceType = "ai_seat"
ResourceTypeChat ResourceType = "chat"
)
func (r ResourceType) FriendlyString() string {
@@ -106,6 +107,8 @@ func (r ResourceType) FriendlyString() string {
return "task"
case ResourceTypeAISeat:
return "ai seat"
case ResourceTypeChat:
return "chat"
default:
return "unknown"
}
+1
View File
@@ -20,6 +20,7 @@ We track the following resources:
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>true</td></tr><tr><td>chat_spend_limit_micros</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
| AuditableOrganizationMember<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>roles</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| Chat<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>agent_id</td><td>false</td></tr><tr><td>archived</td><td>true</td></tr><tr><td>build_id</td><td>false</td></tr><tr><td>client_type</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>dynamic_tools</td><td>false</td></tr><tr><td>heartbeat_at</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>labels</td><td>true</td></tr><tr><td>last_error</td><td>false</td></tr><tr><td>last_injected_context</td><td>false</td></tr><tr><td>last_model_config_id</td><td>false</td></tr><tr><td>last_read_message_id</td><td>false</td></tr><tr><td>mcp_server_ids</td><td>true</td></tr><tr><td>mode</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>parent_chat_id</td><td>false</td></tr><tr><td>pin_order</td><td>true</td></tr><tr><td>plan_mode</td><td>false</td></tr><tr><td>root_chat_id</td><td>false</td></tr><tr><td>started_at</td><td>false</td></tr><tr><td>status</td><td>false</td></tr><tr><td>title</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>worker_id</td><td>false</td></tr><tr><td>workspace_id</td><td>true</td></tr></tbody></table> |
| CustomRole<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_system</td><td>false</td></tr><tr><td>member_permissions</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>org_permissions</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>site_permissions</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_permissions</td><td>true</td></tr></tbody></table> |
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| GroupSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>auto_create_missing_groups</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>legacy_group_name_mapping</td><td>false</td></tr><tr><td>mapping</td><td>true</td></tr><tr><td>regex_filter</td><td>true</td></tr></tbody></table> |
+3 -3
View File
@@ -8487,9 +8487,9 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
#### Enumerated Values
| Value(s) |
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `ai_seat`, `api_key`, `convert_login`, `custom_role`, `git_ssh_key`, `group`, `health_settings`, `idp_sync_settings_group`, `idp_sync_settings_organization`, `idp_sync_settings_role`, `license`, `notification_template`, `notifications_settings`, `oauth2_provider_app`, `oauth2_provider_app_secret`, `organization`, `organization_member`, `prebuilds_settings`, `task`, `template`, `template_version`, `user`, `workspace`, `workspace_agent`, `workspace_app`, `workspace_build`, `workspace_proxy` |
| Value(s) |
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `ai_seat`, `api_key`, `chat`, `convert_login`, `custom_role`, `git_ssh_key`, `group`, `health_settings`, `idp_sync_settings_group`, `idp_sync_settings_organization`, `idp_sync_settings_role`, `license`, `notification_template`, `notifications_settings`, `oauth2_provider_app`, `oauth2_provider_app_secret`, `organization`, `organization_member`, `prebuilds_settings`, `task`, `template`, `template_version`, `user`, `workspace`, `workspace_agent`, `workspace_app`, `workspace_build`, `workspace_proxy` |
## codersdk.Response
+30
View File
@@ -29,6 +29,7 @@ var AuditActionMap = map[string][]codersdk.AuditAction{
"License": {codersdk.AuditActionCreate, codersdk.AuditActionDelete},
"Task": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
"AiSeatState": {codersdk.AuditActionCreate},
"Chat": {codersdk.AuditActionCreate, codersdk.AuditActionWrite}, // chats get 'archived' by users, not deleted.
}
type Action string
@@ -378,6 +379,35 @@ var auditableResourcesTypes = map[any]map[string]Action{
"created_at": ActionIgnore, // Never changes.
"deleted_at": ActionIgnore, // Changes, but is implicit when a delete event is fired.
},
&database.Chat{}: {
"id": ActionTrack,
"owner_id": ActionTrack,
"organization_id": ActionIgnore, // Never changes after creation.
"workspace_id": ActionTrack,
"build_id": ActionIgnore, // Internal lifecycle.
"agent_id": ActionIgnore, // Internal lifecycle.
"title": ActionTrack,
"status": ActionIgnore, // Churns every message.
"worker_id": ActionIgnore, // Internal.
"started_at": ActionIgnore,
"heartbeat_at": ActionIgnore,
"created_at": ActionIgnore, // Never changes.
"updated_at": ActionIgnore, // Bumped on every mutation.
"parent_chat_id": ActionIgnore, // Immutable after creation.
"root_chat_id": ActionIgnore, // Immutable after creation.
"last_model_config_id": ActionIgnore, // Churns every message.
"archived": ActionTrack,
"last_error": ActionIgnore,
"mode": ActionTrack,
"mcp_server_ids": ActionTrack,
"labels": ActionTrack,
"pin_order": ActionTrack,
"last_read_message_id": ActionIgnore, // User-scoped read cursor.
"last_injected_context": ActionIgnore, // Internal lifecycle.
"dynamic_tools": ActionIgnore, // Internal lifecycle.
"plan_mode": ActionIgnore, // Can flip back and forth during a session.
"client_type": ActionIgnore, // Set at creation.
},
}
// auditMap converts a map of struct pointers to a map of struct names as
+2
View File
@@ -6236,6 +6236,7 @@ export interface ResolveAutostartResponse {
export type ResourceType =
| "ai_seat"
| "api_key"
| "chat"
| "convert_login"
| "custom_role"
| "git_ssh_key"
@@ -6265,6 +6266,7 @@ export type ResourceType =
export const ResourceTypes: ResourceType[] = [
"ai_seat",
"api_key",
"chat",
"convert_login",
"custom_role",
"git_ssh_key",