Commit Graph
1831 Commits
Author SHA1 Message Date
Michael Suchacz 2dc16cde0a Merge remote-tracking branch 'origin/main' into mike/move-chats-to-projects
# Conflicts:
#	site/src/pages/AgentsPage/AgentsPageLayout.test.tsx
2026-10-15 20:08:35 +00:00
Steven Masley 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.*
2026-10-15 14:20:44 -05:00
Steven Masley 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.
2026-10-15 13:32:40 -05:00
Michael Suchacz 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.
2026-10-14 15:31:47 +00:00
Garrett Delfosse 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.
2026-10-14 10:42:00 -04:00
Ehab Younes 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
2026-10-14 13:54:17 +03:00
Bobby Ho 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.

- Rename 000618_mcp_server_oauth2_resource.{up,down}.sql to 000622, the
next
  free number after 000621_skills.
- No schema change; dump.sql regenerates identically.
2026-10-13 23:08:37 +00:00
Michael Suchacz 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.

![MCP server edit form showing the Resource URL field filled by
auto-discovery](https://github.com/user-attachments/assets/319bc8a0-f6e1-4b7d-8878-8e341c94e590)

## 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 2b875068e0: literal verdict FAIL. When the resource
changed between connect and callback, the callback redeemed the code for
the new resource and stored a grant. The 409 binding fixes it.
- Round 2 on 4b54ffb5f9: PASS. The mid-flow change returned 409 with no
token request in every variant (strict and lenient servers, set,
cleared, abandoned flows, parallel configs); auto-discovery, connect,
refresh, validation, and the docs matched. Minor observations: the
binding cookie is an unkeyed digest, so a member who edits their own
HttpOnly cookie can redeem a code with the current resource, which a
fresh connect also gives them; other settings changed mid-flow still
reach the token endpoint, as on main; the 409 popup shows raw JSON; the
UI does not warn that a resource change disconnects users; validation
toasts omit the server's detail.
- Round 3 on the final head 2d6abfac49: PASS. The head moved during the
round, so it ran in four parts, the last on 2d6abfac49. That part
covered the matching rules (queries including empty ones, default and
zero-padded ports, `%2F`, escaped dot segments, trailing slashes, IPv6
literals), the invalid fallback (stored empty, no `resource` sent),
every validation value through the API and the UI, and the discovery,
connect, refresh, and mid-flow 409 flows. The equivalent-encoding cases
last ran live on 5c23fe00dca; unit tests cover them on the final head.
Observation: the path-aware metadata URL is built from the decoded path,
so `/mcp%2Ftenant` fetches metadata for `/mcp/tenant`, as on main.
- `frontend-review` and the eight-role `coder-frontend-review` ran on
the `site/` diff; every role passes after the label, help text, row
layout, API doc wording, and form test fixes. Declined: linking field
descriptions with `aria-describedby`, because the form's shared `Field`
primitive renders every description without one, so it belongs in a
change to all fields.

## Review record

| Review | Commit | Findings | Disposition |
|---|---|---|---|
| [Codex round
1](https://github.com/coder/coder/pull/30651#pullrequestreview-5492085359)
| 4b54ffb5f9 | P2: a resource with another query was treated as
covering the server URL | Reproduced with a failing test, fixed in
bf0879560c, resolved |
| [Codex round
2](https://github.com/coder/coder/pull/30651#pullrequestreview-5492305363)
| bf0879560c | P2: explicit default ports; P2: `%2F` treated as a
segment boundary; P2: `url.Parse` accepts spaces and invalid escapes |
Each reproduced with a failing test, fixed in 16a1d783fa, resolved |
| [Codex round
3](https://github.com/coder/coder/pull/30651#pullrequestreview-5492677517)
| 16a1d783fa | P1: docs wrapped mid-sentence (DOC1); P2: equivalent
percent-encodings compared unequal; P2: brackets accepted outside an IP
literal host | Fixed in 8d617f0528 (the P2s reproduced with failing
tests; the same pass also covers the query, dot segment, and user
information cases), resolved |
| [Codex round
4](https://github.com/coder/coder/pull/30651#pullrequestreview-5493033885)
| 8d617f0528 | P2: the server URL fallback was not validated; P2:
escaped dot segments were compared before decoding | Each reproduced
with a failing test, fixed in 5c23fe00dc, resolved |
| [Codex round
5](https://github.com/coder/coder/pull/30651#pullrequestreview-5493342889)
| 5c23fe00dc | P2: an empty query (`?`) was treated as no query |
Reproduced with a failing test, fixed in 72c019ff5e, resolved |
| [Codex round
6](https://github.com/coder/coder/pull/30651#pullrequestreview-5493524846)
| 72c019ff5e | P2: `/mcp/` covered `/mcp`; P2: equivalent IPv6 literals
compared unequal; P2: malformed IP literals accepted | The first two
reproduced with failing tests. Go 1.26.5 `url.Parse` already rejects the
hosts named in the third, but it accepted an IPv6 zone identifier. All
fixed in ef2df2c4e7, resolved |
| [Codex round
7](https://github.com/coder/coder/pull/30651#pullrequestreview-5493668131)
| ef2df2c4e7 | P2: leading zeros in a port made equal origins differ |
Reproduced with a failing test, fixed in 2d6abfac49, resolved |
| [Codex round
8](https://github.com/coder/coder/pull/30651#issuecomment-6128743972) |
2d6abfac49 | None: "Didn't find any major issues" | Clean verdict on
the current head |

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-13 23:03:08 +02:00
Michael Suchacz 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.
2026-10-13 16:43:15 +00:00
Michael Suchacz 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.
2026-10-13 16:24:45 +00:00
Michael Suchacz 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.
2026-10-13 15:51:31 +00:00
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>
2026-10-13 19:15:16 +07:00
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>
2026-10-13 16:45:44 +07:00
Bobby Ho 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.
2026-10-13 03:43:55 +00:00
Ehab Younes 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.
2026-10-12 23:58:35 +03:00
Ehab Younes 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
2026-10-12 15:56:40 +03:00
Thomas Kosiewski 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`_

![Agents sidebar after the upgrade, with no Automations entry](https://github.com/user-attachments/assets/bd446035-7fc8-4ca4-a735-3a4f36380738)
2026-10-11 19:57:49 +02:00
McKayla はな 86ebc72236 chore: remove unused autofill-parameters endpoint and experiment (#30525) 2026-10-09 16:42:41 -06:00
Michael Suchacz 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 f8e053e385, upgraded in place to
9abea171ba, then ran the same queries. Result rows and HTTP bodies were
identical.

| Query (fixture chat) | Without the index | With the index |
|---|---|---|
| Newest page, idle chat | pkey backward, 83,471 buffers, 391 ms | new
index, 13 buffers, 0.13 ms |
| Ascending `after_id=0, limit 1`, recent chat | pkey forward, 80,843
buffers, 404 ms | new index, 5 buffers, 0.06 ms |
| `POST /messages` to a default-title chat | about 0.5 s | about 0.02 s
|

- A sweep of every `chat_id`-filtered `chat_messages` query in
`chats.sql`, plus the two soft-delete updates and the chat-list
`has_unread` checks, found no query more than 2x worse.
- The UI loaded older pages of long chats, an empty chat, and a chat
with soft-deleted messages correctly.
- Running the down migration restores the base plan.
- Local tests: `TestMigrate`, `TestMigrateUpWithFixtures`,
`TestMigrationChain`, `TestCheckLatestVersion`,
`TestGetChatMessagesByChatIDOrdersByID`,
`TestGetChatMessages_Pagination`, `TestGetChatMessages_TurnStartID`, and
the pre-commit hook (`make gen` changed only `dump.sql`).
- No plan test is added. The repo has none, and the bad plan needs
production-shaped data: on a 200k-row local fixture with rows under
about 250 B, the planner kept the pkey even with the index. Dogfood rows
average about 1 KB, which by the same cost model favors the index for
any chat under about 3% of the table (the largest is under 0.4%).

## Review record

- Polish pass: no changes.
- Codex review: no findings on 9abea171ba.
- Test audit: no tests in scope. The audited tree is the head commit
9abea171ba.
- Remote UAT round 1: PASS on 9abea171ba. Two informational notes: the
write lock described above, and no `IF NOT EXISTS` (only 11 of 111 index
migrations use it).

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-09 19:23:25 +02:00
Michael Suchacz 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.

![Before: partial label for a 200-step
turn](https://github.com/user-attachments/assets/6b4e4abf-4bc6-4476-8ed5-52c39c8eebe6)

**After**, on `186a9b053a0`, after the first page: "Worked for 6m 39s
(200 steps)".

![After: exact label for the same 200-step
turn](https://github.com/user-attachments/assets/22ec1900-1e7b-4c98-87a9-0463e1d852c2)

A 750-step turn, larger than one 1000-message page, is exact once the
next page loads:

![After: 750-step turn,
expanded](https://github.com/user-attachments/assets/f927a848-62b1-4eb7-a548-fbdff61fc9c4)

A malformed fill response keeps the loaded messages and the partial
label instead of showing "Failed to load chat", as `ecdfb27ec29` did:

![Malformed fill response: partial label, no error
screen](https://github.com/user-attachments/assets/6d393df9-15a2-471e-9bb6-71e919aca570)

## 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.
2026-10-09 13:32:39 +02:00
Sas Swart 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
2026-10-08 08:27:43 +02:00
Michael Suchacz 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.
2026-10-06 22:42:56 +02:00
Michael Suchacz 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`) |
|---|---|
| ![Organization admin saves organization instructions and sees a
success
toast](https://gist.githubusercontent.com/ibetitsmike/1027eba97c252d6f3e921d2f46debd01/raw/5a70872eae4170b4e9330c07696727071f0962e8/r1-admin-saved.png)
| ![Organization auditor sees the instructions
read-only](https://gist.githubusercontent.com/ibetitsmike/1027eba97c252d6f3e921d2f46debd01/raw/5a70872eae4170b4e9330c07696727071f0962e8/r1-auditor-readonly.png)
|
| **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`)** |
| ![Loading spinner with no textarea or
buttons](https://gist.githubusercontent.com/ibetitsmike/1027eba97c252d6f3e921d2f46debd01/raw/5a70872eae4170b4e9330c07696727071f0962e8/r2-loading.png)
| ![Error alert above the instructions field with the unsaved edit
preserved](https://gist.githubusercontent.com/ibetitsmike/1027eba97c252d6f3e921d2f46debd01/raw/5a70872eae4170b4e9330c07696727071f0962e8/r2-refetch-error-edit-kept.png)
|

## 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.
2026-10-06 10:48:07 +02:00
Danielle Maywood 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.
2026-10-06 09:32:46 +01:00
Garrett DelfosseandClaude Opus 5.5 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>


![save_memory](https://raw.githubusercontent.com/coder/coder/f0ssel/chat-projects-assets/v3/pr2-01-chat-saves-memory-raw.png)

![read_memory](https://raw.githubusercontent.com/coder/coder/f0ssel/chat-projects-assets/v3/pr2-02-chat-reads-memory-raw.png)
![Agent reads every
memory](https://raw.githubusercontent.com/coder/coder/f0ssel/chat-projects-assets/v3/pr2-agent-lists-memories-raw.png)

</details>

---
Generated with Coder Agents on behalf of @f0ssel.

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 12:52:10 -04:00
Mathias Fredriksson 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
2026-10-02 22:36:59 +03:00
Thomas Kosiewski 122757d1d2 fix(coderd/database/migrations): lock chat tables before 000609 alters them (#30297) 2026-10-02 20:52:43 +02:00
Thomas Kosiewski 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`_
2026-10-02 15:54:55 +00:00
Thomas Kosiewski 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`_
2026-10-02 15:54:53 +00:00
Thomas Kosiewski 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>
2026-10-02 15:54:53 +00:00
Thomas Kosiewski 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`_
2026-10-02 15:54:52 +00:00
Thomas Kosiewski 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>
2026-10-02 15:54:51 +00:00
Thomas Kosiewski 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`_
2026-10-02 15:54:51 +00:00
Thomas Kosiewski 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>
2026-10-02 15:54:50 +00:00
Thomas Kosiewski 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>
2026-10-02 15:54:49 +00:00
Thomas Kosiewski 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`_
2026-10-02 15:54:49 +00:00
Thomas Kosiewski 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>
2026-10-02 15:54:48 +00:00
Mathias Fredriksson 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
2026-10-02 12:58:53 +00:00
Thomas Kosiewski 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`_
2026-10-02 00:04:55 +02:00
Garrett Delfosse 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.
2026-10-01 13:10:33 -04:00
Garrett DelfosseandClaude Opus 5.5 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>

![Create
project](https://raw.githubusercontent.com/coder/coder/f0ssel/chat-projects-assets/v3/pr1-01-create-project-dialog.png)
![Sidebar
folders](https://raw.githubusercontent.com/coder/coder/f0ssel/chat-projects-assets/v3/pr1-02-sidebar-folders.png)
![Project
page](https://raw.githubusercontent.com/coder/coder/f0ssel/chat-projects-assets/v3/pr1-03-project-page.png)
![Folder
menu](https://raw.githubusercontent.com/coder/coder/f0ssel/chat-projects-assets/v3/pr1-05-folder-menu.png)
![Another member sees no
projects](https://raw.githubusercontent.com/coder/coder/f0ssel/chat-projects-assets/v3/pr5-member-sees-no-projects.png)

</details>

---
Generated with Coder Agents on behalf of @f0ssel.

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 11:49:21 -04:00
Thomas Kosiewski 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`_
2026-10-01 17:41:56 +02:00
Thomas Kosiewski 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`_
2026-10-01 16:42:51 +02:00
Thomas Kosiewski 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`_
2026-10-01 16:40:24 +02:00
Thomas Kosiewski 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`_
2026-10-01 14:10:00 +02:00
Danielle Maywood 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.
2026-10-01 10:59:27 +01:00
Sas Swart 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.
2026-09-30 14:13:42 +02:00
Ehab Younes 0285a3cf40 feat: recognize VS Code forks and JetBrains IDEs as session apps (#30116)
Closes #29928
2026-09-29 23:41:43 +03:00
Susana Ferreira 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.
2026-09-29 17:47:28 +01:00
Thomas Kosiewski 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>
2026-09-29 12:22:59 +00:00