mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
mike/move-chats-to-projects
1831
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2dc16cde0a |
Merge remote-tracking branch 'origin/main' into mike/move-chats-to-projects
# Conflicts: # site/src/pages/AgentsPage/AgentsPageLayout.test.tsx |
||
|
|
f25b6294b6 |
feat: set workspace secrets on workspace builds (#30250)
Lets clients set secrets when creating a workspace or a build. Secrets are saved with the build and are never sent to the provisioner. Part of [RELAY-7](https://linear.app/codercom/issue/RELAY-7/agent-relay-deliver-provider-credentials-via-coder-secrets-instead-of). **Stack** (design decisions in #30248) 1. #30248 Database schema, queries, and RBAC 2. #30249 Encryption at rest 3. **#30250 Set secrets on workspace builds (this PR)** 4. #30251 Deliver secrets to the agent 5. #30252 Docs 6. #30388 List a build's secrets 7. #30459 Audit a build's secrets - `secrets` is a new field on the create workspace and create build requests. - Each secret is delivered as an env var, a file, or both. `ephemeral` secrets last for one build only. - Secrets carry forward to every later build until a request replaces or removes them. - A secret set on another secret's env var or file path replaces that secret entirely. - A null `value` removes a secret, which needs `update` permission on the owner's workspace secrets. - Deleting a workspace clears its secret values. Secrets are stored but not delivered to the agent until #30251. <details> <summary>Example request</summary> ```json POST /api/v2/workspaces/{workspace}/builds { "transition": "start", "secrets": [ { "name": "openai-key", "value": "sk-...", "env_name": "OPENAI_API_KEY" }, { "name": "kubeconfig", "value": "apiVersion: v1 ...", "file_path": "~/.kube/config" }, { "name": "session-jwt", "value": "eyJ...", "env_name": "SESSION_JWT", "ephemeral": true }, { "name": "old-token", "value": null } ] } ``` </details> <details> <summary>Implementation details</summary> - Validation reuses the user secret field rules and the file path delivery policy. It rejects: - duplicate names, env names, or file paths within one request - a removal that also sets a target or `ephemeral` - In the build transaction, the builder: 1. copies the previous build's live, non-ephemeral secrets that the request does not name or take over 2. inserts the requested secrets 3. "Earlier builds' rows are cleared when the new build's job completes - Copying and clearing run as `AsWorkspaceSecretManager`, so autostart and other system-initiated builds work without secret permissions. Requested secrets are authorized as the build's actor. - Each row records `source` (`request` or `carry_forward`). - Per-build limit violations return a 400. - A completed delete in `provisionerdserver`, or an orphan delete in the builder, clears the workspace's values and keeps the rows. </details> --- *Created by Coder Agents on behalf of @Emyrk.* |
||
|
|
7aef110ca2 |
feat: add workspace secrets table, queries, and RBAC (#30248)
Adds the database table, queries, and permissions for workspace secrets: secrets set on a workspace build and delivered to the agent, never to Terraform. Each secret belongs to one workspace build. A new build copies the previous build's secrets, then the old copies lose their values but stay as history. |
||
|
|
1f99314ddf |
feat: move chats between projects through the chat update API
PATCH /api/v2/chats/{chat} now accepts project_id. A UUID moves an idle
root chat into a project of its owner and organization; uuid.Nil removes
it from its project. The project checks are the ones of chat creation,
now shared by validateChatProjectSelection.
The chatstate machine gains a SetProject transition, allowed only from
W, E0 and E1. SetFamilyProject locks the root first and then each
subagent chat, so a busy family returns 409 and the move never overlaps
a step. chatd publishes a project_change watch event for each family
member after the commit.
|
||
|
|
2b9fad8bba |
feat: add chat project sharing database layer (#30479)
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. |
||
|
|
f573ec3533 |
feat: report agent connection methods and app identities (#30467)
Agents now report how a connection was made and which app made it, so the connection log records the method and the client app separately. - Bump the Agent API to v2.13. `Connection` gains `connection_method` and `app_name`, and `type` is deprecated. - SSH and reconnecting PTY handlers set the method themselves, so a client-supplied app name can't change it. - SSH sessions report the app from `CODER_SSH_SESSION_TYPE`. Sessions without one count as SSH but report no app. - Disconnect reports repeat the connect report's method, app, and client session ID. - Coderd logs the reported method and normalized app name. For agents older than v2.13, it maps `type` as before. - Coderd rejects unknown methods instead of logging them as SSH. Closes CLIENT-1037 |
||
|
|
0ea3f16dd3 |
fix(coderd/database/migrations): renumber MCP OAuth2 resource migration to 000622 (#30722)
Main is red: #30625 and #30651 both added a migration numbered 000618,
and the
loader refuses to start with a duplicate number. See the failing run on
|
||
|
|
fa7cd62c8b |
feat: send RFC 8707 resource parameter on MCP server OAuth2 flows (#30651)
Coder's MCP OAuth2 client never sent the RFC 8707 `resource` parameter, which the MCP authorization spec requires on authorization and token requests. Authorization servers that enforce it cannot be connected: with Dagster+, discovery and dynamic client registration succeed, then the authorize request fails with "MCP OAuth requests must include resource". MCP server configs get an `oauth2_resource` field (migration 000618, admin-only in API responses, audited). Coder sends it on the authorize redirect, the authorization-code exchange, and token refresh. Refresh now goes through `clientcredentials` with `grant_type=refresh_token`, because `oauth2.Config` cannot add parameters to refresh requests; it keeps the same auth-style detection and error types, and adds no dependency. Auto-discovery fills the field with the most specific Protected Resource Metadata `resource` that covers the server URL, stored as advertised. As in the MCP SDKs, a resource covers URLs on its origin at or under its path, so `/mcp` covers `/mcp/` but `/mcp/` does not cover `/mcp`. URLs compare in RFC 3986 normal form (default ports, percent-encodings, dot segments, and an empty path as `/`), ports ignore leading zeros, IPv6 literals compare by address, and `%2F` stays inside its segment. Unlike the SDKs, a resource with a query, even an empty one, only covers a server URL with the same query. When none covers it, Coder uses the server URL with the scheme and host lowercased and user information and fragment removed, or sends no resource when that is not a valid resource indicator. An explicit value in the create request wins. Manual configurations send no `resource` unless an admin sets one, because some providers reject it (Microsoft Entra ID v2 fails with AADSTS901002). Values must be absolute RFC 3986 URIs without a fragment, user information, or an IPv6 zone identifier. The resource binds grants like the other OAuth settings: changing it deletes stored user tokens, and a callback whose config resource changed after the user started connecting returns 409 before the code is redeemed. The connect step records the resource in a callback-scoped cookie for that check. Discovery only runs when a configuration is created, so existing auto-discovered configurations, including the Dagster+ one that prompted this, keep an empty resource until an admin sets the new "Resource URL" field or re-creates the server.  ## Testing - Go: discovery (most specific match, path and trailing-slash boundaries, query matching including empty queries, equivalent ports, IPv6 literals, escapes, and dot segments including escaped ones, the normalized fallback and an invalid one, explicit override), validation (URI characters, brackets, IP literals, user information, trimming), grant deletion on change, clearing on an auth type change, redaction for members, refresh with and without a resource, the 409 when the resource changes mid-flow (the authorization server never receives the code), and a connect and callback flow against an authorization server that rejects requests without the resource. Each new test was checked to fail when the behavior it covers is broken, except two malformed IP literal hosts that Go's URL parser already rejects. - Vitest: the form round-trips the stored value, and clearing the field sends `""` on update. - Remote UAT through Coder Agents ([chat](https://dogfood.cdr.dev/agents/58695d5b-2112-400b-88f7-ab64c678f756)) on a test deployment with loopback MCP and authorization server mocks: - Round 1 on |
||
|
|
2c432a12a3 |
feat: add organization skill sharing ACL routes (#30611)
## Stack Part of [CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents). This stack adds organization skills to Coder Agents: admins publish a skill once per organization, share it with groups or members, and chats in that organization load it next to personal and workspace skills. It replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to top: 1. #30609 data model: one migration, queries, RBAC and dbauthz 2. #30610 organization skill API routes and the scope-neutral SDK 3. **#30611** sharing ACL routes (this PR) 4. #30612 settings UI: shared skills table, admin page, member section 5. #30613 chats: chatd loading, `/` menu, docs ## What changes This PR adds the routes that let admins choose who can read an organization skill. It reuses the MCP server ACL machinery rather than adding a second implementation. Routes under `/api/experimental/organizations/{organization}/skills/{skillName}`, all behind `httpmw.ExtractOrganizationSkillParam` with `share`, so a caller without share gets 404: - `GET /acl` returns the resolved user and group grants. - `GET /acl/available` returns the organization's users and groups that can be granted, with the same `q`, `after_id`, `limit` and `offset` paging as the MCP endpoint. - `PATCH /acl` takes a sparse `UpdateOrganizationSkillACLRequest`: only the listed principals change, `read` grants and `""` removes. It returns 204 and writes a `write` audit entry with the old and new ACL. **Concurrency (please review closely):** the PATCH transaction first locks the granted users' `organization_members` rows `FOR KEY SHARE` (new query `LockOrganizationMembersByUserIDsForShare`, run as system), then locks the skill row `FOR UPDATE`, then validates the update on the transaction handle. Member removal takes the same locks in the same order (its membership row, then the skill rows through the #30609 trigger), so the two can't deadlock, and a removal can't miss a grant written by a concurrent PATCH. A skill deleted between the middleware read and the row lock stays a 404. Grants are users and groups, the same as MCP server configs (Mike's decision, for parity). Only the `read` role exists; any other role is a validation error, as are users or groups outside the organization. Supporting changes: - `coderd/mcp_acl.go`: the MCP ACL handlers' resolution, candidate listing and validation move into generic helpers (`writeOrganizationACLAvailable`, `validateOrganizationACLUpdate`, `resolveACLUsers`, `resolveACLGroups`) shared by both resources; `acl.Validate` accepts a named `acl.Role` constraint that now includes `codersdk.OrganizationSkillRole`. - `codersdk/skills.go`: `OrganizationSkillRole`, `OrganizationSkillACL` with its user and group entries, `UpdateOrganizationSkillACLRequest`, and client methods. - New sqlc query `UpdateOrganizationSkillACLByID` with its dbauthz wrapper (requires `share`). ## Commits The first commit reproduces the reviewed code from the old stack; each later commit is a labeled trim or fix. - `b1c85560b69` feat: add organization skill sharing ACL routes. Reproduction: every file this commit changes equals the reviewed old `acl-routes` head `80c1c3594e6`; the rest of the tree differs only by #30609's migration merge and main. - `b5d2ee265c8` test(coderd): merge duplicate organization skill ACL route tests (+49/-111). The removed cases repeated `TestMCPServerConfigACL`, `TestMCPServerConfigACLAvailable` or `TestRolePermissions`; with the MCP tests included, the covered block set of `organizationskills_acl.go`, `mcp_acl.go` and `rbac/acl/updatevalidator.go` is identical before and after. Kept: `TestOrganizationSkillACL` (`OrgAdminManagesACL`, `ReadersGetNotFound`, `SparseUpdateChangesReaders` with the audit diff, `RejectsUnsupportedRole`, `ScopedAPIKeys`), `TestOrganizationSkillACLConcurrentDelete`, and the lock order tests `TestOrganizationSkillACLMemberRemoval` (`RemovalBeforeGrant`, `RemovalAfterGrant`). `OrgAdminManagesACL` now also checks that users from another organization are not candidates (mutation checked). Sharing the param extractor with MCP was skipped: it would save about 10 lines of security-sensitive code. - `e09c4dba66b` Merge the updated org-api layer into sharing. Brings in #30610's `ad0a7a37504`; the merge has no changes of its own. - `1d3817dc639` and `c8c88f84d18` merge the updated #30610 layer, including main; no changes of their own. - `c4d22c47990` merges the updated #30610 layer (main at `e7b76872098`); no changes of its own. ## Size Production +431/-102, tests +377/-0, generated +918/-0; total +1726/-102. This is above the 500-line reviewer-friendly limit on purpose: Mike asked for 4 to 5 PRs instead of the 21 small ones this stack replaces. ## Replaces #30591 (ACL queries), #30592 (ACL routes). ## Validation - `go build` and `go vet` passed; `make gen` exited 0 with no diff. - `go test` passed for `coderd` (skill, MCP and ACL tests), `coderd/database`, `dbauthz`, `migrations`, `gentest`, `coderd/rbac/...`, `coderd/httpmw`, `coderd/x/skills/...`, `coderd/x/chatd/...`, and the audit and skill tests in `enterprise/audit` and `enterprise/coderd`. - The covered block comparison above, before and after the test merge. - On `c4d22c47990`: the same checks as #30610's head, plus `go test ./coderd/ -run 'Skill|MCP|ACL'`, pass. - Remote UAT round 7 through Coder Agents passed on the whole stack; see #30613's Validation for scope and findings. > Xum opened this PR on behalf of @ibetitsmike. |
||
|
|
db7f5a7703 |
feat: add organization skill API routes (#30610)
## Stack Part of [CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents). This stack adds organization skills to Coder Agents: admins publish a skill once per organization, share it with groups or members, and chats in that organization load it next to personal and workspace skills. It replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to top: 1. #30609 data model: one migration, queries, RBAC and dbauthz 2. **#30610** organization skill API routes and the scope-neutral SDK (this PR) 3. #30611 sharing ACL routes 4. #30612 settings UI: shared skills table, admin page, member section 5. #30613 chats: chatd loading, `/` menu, docs ## What changes This PR adds the organization skill CRUD routes and makes the skill SDK types owner-neutral so personal and organization skills share them. **API contract change (experimental API, personal and organization skills):** skill list and get responses gain `enabled`, and `PATCH .../skills/{skillName}` now takes `{content?, enabled?}`, so `content` is optional and a toggle doesn't resend it. A PATCH with neither field returns 400; `content`, when present, is validated as before. In the SDK, `UserSkill`, `UserSkillMetadata`, `CreateUserSkillRequest` and `UpdateUserSkillRequest` become `Skill`, `SkillMetadata`, `CreateSkillRequest` and `UpdateSkillRequest` (`codersdk/userskills.go` moves to `codersdk/skills.go`), and `UpdateSkillRequest.Content` is now `*string`. The personal client method names are unchanged. The site changes are the handwritten edits that rename forces (`api.ts`, `queries/userSkills.ts`, `testHelpers/skills.ts` and three call sites), with no UI change. Organization routes under `/api/experimental/organizations/{organization}/skills`, not gated by an experiment or license, like the personal ones: - `GET` lists with the caller's own permissions, so admins, the site owner and org auditors see every skill and members see only skills shared with them. `GET /{skillName}` goes through the new `httpmw.ExtractOrganizationSkillParam` and admits only read, so a missing skill and a denied one both return 404. - `POST` checks `create` on the organization before reading the body, then seeds `group_acl` with the Everyone group (whose ID is the organization ID) so a new skill reaches every member until an admin narrows it. The 100-skill cap and a duplicate name return 409. - `PATCH` admits only callers who can read the skill, because its response includes the content; `DELETE` admits callers who can read or delete it. A reader who can't change the skill gets a write-specific 403 from the database wrapper, and everyone else gets 404. PATCH locks the row as system, updates as the caller in the same transaction, and audits the locked row only after the transaction commits. - Audit: `database.AuditableOrganizationSkill` records resource type `organization_skill` with the organization ID; personal skills keep `user_skill`. Personal and organization handlers share one create and update path in `coderd/userskills.go` (`createSkill`, `updateSkill`, and a `skillWriteErrors` table per owner and action). ## Commits The first commit reproduces the reviewed code from the old stack; each later commit is a labeled trim or fix. - `49cb2c84276` feat: add organization skill API routes. Reproduction: every file this commit changes, including the SDK layer's site edits, equals the reviewed old `write-routes` head `aa597100f28`; the rest of the tree differs only by #30609's migration merge and main. - `dc893bc60d6` refactor(coderd): share the skill create and update path across owners (production +164/-188). It keeps the organization create check before the body read, the lock as system then update as caller order, and audit assignment after the transaction. No swagger, `codersdk` or `typesGenerated.ts` change; `make gen` is clean. - `f94925425ef` test(coderd): merge duplicate personal and organization skill tests. `TestSkillsCRUD`, `TestSkillValidationAndConflicts` and `TestSkillAudit` are table-driven over Personal, OrgAdmin and SiteOwner, alongside `TestOrganizationSkillAuditsDeniedWrites` and one `ScopedAPIKeys` subtest; `TestPatchUserSkill` is deleted because none of its blocks were unique. Coverage of `userskills.go`, `organizationskills.go` and `httpmw/organizationskillparam.go` is 101 of 117 blocks before and after, the same block set. - `ad0a7a37504` fix(coderd): require read access to patch an organization skill. The organization PATCH route now admits only `read`, because its response carries the skill's content: a key scoped to `organization_skill:update` without read gets 404, like GET, instead of 200 with the content. Readers without update still get 403, and DELETE is unchanged. `TestOrganizationSkillAccess/ScopedAPIKeys` asserts the 404, that the response has no content, and that the skill is unchanged; against the old route it fails with 200. - `69d00ded0b6` and `f67b79343ec` merge the updated #30609 layer, including main; `69d00ded0b6` resolves a conflict in the personal skills page with main. No other changes of their own. - `18373ffd59e` merges the updated #30609 layer (main at `e7b76872098`) and reapplies the `AuditableOrganizationSkill` entry in `enterprise/audit/table.go` alongside main's new `AuditableChatProjectInstructions` entries. ## Size Production +838/-308, tests +733/-322, generated +801/-274; total +2372/-904. This is above the 500-line reviewer-friendly limit on purpose: Mike asked for 4 to 5 PRs instead of the 21 small ones this stack replaces. ## Replaces #30587 (scope-neutral SDK and personal enabled state), #30588 (read routes), #30589 (write routes and audit). ## Validation - The coverage comparison above, before and after the test merge. - `make gen` leaves no diff after the shared create and update refactor. - On `18373ffd59e`: `make gen` is clean; build, vet, the `coderd` and `enterprise/coderd` skill tests, `rbac`, `httpmw`, `dbauthz` and `enterprise/audit` pass; `pnpm check`, `pnpm lint` and `make pre-commit` pass. - Remote UAT round 7 through Coder Agents passed on the whole stack; see #30613's Validation for scope and findings. > Xum opened this PR on behalf of @ibetitsmike. |
||
|
|
b67587c875 |
feat: add the organization skills data model (#30609)
## Stack Part of [CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents). This stack adds organization skills to Coder Agents: admins publish a skill once per organization, share it with groups or members, and chats in that organization load it next to personal and workspace skills. It replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to top: 1. **#30609** data model: one migration, queries, RBAC and dbauthz (this PR) 2. #30610 organization skill API routes and the scope-neutral SDK 3. #30611 sharing ACL routes 4. #30612 settings UI: shared skills table, admin page, member section 5. #30613 chats: chatd loading, `/` menu, docs ## What changes This PR adds the data model and authorization for organization skills. Personal skills keep working unchanged: `git diff origin/main -- codersdk site` shows only generated RBAC and API key scope additions (`apikey_scopes_gen.go`, `rbacresources_gen.go`, `rbacresourcesGenerated.ts`, the RBAC and scope unions in `typesGenerated.ts`) and `codersdk.ResourceTypeOrganizationSkill` with its friendly string. No personal route, SDK type or site component changes, and `coderd/userskills_test.go` changes only by mechanical constant renames. **One migration, `000621_skills`** (please review it closely): - renames `user_skills` to `skills` with its constraints, index and triggers, and recreates the cap function and `delete_deleted_user_resources`, whose PL/pgSQL bodies name the table; - adds `organization_id` and `project_id` (`ON DELETE CASCADE`), `enabled` (default true), and `group_acl` / `user_acl` (`jsonb`, default `{}`), with CHECKs for exactly one owner, ACLs that are JSON objects, and ACLs that stay empty unless an organization owns the row, plus partial unique name indexes per owner kind; - replaces the cap trigger with `enforce_skills_per_owner_limit()`: 100 rows per owner, disabled rows included, locking the owner row `FOR NO KEY UPDATE` so unrelated foreign key checks are not blocked; - adds a `BEFORE DELETE` trigger on `organization_members` that removes a departing member from `user_acl` on that organization's skills, so rejoining doesn't restore a direct grant; - adds the `organization_skill` `resource_type` and `api_key_scope` enum values last; nothing in the migration uses them. The down migration undoes the three old migrations in reverse order: it drops organization and project rows and restores `user_skills`, its names, the cap function and `user_id NOT NULL`. Authorization: a new RBAC resource `organization_skill` (create, read, update, delete, share). Owner and org admin get it; site and org auditors get read; members read only through the skill's ACL, which applies only to members of its organization. `Skill.RBACObject()` returns the organization object with both ACLs; project rows stay denied to everyone. New sqlc queries cover organization insert, get, list (SQL-filtered through `GetAuthorizedOrganizationSkillMetadata` and a `regosql` converter), update and delete, with dbauthz wrappers and the audit table entries. No routes yet. ## Commits The first commit reproduces the reviewed code from the old stack; each later commit is a labeled trim or fix. - `baa3150e3b7` feat: add the organization skills data model. Reproduction: every non-site file this commit changes equals the reviewed old `authz` head `1e16b3004c4`, except that the old stack's migrations 000615, 000616 and 000617 are merged into one migration (then `000615_skills`, now `000621_skills`) with one fixture (`testdata/fixtures/000621_skills.up.sql`) and one migration test (now `testMigration000621Skills`). At that commit, `dump.sql` was byte-identical to the old head's. - `1b84e83463a` test(coderd/database): keep only the SQL paths of the org skill matrix. `TestOrganizationSkillAuthorization` becomes `TestOrganizationSkillQueries` instead of being deleted, because coverage showed it is this PR's only run of the `GetAuthorizedOrganizationSkillMetadata` row scan (80% to 65% without it) and of the organization update and delete queries (100% to 0%). Swapping `group_acl` and `user_acl` in the converter makes `GrantedMember` fail. The removed cases are decided by `TestRolePermissions`, `TestSkillRBACObjectOrganizationACL` and the dbauthz method suite. - `e2931b412e9` test(coderd/database): fold the skill cap cases into existing coverage. The user case is covered by `TestUserSkillLimit` and `TestUserSkillLimitConcurrentCreates`; the organization case folds into `ConcurrentOrganizationInsertsHonorCap`, which now also checks that disabled rows count and another organization is unaffected (mutation checked); the project case stays. - `be62aea9021` test(coderd/rbac): drop `TestOrganizationSkillReadScope`, covered by `TestExternalScopeNames` and the generated scope file. - `0c275ba37de` merges main at `454a2fcc697`, which took 000615 to 000618: it renumbers the migration to 000619 and resolves `dump.sql`, `check_constraint.go` and the generated API docs. `8f2b4b87ee2` merges main at `ad85646164e`, which took 000619: it renumbers the migration to 000620. - `5248d56b64f` merges main at `e7b76872098`, which took 000620 (`000620_audit_chat_project_instructions_resource_type`): it renumbers the migration, fixture and migration test to `000621_skills` (step `Migration000621Skills`, version 621), and resolves `enterprise/audit/table.go` (keeping main's `AuditableChatProjectInstructions` entries and the `Skill` entry), `dump.sql` and `models.go` (the `resource_type` enum gets `chat_project_instructions`, then `organization_skill`) and the regenerated `schemas.md`. The up and down SQL are unchanged by every renumbering. ## Size Production +549/-67, tests +728/-33, generated +881/-314; total +2158/-414. This is above the 500-line reviewer-friendly limit on purpose: Mike asked for 4 to 5 PRs instead of the 21 small ones this stack replaces. ## Replaces #30584 (rename `user_skills` to `skills`), #30585 (skill owners), #30586 (organization skill authorization). ## Validation - Migration test: up keeps the existing personal row and its ID; down restores the `user_skills` names, cap function and `NOT NULL`, and drops organization rows; soft-deleting a user removes their skills after up, after down, and after re-up. - `go test` passed on `be62aea9021`, before the main merges, for `coderd` (skill tests), `coderd/database`, `dbauthz`, `migrations`, `gentest`, `coderd/rbac/...`, `coderd/x/chatd`, `coderd/x/skills`, `enterprise/audit` and `enterprise/coderd`; `make lint/check-scopes` passed; the four personal skills Vitest files pass unchanged. - On `5248d56b64f`: build, vet, `TestMigrate`, `TestMigrationChain`, the `migrations`, `database`, `dbauthz`, `gentest`, `rbac` and `enterprise/audit` packages pass; `make gen` run twice is clean, and `make pre-commit` passes. - Remote UAT round 7 through Coder Agents passed on the whole stack; see #30613's Validation for scope and findings. > Xum opened this PR on behalf of @ibetitsmike. |
||
|
|
97c5260adb |
feat: add chat project instructions API (#30368)
Adds the experimental API for chat project instructions: `GET`, `PUT`,
and `DELETE
/api/experimental/organizations/{organization}/chats/projects/{project}/instructions`,
behind the `chat-projects` experiment.
- `GET` returns 200 with empty instructions and null
`updated_by`/`updated_at` when nothing is set, so clients don't need a
404 path.
- `PUT` requires update access only (the read-back runs under a system
context, so a custom role with update but not read can save). It
sanitizes the text with `SanitizePromptText`. It rejects blank text with
400, pointing the caller to `DELETE`, and rejects text over the chat
prompt byte limit. It records the caller as `updated_by`. The upsert and
the read-back that returns the editor's profile run in one transaction.
- `DELETE` returns 204, including when nothing is set.
- Users without access to the project get 404, like the other project
routes.
- `PUT` and `DELETE` are audited as a new `chat_project_instructions`
resource (`write` and `delete`), with the project as the resource ID and
its name as the target. The diff tracks only the instruction text.
Saving unchanged text and deleting unset instructions are not audited. A
per-project advisory lock serializes reading the previous value with the
write, so concurrent edits produce accurate diffs. Migration `000615`
adds the enum value.
The codersdk client gains `ChatProjectInstructions`,
`UpdateChatProjectInstructions`, and `DeleteChatProjectInstructions`.
**What to scrutinize:** writes are last-writer-wins with no version
precondition, matching the deployment, user, and organization prompt
settings.
**Generated:** `coderd/apidoc/docs.go`, `coderd/apidoc/swagger.json`,
and `docs/reference/api/schemas.md` come from the Swagger annotations;
`site/src/api/typesGenerated.ts` comes from `codersdk`;
`docs/admin/security/audit-logs.md`, `coderd/database/dump.sql`, and
`coderd/database/models.go` come from the audit table and migration.
Together they are about 400 of this PR's lines.
**Validation:** `make gen` leaves no diff. `go test ./coderd -run
TestChatProject` and `go test ./enterprise/coderd -run
TestChatProjectInstructions` pass. `TestChatProjectInstructionsAudit`
checks the audit entries for create, update, and delete, and that no-op
saves and deletes are not audited. It covers unset GET, create, update,
blank and oversized PUT, delete, access denial, and cascade on project
delete.
---
Stack (merge top to bottom). #30377 through #29583 are f0ssel's split of
the original #29583, and this stack sits on #29583:
- #30377 data layer and project dialog (f0ssel)
- #30378 new-chat form locked to a project (f0ssel)
- #30379 project page route (f0ssel)
- #29583 sidebar folders (f0ssel), the base of this stack
- #30291 composer frame
- #30292 project chat list caches
- #30293 project page header
- #30294 project chats list
- #30367 instructions storage and prompt injection
- #30368 instructions API 👈
- #30369 instructions editor dialog
- #30370 instructions Details panel
---------
Co-authored-by: Garrett Delfosse <delfossegarrett@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
||
|
|
ad85646164 |
feat(coderd): add chat project instructions storage and prompt injection (#30367)
Adds storage for project-level instructions in Coder Agents chat projects and injects them into every chat in the project. This is the first of four stacked PRs that replace #30349. A new `chat_project_instructions` table (migration 000613) holds at most one row per project, scoped to the project's organization. It records `updated_by` and cascades on project delete. The dbauthz wrappers authorize against the parent project: read access to get the instructions, update access to upsert or delete them. No new RBAC resource is added; project RBAC comes in a later PR. chatd loads the instructions on every turn, so edits reach existing chats on their next turn. They are added to the system prompt as a `<project-instructions>` block after the memory guidance and before personal instructions, so a user's personal instructions still win a conflict. Subagent chats carry no `project_id`, so they use their root chat's project. A load error other than no rows is logged, and the turn continues without instructions. **What to scrutinize:** the prompt ordering in `buildSystemPrompt`, and the choice to inject per turn rather than snapshot at chat creation. Per-turn injection costs one prompt-cache miss after each edit; the org instructions in #30272 use a creation-time snapshot instead. **Generated** (`make gen` from the migration and `queries/chatprojectinstructions.sql`): `dump.sql`, `models.go`, `querier.go`, `queries.sql.go`, `dbmetrics/querymetrics.go`, `dbmock/dbmock.go`, and the `*_constraint.go` files. `coderd/x/chatd/ARCHITECTURE.md` gets two TODOs for the PR author to replace with the real description, per AGENTS.md. **Validation:** `make gen` leaves no diff. `go test ./coderd/database/dbauthz -run TestMethodTestSuite/TestChats`, `go test ./coderd/x/chatd -run 'SystemPrompt|ProjectInstructions|Memory'`, and `go test ./coderd/database/migrations/...` pass. --- Stack (merge top to bottom). #30377 through #29583 are f0ssel's split of the original #29583, and this stack sits on #29583: - #30377 data layer and project dialog (f0ssel) - #30378 new-chat form locked to a project (f0ssel) - #30379 project page route (f0ssel) - #29583 sidebar folders (f0ssel), the base of this stack - #30291 composer frame - #30292 project chat list caches - #30293 project page header - #30294 project chats list - #30367 instructions storage and prompt injection 👈 - #30368 instructions API - #30369 instructions editor dialog - #30370 instructions Details panel --------- Co-authored-by: Garrett Delfosse <delfossegarrett@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
dae7967bd1 |
feat: persist script resource addresses and dependencies (#30625)
feat(coderd): persist workspace agent script dependencies Persist Terraform resource addresses and script dependencies supplied by the provisioner (#30451, #30491), and expose them through the SDK and REST API. - Add migration 000618 with `resource_address` and `dependencies` columns, empty defaults, and a JSON-array constraint. API responses always include the address and omit empty dependency lists. - Preserve `success` and `completion` requirements, including scripts on devcontainer subagents. Legacy scripts and older provisioners remain compatible through empty defaults. - Reject empty prerequisite addresses and unsupported requirements, rolling back the entire job. Invalid stored dependencies return HTTP 500 rather than silently dropping ordering. - Add persistence, validation, rollback, devcontainer, API, and migration coverage; regenerate API docs and TypeScript types. Unordered templates retain their existing execution behavior. Dependency lists remain empty until #30556. Agent manifest wiring and the agent API version bump are deferred to a follow-up. Refs PLAT-548. |
||
|
|
2f2169ed19 |
fix: deduplicate template insights family metrics (#30497)
coderd_insights_applications_usage_seconds summed each family's per-app minutes, so a user with VS Code and Cursor open in the same minute was counted as two VS Code minutes. GetTemplateInsightsByTemplate now takes the registry's app-to-family mapping as jsonb, groups the raw agent stats by family, and counts each minute once per user and family. - Keep the metric's four built-in series (Visual Studio Code, JetBrains, Web Terminal, SSH) and their labels; only their values change. The HELP text now says built-in apps are grouped by family. - Add AppFamilyName.DisplayName() and remove the duplicate TemplateBuiltinAppDisplayName* constants from codersdk. The template insights handler returns the same display names. - Pin how the migration 000596 rollback approximates family minutes from per-app rows. Family values can drop wherever the old per-app sum double-counted overlapping apps. No migration or storage changes. |
||
|
|
5b060beeb1 |
feat: separate connection log methods from app names (#30466)
Connection logs now record how a connection was made (`connection_method`) separately from which app made it (`app_name`). Storage, the HTTP API, search filters, and the UI move to the new fields. Legacy agent reports and the deprecated `type` field keep working. - Add `connection_method` (`ssh`, `reconnecting_pty`, `workspace_app`, `port_forwarding`, `tunnel`) and optional `app_name` and `app_display_name` to `GET /api/v2/connectionlog`. Deprecate `type` and make it a plain string in the API and the Go SDK. - Add `method:` and repeatable `app:` search filters. `type:` keeps its old results but can't be combined with the new filters. - Replace the Type menu with a Method menu, show the app next to the method in each row, and warn when a filter uses `type:`. - Change the Active SSH connections preset to `method:ssh`, so it also includes SSH sessions from VS Code and JetBrains apps. - Generate `codersdk.ConnectionLogMethod` from the database enum with `typegen dbenums`. - Migration 000617 rewrites `connection_logs` under an ACCESS EXCLUSIVE lock, which takes about 50 seconds for 10 million rows. `vscode` and `jetbrains` rows become `ssh` rows with that app. Rolling back folds apps into the old types and loses per-app names. - Document the new filters and how to replace each `type:` value. Closes #27412 |
||
|
|
f923f4a525 |
chore!: remove the chat automations experiment (#30412)
Removes the experimental Chat Automations feature (`chat-automations` experiment) from main. This reverts the 16-PR stack that added it and adds two forward migrations that undo its schema and credential changes.
> [!WARNING]
> **Upgrade consequences**
>
> - **Automation data is deleted.** Migration 000615 drops the `chat_automations` table and deletes every queued message an automation delivered. Chats, their messages and ordinary queued messages stay. Messages lose their automation provenance (`automation_id`, `input_id`).
> - **Automation-only credentials are revoked.** Migration 000616 deletes API keys, OAuth2 codes and OAuth2 tokens whose only scopes (or allow-list entries) were `chat_automation:*`. Mixed credentials keep their other scopes. Custom roles lose their `chat_automation` permissions. OAuth2 apps that allowed only `chat_automation` scopes get the whitespace allowlist that grants nothing, as in 000595.
> - **A coordinated restart is required.** Stop all old replicas before the new version runs the migrations. Old replicas read the dropped table and columns, so a rolling deploy is not supported.
> - **Remove `--chat-max-automations-per-owner` before upgrading.** The flag, the YAML key `chat.maxAutomationsPerOwner` and the env var `CODER_CHAT_MAX_AUTOMATIONS_PER_OWNER` are removed. A deployment that still sets the flag or the YAML key will not start (`unknown flag` / `unknown option`). The env var is ignored. This includes dogfood. No release or tag contains the flag.
> - **Down migrations restore the schema only.** 000616 down and 000615 down recreate the 000611 schema with empty tables. They do not restore deleted automations, webhook secrets, provenance, queued deliveries, revoked credentials, stripped permissions or the deleted experiment rule.
## What is removed
- The `chat-automations` experiment and its stored runtime rule (`experiment_rule:chat-automations`).
- The `--chat-max-automations-per-owner` deployment option.
- The `chat_automation` RBAC resource, its member and chatd grants, and the `chat_automation:*` API key scopes.
- The automation sqlc queries, dbauthz wrappers and audit table entries.
- The management routes under `/api/experimental/organizations/{organization}/chat-automations`, and the webhook route `POST /api/experimental/chat-automations/{automation}/events` with its rate limiter.
- The chatd management service, webhook publish, schedule loop, the `manage_automations` tool, and the chatstate admission seam and queue promotion guard that only automations used.
- The Agents Automations page, editor, nav entry, chat labels, the Manage automations toggle and the API client.
- The `archived:any` chat search value that the automations "View chats" dialog added.
- `docs/ai-coder/agents/automations.md`, its manifest entry, the glossary entry and the related rows.
Reverted PRs: #30085, #30096, #30112, #30117, #30127, #30134, #30160, #30166, #30167, #30171, #30187, #30207, #30214, #30217, #30219, #30220.
## What stays, and why
- Migrations 000609, 000610 and 000611, their fixture, and the #30297 lock fix with `migration000609_test.go` stay unchanged. Deployments such as dogfood already ran them.
- New migration **000615** reverses the schema changes of 000609 and 000611 against the current schema. It uses the same short-wait lock-and-retry block as 000609, and it rebuilds `chats_expanded` from main's current definition minus the two automation columns. New migration **000616** follows the 000595 Tasks removal for credentials and recreates `api_key_scope` without the five `chat_automation:*` values. It keeps the `chat_project_memory:*` values from 000612.
- The `chat_automation` value stays in the `resource_type` enum, and `codersdk.ResourceTypeChatAutomation` stays. Old audit log rows stay readable, as with `task` after the Tasks removal.
- The 000615 delete fires the existing queue-version trigger, so `queue_version` moves to `snapshot_version` for each affected chat. `snapshot_version` is not bumped: the coordinated restart restarts every stream loop from an empty state, and the site does not read the version fields.
- `coderd/x/chatd/ARCHITECTURE.md` drops the sections the stack added and gets back its pre-stack wording, merged with the later main edits to the same file.
## Notes for reviewers
- #30379 (chat project page) merged while this PR was in progress. It passed `manageAutomationsEnabled` in `AgentCreatePage.test.tsx` and listed an `automations` route in `AgentsPageLayout.test.tsx`. Later main commits also added automation props in `ConversationTimeline.test.tsx` and `ConversationTimelineWorkingBlocks.stories.tsx`. This PR removes all of them.
- Remaining `automation` matches under `coderd`, `codersdk`, `enterprise`, `site/src` and `docs` are intended:
- migrations 000609 to 000611, their fixture and `migration000609_test.go`;
- the new 000615 and 000616 migrations and `migration000615_test.go`;
- the `chat_automation` resource type in `dump.sql`, `models.go`, `codersdk/audit.go`, the generated API docs, `swagger.json` and `typesGenerated.ts`;
- unrelated uses of the word "automation" (docs, `chatlabels_test.go`, `ratelimit.go`, `autostop.go`, `subagent_catalog.go`, `WorkspaceSharingSection.tsx`, `AgentChatPage.stories.tsx`).
Remote UAT on an isolated dev deployment passed. It upgraded a populated pre-removal database, then checked chats, queued messages, tokens, the 404 routes, the removed page and nav entry, and the experiments list.
---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

|
||
|
|
86ebc72236 | chore: remove unused autofill-parameters endpoint and experiment (#30525) | ||
|
|
d28a433b23 |
perf(coderd/database): index chat message pagination (#30562)
Loading a chat's history can take seconds when its messages sit far from the end of the `chat_messages` id space, because no index returns one chat's live messages in id order. Given the `LIMIT`, the planner chooses `chat_messages_pkey` instead and filters by `chat_id`, so it reads other chats' messages until it reaches this chat's. On dogfood, the newest page of one idle long chat took 21 s: `Index Scan Backward using chat_messages_pkey` discarded 614,776 rows and did 78,749 disk reads. Over the past 7 days, 108 of 33,745 `GetChatMessagesByChatIDDescPaginated` calls took more than 1 s, and 10 took more than 10 s. `GetChatMessagesByChatIDAscPaginated` has the same problem in the other direction: for `after_id = 0, limit 1`, which chatd uses as a first-turn probe, dogfood's plan scans forward from the table's oldest row. 305 of its 2,863 calls took more than 1 s. This PR adds migration 000614: `CREATE INDEX idx_chat_messages_chat_id ON chat_messages (chat_id, id DESC) WHERE deleted = false`. No queries change. **Migration cost:** a plain `CREATE INDEX` inside the migration transaction, which blocks writes to `chat_messages` until the upgrade commits. Reads continue. On a 1.01M-row fixture (641 MB heap) the build took 0.8 s and the index is 39 MB. Dogfood has 5.85M rows and a 5.7 GB heap, so expect seconds and about 230 MB. ## Testing Remote UAT through Coder Agents (round 1 PASS) on the 1.01M-row fixture, Postgres 13.21. It ran base |
||
|
|
782fb30f09 |
feat: load whole chat turns per history page (#30524)
Closes [CODAGT-1372](https://linear.app/codercom/issue/CODAGT-1372/load-whole-turns-per-history-page-so-collapsed-working-blocks-fill-in). With collapsed working blocks, a long agent turn renders as one row, but chat history pages hold a fixed 50 messages. A 400-step turn took about eight sequential page loads to fill, and its block showed partial labels ("at least", "or more") until then. `GET /api/v2/chats/{chat}/messages` now returns `turn_start_id` on newest-first pages: the ID of the user prompt that starts the turn containing the page's oldest message. It comes from a new `GetChatTurnStartID` query on the existing `idx_chat_messages_user_prompts` index, so there is no migration. Soft-deleted prompts and model-only user rows never start a turn. The history query then fills each page back to that prompt with requests of up to 200 messages, capped at 1000 messages per page, so a page holds whole turns and a complete block renders without partial labels. Past the cap, or when a fill request fails or returns no messages, the page keeps what loaded and ends mid-turn, and the next page continues it, as before. Because the fill runs inside the page fetch, a chat now waits for its fill requests (one to five) before it first renders: with an injected 500 ms per-request delay, a chat whose newest turn hits the cap appeared after 3.8 s instead of 1.1 s. ## Screenshots From the dogfood UAT, the same 200-step turn with its next history request held, so each label shows only what loaded before it: **Before**, on the base commit `29e876df4da`, after two 50-message pages: "Worked for at least 1m 35s (48 steps or more)", partly hidden by the loading indicator.  **After**, on `186a9b053a0`, after the first page: "Worked for 6m 39s (200 steps)".  A 750-step turn, larger than one 1000-message page, is exact once the next page loads:  A malformed fill response keeps the loaded messages and the partial label instead of showing "Failed to load chat", as `ecdfb27ec29` did:  ## Testing - Go: `TestGetChatMessages_TurnStartID` (pages spanning turns, deleted prompts, model-only user rows, polling and empty pages) and the dbauthz suite entry. - Vitest: fill request parameters, the 1000-message cap, failed, malformed, and empty fill responses, and servers without `turn_start_id`. - Dogfood UAT through Coder Agents: PASS on `186a9b053a0` (round 4): whole-turn first pages, the 1000-message cap and its continuation, fill failures, a live turn, and API checks for deleted prompts and model-only user rows. Round 3 on `ecdfb27ec29` failed: a malformed fill failed the chat load, and an empty fill reporting `has_more` repeated forever. `7266f468290` fixes both, with regression tests. ## Review record - Test audit with mutation checks: the Go test now covers a page spanning two turns, which catches a lookup keyed on the newest message. A re-audit of later test changes changed nothing; the one surviving mutation drops the `!older.has_more` stop, costing one extra request when the prompt is deleted mid-fill. - Frontend review (8 roles, 5 rounds): fixed doc, naming, loose-type, and test findings. Its suggestions to catch only the fill request (DF5) and drop the empty-page stop (SD4) caused the round 3 failures and are reverted. Kept: SD1 (`MAX_MESSAGES_PER_REQUEST` is used once, named like `MESSAGES_PAGE_SIZE`) and round 5 SD4 (`!older.has_more` is the endpoint's pagination signal). Declined DF6: the `queued_messages` refetch race predates this change, and a failed refetch fill only unloads older history that reloads on scroll. - Codex: no findings on `186a9b053a0`. - Known: a hung fill delays the first render (no client timeout); fills are not cancelled when the user leaves; the top of history costs one extra empty request; injected malformed fill rows break the page as they do on the first page. > Xum opened this PR on behalf of @ibetitsmike. |
||
|
|
ef590afda7 |
fix: coalesce AI session metadata in list query (#30351)
Update `ListAIBridgeSessions` to return `{}` when selected
session/interception metadata is SQL NULL, regenerate database code, and
add a regression test that inserts an interception with NULL metadata
and verifies the sessions list succeeds with empty metadata. No schema
migration is included; existing insert-path normalization remains
unchanged.
Fixes AIGOV-694
|
||
|
|
e4682343e6 |
feat: match PR number prefixes in chat search (#30404)
Chat search now matches PR numbers by prefix, so typing `3028` already finds the chat for PR `30281` instead of showing zero results mid-typing. The free-text `search:` branch in `GetChats` switches from `pr_number = @search::bigint` to `pr_number::text LIKE @search || '%'`, still guarded by a digits-only regex so the value can never carry LIKE wildcards. The lookup stays a correlated per-chat EXISTS on the `chat_diff_statuses` primary key, which leads with `chat_id`, so the cost is the same as before and no new index is needed. The explicit `pr:` filter stays exact, and ordering is unchanged (no exact-match ranking) so cursor pagination is unaffected. Behavior note: values with leading zeros (`030281`) no longer match PR `30281`, since the old bigint cast normalized them. PR numbers never have leading zeros, so this seemed fine. Closes CODAGT-1342 ## Testing - `TestGetChatsSearch` gains prefix (`4` -> PR 42), no-infix (`2` -> none), and LIKE-wildcard (`4%` -> none) cases; the prefix case fails on the old predicate. - Dogfood UAT through Coder Agents on the branch: searching `3`, `302`, `3028`, `30281` all surface the PR-linked chat; `0281` and `302810` do not; `pr:3028` stays exact. > [!NOTE] > This PR was created by Xum acting on behalf of @ibetitsmike. |
||
|
|
7993c73c87 |
feat: add organization-scoped Coder Agents instructions (#30272)
Adds organization-scoped Coder Agents instructions. Organization admins
set them in **Admin settings** > **AI** > **Coder Agents** >
**Organization settings**, or with `GET`/`PUT
/api/v2/organizations/{organization}/chats/config/system-prompt`.
Implements [CODAGT-984](https://linear.app/codercom/issue/CODAGT-984).
The instructions add to the deployment system prompt and never replace
it. When a chat is created, chatd stores the organization prompt as its
own system row right after the deployment row, only for chats in that
organization. Subagents resolve their parent organization's current
prompt when they spawn, and existing chats keep their stored rows.
Access reuses the organization `chat_model_config` permission: owners
and organization admins edit, auditors read, members get 404 on read and
403 on write. Writes reuse the deployment prompt's sanitization, size
limit (128 KiB by default), and audit behavior, with a new
`chat_organization_system_prompt` audit resource type. Migration
`000613` (the next number after `000612` on `main`) adds the
`chat_organization_system_prompts` table.
## Testing
- Go: enterprise authorization (including an update-only custom role),
validation, and audit tests; chatd row order and subagent tests.
- Frontend: Vitest interaction tests and stories for the card (loading,
load error, refetch error over the cached value, save error, read-only).
- Remote dogfood UAT, [evidence
chat](https://dogfood.cdr.dev/agents/b595a709-0709-44e6-9535-96a922974d61):
round 1 **PASS** on `b027cb5ca67` (permission matrix, validation, UI,
row order, cross-organization isolation, audit, subagents, regression).
Round 2 **PASS** on `618bff67c75` (loading and load-error states,
refetch error over the cached value with unsaved edits kept, update-only
custom role saves over the API, row order, the documented size limit,
rapid organization switching, double-click save).
- CI: rebased onto `main` (`98332c739b0`) as `3850fbaa922`; other commit
SHAs in this description are from before the rebase. Every check passed
on `3850fbaa922`, with `test-go-pg` green on the first attempt. After
the review fixes in item 7, `main` was merged in as `006c4951749` to
resolve new conflicts in generated files and renumber the migration to
`000613`. Every check passes on `006c4951749`. Before the rebase, every
check passed on `b4e76543df2`. `test-go-pg (ubuntu-latest)` passed on
its fourth attempt; the first three failed only the `cli` package
goroutine leak check, with every test passing. Two of those leaked a
coderd instance, which also happens intermittently on `main`:
`TestServer/Logging` cancels `coder server` during startup, and an early
return in `cli/server.go` skips closing coderd. That fix is in #30280.
The first attempt leaked an idle HTTP connection instead (not
investigated).
## Screenshots
From remote UAT on a test deployment (test data only).
| Organization admin saves instructions (`b027cb5ca67`) | Auditor sees
read-only instructions, no save actions (`b027cb5ca67`) |
|---|---|
| 
| 
|
| **Loading state while the first fetch is pending (`618bff67c75`)** |
**Refetch error shown above the field; the unsaved edit is kept and Save
stays enabled (`618bff67c75`)** |
| 
| 
|
## Review record
1. Codex,
[review](https://github.com/coder/coder/pull/30272#pullrequestreview-5387245297)
on `c42b5f9e05c`: 6 findings. Fixed the missing loading state, refetch
failures hiding cached instructions, and two docs style issues
(`58eab5603a4`, `cc73b714630`). Two were first declined in error, then
corrected: the read-only test now checks that typing leaves the value
unchanged (`cc73b714630`), and update-only custom roles no longer get a
500 on save (`618bff67c75`).
2. Independent security review by a clean-context agent,
[comment](https://github.com/coder/coder/pull/30272#issuecomment-5943505041)
on `c42b5f9e05c`: no P0 to P2 findings. A stale P3 comment was fixed in
`34c17aadb7f`; returning `err.Error()` in 500 details was kept to match
the deployment prompt handlers.
3. Codex,
[review](https://github.com/coder/coder/pull/30272#pullrequestreview-5387323380)
on `58eab5603a4`: 2 findings (refetch errors not surfaced, docs line
breaks), both fixed in `cc73b714630`.
4. Codex, blockers only,
[review](https://github.com/coder/coder/pull/30272#pullrequestreview-5387462070)
on `618bff67c75`: 1 P2 (a failed refresh right after a successful save
shows the previous value). Not a blocker, deferred.
5. Final fresh advisory by a clean-context agent,
[comment](https://github.com/coder/coder/pull/30272#issuecomment-5944285083)
on `618bff67c75`: **READY**, no blockers and nothing to cut. It
independently checked authorization, audit, the migration, chatd row
insertion and subagent inheritance, the tests, and soft-deleted
organization handling, and agreed with every deferral. No code changes
followed: the snapshot behavior it suggested documenting is already
documented, and its docs wording note joins the deferred docs item.
6. Codex,
[comment](https://github.com/coder/coder/pull/30272#issuecomment-5952100918)
on `b4e76543df2` (the docs-only follow-up below): no major issues. Mike
asked for a passing Codex review loop after item 5, so this review goes
past the planned stop at the final advisory. No code changes followed.
7. ethanndickson,
[review](https://github.com/coder/coder/pull/30272#pullrequestreview-5409332502)
on `3850fbaa922`: approved with 4 comments. Fixed 3: the audit target is
now empty (`91e77d3fa8c`), the docs no longer re-explain audit logs
(`a0fb6891af5`), and the organization instructions stories went from 9
to 5 (`e836097f93f`). Not fixed: the organization prompt read fails open
on a database error, like the deployment prompt, and ethanndickson is
changing both after merge
([thread](https://github.com/coder/coder/pull/30272#discussion_r4180191267)).
UAT did not rerun for these changes.
Not counted as reviews: the pre-PR frontend self-review, simplify pass,
and test audit (`de8377e94e1`), and a second test audit of
`de8377e94e1..cc73b714630` (pass with nits; the update-only test case
was added after it).
Docs-only follow-up after the advisory, requested by Mike: `b4e76543df2`
adds audit log guidance for prompt changes to the platform controls page
and an update-only custom role note to the organizations page. Codex
reviewed it (item 6). No UAT covers it, and the code is unchanged since
`618bff67c75`. The deferred claim that the Instructions page path is
outdated was wrong, so it's dropped. Item 7 removed the audit log
guidance again; the custom role note stays.
## Soft-deleted organizations
UAT found that a soft-deleted organization's prompt stays readable and
writable by UUID. This is inherited behavior, not new exposure:
`ExtractOrganizationParam` resolves UUIDs without a deleted filter, so
the sibling organization chat routes (MCP servers, model overrides,
models) behave the same. The same `chat_model_config` RBAC still
applies, nobody gains access to another organization, and writes are
audited.
## Deferred (owner: Mike)
- A failed refresh right after a successful save shows the previous
value with an error alert, so a further edit from that view starts from
the old text (Codex round 4, UAT round 2). Trigger: reports of stale
values after saving on unreliable networks.
- Update-only custom roles can save over the API, but the card stays
hidden without read access. Trigger: a customer uses such a role.
- Soft-deleted organization access by UUID, above. Trigger: the platform
starts rejecting deleted organizations in `ExtractOrganizationParam` or
purging their configuration.
- A contrived "code word" instruction was refused as possible prompt
injection; neutral instructions work. Trigger: reports of ignored
organization instructions.
- UX polish: there is no retry action after a failed first load, the
save toast can cover Cancel, there is no unsaved-changes guard, the
oversize error omits the limit, and whitespace-only text clears the
prompt without notice. Trigger: the next settings UX pass.
- Docs: the 413 response and organization name paths are undocumented.
Trigger: the next Coder Agents docs pass.
- Test audit nits: a redundant `waitFor` and an optional read-only plus
refetch story. Trigger: the next change to these tests.
> Xum, an AI coding agent, implemented, tested, and opened this PR on
behalf of @ibetitsmike.
|
||
|
|
49b1a2cf5d |
feat: expose per-ref diff statuses (#29082)
Part 2 of 6. Depends on #29081. The API now exposes every ref a chat tracks. All API changes are additive. - `Chat.diff_statuses` lists every ref on the chat GET and in chat lists (including child chats). The order is stable: refs are listed by first report, so the list does not change order when a ref gets a new git report. - `Chat.diff_status` is the primary: the ref with the most recent git report. It is the same rule as before. Use it to find the primary, not the list position. - `ChatDiffStatus` gains `remote_origin` and `git_branch`. - `diff_status_change` watch events carry `changed_diff_status` (`{ref, status}`) naming the ref that changed. If that ref's row is gone, `status` contains only `chat_id`. - `GET /chats/{chat}/diff` accepts optional `origin` and `branch` query parameters. Set both or omit both (otherwise 400). A selector that matches no stored ref returns 404. Without a selector the endpoint returns the primary, as before. The Go SDK adds `WithChatDiffStatusRef` for this. - A git report without a chat ID (for example, from a terminal or an agent older than v2.32) now refreshes only the chats that already track that ref. It does not add the ref to other chats. Such a report can no longer give a chat its first ref. Generated by Coder Agents. |
||
|
|
a08ba6a70c |
feat: add shared project memory API for chats in a project (#29301)
- Experiment, stacked on #29242. UI is in #29583. - Adds **project memory**: `chat_project_memories` rows with a name (lowercase slug, unique per project), a one-line description (150 characters) and a body (8KB). Up to 200 per project, shared by every root chat in the project. Chats outside a project and subagents have no memory. - Memories are immutable: they are created and deleted, never edited. Agent tools are `read_memory`, `save_memory`, `delete_memory` and `consolidate_memory`. Saving an existing name fails and points the agent at `consolidate_memory` to replace it. - **Consolidation is agent-driven, in the turn**, modeled on how Claude Code compacts its memory index when warned it is near its limit. From 160 memories (80%), save results ask the agent to get under 140 (70%) with `consolidate_memory`, choosing what to merge and drop. That tool deletes and saves memories in one transaction under the per-project advisory lock, and rolls back entirely if a delete is missing, a name is taken, or the result exceeds 200. There is no background extraction or consolidation. - **The memory index reaches the model as model-only conversation messages**, not in a tool description or the system prompt. At turn start the chat gets a full `<project-memory-index>` snapshot when its prompt has none (first turn, or after compaction), otherwise a `<project-memory-index-update>` listing only what changed. Tool definitions and the system prompt carry no memory state, so memory writes never invalidate the provider's cached prompt prefix. Writes by other chats, or by this chat mid-turn, reach the model at its next turn. - REST API (create, list, get, delete) under `/api/experimental/organizations/{organization}/chats/projects/{project}/memories`. Memory permissions follow projects onto the `agents-access` role. - **Every memory change is audited** as `ChatProjectMemory` create or delete. REST changes are attributed to the caller. Tool changes (`save_memory`, `delete_memory`, `consolidate_memory`) are attributed to the chat owner, carry the `chat_id`, and record one entry per memory after the change commits, so a rolled-back consolidation records nothing. ### Examples **What the model sees.** The system prompt gets a fixed guidance block with no memory state: ```text <memory> Memory is durable context shared by every chat in the project "platform". Save facts that will matter in future chats: … The memory index is in the conversation: a <project-memory-index> message lists every saved memory, and <project-memory-index-update> messages at the start of later turns list what changed. Changes made during a turn, including your own, appear in the next turn's update. … Memories cannot be edited: replace one that no longer holds with consolidate_memory, or delete it. </memory> ``` On the first turn, after the user's message, the chat gets a model-only row with the full index (hidden in the UI): ```text <project-memory-index> - deploy-day: Deploys happen on Tuesdays - release-owner: Alice owns releases; ping her before tagging </project-memory-index> ``` On a later turn where memories changed, it gets only the difference: ```text <project-memory-index-update> Added or changed: - deploy-day: Deploys happen on Wednesdays (changed 2026-09-30) - staging-url: Staging lives at staging.example.com Removed: - old-ci-notes </project-memory-index-update> ``` Resulting request layout, with the provider's cached prefix unchanged by memory writes: ```text [tools: fixed] [system: fixed <memory> guidance] [turn 1 user] [index snapshot] [assistant/tools …] [turn 2 user] [index update] [assistant …] ``` **Tool calls and results** (produced by the tools in this PR): ```text save_memory {"name":"staging-url","description":"Staging lives at staging.example.com","body":"Staging is https://staging.example.com; deploys from main."} → {"count":3,"saved":"staging-url"} save_memory {"name":"deploy-day", …} // name already exists → error: a memory named "deploy-day" already exists; read it, then replace it with consolidate_memory by deleting "deploy-day" and saving the new version in one call consolidate_memory {"delete":["deploy-day"],"save":[{"name":"deploy-day","description":"Deploys happen on Wednesdays (changed 2026-09-30)","body":"The team deploys every Wednesday since 2026-09-30."}]} → {"count":3,"deleted":["deploy-day"],"saved":["deploy-day"]} read_memory {"name":"deploy-day"} → {"name":"deploy-day","description":"…","body":"…","created_at":"2026-09-30T15:04:05Z","created_by":"alice"} ``` Near and at the cap: ```text save_memory … → {"count":160,"saved":"one-more","warning":"memory is 160/200, approaching the limit. Consolidate it to under 140 memories now with consolidate_memory: merge overlapping memories into one, and drop stale, wrong, or superseded ones."} save_memory … → error: memory is full (200/200); use consolidate_memory to merge overlapping memories and drop stale ones, and include this memory in its save list ``` **REST memory object** (`GET .../chats/projects/{project}/memories`): ```json { "id": "59500000-0000-4000-8000-000000000001", "project_id": "7c0e4a3e-5b1f-4f7d-9a51-2f3c8f6d2b10", "organization_id": "bb640d07-ca8a-4869-b6bc-ae61ebb2fda1", "name": "deploy-day", "description": "Deploys happen on Wednesdays (changed 2026-09-30)", "body": "The team deploys every Wednesday since 2026-09-30.", "created_by": "0ed9befc-4911-4ccf-a8e2-559bf72daa94", "created_by_username": "alice", "created_at": "2026-09-30T15:04:05Z" } ``` Create takes `{"name", "description", "body"}` and answers 409 on a duplicate name or at the cap. <details> <summary>Screenshots (predate the immutable-memory rework; tool output shape has since changed)</summary>    </details> --- Generated with Coder Agents on behalf of @f0ssel. --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
0f78361750 |
fix(coderd): cancel tool calls in messages deleted on edit (#30194)
Editing a message removes it and every later message from the chat. Before, a tool call in those messages kept going in the workspace: a foreground command ran to completion, and a file edit or write could still apply, while the model no longer knew about it. Now chatd sends the workspace agent a cancel request for each such call before the new turn's first model call. Calls that already returned, such as background processes the model was told about, keep running. Fixes CODAGT-1222 |
||
|
|
122757d1d2 | fix(coderd/database/migrations): lock chat tables before 000609 alters them (#30297) | ||
|
|
fb0951b850 |
feat: add the manage_automations chat tool switch and read actions (#30214)
Adds the `manage_automations` chat tool with its read and stop actions
(`list`, `get`, `disable`, `delete`), gated by an interim per-chat
switch. Tool sets have not landed, so this ships
`chats.manage_automations_enabled` as the RFC describes for that case;
tool sets later replace it and migrate its values.
Part of CODAGT-1212 (slice 12a of 3). Part of CODAGT-1135.
## What changes
- **Switch.** Migration `000611` adds `chats.manage_automations_enabled`
(default off). It is set through `POST /api/v2/chats` and `PATCH
/api/v2/chats/{chat}`. After creation only the chat owner may send the
field (403 otherwise). Turning it on returns 400 when the
`chat-automations` experiment is off for the chat owner (POST checks the
resolved `owner_id`, not the caller) or when the chat is a sub-agent
chat. Turning it off is always accepted. The audit table tracks it.
- **Offering.** The tool is offered only in root chats, outside plan
mode and explore mode, with the switch on, and with the experiment on
for the chat owner (decided once per turn, like `mcp-tool-search`). It
is not on the plan-mode or explore allowlists, and sub-agent chats are
created with the switch off.
- **Per-call recheck.** Every call reloads the chat and checks all rules
again, with a fresh experiment evaluation, before doing anything.
- **Scoping.** Owner and organization come from the chat, never from
arguments. Reads run as the owner through a new query that filters on
organization and owner. Other members' automations are "not found", even
when the chat owner is an organization admin.
- **Automation-reached turns.** Detected at call time from the full
history (compaction replays user rows without `automation_id`, so the
prompt window is not enough). In such turns only automations that target
the chat or created it (`chats.automation_id`) are visible.
- **Visibility of prompts and deletes.** `list` leaves out prompts (tool
results stay in the chat, which may be shared); `get` returns the prompt
of the automation it names. In a turn an automation reached, `delete`
only removes the triggering automation; the others can still be
disabled.
- **Audit.** `disable` and `delete` go through the management service
and write a background audit entry with `chat_id`, plus the triggering
`automation_id` and `input_id` in reached turns. Results use the API
shape and never contain a secret or secret hash.
`create`, `update`, `enable`, and `run_now` come in the next PR and
return "not available in this version" here.
Follow-up: CODAGT-1270 (bound the history read that reached-turn
detection does on each call).
## Size
About 1,380 hand-written lines: about 460 production, 680 tests, and 240
SQL that has to be there (the `chats_expanded` view rebuilt in both
migrations and the 17 inline column lists). Generated: +649/-280 (sqlc,
dbmock, dbmetrics, dump.sql, apidoc, API and audit docs,
`typesGenerated.ts`).
## Validation
- Unit and DB tests for every rule above (switch 403/400 cases, offering
rules, per-call recheck, owner/org scoping with an org-admin owner,
reached-turn detection including compaction replay, audit fields, secret
scan), plus a separate test audit. Full `make pre-push` passed before
push.
- Remote dogfood UAT passed on this slice: switch API cases, tool absent
with the switch off, in plan mode, in sub-agents and with the experiment
off for the owner (checked in the server's per-step tool lists),
list/get/disable/delete, and a secret scan of all tool output. Audit
entries are verified in Go tests only.
> [!NOTE]
> ARCHITECTURE.md prose was drafted by an agent at the user's request;
the user will review it.
## Stack
Stacked on the Automations for Coder Agents stack (30085 ... 1214b).
Next: slice 12b (write actions), then 12c (composer toggle).
---
_Generated with [`xum`](https://github.com/coder/xum) • Model:
`anthropic:claude-opus-5-5` • Thinking: `high`_
|
||
|
|
a3fbd2384d |
feat: add the Agents automations list page and chat-list automation filter (#30171)
Closes CODAGT-1213. Part of CODAGT-1135 (Automations for Coder Agents,
RFC step 7).
Adds an Automations list to the Agents UI and an `automation_id` filter
on the chat list, so users can see their automations and find the chats
an automation created or wrote to.
## Changes
- **Chat-list filter:** `GET /api/v2/chats?automation_id=<uuid>` returns
root chats whose `chats.automation_id` matches (chats the automation
created) and chats with a non-deleted message whose
`chat_messages.automation_id` matches (chats it wrote to). The nil UUID
is rejected, because the query uses it as "no filter". It is honored
only when the `chat-automations` experiment is on for the caller;
otherwise it is ignored, as the RFC says, and the evaluator runs only
when the parameter is present. Chat authorization is unchanged. Swagger
documents the parameter, and `codersdk.ListChatsOptions` gains
`AutomationID`. The existing partial indexes back the filter.
- **List page:** `/agents/automations`, with an **Automations** sidebar
entry. Both are hidden when the experiment is off for the viewer
(per-user experiment list). The page lists the automations of the
organization selected in the Agents organization picker; it shares the
stored selection with the new-chat form and has its own picker. Columns:
name (with the creating chat for agent-created automations), trigger
(cron and time zone, or webhook single or multi use), target (chat link,
"New chat each run", or a **Missing target** badge for a deleted or
archived target), next run from the server's `next_run_times`, an
enabled switch (PATCH; a non-owner cannot re-enable), **Run now** (owner
only, schedules only, disabled for a missing target and while another
run is pending), and **View chats** (owner only; a paged dialog backed
by the new filter). Chat titles are looked up only for the viewer's own
automations. The list refetches every minute so the next run stays
current. On mobile, the chats header links to the page, which opens full
width with a back link. A Run now failure, such as `409` for a busy chat
with skip or `429` for a full queue or share, shows the server message
in an alert above the table.
- Out of scope, per the ticket: the **Last skipped run** column, and the
editor (CODAGT-1214). The empty state points to the API because the
`manage_automations` tool is not in this stack yet.
## Validation
- Go: `TestListChats/AutomationFilter` covers:
- created and written-to chats, deleted messages, and another user's
chat;
- an unknown id, a malformed id (`400`), and the nil UUID (`400`);
- the experiment turned off by a per-user rule, where the parameter is
ignored, including a malformed id.
- Vitest (`AgentAutomationsPage.test.tsx`), all asserting requests:
- the selected organization's list is requested, and switching the
picker refetches and stores the choice;
- the switch sends `PATCH {enabled:false}`;
- Run now sends its POST when the server answers `409` or `429`;
- View chats requests `?automation_id=`, and Load more requests the next
page;
- nothing is requested with the experiment off.
- Storybook (Pixel): `WithAutomations`, `OtherOwner`, `Empty`,
`MissingTarget`, `RunNowError`, `ChatsDialog`,
`ChatsDialogLoadMoreError`, `Loading`, `RefetchError`, `ListError`, and
`ChatsSidebar/MobileWithAutomations`.
- Remote dogfood UAT: PASS in round 1 (`fa60a15767`) and round 2 on the
current head `b37e9d99d0`. Round 2 repeated every criterion and verified
the review fixes. Screenshots are in the comments below.
- `coder-frontend-review` self-review: every role passes after fixes on
each update, except two items:
- the experiment-off test waits for the not-found page only as a sync
point before asserting that no request was sent;
- there is no story for a chat-title lookup error, because the Storybook
query helper cannot inject errors.
- Codex review: 4 rounds (8, 4, 4 and 6 P2 findings, no P0 or P1).
- Rounds 1 to 3 were fixed, except the model-availability item
(CODAGT-1244).
- Round 4 found no blocker and is tracked in CODAGT-1246, together with
the minor UAT round 2 findings.
- All threads are resolved. There is no clean Codex verdict on the
current head. Further rounds were not requested because findings were
not converging and none was a blocker; the stack orchestrator accepted
stopping at round 4.
- CI: `ci` run 36703512580, started with workflow_dispatch, is green on
the current head `b37e9d99d0`. Pushes to existing stacked PRs did not
start the pull_request `ci` run (noted in the comments). The earlier
head `8f2ee30d8f` was green through a normal pull_request run.
Size: about 1,370 hand-written lines (about 560 of them tests and
stories), plus generated code. This is over the 1,000-line guideline,
mostly from review fixes.
## Stack
Stacked on #30085, #30096, #30112, #30117, #30127, #30134, #30160,
#30166 and #30167 (CODAGT-1215). Base:
`tk/codagt-1215-automations-chat-labels`, merged in (not rebased).
Follow-ups: CODAGT-1244, CODAGT-1246.
---
_Generated with [`xum`](https://github.com/coder/xum) • Model:
`anthropic:claude-opus-5-5` • Thinking: `high`_
|
||
|
|
b96f1b970c |
feat: label automation messages in agent chats (#30167)
Closes CODAGT-1215. Part of CODAGT-1135 (Automations for Coder Agents,
RFC step 9).
Chat messages and queued inputs delivered by a chat automation (webhook,
schedule, Run now) now show a label: `Automation run · <automation name>
· input <first 8 chars of input ID>`. A tooltip holds the full
automation and input IDs. While names load, the label omits the name
part. When the name cannot be resolved (deleted automation, a
shared-chat viewer who cannot read it, or experiment off), the label
shows the automation ID instead. Ordinary messages have no label.
## Changes
- **Backend (minimal, SDK exposure only):** `codersdk.ChatMessage` and
`codersdk.ChatQueuedMessage` gain `automation_id` and `input_id`
(omitted when unset). `db2sdk` fills them from the existing columns, so
REST messages and the stream `message` and `queue_update` events carry
them. No behavior change in the lower layers.
- **Frontend:** `API.experimental.getChatAutomations`, a
`chatAutomations` query, and `useChatAutomationNames`. The hook fetches
`GET /api/experimental/organizations/{org}/chat-automations` only when
the user-scoped `chat-automations` experiment is on and the chat holds
automation input. The `AutomationLabel` component uses the shared
`Badge` and `Tooltip`, and is wired into `ConversationTimeline`
(history) and `QueuedMessagesList` (queue). The chat store refetches
names when new automation input arrives, so automations created or
renamed after page load show their current name.
- Out of scope, per the ticket: the occurrence label and the
requested-versus-resolved effort display.
## Validation
- Vitest: the automations list is requested for the chat's organization
only when the chat holds automation input and the experiment is on;
`useChatAutomationNames` reports loading until the list resolves and
then maps IDs to names; the chat store refetches names on new live
automation input. Go: `TestChatMessageAutomationProvenance` for both
converters.
- Storybook (Pixel): `ConversationTimeline/AutomationUserMessages` and
`QueuedMessagesList/AutomationMessages` (named, deleted by ID, long name
at phone width, no label on ordinary items) and
`QueuedMessagesList/AutomationMessagesLoading` (name still loading: no
UUID flash).
- Remote dogfood UAT on a develop.sh instance with the experiment on:
PASS in round 2. It covered webhook input in an existing chat (history
and queue), a schedule, a new_chat first message, the ID fallback after
deletion, and no label on ordinary messages. Screenshots are in a
comment below. Run now was not on the base branch during UAT; it uses
the same fields and path.
- coder-frontend-review self-review: all roles PASS, after fixes on each
update.
## Known limitations (follow-ups under CODAGT-1135)
- A shared-chat viewer who does not own the automation sees its ID,
because automation RBAC has no chat ACL (CODAGT-1239).
- Copying the label text yields three lines, because the name span
truncates separately (CODAGT-1238).
## Stack
Stacked on #30085, #30096, #30112, #30117, #30127, #30134, #30160 (1211
7a) and #30166 (1211 7b). Base: `tk/codagt-1211b-automations-run-now`.
---
_Generated with [`xum`](https://github.com/coder/xum) • Model:
`anthropic:claude-opus-5-5` • Thinking: `high`_
---------
Signed-off-by: Thomas Kosiewski <tk@coder.com>
|
||
|
|
8620361fcc |
feat(coderd): run chat automation schedules (#30160)
Part of CODAGT-1211. Part of CODAGT-1135. > Stack: PR 7a of "Automations for Coder Agents", on top of 1210 5b (#30134, `tk/codagt-1210b-automations-new-chat`). Run now (`POST .../runs`) is split into PR 7b (#30166) on top of this one, which closes CODAGT-1211. Schedule automations now run. Every coderd instance scans for due schedules at start and every 30 seconds, and publishes each occurrence through the existing `Publish` paths (existing_chat `SendMessage(queue)` and new_chat `Create`, with their `AdmitInTx` callbacks). There is no second admission path. ## How a scan works 1. An unlocked read returns enabled schedule automations whose cursor (`schedule_next_run_at`) is due, in pages of 500 by (cursor, id) until a short page. It leaves out inactive or deleted owners and existing_chat targets that are gone or archived. 2. The `chat-automations` experiment is evaluated once per owner per tick, outside any callback. Owners with it off are dropped without a cursor write. `Publish` checks the experiment for the owner again before the transaction (webhooks map that refusal to 404, like the handler's own check). 3. A cursor more than 60 seconds (the grace window) old is missed: a guarded write moves it to the next future time, nothing is published, and nothing is replayed. 4. Otherwise `Publish` runs (up to eight occurrences at once, so one slow hook or lock wait does not hold back the rest) with the observed revision and cursor. The pre-check refuses a stale or expired occurrence before any hook runs. Under the chat and automation locks, the callback requires the same revision and cursor, reads the clock after the locks, requires the occurrence to be due and within grace, and moves the cursor to the first cron time after that read in the same transaction. 5. A busy chat with `skip`, a full queue or automation share, a hook denial, lost permission, or an occurrence that expired while waiting for a lock skips the occurrence with the same guarded write and an info log. A stale, disabled, or deleted automation is left alone. Other errors keep the cursor, so the next scan retries within grace. That includes an experiment-off refusal from `Publish`, because the evaluator also reports a failed read as off. New chats created by a schedule are titled with the occurrence's scheduled time in the schedule's time zone, for example `Tokyo digest 2026-09-30 08:14 JST` (the ticket says acceptance time; the two differ by at most the grace window, and the scheduled time cannot drift across a minute while hooks or locks run). Webhook titles stay in UTC. Each such chat gets an audit entry as created by the automation owner, with the automation and input ids, like the chats the webhook endpoint creates. ## Decisions reviewers should check - **Clock (deviates from the ticket's "fresh database clock").** The callback reads the injected quartz clock after the chat and automation locks are held, not transaction-start `now()`. The ticket also requires fake-clock tests, and the lower layers already read the clock after locks. Exactly-once rests on the revision and cursor tuple, not on time. Inter-instance clock skew cannot cause a double acceptance or a replay: an acceptance commits only together with the conditional cursor write under the automation lock, and every cursor write moves the cursor strictly forward (`advanceAutomationSchedule` refuses a next run at or before the cursor). A clock that is off by `d` only shifts the acceptance window to `[cursor-d, cursor+grace+d]` in true time; a clock that runs ahead can skip occurrences within `d`. See the comment on `checkAutomationOccurrence` and the `CursorOnlyMovesForward` test. Swapping in `clock_timestamp()` later is a one-line change. - **Authorization of the cursor write.** `AdvanceChatAutomationScheduleCursor` follows the object pattern of `ConsumeChatAutomationWebhookByID` and `UpdateChatAutomationByID`: the dbauthz wrapper loads the automation and authorizes `update` on it. The scanner and the callback call it as chatd, so the chatd subject gains `update` on `chat_automation` (it already had `read`). This is the smallest grant; chatd still cannot create or delete automations (pinned in `TestAsChatd`), and owner-only changes stay in the chatd automation service. No 1207 migration is touched. - **Narrow write.** The query sets only `schedule_next_run_at` and `updated_at`, guarded by `id`, `kind = 'schedule'`, `enabled`, the observed `schedule_revision`, and the observed cursor. It never touches owner-controlled fields or the revision. - **Known limitation (CODAGT-1234).** Two replicas that read the same due row can both reach the `user_prompt_submit` hook before one loses under the lock; only one message or chat commits. This matches the lower layer's best-effort unlocked pre-check for webhooks. A claim-first cursor move would break the ticket's retry-within-grace rule or the forward-only cursor, so a lease column is proposed as a follow-up. - The schedule tests run their servers through dbauthz, so they use the chatd subject's real permissions (they fail without the grant). ## Tests `coderd/x/chatd/automation_schedules_internal_test.go` uses a mock clock, real Postgres, and a second connection that holds row locks (waiting on `pg_stat_activity`): two instances accept one occurrence (existing_chat, and new_chat with no orphan chat); busy `skip`, automation share full, and queue full each advance the cursor once; a cron edit rolls back an in-flight acceptance; restart after missed intervals publishes nothing, restart within grace accepts once; previews match accepted runs; an occurrence that expires while waiting for a lock is not accepted; a DST change keeps 09:00 wall-clock time; the experiment off gets no run (scan and Publish); the cursor only moves forward; the loop scans at start. Dogfood UAT ran on a real dev instance through Coder Agents and passed: a one-minute schedule delivered one message per minute with no browser open, a busy chat skipped its occurrence, disable stopped runs and re-enable did not replay, new_chat titles used Asia/Tokyo, two coderd instances produced no duplicates, and downtime was not replayed. ## Size About 994 hand-written lines (about 560 of them tests). Generated: `queries.sql.go`, `querier.go`, `dbmetrics`, `dbmock` (181 lines). ARCHITECTURE.md prose was drafted by an agent at the user's request; the user will review it. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_ |
||
|
|
1e1e884c25 |
feat(coderd): create a chat for each webhook event of a new_chat automation (#30134)
Closes CODAGT-1210. Part of CODAGT-1135. > Stack: PR 6 of "Automations for Coder Agents", on top of #30127 (CODAGT-1210 part 1, webhook events for `existing_chat`). This PR delivers webhook events to `new_chat` automations and replaces part 1's `501` for them. ## What this adds - A webhook event for a `new_chat` automation creates a new chat as the automation owner through chatd `CreateChat` with an `AdmitInTx` callback, and the endpoint answers `202` with the new `chat_id`. - The chat uses the automation's model config and reasoning effort and client type `api`, and selects no MCP servers (the organization's Force On servers still apply). Its title is set explicitly to the automation name plus the acceptance time in UTC (`<name> 2006-01-02 15:04 UTC`), so automatic title generation does not run and event data never becomes the title. - Before the create, the same input checks as part 1 run (enabled, secret version, single-use unused), plus: the owner may create a chat in the organization (`403`), and the owner can read the automation's model config, which must be enabled and in the same organization (`409`). A `user_prompt_submit` denial creates no chat. - Under the automation lock, the callback rechecks all of that, sets `chats.automation_id` on the new chat (new query `UpdateChatAutomationIDByID`, dbauthz: update on the chat), consumes a single-use webhook, and returns the provenance. A refusal rolls back the whole creation. - The events endpoint records an audit entry for each created chat (action create, user = the automation owner, with `automation_id` and `input_id` in the additional fields), as chat creation through the chat API does. - Part 1's lock and consumption code is shared by both target modes. ## Notes for reviewers - ARCHITECTURE.md prose was drafted by an agent at the user's request; the user will review it. - Size: about 600 hand-written lines (about 380 of them tests). Generated: about 65 lines (`coderd/apidoc`, `dbmock`, `dbmetrics`, `querier.go`, `queries.sql.go`). - Dogfood UAT ran through Coder Agents on a real dev deployment: curl deliveries created chats with the explicit UTC title, client type `api`, two message parts, and `automation_id` set; single-use concurrency created exactly one chat; a disabled model config gave `409` with no chat. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_ --------- Signed-off-by: Thomas Kosiewski <tk@coder.com> |
||
|
|
b517db4beb |
feat: deliver webhook events to existing chats through chat automations (#30127)
Part of CODAGT-1210 (1 of 2). Part of CODAGT-1135. > Stack: PR 5 of "Automations for Coder Agents", on top of #30117. The ticket is split in two by operation: this PR delivers webhook events to `existing_chat` targets; the stacked follow-up delivers them to `new_chat` targets and closes CODAGT-1210. Until then a `new_chat` automation's events endpoint answers `501`. ## What this adds - `POST /api/experimental/chat-automations/{automation}/events`. External systems send the automation's webhook secret as `Authorization: Bearer <secret>` and a JSON body of at most 256 KiB. The route has no API key middleware and sits outside the `/api/experimental` group, behind its own rate limiter keyed by caller and a fixed endpoint key (IP address for unauthenticated callers), so varying the automation id does not reset the budget. - Secret check: SHA-256 of the bearer value, compared in constant time. Unknown ids, other kinds, and wrong secrets all get the same `401`. The `chat-automations` experiment is checked for the automation owner only after the secret matches (`404` when off). The body is then read: over the limit `413`, not JSON `400`. - `chatd.PublishAutomationWebhook` sends as the owner (`dbauthz.As` with the owner's subject) through `SendMessage` with the `queue` busy behavior set explicitly, so automations never interrupt or steer. `user_prompt_submit` hooks run first: a denial is `403`, an unreachable hook service `502`. - One admission check runs before the send (so refused events never reach hooks) and again in the `AdmitInTx` callback, which locks the automation together with the chat's queued automations in one ascending-order call. Under the lock it rechecks: enabled, secret version (a request verified before a rotation but committed after it gets `401`), single-use state, target chat (root chat of the owner, same org, not archived), owner status, and the owner's `chat:update` permission. It then applies **When busy**: `skip` refuses a busy chat (`409`), `queue` refuses input beyond the automations' share, half the chat queue cap rounded down and at least 1 (`429`); busy and the share count only queued rows that promotion would still deliver. It marks a single-use webhook consumed and returns the provenance. - The message has two parts: the saved prompt, and a labeled part with the body as untrusted event data inside `<automation_event_data>` tags. `json.HTMLEscape` escapes `<`, `>`, and `&` in the body, which keeps the JSON value and makes the closing tag impossible to forge. - `chatd.Config.Authorizer` (wired from `options.Authorizer`) for the explicit owner checks; publishing fails closed without one. - New query `ConsumeChatAutomationWebhookByID` with a dbauthz wrapper. Status codes follow the RFC's failure outcomes table; the chat-without-a-model case returns the same `400` as a person's message. ## Notes for reviewers - Single-use consumption runs from the chatd publish code as the owner, because the chatd role only reads `chat_automation`. No lower-layer RBAC change. - ARCHITECTURE.md prose was drafted by an agent at the user's request; the user will review it. - Size: about 1,460 hand-written lines after review fixes (about 770 of them tests), over the ~1,000 budget because of the review hardening. Generated: about 360 lines (`coderd/apidoc`, `dbmock`, `dbmetrics`, `querier.go`, `queries.sql.go`, `docs/reference/api/schemas.md`, `typesGenerated.ts`). - Dogfood UAT ran through Coder Agents on a real dev deployment with curl deliveries. Round 1 found error-mapping defects, fixed here; rounds 2 and 3 passed, the last on the final code (the head after it changes only ARCHITECTURE.md). - Deferred: CODAGT-1233 (stale automation rows filling the physical queue cap; lower-layer `SendMessage` path). --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_ |
||
|
|
5c1662ea4c |
feat: disable, re-enable, and rotate chat automations with schedule preview (#30117)
Completes the chat automations management API from RFC step 3: disable and re-enable, delete that disables first, webhook secret rotation, and a server-side schedule preview. Stacked on #30112. - **Disable** (`PATCH` with `{"enabled": false}`) runs in two steps. A short transaction that touches only the automation row sets `enabled = false` and increments `queue_generation`, the cutoff. After it commits, the automation's queued rows below the cutoff are deleted one chat at a time through chatd's existing `DeleteQueued` transition, run as chatd, so queue versions and stream updates match a manual delete. If a row is missed, for example after a crash between the steps, the queue promotion guard from #30096 discards it, because its generation is stale. Running turns are not interrupted. - **Delete** deletes the row and then removes all of the automation's queued rows the same way. Deleting the row is itself the cutoff, because the promotion guard discards rows whose automation no longer exists. So delete needs only delete permission, not update. This differs from the RFC's "disable, then delete" order, with the same outcome. - **Re-enable** (`{"enabled": true}`) moves a schedule's cursor to the next future cron time, so the disabled period is not replayed. The generation never resets. - **Permissions:** only the owner can enable, edit, or rotate. Organization admins and site owners can send exactly `{"enabled": false}` (anything combined with it is 403), and can still list, read, and delete. - **Rotation:** `POST .../chat-automations/{automation}/secret/rotate` (owner only, webhooks only) returns the new secret once and increments `webhook_secret_version`. Only the SHA-256 hash is stored, and the rotation is audited with the hash redacted. - **Preview:** `POST .../chat-automations/schedule-preview` evaluates an unsaved cron expression and time zone with create's rules and returns the next five run times, or 400. - `coderd/x/chatd/ARCHITECTURE.md` gets a `TODO (CODAGT-1209)` note about disable using the delete-queued transition. **Author action before merge:** replace that TODO with the architecture prose (agents do not write that document). Known gap, tracked in CODAGT-1227: when the target chat is archived, disable and delete cannot remove its queued automation rows, because `DeleteQueuedMessage` is not allowed from archived states. The rows never run, since the promotion guard discards them, but they stay visible after the chat is unarchived. The fix needs a chatstate transition from the lower layer. Dogfood UAT ran on a real dev instance and passed. It covered disable removing only that automation's queued rows (the chat API agreed), delete, re-enable without replay, the permission matrix for an organization admin and the site owner, rotation (the stored hash matches the new secret, and neither secret appears in get or list), preview, and a runtime experiment rule. It also checked that a leftover row is discarded on promotion. Closes CODAGT-1209 Part of CODAGT-1135 Refs CODAGT-1217 Stack: PR 3b of the Automations for Coder Agents stack, on top of #30112 (PR 3a), #30096, and #30085. > This PR was written by an AI agent (xum) and reviewed by the agent's own test audit and UAT. A human has not reviewed it yet. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_ --------- Signed-off-by: Thomas Kosiewski <tk@coder.com> |
||
|
|
9316d67440 |
feat: add chat automations management API behind chat-automations (#30112)
Adds the management API for chat automations under
`/api/experimental/organizations/{organization}/chat-automations`,
behind the new user-scoped `chat-automations` experiment. This is the
first of two PRs for RFC step 3; nothing publishes automation messages
yet.
- **Experiment:** `chat-automations` is in `ExperimentsKnown`,
`ExperimentsUserScoped`, and the ruleguard list in `scripts/rules.go`.
Every route asks `chatd.AutomationsEnabled(ctx, api.ExperimentEvaluator,
callerID)`, which fails closed on a missing subject or evaluator; the
routes stay mounted and return 404 when the experiment is off for the
caller, so a runtime rule for one user applies on the next request
without a restart. There is no `/api/v2` alias.
- **Routes:** list, create (201), get, `PATCH`, delete (204), with
Swagger annotations and audit entries. List returns the automations in
the organization the caller can read (members see their own,
organization admins see all).
- **Service** (`coderd/x/chatd/automations.go`): create validates kind
and target shape, a non-empty prompt, a five-field cron and IANA time
zone, model access, and that an `existing_chat` target is a root chat of
the owner in the same organization. It defaults `when_busy` to `skip`
for schedules and `queue` for webhooks and computes the first schedule
cursor. `PATCH` runs the same per-field validation on each changed
field, rejects `kind` and `target_mode`, bumps `schedule_revision` on
prompt, cron, or time zone changes, and recomputes the cursor on cron or
time zone changes.
- **Owner-only:** only the owner can `PATCH` an automation, even with
organization-admin or site-owner roles (403); admins can list, read, and
delete.
- **Secrets:** a webhook's secret (`coder_automation_` prefix) is
returned once in the create response. Only its SHA-256 is stored, and it
appears in no list, get, audit, or log output.
- **Cap:** new option `--chat-max-automations-per-owner` (default 50)
caps automations per owner across all organizations; creating one more
returns 409. Creates serialize on a per-owner advisory lock.
- **Cron:** `cron.Standard(spec, timeZone)` accepts any standard
five-field expression (the existing `Weekly` rejects day-of-month and
month restrictions). Responses include the next five run times of an
enabled schedule.
PR 3b (stacked on this one) adds disable and re-enable with the queue
cutoff, makes delete run disable first, and adds secret rotation and the
schedule preview route. Until then, the queue promotion guard from
#30096 discards queued rows whose automation was deleted.
Dogfood UAT ran on a real dev instance and passed:
create/list/get/patch/delete via curl, owner-only 403s for an
organization admin and the site owner, the cap (including concurrent
creates and cross-organization counting), a runtime rule turning the
experiment off for one user without a restart, and `/api/v2` 404s.
Part of CODAGT-1209 (PR 3b closes it)
Part of CODAGT-1135
Refs CODAGT-1217
Stack: PR 3 of the Automations for Coder Agents stack, on top of #30096
(which is on top of #30085).
> This PR was written by an AI agent (xum) and reviewed by the agent's
own test audit and UAT. A human has not reviewed it yet.
---
_Generated with [`xum`](https://github.com/coder/xum) • Model:
`anthropic:claude-opus-5-5` • Thinking: `high`_
---------
Signed-off-by: Thomas Kosiewski <tk@coder.com>
|
||
|
|
eaca2290b0 |
feat(coderd): add chatd automation admission seam and queue promotion guard (#30096)
Adds the backend-only seam that lets a later `Publish` operation admit an automation message inside chatd's existing create-chat and send-message transactions, and the guard that drops stale automation input when the queue promotes it. Only Go test callbacks use the seam in this PR, so ordinary chat behavior does not change. Closes CODAGT-1208 Part of CODAGT-1135 > Stack: PR 2, on top of #30085 (schema, RBAC resource, and audit entries). No migration changes; this PR adds and extends sqlc queries only. ARCHITECTURE.md prose was drafted by an agent at the user's request; the user will review it. ## Changes - **Admission seam.** `chatstate.AdmitFunc` is a nil-default `AdmitInTx` field on `chatd.CreateOptions`, `chatd.SendMessageOptions`, `chatstate.CreateChatInput`, and `chatstate.SendMessageInput`. `CreateChat` runs it after inserting the chat and before the initial history, and requires exactly one initial user message. `SendMessage` runs it on the locked chat before any write. The callback returns an `AutomationProvenance` (automation ID, input ID, queue generation), which is validated and stamped on the message. A callback error rolls back the whole transaction, so no chat, message, or queue row survives. `Server.CreateChat` and `Server.SendMessage` keep their signatures. - **Provenance.** `chatstate.Message.Automation` is written to queued rows (`automation_id`, `input_id`, `queue_generation`) and history rows (`automation_id`, `input_id`). `InsertChatMessages` and `InsertChatQueuedMessageWithCreator` gain the matching parameters, and `messageFromQueuedRow` copies them on promotion. - **Promotion guard.** `chatstate.LockAutomations` is the only way to lock automation rows: it locks them `FOR UPDATE` in ascending ID order, running as chatd. The chatd subject gains read on `chat_automation`. A queued automation row passes only if its automation exists, is enabled, and has the row's `queue_generation`. Two helpers apply the check: - `nextPromotableQueueHead` serves `sendMessageE1`, `FinishTurn`, and `FinishInterruption`. A head without an automation costs no extra query. Otherwise it locks every automation in the queue up front, deletes failing heads until one passes, and leaves rows behind that head alone. If the queue empties, `FinishTurn` and `FinishInterruption` land in `waiting`, as from `R0` and `I0`. - `guardQueuedRows` serves explicit `PromoteQueuedMessage`. A failing row is deleted and the transition returns `Rejected` without an error, so the delete commits. `Server.PromoteQueued` then returns `ErrQueuedMessageNotFound` (HTTP 404) without reordering, substituting a message, or changing status. - The steer paths (`DeliverSteerMessages` and the steer batches from #30073/#30074) add one `guardQueuedRows` call each, whichever PR lands second. - **Deadlocks.** `database.IsDeadlockError` is new. `ChatMachine.Update` reruns the transaction, at most 3 attempts with a fresh publish buffer each, only when an attempt that took automation locks hit a deadlock abort. Attempts without automation locks, and nested Updates, never retry. The `SendMessage` and `PromoteQueued` closures reset their results so a rerun is safe. - **Transition matrix.** Adds the outputs the guard can produce, such as `R1 -> FinishTurn -> W` and `E1 -> SendMessage -> R0`. - **`ARCHITECTURE.md`.** Describes the admission seam, provenance, the queue promotion guard, the lock order, and the deadlock retry, and adds the guard's outcomes to the state diagram. ## Validation - Go tests on real PostgreSQL cover the ticket's checks: - A callback error leaves no chat, message, or queue row. - A guard rejection deletes only the rejected heads, on every promotion path and for each failure reason (missing, disabled, disabled with the same generation, generation mismatch). - Explicit promotion of a failing row returns not found, and the delete is committed. - Provenance round-trips through queue and history. - `LockAutomations` works through dbauthz. - `Update` retries only when an attempt that took automation locks hits a deadlock. - A concurrency test of acceptance, promotion, and disable across mixed automation IDs sees zero deadlock aborts. - Mutation checks confirmed that the guard, retry, and permission tests fail when their behavior is removed. - Remote dogfood UAT passed on a real dev instance: sending, queueing, head and explicit promotion, interrupting, and deleting queued messages all behaved as before. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_ |
||
|
|
f55aa6c50d |
feat: add chat automations schema, RBAC resource, and audit entries (#30085)
Adds the storage and authorization pieces for Automations for Coder Agents. No route, SDK request type, or chatd code uses them yet, so nothing is exposed. This is the bottom PR of the Automations stack (RFC step 1). Later PRs add the chatd acceptance seam, management routes, webhooks, and schedules on top of these names and constraints. **Schema** (migrations 000609, 000610) - `chat_automations` table with enums `chat_automation_kind`, `chat_automation_target_mode`, `chat_automation_webhook_use`, and `chat_automation_when_busy`. It includes `created_by_chat_id`, the webhook secret hash and version, the single-use `webhook_consumed_at` marker, the schedule cursor and revision, and `queue_generation`. Named CHECKs enforce the target and kind shapes from the RFC. A composite foreign key keeps `new_chat_model_config_id` in the automation's organization, the same pattern the model override tables use. A trigger (`chat_automations_chat_organization`) keeps `target_chat_id` and `created_by_chat_id` in that organization too. A composite foreign key cannot do that while keeping `ON DELETE SET NULL` on PostgreSQL 13. The trigger validates a reference only on insert or when it changes, so chat deletion can still clear both references. Indexes cover the owner and both chat references for cascades and `SET NULL`. - Provenance columns with partial indexes and no foreign key to `chat_automations`: `automation_id`, `input_id`, `queue_generation` on `chat_queued_messages` (all set or all null), `automation_id` and `input_id` on `chat_messages`, and `chats.automation_id`. `chats_expanded` is recreated to include `automation_id`. - `chat_automation` values for `api_key_scope` and `resource_type`. The down migration is a no-op because enum values cannot be dropped. **Authorization** - New organization-scoped RBAC resource `chat_automation` (create, read, update, delete). The `agents-access` role grants it for the member's own automations. It is excluded from the member role's user-level permissions, so a member without `agents-access` gets nothing. Site owners and organization admins get it through their existing all-permissions lists. The owner-only update, enable, and run rule from the RFC belongs to the management service in a later PR. - The generated scopes stay out of `externalLowLevel`, so API keys gain no user-requestable scopes. - `dbauthz` wrappers for the three queries this PR adds: `InsertChatAutomation`, `GetChatAutomationByID`, and `DeleteChatAutomationByID`. `InsertChatAutomation` also requires update on the target chat, read on the creating chat, and read on the new-chat model config (its user and group ACL). There is also a `dbgen.ChatAutomation` helper. **Audit** - `enterprise/audit/table.go` entries for `ChatAutomation`, with `webhook_secret_hash` and `prompt` marked `ActionSecret`. The prompt is treated as chat content, like chat titles. The schedule cursor, revision, and queue generation are ignored as internal bookkeeping. `Chat.automation_id` is tracked. **Required checks from the ticket** - Migrations apply and roll back: `TestMigrate` and `TestMigrateUpWithFixtures` (new fixture for 000609). Dogfood UAT also applied the migrations on a dev instance, rolled them back, re-applied them, and compared schema dumps. After rollback, the only differences from main were the enum values that cannot be dropped and develop.sh's own `_develop` schema. After re-applying, the only difference from this branch's `dump.sql` was the `_develop` schema. - Secret hash marked secret: `Test_chatAutomationSecretFieldsRedacted` checks the table entries and that an audit diff contains neither the raw hash, its base64 form, nor the prompt text. Also covered: `TestRolePermissions` cases for own, other owner, other organization, and a member without `agents-access`. `TestChatAutomationShapeConstraints` covers the named CHECKs, the cross-organization foreign key and trigger, a cross-organization retarget, and deleting referenced chats. Under `site/`, only generated files changed (`typesGenerated.ts`, `rbacresourcesGenerated.ts`). Closes CODAGT-1207 Part of CODAGT-1135 --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_ --------- Signed-off-by: Thomas Kosiewski <tk@coder.com> |
||
|
|
ff158c0589 |
feat: preserve user-supplied chat titles (#29486)
Chats created through the API or MCP couldn't be given a title, and automatic title generation could overwrite a title the user had set, at creation or by renaming during generation. #30254 tried to keep renames by comparing title text, but text can't tell a user title from a generated one that has the same text. Chat creation over the API and MCP now accepts a title. Each chat records where its title came from: fallback (derived from the prompt), generated, or user. A title can only be replaced by a title from the same source or a higher one, so automatic generation never overwrites a user's title. Title changes no longer reorder the chat list, and clients apply them in the order they were written. Chats created before this change are treated as having fallback titles. Closes CODAGT-1047 Reverts #30254 |
||
|
|
bb31a19e5b |
fix(coderd): return queued chat messages in promotion order (#30255)
Clients read the chat queue through GetChatQueuedMessages, which ordered by created_at, while chatstate promotes by position. Promoting a queued message changes only its position, and concurrent senders can commit with created_at and position in opposite orders, so clients showed the wrong next message. Order GetChatQueuedMessages by position, id. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_ |
||
|
|
a328bee007 |
fix(coderd/database): fix duplicate chat projects migration and stale chat title query (#30257)
Fixes two semantic conflicts on main from merging #29242 (chat projects): - **Duplicate migration 000606.** #29242 and #29081 both added `000606`, so golang-migrate refuses to load the migrations (`duplicate migration file: 000606_chat_projects.down.sql`), breaking coderd startup and database tests. `000606_chat_projects` (up, down, fixture) is renamed to `000607`. Main has never been able to migrate with the duplicate, so no deployment has recorded version 606 as chat projects. - **Stale generated code.** #30254 added `UpdateChatTitleByIDIfTitle` with an explicit chat column list that lacks the `project_id` column #29242 added. On regeneration sqlc emits a separate row type instead of `database.Chat`, and dbauthz fails to compile (the `gen` check). The column is added and `queries.sql.go` regenerated. `make gen` is clean, `go build ./...` passes, and the migration, dbauthz method suite, and chatd title tests pass. --- Generated with Coder Agents on behalf of @f0ssel. |
||
|
|
7cd1b2040b |
feat: add chat projects API (#29242)
- Experiment, behind `chat-projects` (off by default). First of three stacked PRs: #29301 (memory), #29583 (UI). This PR is API only; the screenshots below are from the UI in #29583. - CRUD under `/api/experimental/organizations/{organization}/chats/projects/{project}`. `GET /api/experimental/chats/projects` lists the caller's projects across all organizations. - `project_id` on chat create and as a chat list filter. A chat joins a project only at creation; there is no move. - Projects are identified by ID. Names are display labels (64 characters, description 1024) with no uniqueness constraint. An optional `icon` URL (usually an emoji image under `/emojis`) is capped at 256 characters and audited. - A user can own at most 100 projects across all organizations; creating another answers 409. Creates count and insert under a per-owner advisory lock so concurrent requests cannot overshoot. - Projects are private to their `owner_id`. Members may only create their own, and a chat can only be bound to a project its owner owns. An unreadable or foreign project on chat create answers 404 so IDs cannot be probed; a project from another organization answers 400. - Permissions ride on the `agents-access` role next to chats (after #30004), so whoever can use Coder Agents can use projects. Owners and org admins reach every project. ### Examples Create a project in an organization: ```http POST /api/experimental/organizations/{organization}/chats/projects {"name": "Platform", "description": "Infra and deploy work", "icon": "/emojis/1f680.png"} ``` ```json { "id": "7c0e4a3e-5b1f-4f7d-9a51-2f3c8f6d2b10", "organization_id": "bb640d07-ca8a-4869-b6bc-ae61ebb2fda1", "owner_id": "0ed9befc-4911-4ccf-a8e2-559bf72daa94", "name": "Platform", "description": "Infra and deploy work", "icon": "/emojis/1f680.png", "created_at": "2026-09-30T15:04:05Z", "updated_at": "2026-09-30T15:04:05Z" } ``` List the caller's projects across organizations with `GET /api/experimental/chats/projects` (an array of the object above). Update with `PATCH .../chats/projects/{project}` and any of `name`, `description`, `icon`; delete with `DELETE`, which detaches its chats. Create a chat in the project, and list only its chats: ```http POST /api/v2/chats {"organization_id": "bb640d07-…", "project_id": "7c0e4a3e-…", "content": [{"type": "text", "text": "Plan the deploy"}]} GET /api/v2/chats?project_id=7c0e4a3e-… ``` At the limit: ```json 409 {"message": "You can have at most 100 chat projects. Delete a project to create another."} ``` <details> <summary>Screenshots (UI from #29583)</summary>      </details> --- Generated with Coder Agents on behalf of @f0ssel. --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
7b40aa7fa6 |
fix(coderd): keep user chat renames made during title generation (#30254)
Automatic title generation captured the chat title when it was scheduled, ran the model call, then wrote the generated title with an unconditional UPDATE. A rename that landed while the model call ran was silently overwritten. Add UpdateChatTitleByIDIfTitle, a compare-and-set variant that only writes when the stored title still equals the title generation started from. When it matches no rows, generation keeps the current title and skips recording and publishing the generated one. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_ |
||
|
|
d11809f095 |
fix(coderd): enforce chatd concurrent-agent capacity on every path into running (#30243)
## Problem
The concurrent-agent cap (5 root chats and 10 subagent chats per
deployment by default) could be exceeded. Three paths put more chats in
`running` with a fresh lease than the pool allows:
1. **Interrupting a waiting chat.** A user sends a message with
`busy_behavior=interrupt` to a chat that is waiting for capacity. The
chat becomes `interrupting` while unowned, a worker acquires it without
admission, and `FinishInterruption` promotes the queued message into a
running turn under that ownership.
2. **Taking over an orphaned `requires_action` chat.** A replica dies
while its chat is in `requires_action`, its lease goes stale and another
chat takes the slot. A worker acquires the `requires_action` chat
without admission, and a tool result, an interrupt or the action
deadline returns it to `running` under that ownership.
3. **Heartbeat reviving a stale lease.** A replica loses the database
for longer than the 30 second stale threshold and another replica admits
a second chat into the slot. When the first replica comes back, its
heartbeat upsert renews the stale lease unconditionally, and its runner
keeps generating until the turn ends.
## Root cause
`agentCapacityLimiter.Admit` returned `true` without taking the
admission lock or counting for every status other than `running`. The
count query already treats every owned chat with a fresh lease as
holding a slot, so the shortcut let chats acquire a slot uncounted and
later move back to `running` under that ownership. Separately,
`BatchUpsertChatHeartbeats` had no freshness or ownership condition.
## Fix
- **Admission for every runnable status.** `Admit` now counts for
`running`, `interrupting` and `requires_action`, and a refused pool
skips the rest of its candidates in the pass regardless of status. An
owned chat holds its slot until it releases ownership, so every later
transition back to `running` stays within capacity.
- **Fenced heartbeat renewal.** `BatchUpsertChatHeartbeats` is replaced
by `RenewChatHeartbeats`, which only extends rows that are still fresh
and still own their chat, using `statement_timestamp()` read after the
capacity admission lock is granted. The renewal runs under that lock
with a `lock_timeout` (`HeartbeatLockTimeout`) and a per-tick timeout
(`HeartbeatRenewalTimeout`, a third of the stale threshold). Admission
also waits at most 5 seconds for the lock.
- **Stop runners that lost their lease.** `heartbeatOnce` requests
cleanup of every runner whose lease was not renewed, which cancels its
generation. The chat stays owned by the stale lease until an admitted
worker takes it over. A tick that fails or times out cleans up no
runners and retries on the next tick.
- **Instant Stop on a queued chat.** Requiring admission for
`interrupting` would have made Stop on a chat waiting for capacity hang
until a slot frees. When `Interrupt`, or `SendMessage` with
`busy_behavior=interrupt`, runs on a running chat with no owner, the
transition now applies `FinishInterruption` in the same transaction and
cancels orphaned tool calls. An empty queue lands in `waiting`;
otherwise the queue head is promoted into `running`, still unowned, so
it goes through admission.
- **Queued metric.** `CountChatCapacityQueuedByPool` (behind
`coderd_chatd_agents_queued_for_capacity`) now also counts
`interrupting` and `requires_action` chats without a fresh lease, since
they also wait for admission.
## How it was found
A TLA+ model of capacity admission found a counterexample for each path.
Go tests reproduced all three against the previous code (each failed
with `"2" is not less than or equal to "1"` for a root pool of capacity
1), and the model with the fix enabled passes the safety invariant and
the liveness properties. The model itself is not part of this PR.
## Tests
The three reproductions are kept as regression tests in
`coderd/x/chatd/agentcapacity_regression_internal_test.go`:
`TestAdmission_InterruptWithMessageOnWaitingChatStaysWithinCapacity`,
`TestAdmission_RequiresActionTakeoverStaysWithinCapacity` and
`TestRunnerManager_HeartbeatDoesNotReviveStaleLease`. The original
reproductions failed on the base commit. Restoring the admission
shortcut makes the `requires_action` test fail, and restoring the
unconditional heartbeat upsert, or dropping the runner cleanup, makes
the heartbeat test fail. New coverage also includes
`TestAdmission_StopWaitingChatNeedsNoCapacity`,
`TestRenewChatHeartbeats_OnlyRenewsFreshOwnedLeases`,
`TestRunnerManager_FailedHeartbeatTickKeepsRunners`, and chatstate tests
for the unowned interrupt paths. Existing tests that encoded the old
behavior (`requires_action` chats holding no slot, interrupting chats
always acquirable, unconditional heartbeat renewal) were updated.
## Behavior / API change
- `POST /api/v2/chats/{chat}/messages` with `busy_behavior=interrupt` on
a chat that no worker owns (for example one waiting for capacity) now
returns the promoted message with `queued=false` instead of a queued
message, and the chat stays in `running` instead of `interrupting`. If
the chat already had queued messages, the queue head is promoted and the
new message stays queued.
- `POST /api/v2/chats/{chat}/interrupt` on such a chat lands it in
`waiting` immediately (or in `running` with the queue head promoted)
instead of `interrupting`.
- An unowned `interrupting` or `requires_action` chat (for example after
its replica died) now waits for a free slot before its runner starts,
which also delays enforcing its action deadline while the pool is full.
- New internal chat worker settings `HeartbeatRenewalTimeout` and
`HeartbeatLockTimeout`, defaulting to a third of the stale threshold and
half of that.
- Heartbeat ticks now take the capacity admission advisory lock briefly
on each replica.
## Out of scope / follow-ups
- Interrupting a chat still owned by a dead runner (stale lease,
`worker_id` set) still waits for a slot.
- An admission lock timeout should end the acquisition pass instead of
logging a warning per candidate.
- The per-chat `queued_for_capacity` flag in the chat API still reports
only `running` chats.
- Alert on failed heartbeat ticks.
`coderd/x/chatd/ARCHITECTURE.md` gains TODO lines for the affected
sections; the prose is left for the owner.
---
_Generated with [`xum`](https://github.com/coder/xum) • Model:
`anthropic:claude-opus-5-5` • Thinking: `high`_
|
||
|
|
cdc87ee047 |
fix(coderd): recheck auto-archive candidates under the family lock (#30242)
Auto-archive read its candidates without locks and then archived each family without rechecking, so a chat that became active after selection (a new message in the root or a child, a pin, a status change) could still be archived. SetFamilyArchivedInput gains an optional Precondition that runs after the root and every family member row are locked root-first; auto-archive uses it to rerun the candidate filters for that root through the new GetAutoArchiveInactiveChatCandidateByID query and skips chats that no longer qualify. A parity test keeps the batch and by-ID queries in sync, regression tests cover root, pin and child activity races, and ARCHITECTURE.md describes the recheck. Builds on #30240. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_ |
||
|
|
9ac4628947 |
fix(coderd): stop announcing stale chat retries and fence retried attempts (#30241)
Stop announcing a pending chat retry once the chat leaves running, and stop streaming preview parts of a failed generation attempt after its retry is recorded. UpdateChatExecutionState now clears retry_state on every non-running status write (the trigger advances retry_state_version), the stream loop only emits retry while the chat is running, and it retires the current attempt once it observes a retry payload or an advanced retry_state_version within an unchanged episode. Regression tests cover the error, interrupt and requires_action exits, late parts after a retry, and a retry cleared before the stream syncs; ARCHITECTURE.md documents the new rules. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_ |
||
|
|
63a95d87f3 |
feat(coderd): key chat diff statuses by ref (#29081)
Part 1 of 6. `chat_diff_statuses` had one row per chat, so a chat could only track one PR, and switching branches overwrote the old branch's PR data. The primary key is now `(chat_id, git_remote_origin, git_branch)`, so each ref the agent reports gets its own row. The API still returns a single `diff_status`: the first row of the chat's statuses, ordered by `updated_at`, newest report first. The primary ref changes only when the agent reports a ref: refreshes, claims, backoffs and URL writes do not advance `updated_at`, so they cannot reorder the primary. The gitsync worker now claims, backs off and clears rows by the full key, not the whole chat. When the diff GET discovers a PR URL, it updates the existing ref row and no longer inserts one. Telemetry picks one row per chat. **NOTE**: Migration `000606` swaps the primary key under an ACCESS EXCLUSIVE lock. The table is small. Existing rows already fit the new key, so no data is rewritten. The down migration keeps only the primary row per chat. Stop old coderd instances before migrating: their `ON CONFLICT (chat_id)` upserts fail once the old key is gone. Generated by Coder Agents. |
||
|
|
1e7a300dab |
fix: return token usage for every thread in AI Gateway session detail (#30087)
The AI Gateway session detail endpoint (GET
/api/v2/ai-gateway/sessions/{session_id}) reports zero token usage for
every thread after the 50th in a session. The session summary metadata
is also computed from only the first 50 threads.
To aggregate token usage, the handler lists every thread in the session
with ListAIBridgeSessionThreads and leaves Limit unset. The query treats
a limit of 0 as "use the default of 50", so token usage for later
threads is never fetched and those threads report zero.
Make a limit of 0 mean "no limit", as groups.sql and aicostcontrol.sql
already do. The only other caller is the page fetch in the same handler,
which always passes a limit between 1 and 200, so pagination does not
change. Also remove the nolint:exhaustruct directive that hid the
missing field.
Each page request still loads every interception in the session. A
follow-up will fetch per-thread usage for the current page only and
compute the session metadata separately.
Fixes AIGOV-640
Generated by Coder Agents on behalf of @SasSwart.
|
||
|
|
0285a3cf40 |
feat: recognize VS Code forks and JetBrains IDEs as session apps (#30116)
Closes #29928 |
||
|
|
34db9ca638 |
feat: price AI Gateway token usage by provider model (#30090)
## Description When the requested model has no price, price AI Gateway token usage by the model the provider reports instead of leaving it unpriced. This covers providers that resolve an alias to a specific model, such as `codex-auto-review` served as `gpt-5.6-luna`. Stacked on #30086, which records the provider model. Closes https://linear.app/codercom/issue/AIGOV-650 ## Changes - Look up the price for the requested model first, then the provider model, preferring a custom price over the price book for each model. - Store the provider model and the model used for pricing (`priced_model`) on each token usage. - Add a `provider_model` label to `unpriced_token_usage_records_total`. - Document the lookup order and the new metric label. > [!NOTE] > Generated by Claude Code on behalf of @ssncferreira. |
||
|
|
a0b00b5e4b |
feat: store experiment rules with revisions and audit types (#29898)
Slice 2 of 4 for runtime experiment rules: persist one rule per experiment in `site_configs`, with revisions that only increase, and add an audit record type. Nothing calls this code yet, so behavior does not change. ## What's in it - **Queries** in `siteconfig.sql`, all checked by dbauthz against `ResourceDeploymentConfig`: - `GetExperimentRules` (read) lists `experiment_rule:` keys with `starts_with`, not `LIKE`, because `_` is a `LIKE` wildcard. - `GetExperimentRule` (read) reads one row. - `UpsertExperimentRule` (update) writes one row. - **`experiments.WriteRule(ctx, db, actor, ex, rule, expectedRevision)`** validates the rule first, then does everything in one `READ COMMITTED` transaction: - It takes an advisory lock for that experiment, waiting at most 5 s. - It reads the current row; a missing row counts as revision 0. - It returns `*RevisionConflictError` when the expected revision is stale. - An identical mode and condition writes nothing and returns `changed=false`. The first `inherit` on a missing row is a change and stores revision 1. - Otherwise it writes `revision+1` with the actor and time, and returns the old and new rule as read and written inside the transaction. - **`experiments.NewDBStore(db)`** implements the evaluator's `Store`. It reads rules by prefix, plus the user's id, username, email, explicit site roles, member org names, and `<org>/<group>` group names (including `<org>/Everyone`). It runs as the system. - **Audit:** - A new `resource_type` enum value `experiment_rule` (migration `000602`; the down migration is a comment because PostgreSQL cannot drop enum values). - A new struct `database.ExperimentRule`. `experiments.AuditRecord` derives its ID from the experiment name, so audit history groups by experiment. - In the audit table, `experiment`, `mode` `revision` and `condition` are tracked (`ActionTrack`), so the audit diff shows the old and new condition. In Enterprise, the slog audit backend also writes that diff to the server log. Admins and auditors can already read the condition through the rules API. - The audit page filter includes the new resource type. ## Testing - `WriteRule` and `NewDBStore` tests run against real PostgreSQL. They cover stale revisions after a reset and a recreate, two concurrent writers with the same expected revision (exactly one wins), no-ops, and rollback after a forced error. - dbauthz method tests and an audit diff test that shows both condition texts. Remote dogfood UAT through Coder Agents passed the upgrade from slice 1 (existing state kept, enum value added), a fresh install, unchanged `/api/v2/experiments`, and the `WriteRule` tests under `-race`. The licensed audit-page check could not run: the test workspace has no premium license. That round tested `015979ca96`. The commit after it only adds `AuditRecord`, and a follow-up UAT round will cover the final head. > [!NOTE] > Migration `000604` was the next free number when this was written. Recheck it before merging. ## Stack 1. #29869: evaluator library 2. **This PR:** storage and audit types 3. Enforcement and discovery (`/api/v2/experiments`, meta tag, chatd `mcp-tool-search`, ruleguard) 4. Control plane (experimental API, `coder exp experiment-rules` CLI, docs) > [!NOTE] > This PR was written by an AI agent (Xum). --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_ --------- Signed-off-by: Thomas Kosiewski <tk@coder.com> |