diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 95bff0ecf5..9d927e03a5 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -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": {
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index ecb1e47eb4..5c0e1af16a 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -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": {
diff --git a/coderd/audit.go b/coderd/audit.go
index 3d8aed3005..b63c0c0eff 100644
--- a/coderd/audit.go
+++ b/coderd/audit.go
@@ -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 ""
}
diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go
index e085c7d9ea..a2c609d91d 100644
--- a/coderd/audit/diff.go
+++ b/coderd/audit/diff.go
@@ -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
diff --git a/coderd/audit/fields.go b/coderd/audit/fields.go
index a9944767c2..cd6eaeb7ce 100644
--- a/coderd/audit/fields.go
+++ b/coderd/audit/fields.go
@@ -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("{}")
}
diff --git a/coderd/audit/request.go b/coderd/audit/request.go
index 147e53e4f7..c73bedcd7d 100644
--- a/coderd/audit/request.go
+++ b/coderd/audit/request.go
@@ -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))
}
diff --git a/coderd/audit_internal_test.go b/coderd/audit_internal_test.go
index f3d3b160d6..cc7fddf3e0 100644
--- a/coderd/audit_internal_test.go
+++ b/coderd/audit_internal_test.go
@@ -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 {
diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql
index 761dd2de80..88e1cac439 100644
--- a/coderd/database/dump.sql
+++ b/coderd/database/dump.sql
@@ -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 (
diff --git a/coderd/database/migrations/000472_chat_resource_type_audit.down.sql b/coderd/database/migrations/000472_chat_resource_type_audit.down.sql
new file mode 100644
index 0000000000..e72f1886be
--- /dev/null
+++ b/coderd/database/migrations/000472_chat_resource_type_audit.down.sql
@@ -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.
diff --git a/coderd/database/migrations/000472_chat_resource_type_audit.up.sql b/coderd/database/migrations/000472_chat_resource_type_audit.up.sql
new file mode 100644
index 0000000000..31a80036c3
--- /dev/null
+++ b/coderd/database/migrations/000472_chat_resource_type_audit.up.sql
@@ -0,0 +1 @@
+ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'chat';
diff --git a/coderd/database/models.go b/coderd/database/models.go
index 298138de60..257c1c5c2a 100644
--- a/coderd/database/models.go
+++ b/coderd/database/models.go
@@ -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,
}
}
diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go
index b4f8633a65..a4205851db 100644
--- a/coderd/exp_chats.go
+++ b/coderd/exp_chats.go
@@ -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)
}
diff --git a/coderd/exp_chats_test.go b/coderd/exp_chats_test.go
index 2aaecb0bcc..2693c5b43c 100644
--- a/coderd/exp_chats_test.go
+++ b/coderd/exp_chats_test.go
@@ -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()
diff --git a/codersdk/audit.go b/codersdk/audit.go
index ac0b4e908f..d8dc880589 100644
--- a/codersdk/audit.go
+++ b/codersdk/audit.go
@@ -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"
}
diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md
index 9337d84623..4876e0fde4 100644
--- a/docs/admin/security/audit-logs.md
+++ b/docs/admin/security/audit-logs.md
@@ -20,6 +20,7 @@ We track the following resources:
| AuditOAuthConvertState
|
| Field | Tracked |
| | created_at | true |
| expires_at | true |
| from_login_type | true |
| to_login_type | true |
| user_id | true |
|
| Group
create, write, delete | | Field | Tracked |
| | avatar_url | true |
| chat_spend_limit_micros | true |
| display_name | true |
| id | true |
| members | true |
| name | true |
| organization_id | false |
| quota_allowance | true |
| source | false |
|
| AuditableOrganizationMember
| | Field | Tracked |
| | created_at | true |
| organization_id | false |
| roles | true |
| updated_at | true |
| user_id | true |
| username | true |
|
+| Chat
create, write | | Field | Tracked |
| | agent_id | false |
| archived | true |
| build_id | false |
| client_type | false |
| created_at | false |
| dynamic_tools | false |
| heartbeat_at | false |
| id | true |
| labels | true |
| last_error | false |
| last_injected_context | false |
| last_model_config_id | false |
| last_read_message_id | false |
| mcp_server_ids | true |
| mode | true |
| organization_id | false |
| owner_id | true |
| parent_chat_id | false |
| pin_order | true |
| plan_mode | false |
| root_chat_id | false |
| started_at | false |
| status | false |
| title | true |
| updated_at | false |
| worker_id | false |
| workspace_id | true |
|
| CustomRole
| | Field | Tracked |
| | created_at | false |
| display_name | true |
| id | false |
| is_system | false |
| member_permissions | true |
| name | true |
| org_permissions | true |
| organization_id | false |
| site_permissions | true |
| updated_at | false |
| user_permissions | true |
|
| GitSSHKey
create | | Field | Tracked |
| | created_at | false |
| private_key | true |
| public_key | true |
| updated_at | false |
| user_id | true |
|
| GroupSyncSettings
| | Field | Tracked |
| | auto_create_missing_groups | true |
| field | true |
| legacy_group_name_mapping | false |
| mapping | true |
| regex_filter | true |
|
diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md
index fd3fef6764..716e93e01e 100644
--- a/docs/reference/api/schemas.md
+++ b/docs/reference/api/schemas.md
@@ -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
diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go
index fbf7fe1a47..e43bccae7a 100644
--- a/enterprise/audit/table.go
+++ b/enterprise/audit/table.go
@@ -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
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index a1ed0d3ea9..a012981818 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -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",