Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
c77bc7c
feat: add organization scoping to chats
johnstcn Mar 30, 2026
46ea130
fix: update enterprise chatd tests for org-scoped chats
johnstcn Mar 30, 2026
8aec990
fix: update enterprise exp_chats_test for org-scoped chats
johnstcn Mar 30, 2026
0d975ce
fix: add OrganizationID to all remaining InsertChat test callsites
johnstcn Mar 30, 2026
2d164cb
fix: update rego SQL compile tests for org-scoped ChatConverter
johnstcn Mar 31, 2026
4371364
fix: address deep-review findings for chat org scoping
johnstcn Mar 31, 2026
09dac32
merge: resolve conflicts with main (chat_access_role migration)
johnstcn Mar 31, 2026
65abac7
fix: appease the org-scoping gods with OrganizationID in all remainin…
johnstcn Mar 31, 2026
ba3c756
fix(coderd/rbac): plug org-member permission leak for chats and fix l…
johnstcn Mar 31, 2026
b49e81f
fix: post-merge compilation fixes — seedChatDependencies 3-return, mi…
johnstcn Mar 31, 2026
685239c
fix: archive chat tests need OrganizationID too, naturally
johnstcn Mar 31, 2026
edec8a7
test(coderd/rbac): add orgMemberMe subject to ChatUsage RBAC test
johnstcn Apr 1, 2026
9a18c4b
test(coderd): add handler validation tests for org-scoped chat creation
johnstcn Apr 1, 2026
b385915
merge: reconcile with origin/main — renumber migration 000458→000465,…
johnstcn Apr 9, 2026
d1538cb
fix(coderd): exorcise ghost test resurrected during merge
johnstcn Apr 9, 2026
61af75b
fix(coderd): teach two more tests about the org-scoping revolution
johnstcn Apr 9, 2026
1aa7b37
fix(coderd/x/chatd/chattool): reject cross-org templates before works…
johnstcn Apr 9, 2026
6095ab2
Merge remote-tracking branch 'origin/main' into cian/chat-org-scoping
johnstcn Apr 10, 2026
8775851
refactor(site): swap uncontrolled org autocomplete for controlled Select
johnstcn Apr 10, 2026
021998c
Merge branch 'main' into cian/chat-org-scoping
johnstcn Apr 10, 2026
9d2d925
docs: add organization_id to chats API documentation
johnstcn Apr 10, 2026
941f35d
Merge remote-tracking branch 'origin/cian/chat-org-scoping' into cian…
johnstcn Apr 10, 2026
14f994e
fix: address copilot review nits
johnstcn Apr 10, 2026
841d7a1
docs(coderd/x/chatd/chattool): explain intentional double template lo…
johnstcn Apr 10, 2026
960dacd
Merge remote-tracking branch 'origin/main' into cian/chat-org-scoping
johnstcn Apr 10, 2026
99efebe
refactor(site): swap Radix Select for controlled OrganizationAutocomp…
johnstcn Apr 10, 2026
f063a2b
fix(coderd/x/chatd): remove zombie OpenAI round-trip tests resurrecte…
johnstcn Apr 10, 2026
75ac297
test(coderd): add happy-path test for chat creation in non-default org
johnstcn Apr 10, 2026
22f7de9
test(enterprise/coderd): move multi-org chat test to enterprise
johnstcn Apr 10, 2026
ea336fa
fix(enterprise/coderd): use ptr.Ref instead of local pointer helpers
johnstcn Apr 10, 2026
a160053
fix(site): clear file attachments on org change with confirmation
johnstcn Apr 10, 2026
876b969
fix: rename misleading test name and add multi-org story
johnstcn Apr 10, 2026
002434b
Merge remote-tracking branch 'origin/main' into cian/chat-org-scoping
johnstcn Apr 10, 2026
ee5dead
merge: reconcile with origin/main — fix new setupChatInfra callsite
johnstcn Apr 10, 2026
a95117e
make -B gen
johnstcn Apr 10, 2026
65b197b
fix: clear workspace on org change and handle dbauthz not-authorized
johnstcn Apr 10, 2026
c3316f4
fix: address panel review — gate postChatMessages, remove dead SQL fi…
johnstcn Apr 10, 2026
a886c22
fix: replace em-dashes with ASCII punctuation in new comments
johnstcn Apr 10, 2026
eacc1a3
chore: remove accidentally staged zip file
johnstcn Apr 10, 2026
f47be73
fix: extend agents-access gate to all AI-triggering endpoints
johnstcn Apr 10, 2026
f8da2f1
Merge remote-tracking branch 'origin/main' into cian/chat-org-scoping
johnstcn Apr 10, 2026
e6e3fe8
merge: renumber migration 000466 to 000467 after main collision
johnstcn Apr 10, 2026
61418c9
fix: regenerate dump.sql after merge with dropped index migration
johnstcn Apr 10, 2026
bc58a5e
fix: add OrganizationID to merged PinnedOnFirstPage and CursorWithPin…
johnstcn Apr 10, 2026
51d249c
fix(coderd): address review comments from PR #23827
johnstcn Apr 13, 2026
6694df9
fix(coderd/rbac): correct misleading agentsAccess role comment
johnstcn Apr 13, 2026
0744a85
Merge remote-tracking branch 'origin/main' into cian/chat-org-scoping
johnstcn Apr 13, 2026
4995fa3
fix(coderd/x/chatd): add missing OrganizationID to InsertChat calls
johnstcn Apr 13, 2026
6de14a4
stop code cyclists creeping into the error check
johnstcn Apr 13, 2026
137828c
fix: address round 5 review findings across frontend and backend
johnstcn Apr 13, 2026
ef269be
fix(site): remove dead organizationId fallback that could never trigger
johnstcn Apr 13, 2026
3f6963d
fixup gitignore
johnstcn Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,6 @@ PLAN.md

# Ignore any dev licenses
license.txt
-e

# Agent planning documents (local working files).
docs/plans/
1 change: 1 addition & 0 deletions coderd/database/db2sdk/db2sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -1550,6 +1550,7 @@ func Chat(c database.Chat, diffStatus *database.ChatDiffStatus, files []database
}
chat := codersdk.Chat{
ID: c.ID,
OrganizationID: c.OrganizationID,
OwnerID: c.OwnerID,
LastModelConfigID: c.LastModelConfigID,
Title: c.Title,
Expand Down
1 change: 1 addition & 0 deletions coderd/database/db2sdk/db2sdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ func TestChat_AllFieldsPopulated(t *testing.T) {
input := database.Chat{
ID: uuid.New(),
OwnerID: uuid.New(),
OrganizationID: uuid.New(),
WorkspaceID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
BuildID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
AgentID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
Expand Down
2 changes: 1 addition & 1 deletion coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -4846,7 +4846,7 @@ func (q *querier) InsertAuditLog(ctx context.Context, arg database.InsertAuditLo
}

func (q *querier) InsertChat(ctx context.Context, arg database.InsertChatParams) (database.Chat, error) {
return insert(q.log, q.auth, rbac.ResourceChat.WithOwner(arg.OwnerID.String()), q.db.InsertChat)(ctx, arg)
return insert(q.log, q.auth, rbac.ResourceChat.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID), q.db.InsertChat)(ctx, arg)
}

func (q *querier) InsertChatFile(ctx context.Context, arg database.InsertChatFileParams) (database.InsertChatFileRow, error) {
Expand Down
2 changes: 1 addition & 1 deletion coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,7 @@ func (s *MethodTestSuite) TestChats() {
})
chat := testutil.Fake(s.T(), faker, database.Chat{OwnerID: arg.OwnerID})
dbm.EXPECT().InsertChat(gomock.Any(), arg).Return(chat, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat.WithOwner(arg.OwnerID.String()), policy.ActionCreate).Returns(chat)
check.Args(arg).Asserts(rbac.ResourceChat.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID), policy.ActionCreate).Returns(chat)
}))
s.Run("InsertChatFile", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
arg := testutil.Fake(s.T(), faker, database.InsertChatFileParams{})
Expand Down
29 changes: 16 additions & 13 deletions coderd/database/dbpurge/dbpurge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1670,9 +1670,10 @@ func TestDeleteOldChatFiles(t *testing.T) {

// createChat inserts a chat and optionally archives it, then
// backdates updated_at to control the "archived since" window.
createChat := func(ctx context.Context, t *testing.T, db database.Store, rawDB *sql.DB, ownerID, modelConfigID uuid.UUID, archived bool, updatedAt time.Time) database.Chat {
createChat := func(ctx context.Context, t *testing.T, db database.Store, rawDB *sql.DB, ownerID, orgID, modelConfigID uuid.UUID, archived bool, updatedAt time.Time) database.Chat {
t.Helper()
chat, err := db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: orgID,
OwnerID: ownerID,
LastModelConfigID: modelConfigID,
Title: "test-chat",
Expand Down Expand Up @@ -1736,7 +1737,7 @@ func TestDeleteOldChatFiles(t *testing.T) {
require.NoError(t, err)

// Create an old archived chat and an orphaned old file.
oldChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
oldChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
oldFileID := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now.Add(-31*24*time.Hour))

done := awaitDoTick(ctx, t, clk)
Expand Down Expand Up @@ -1766,7 +1767,7 @@ func TestDeleteOldChatFiles(t *testing.T) {
require.NoError(t, err)

// Old archived chat (31 days) — should be deleted.
oldChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
oldChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
// Insert a message so we can verify CASCADE.
_, err = db.InsertChatMessages(ctx, database.InsertChatMessagesParams{
ChatID: oldChat.ID,
Expand All @@ -1791,10 +1792,10 @@ func TestDeleteOldChatFiles(t *testing.T) {
require.NoError(t, err)

// Recently archived chat (10 days) — should be retained.
recentChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-10*24*time.Hour))
recentChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-10*24*time.Hour))

// Active chat — should be retained.
activeChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, false, now)
activeChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, false, now)

done := awaitDoTick(ctx, t, clk)
closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{}, clk, prometheus.NewRegistry())
Expand Down Expand Up @@ -1839,7 +1840,7 @@ func TestDeleteOldChatFiles(t *testing.T) {

// File B: 31 days old, in an active chat -> should be retained.
fileB := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now.Add(-31*24*time.Hour))
activeChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, false, now)
activeChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, false, now)
_, err = db.LinkChatFiles(ctx, database.LinkChatFilesParams{
ChatID: activeChat.ID,
MaxFileLinks: 100,
Expand Down Expand Up @@ -1887,7 +1888,7 @@ func TestDeleteOldChatFiles(t *testing.T) {

// File D: 31 days old, in a chat archived 31 days ago -> should be deleted.
fileD := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now.Add(-31*24*time.Hour))
oldArchivedChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
oldArchivedChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
_, err = db.LinkChatFiles(ctx, database.LinkChatFilesParams{
ChatID: oldArchivedChat.ID,
MaxFileLinks: 100,
Expand All @@ -1901,7 +1902,7 @@ func TestDeleteOldChatFiles(t *testing.T) {

// File E: 31 days old, in a chat archived 10 days ago -> should be retained.
fileE := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now.Add(-31*24*time.Hour))
recentArchivedChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-10*24*time.Hour))
recentArchivedChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-10*24*time.Hour))
_, err = db.LinkChatFiles(ctx, database.LinkChatFilesParams{
ChatID: recentArchivedChat.ID,
MaxFileLinks: 100,
Expand All @@ -1914,7 +1915,7 @@ func TestDeleteOldChatFiles(t *testing.T) {

// File F: 31 days old, in BOTH an active chat AND an old archived chat -> should be retained.
fileF := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now.Add(-31*24*time.Hour))
anotherOldArchivedChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
anotherOldArchivedChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
_, err = db.LinkChatFiles(ctx, database.LinkChatFilesParams{
ChatID: anotherOldArchivedChat.ID,
MaxFileLinks: 100,
Expand All @@ -1925,7 +1926,7 @@ func TestDeleteOldChatFiles(t *testing.T) {
now.Add(-31*24*time.Hour), anotherOldArchivedChat.ID)
require.NoError(t, err)

activeChatForF := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, false, now)
activeChatForF := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, false, now)
_, err = db.LinkChatFiles(ctx, database.LinkChatFilesParams{
ChatID: activeChatForF.ID,
MaxFileLinks: 100,
Expand Down Expand Up @@ -1964,7 +1965,7 @@ func TestDeleteOldChatFiles(t *testing.T) {
fileB := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now)
fileC := createChatFile(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, now)

chat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, false, now)
chat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, false, now)
_, err := db.LinkChatFiles(ctx, database.LinkChatFilesParams{
ChatID: chat.ID,
MaxFileLinks: 100,
Expand Down Expand Up @@ -2009,14 +2010,16 @@ func TestDeleteOldChatFiles(t *testing.T) {
// Test parent+child cascade: deleting files should
// clean up links for both parent and child chats
// independently via FK cascade.
parentChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, false, now)
parentChat := createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, false, now)
childChat, err := db.InsertChat(ctx, database.InsertChatParams{
OrganizationID: deps.org.ID,
OwnerID: deps.user.ID,
LastModelConfigID: deps.modelConfig.ID,
Title: "child-chat",
Status: database.ChatStatusWaiting,
})
require.NoError(t, err)

// Set root_chat_id to link child to parent.
_, err = rawDB.ExecContext(ctx, "UPDATE chats SET root_chat_id = $1 WHERE id = $2", parentChat.ID, childChat.ID)
require.NoError(t, err)
Expand Down Expand Up @@ -2104,7 +2107,7 @@ func TestDeleteOldChatFiles(t *testing.T) {

// Create 3 deletable old archived chats.
for range 3 {
createChat(ctx, t, db, rawDB, deps.user.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
createChat(ctx, t, db, rawDB, deps.user.ID, deps.org.ID, deps.modelConfig.ID, true, now.Add(-31*24*time.Hour))
}

// Delete with limit 2 — should delete 2, leave 1.
Expand Down
8 changes: 7 additions & 1 deletion coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions coderd/database/foreign_key_constraint.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE chats DROP COLUMN organization_id;
20 changes: 20 additions & 0 deletions coderd/database/migrations/000467_chat_organization_id.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- Step 1: Add nullable column with FK.
Comment thread
johnstcn marked this conversation as resolved.
ALTER TABLE chats
ADD COLUMN organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE;

Comment thread
johnstcn marked this conversation as resolved.
-- Step 2: Backfill from workspace org (primary path). Fall back to
-- user's oldest org membership, then default org for rows where
-- workspace_id was NULLed out by ON DELETE SET NULL or never set.
UPDATE chats c
SET organization_id = COALESCE(
(SELECT w.organization_id FROM workspaces w WHERE w.id = c.workspace_id),
(SELECT om.organization_id FROM organization_members om
WHERE om.user_id = c.owner_id ORDER BY om.created_at ASC LIMIT 1),
(SELECT id FROM organizations WHERE is_default = true LIMIT 1)
Comment thread
johnstcn marked this conversation as resolved.
);
Comment thread
johnstcn marked this conversation as resolved.

-- Step 3: Enforce NOT NULL going forward.
ALTER TABLE chats ALTER COLUMN organization_id SET NOT NULL;

-- Step 4: Index for efficient lookups by organization.
CREATE INDEX idx_chats_organization_id ON chats (organization_id);
2 changes: 1 addition & 1 deletion coderd/database/modelmethods.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func (t Task) RBACObject() rbac.Object {
}

func (c Chat) RBACObject() rbac.Object {
return rbac.ResourceChat.WithID(c.ID).WithOwner(c.OwnerID.String())
return rbac.ResourceChat.WithID(c.ID).WithOwner(c.OwnerID.String()).InOrg(c.OrganizationID)
}

func (r GetChatsRow) RBACObject() rbac.Object {
Expand Down
1 change: 1 addition & 0 deletions coderd/database/modelqueries.go
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,7 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams,
&i.Chat.LastReadMessageID,
&i.Chat.LastInjectedContext,
&i.Chat.DynamicTools,
&i.Chat.OrganizationID,
&i.HasUnread); err != nil {
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading