feat: add owner_id to CreateChatRequest to create chats for another user - #29581
Conversation
Owners and owner-level service accounts can now start a Coder Agents chat
on behalf of another user, following the /users/{user}/workspaces pattern.
The owner is resolved from the path through ExtractOrganizationMembersParam
and must be a member of the requested organization.
Because chat processing runs with the owner's credentials, creating a chat
for someone else requires the same authority as minting that user's API
keys in addition to org-scoped chat:create, so org admins cannot use it.
The initial user message is attributed to the caller.
A chat connects to its workspace as the owner, so the SSH check must run with the owner's roles rather than the caller's when an administrator creates a chat for someone else. The owner resolver now returns an error through httperror instead of a status and response pair.
Docs previewCheck off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here. |
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 363d45281d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
AI Bridge refuses to authorize model calls for suspended, dormant, and system users, so a chat created on their behalf could never run. Reject the request up front instead of leaving a broken chat behind.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Line 1347 in c065437
When a delegated caller can read an MCP configuration that the target user cannot, this validation runs with the caller's authorization context and accepts the configuration. The ID is then persisted on the member-owned chat, and enabledMCPServerConfigsForChatOrg in coderd/x/chatd/generation_preparer.go deliberately does not recheck ACLs for persisted selections, so the target can use an MCP server they could not select themselves. Validate the requested IDs with ownerCtx so initial selection respects the chat owner's MCP ACL.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Model configs and MCP servers carry user and group ACLs that chatd re-evaluates as the chat owner, silently falling back to the default model or dropping servers the owner cannot read. Validate both under the owner's context so an inaccessible choice is rejected up front.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aa88b60818
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Organization service accounts deliberately hold no chat permissions, yet a site owner could create a chat owned by one because the creation checks ran with the caller's authority. Require the owner to be able to create the chat themselves.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a6fe8075c3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…gates The active and non-system check depended on the caller being able to read the owner's user object and ran before authorization. Look the owner up directly after the caller has proven the authority to act as them, so the check is unconditional and leaks nothing earlier.
|
@codex review |
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
kylecarbs
left a comment
There was a problem hiding this comment.
Why separate route? It looks like we could just add an OwnerID option to CreateChatRequest and it'd be sufficient?
Drop POST /users/{user}/chats and the CreateUserChat SDK method. The
owner is now an optional owner_id in the existing create request,
defaulting to the caller. The delegated path keeps the same gates: the
caller needs api_key:create for the owner, and the owner must be an
active member of the organization with chat permissions.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 62a8be3dd3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…owner An explicit zero owner_id was treated as omitted and created the chat for the caller; it is now a 400. Chat creation and the read-back after it run under the owner's context like the preflight checks already do, so a delegated request behaves exactly as if the owner had created the chat themselves.
|
@codex review |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Owners and owner-level service accounts can now start a Coder Agents chat on behalf of another user by setting
owner_idonPOST /api/v2/chats. The field is optional and defaults to the caller, so existing clients are unaffected. A delegated owner must be an active, non-system member of the requested organization; anything else returns 400Chat owner must be an active member of the organization.once the caller has passed the authorization gates below.A chat runs entirely with its owner's credentials (synthetic API key, workspace access, OIDC and provider tokens), so creating one for someone else is acting as that user. On top of org-scoped
chat:createfor the owner, the caller must hold the authority to create API keys for that user, which is what the token endpoint demands to mint their session. Org admins therefore cannot use it; site owners and owner-role service accounts can. For the same reason the workspace binding, model config, and MCP server IDs are validated as the owner rather than the caller, and the owner must themselves holdchat:create(organization service accounts do not). The initial user message is attributed to the caller throughchatd.CreateOptions.CreatedBy.An earlier revision of this PR exposed the same behavior as
POST /api/v2/users/{user}/chats; that route andcodersdk.Client.CreateUserChatwere removed in62a8be3dd32in favor of the request field. An explicit zeroowner_idis rejected with 400 rather than silently defaulting to the caller. Coverage lives inTestPostChats_OwnerID.Review record (path-based revision, gates unchanged)
1f3189898f0. The Spec axis found the workspace binding was authorized as the caller, fixed in363d45281d8with red-green coverage inWorkspaceNotAccessibleToOwner. The additional API-key gate and theCreatedByattribution go beyond the workspaces pattern and were kept deliberately for the reasons above.363d45281d8: one P2, suspended, dormant, or system users could be targeted and would get a chat that can never run because AI Bridge refuses to authorize them. Fixed inc065437e359withOwnerSuspended(red on the old head); thread replied to and resolved.c065437e359: one P2, an explicit or personal-override model config the caller can read but the owner cannot was accepted and chatd would silently fall back to the default model at run time. Fixed inaa88b608182by resolving the model config and MCP server IDs under the owner's context, withModelConfigNotAccessibleToOwner(red on the old head); thread replied to and resolved.aa88b608182: one P2, an organization service account (which deliberately holds no chat permissions) could be targeted because the chat permission check ran as the caller. Fixed ina6fe8075c30by also requiring the owner subject to holdchat:create, withOwnerWithoutChatPermission(red on the old head); thread replied to and resolved.a6fe8075c30: one P2, the active/non-system check was skipped when the caller could not read the owner's user object. Fixed in0dd8bb4dd44by moving the check behind the authorization gates with a system-restricted lookup; thread replied to and resolved. In62a8be3dd32that lookup became a singleOrganizationMembersquery, which also covers membership and excludes system and deleted users.62a8be3dd32(theowner_idrevision): two P2s. An explicit zeroowner_idwas treated as omitted and created the chat for the caller; fixed inf54b4f33d36with a 400 andOwnerIDNil(red on the old head). Creation and the read-back still ran as the caller while the preflight ran as the owner; fixed in the same commit by passingownerCtxtoCreateChat,GetChatByID, title generation, and the file-metadata fetch. That second scenario is unreachable today (only the built-inownerrole holdsapi_key:createfor others, and site-wide custom roles are refused by both the API and dbauthz), so it has no dedicated red test. Both threads replied to and resolved.Remote UAT record (path-based revision)
Remote UAT on dogfood (chat
d7442df3-693f-4403-805f-2de52f7bdbed, Xum sub-agent as critical runner) tested head21ed6b389cc, the last path-based revision. Licensed dev server, real Haiku 4.5 model, 77 logged API requests, 23 inspected screenshots. Endorsed verdict: PASS. Site owner creates a chat for member X: X sees it, gets real answers to the admin prompt and to follow-ups, and state survives reload and mid-response refresh. The creating admin gets403 Only the chat owner may send messages.and a read-only view; an unrelated member gets 404. Org admin, member, user-admin, and template-admin callers get 403; owner-role service account 201. Suspended, dormant, and service-account targets 400; owner-inaccessible workspace, model config, or MCP server 400; 300 KiB body 413. Theowner_idrevision changes only how the owner is supplied and the non-member response (400 instead of 404); the authorization and eligibility gates it exercised are unchanged and covered byTestPostChats_OwnerID.Non-blocking findings for the author's judgment, none of them regressions of this PR:
created_bycarries the admin but the frontend does not surface it.POST /chatsreturns 403 for the same organization; same asymmetry aspostUserWorkspaces.