Skip to content

feat: add chat project sharing API - #30439

Open
f0ssel wants to merge 110 commits into
mainfrom
f0ssel/chat-projects-acl
Open

f0ssel wants to merge 110 commits into
mainfrom
f0ssel/chat-projects-acl

Conversation

@f0ssel

@f0ssel f0ssel commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

API for chat project sharing (experiment chat-projects). Builds on #30479 (merged).

Owners share a project with users, groups, or the whole organization (the Everyone group, ID = organization ID). Chats in a shared project stay private.

Role Project and instructions Memories ACL Delete
use read read/write read no
admin read/write read/write edit no

Only the owner, and administrators whose role grants delete, can delete a project.

  • API: GET/PATCH .../chats/projects/{project}/acl, same shapes as /chats/{chat}/acl, plus SDK methods. Project responses include the caller's permissions (update, delete, share); the UI hides edit and delete accordingly.
  • Listing: GET /chats/projects returns owned and shared projects. Removes GetChatProjectsByOwnerID.
  • Chat access: creating a chat in a project, and loading its memory and instructions on each turn, requires the chat owner to own the project or have an ACL grant (chatd.ChatProjectUsableBy). Role grants don't count. If the grant is removed, existing chats lose memory and instructions on their next turn.
  • Validation: users and groups must be in the project's organization; keys must be canonical UUIDs; callers can't change their own role; the owner can't be added. Migration 000624 drops a user's grants when they leave the organization, so rejoining doesn't restore access.
  • Instructions: readers who can't edit get 403 instead of 404, and the UI hides editing.
  • Agent prompt: memories are visible to and editable by sharees, so the agent is told not to save secrets or personal details.

Known gap: project deletion still detaches chats, and a sharee's open sidebar keeps showing them until reload. #30683 replaces this.

Not included: share notifications, /acl/available, sharing UI.

Decision log
  • use grants memory read/write over HTTP, because agents in a sharee's chats can already write memories. Making the agent read-only for sharees was the alternative (@f0ssel chose sharees changing memories through use).
  • An administrator with share can grant themselves access and then use the project. Accepted to keep access control simple (@f0ssel).

Generated by Coder Agents.

@f0ssel f0ssel changed the title feat: share chat projects with users, groups, and organizations feat: add chat project sharing API Oct 7, 2026
@f0ssel
f0ssel changed the base branch from main to f0ssel/chat-projects-acl-db October 7, 2026 16:12
@f0ssel
f0ssel marked this pull request as ready for review October 7, 2026 17:47
@f0ssel
f0ssel requested a review from nickvigilante as a code owner October 7, 2026 17:47
@f0ssel
f0ssel requested a review from jaaydenh October 7, 2026 17:48
@f0ssel

f0ssel commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review

This comment has been minimized.

coder-agents-review[bot]

This comment was marked as outdated.

@f0ssel

f0ssel commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This round reviewed the fixes for round 1 and the new project-deletion behavior. Findings: 1 P1, 3 P2, 9 P3, 3 P4, 12 Nit new; two earlier findings are reopened: use chats can still write memories through their agent, and the list-cap comment. Most round 1 findings are verified fixed, including revocation (memory loss on the next turn), admin binding, canonical UUID keys, foreign users, the owner entry, the 403/404 split, the memory prompt, and the flag help text. The duplicate ACL helper finding is closed on your explanation.

Please split the PR. Commit c0c39cb changes DELETE /chats/projects/{project} for every project, shared or not: it deletes the project's chats, including other users' chats and their sub-chats, instead of detaching them, and adds a 409. Most new findings in this round come from that change (the panic without the AI Gateway, the races, the stale worker_id, the 409 message, missing audit, the long transaction, the UI text), and none of them concern who can see a project.

  • This PR: the sharing API, with detach on delete.
  • Separate PR: delete chats with their project. Include Server.DeleteChatProject, the 409 branch, DeleteRemovesChats, the ARCHITECTURE.md note, the GetChatProjectChatsForDelete and DeleteChatProjectChats queries moved out of #30479, and the delete-dialog text in ChatsSidebar.tsx:374 and ProjectHeaderSection.tsx:161.

Out of scope (needs a ticket or explicit acceptance by a human):

  • coderd/database/queries/chatprojects.sql:56: IsChatProjectAccessibleByUserID (from #30479) is a third copy of the project ACL rules, after policy.rego and GetChatProjectsAccessibleByUserID, and it already differs from the list query (it requires read or *; the list matches any key). If one copy changes alone, the list, HTTP authorization, and chatd memory access disagree.
  • coderd/database/queries/chatprojects.sql:57: the IsChatProjectAccessibleByUserID comment repeats the reason the ChatProjectUsableBy doc gives, so the fact lives in both PRs.
  • coderd/rbac/acl/updatevalidator.go:56: the empty-role skip comment still names only workspace and template roles.

Notes:

  • coderd/x/chatd/memory.go:36: revocation applies from the next turn. A sharee removed during a long turn keeps memory read and write until that turn ends, which matches the PR description.
  • coderd/database/queries/chatprojects.sql:114: when a project is deleted, sharees' automations that target or were created by a chat in it lose that reference (chat_automations ON DELETE SET NULL), with no notice.

site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.tsx:374

P1 [CRF-35] The project delete dialog still says "Chats in this project will be kept and move back to the Chats list.", but deleteChatProject now deletes those chats. (Netero)

Server.DeleteChatProject (coderd/x/chatd/projects.go:23) deletes every user's root chats and sub-chats, and DeleteRemovesChats shows both the owner's and the sharee's chat return 404 afterward. An owner who confirms this dialog permanently deletes their own and their sharees' chats after being told they are kept. ProjectHeaderSection.tsx:161 has the same string. Moving the deletion change out of this PR (see the split finding on projects.go:23) makes this text correct again; otherwise update both strings here.

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/x/chatd/projects.go Outdated
Comment thread coderd/chat_projects.go Outdated
Comment thread coderd/chat_projects_test.go
Comment thread coderd/x/chatd/projects.go Outdated
Comment thread coderd/chat_projects.go Outdated
Comment thread coderd/chat_project_acl.go Outdated
Comment thread coderd/chat_project_acl.go Outdated
Comment thread coderd/chat_projects_test.go Outdated
Comment thread coderd/x/chatd/projects.go Outdated
Comment thread coderd/chat_projects_test.go Outdated
@f0ssel
f0ssel requested a review from a team as a code owner October 7, 2026 20:33
@f0ssel

f0ssel commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

Decisions on this round's summary: deleting chats with the project stays in this PR rather than a split (delete dialog text updated in 43b9800), and use sharees writing memories through their chats' agents is intended. (Coder Agents)

@f0ssel

f0ssel commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further review is blocked until one finding gets a response. The maxChatProjectsPerOwner comment at coderd/chat_projects.go:329 still says the cap "bounds the project list every agents page loads", but the list now includes shared projects and has no limit. Round 2 reopened this in the list-performance thread (it stayed marked resolved because unresolving it failed), and it has no reply. Change the comment to say the cap limits owned projects only, add a limit to the list, or reply with why neither is needed.

The round 2 findings on deletion, the AI Gateway panic, locking, stale leases, audit, watch events, and comments have commits or replies. The panel will check those fixes next round, along with your decision to keep chat deletion in this PR.

You say use chats writing memories through their agents is intended. That needs a human decision, because it lets any use sharee put text into the memory index of the owner's chats. The role doc and PR description now describe the behavior. A maintainer should accept it on the PR or file an issue.

🤖 This review was automatically generated with Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review is blocked until the author responds to one finding from the previous round. The other seven are fixed or acknowledged at 1e98984.

No response yet (posted in the previous review's body): [CRF-312] coderd/chat_projects.go:258. Deleting a project detaches every user's chats in one unbounded UPDATE through chats_project_id_fkey ON DELETE SET NULL, which row-locks all of them. On PG13, with 50,000 chats in one project, the FK trigger took 8.9 s, and a concurrent update on one of those chats waited 5.7 s. A project shared with the Everyone group can collect that many chats. A response can be one of these: batch the detach in this PR, record the cost in #30683 (which replaces this delete), or explain why it does not need fixing. If the gap is accepted without a fix or ticket, that needs a human decision.

🤖 This review was automatically generated with Coder Agents.

@f0ssel

f0ssel commented Oct 13, 2026

Copy link
Copy Markdown
Member Author

Ignore the delete issue, it is addressed in a followup PR on the stack

@f0ssel

f0ssel commented Oct 13, 2026

Copy link
Copy Markdown
Member Author

CRF-312: Accepted for this PR. The unbounded detach is main's existing delete behavior, kept here on purpose, and #30683 replaces it with a tombstone plus background archive that takes no chat locks in the request. (Coder Agents)

@f0ssel

f0ssel commented Oct 13, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@f0ssel
f0ssel requested a review from mafredri October 13, 2026 15:25

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This round is the first panel review of head 1e98984. That head merged main, which brought chat project instructions (#30367/#30368). The PR gates resolveProjectInstructions with ChatProjectUsableBy, and subagents check the root chat's owner, so a revoked sharee's chats lose instructions and memory together (Kurapika, Pariston). The HTTP instructions handlers from main did not get the same treatment as the rest of the PR. A use sharee gets 404 instead of the documented 403 (CRF-319), and no sharing test calls those endpoints (CRF-322). CI passed when this round started.

CRF-312 is closed as a human decision: the delete path is handled in #30683.

This round has 1 P3, 8 P4, and 1 Nit.


coderd/chat_project_instructions.go:75

P3 [CRF-319] putChatProjectInstructions (here) and deleteChatProjectInstructions (:188) return 404 to a use sharee. The PR description says use "can't edit them directly (403)", and project PATCH/DELETE and memory create/delete now return 403 through authorizeChatProjectChange. (Netero, Ryosuke, Pariston)

Before sharing, only the owner and role holders could read a project, so this 404 never reached a reader. Now a sharee who just read the instructions with GET gets "Resource not found" from PUT on the same path, and a client treats that as a deleted project (the CRF-12 bug on a sibling endpoint). A scratch test at this head showed use GET returning the owner's text, use PUT and DELETE returning 404, and admin PUT and DELETE succeeding. Call api.authorizeChatProjectChange(rw, r, policy.ActionUpdate, project.RBACObject()) in both handlers. The comments at :135 and :218 ("without read can still save/delete") stop being true once read is checked first, so update them too.

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/chat_project_acl_test.go
Comment thread coderd/x/chatd/project_instructions_internal_test.go Outdated
Comment thread coderd/x/chatd/project_instructions.go Outdated
Comment thread codersdk/chats.go Outdated
Comment thread coderd/x/chatd/project_instructions.go Outdated
Comment thread coderd/x/chatd/projects.go Outdated
Comment thread coderd/x/chatd/project_instructions.go Outdated
Comment thread codersdk/chats.go
Comment thread site/src/pages/AgentsPage/components/ProjectPage/ProjectHeaderSection.tsx Outdated
Comment thread coderd/x/chatd/project_instructions_internal_test.go Outdated
Comment thread coderd/chat_project_acl.go Outdated
Comment thread coderd/x/chatd/projects.go
Comment thread coderd/x/chatd/ARCHITECTURE.md Outdated
f0ssel added a commit that referenced this pull request Oct 14, 2026
Database and authorization layer for chat project sharing (experiment
`chat-projects`), part 1 of 2. #30439 adds the API on top of it. Nothing
here changes behavior: projects start with empty ACLs, no endpoint
writes them yet, and deleting a project still detaches its chats.
Project deletion is in #30683.

- **Schema:** migration `000621` adds `user_acl` and `group_acl` jsonb
columns to `chat_projects`, typed as `ChatACL` like chats, with GIN
indexes. It also adds the `chat_project:share` API key scope.
- **Queries:**
- `GetChatProjectsOwnedOrSharedWithUserID`: owned projects plus projects
with an ACL entry for the user directly, through a group, or through the
Everyone group (whose ID is the organization ID). Entries count only for
members of the project's organization. Keys match regardless of actions,
so dbauthz filters the rows.
- `IsChatProjectAccessibleByUserID`: whether a user owns a project or
holds a `read` or `*` grant on it, for code running under a system
subject. With sharing disabled, dbauthz answers owner-only.
- `GetChatProjectByIDForUpdate`: lets the ACL update read, modify, and
write in one transaction.
  - `UpdateChatProjectACLByID`.
- **RBAC:**
- New `share` action on `chat_project`. dbauthz requires it to update
the ACL.
- `ChatProject.RBACObject` attaches the project ACL. Memory objects get
a derived ACL: project `read` grants memory read, and `read` with
`update` grants memory create and delete. Wildcard grants count. Read
sharees change memories through their chats' agents, which #30439 gates.
Project instructions authorize through the same object, so a `read`
grant reads them and an `update` grant edits them.
- `--disable-chat-sharing` also covers projects: ACLs are ignored, the
ACL update is rejected, and roles deny `chat_project:share`. The help
text and chat sharing docs say so.
- **Audit:** `user_acl` and `group_acl` changes are tracked.
- **Tests:**
- Querier tests run the access queries both raw and through dbauthz
(owner, direct user, group, Everyone, wildcard grants, a member of
another org, grants without read).
- RBAC object and memory ACL tests, with an authorizer check, in both
sharing modes.
- dbauthz cases including the disabled guards, and the
`chat_project:share` negation in the roles test.

The generated TypeScript changes only add the new scope value.

> Generated by Coder Agents.
Base automatically changed from f0ssel/chat-projects-acl-db to main October 14, 2026 14:42
Addresses review: use sharees read and write memories directly, the ACL
response hides entries outside the project's organization, the access
query is renamed IsChatProjectUsableByUserID, and comments and the
architecture doc are tightened.
@f0ssel
f0ssel requested a review from mafredri October 14, 2026 17:25
Share the project access gate between memory and instructions, cover
sharee instruction access, return 403 to readers who cannot edit
instructions, and say that deleting a project deletes its instructions.
@f0ssel

f0ssel commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e479ae8 fixes every round 33 finding (CRF-319 to CRF-329); reviewers checked each one against the code and ran the tests. The main merge (c3dea24) also changed behavior: any read grant now gets memory create and delete over HTTP. The codersdk role doc, ChatProjectRoleActions, the ARCHITECTURE table, the tests, and the audit path all agree with that change (Netero, Kurapika, Pariston, Mafuuu, Knuckle), and it matches the CRF-9 decision. The merge also added a filter that hides ACL entries for users outside the project's organization, and those entries still come back into force (CRF-335). The rest are leftovers from the fixes: a doc comment now attached to the wrong function, use sharees shown edit buttons, duplicate subtests, and docs fixed in one place but not its twin. CI was still pending when this round started.

This round has 3 P3, 8 P4, and 2 Nit.

ProjectDetailsPanelView.tsx and the PR description's role table are outside the diff, so CRF-331 is in the body and CRF-333 covers the PR description too.


site/src/pages/AgentsPage/components/ProjectPage/ProjectDetailsPanelView.tsx:130

P3 [CRF-331] A use sharee who opens a shared project sees the Create (line 116) and Edit instructions (line 130) buttons. Saving or deleting then returns 403 (TestChatProjectSharing/User, chat_project_acl_test.go:94-97), and the user sees "Failed to save instructions." Edit and Delete project are already gated on permissions (ProjectActionsMenuItems.tsx:49), and the PR description says the UI hides actions the caller cannot do. Pass project.permissions.update into ProjectDetailsPanel and hide both buttons when it is false. Add a use story like ProjectActionsWithoutUpdateOrDeletePermission. (Netero)

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/x/chatd/projects.go Outdated
Comment thread coderd/chat_project_acl.go Outdated
Comment thread coderd/database/dbauthz/dbauthz_test.go Outdated
Comment thread coderd/x/chatd/ARCHITECTURE.md Outdated
Comment thread coderd/x/chatd/ARCHITECTURE.md Outdated
Comment thread coderd/chat_project_acl_test.go
Comment thread coderd/exp_chats.go
Comment thread codersdk/chats.go Outdated
Comment thread coderd/database/dbauthz/dbauthz.go Outdated
Comment thread coderd/x/chatd/memory_internal_test.go
Drop a user's project grants when they leave the organization, restore
403/404 for instruction edits without breaking update-only roles, hide
instruction editing from users who cannot update, and tighten docs,
logs, and tests.
@f0ssel

f0ssel commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

CRF-331: Fixed in 250489a. Create and Edit instructions are hidden without update permission; story and vitest added. (Coder Agents)

@f0ssel

f0ssel commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

250489a fixes all 13 round 34 findings (CRF-330 to CRF-342), and reviewers checked each one. The new trigger in migration 000624 drops a departing member's grants, and its backfill is correct (Netero ran it from migration 623). With the indexes it uses, a membership delete touches only the rows that hold the user's key (Knuckle). Listing groups from other organizations is correct, because those entries still grant access (Kurapika, Pariston, Mafuuu, Kite). CI was pending when this round started.

The main new finding is CRF-348: a race between a PATCH and a membership delete still stores the grant that the trigger exists to remove. Two reviewers reproduced it. The other findings are test gaps and comments.

This round has 1 P3, 6 P4, and 2 Nit.

Out of scope: chats.user_acl has no matching organization_members delete trigger, so chat grants come back when a user rejoins (Kurapika, Kite).

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/chat_project_acl.go Outdated
Comment thread coderd/database/querier_test.go
Comment thread coderd/chat_project_instructions.go
Comment thread coderd/chat_project_acl.go
Comment thread site/src/pages/AgentsPage/components/ProjectPage/ProjectDetailsPanelView.tsx Outdated
Comment thread coderd/x/chatd/projects.go Outdated
Comment thread site/src/pages/AgentsPage/components/ProjectPage/ProjectDetailsPanel.test.tsx Outdated
Comment thread codersdk/chats.go Outdated
Validates ACL users inside the transaction after locking their
memberships, so a concurrent membership delete cannot miss a new grant.
Also covers the 000624 backfill, cross-organization trigger scope,
foreign group listing, and update-only instruction deletes, and shows
full instructions to users who cannot edit them.
@f0ssel

f0ssel commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

08f500c fixes all 9 round 35 findings (CRF-343 to CRF-351). The CRF-348 fix holds: PATCH locks the memberships (FOR KEY SHARE) and then the project row, the 000624 trigger locks in the same order, and validation now reads through tx. Knuckle, Kurapika, Pariston, Mafuuu, and Razor each traced this, and none found a deadlock or a missed grant. Removal-only PATCHes take no lock, which is correct. The findings are test gaps, comments, and one audit diff. CI was pending when this round started.

This round has 7 P4 and 1 Nit.

ProjectDetailsPanel.test.tsx has no net change against the base, so CRF-352 is in the body.

Out of scope: the shared validateACLOrganization (mcp_acl.go:394, :414) returns database failures as 400 validation errors for skill and MCP server ACL PATCHes (Razor). This PR's copy returns 500 on them, which is correct.


site/src/pages/AgentsPage/components/ProjectPage/ProjectDetailsPanel.test.tsx:103

P4 [CRF-352] Re-raise of CRF-346. Our suggestion to move the test into ProjectDetailsPanelView stories lost the only check of ProjectDetailsPanel.tsx:95 (project.permissions.update ? openEditor : undefined). The View stories pass onEditInstructions: undefined directly. Netero changed line 95 to onEditInstructions={openEditor}, and the unit and storybook suites in ProjectPage/ both still passed. Add a play to WithoutUpdateOrDeletePermission in ProjectPage.stories.tsx:113, which renders the container: wait for the instructions text, then assert there is no "Edit instructions" button. (Netero P3, lowered because the regression would show a button that returns 403, not grant access; Bisky)

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/chat_project_acl.go
Comment thread coderd/chat_project_acl.go Outdated
Comment thread coderd/chat_project_acl.go
Comment thread coderd/chat_project_acl.go Outdated
Comment thread coderd/database/querier_test.go
Comment thread coderd/database/migrations/migration000628_test.go
Comment thread coderd/chat_project_acl.go Outdated
@f0ssel

f0ssel commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

CRF-352: added a play function to WithoutUpdateOrDeletePermission asserting no Edit instructions button (8829317). (Coder Agents)

@f0ssel

f0ssel commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8829317 fixes all eight open round 36 findings (CRF-352 to CRF-360). Netero checked the new tests by mutation. With the membership lock disabled, TestChatProjectACLMemberRemovalBeforeGrant fails 3 of 3 runs, and it passes with -count=20 -race. The migration, trigger, and container story tests each fail when the code they target is changed. aReq.Old = current is correct on failure: InTx at the default isolation level runs the closure once, so a rolled-back request audits the locked row with no change (Knuckle, Chopper). CI was pending when this round started.

This round has 1 Nit. No findings are open above Nit, so this review approves.

🤖 This review was automatically generated with Coder Agents.

require.NoError(t, tx.Commit())

err = testutil.RequireReceive(ctx, t, patchErr)
requireSDKError(t, err, http.StatusBadRequest)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-361] The test checks only the 400 status. With only that check, a 400 from any other patchChatProjectACL check would also pass. The sibling validation tests (:282-283, :309-310) also assert sdkErr.Validations. Keep the return value of requireSDKError and assert Len(sdkErr.Validations, 1) and Field == "user_roles". (Chopper)

🤖

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants