Commit Graph
53 Commits
Author SHA1 Message Date
Michael SuchaczandEthan Dickson 0ef331fcd0 feat!: scope chat model override settings to organizations (#28442)
Moves chat model override settings from deployment scope to organization
scope, following the org-scoping of chat model configs in #28440.

Depends on #28440 (based on
`ethan/chat-models-availability-consolidation`).

Remote dogfood UAT passed on this change, including multi-organization
scenarios on a licensed deployment with real models.

## Problem

Chat model configs are now owned by organizations, but the override
settings that reference them stayed deployment-wide: admin
subagent/title/compaction overrides in `site_configs`, personal
overrides in `user_configs`, and the advisor model embedded in the
advisor runtime JSON. A single deployment-level override can only point
at one organization's model config, so overrides effectively worked for
the default organization only, and title generation had to soften its
hard-failure contract to tolerate unusable overrides.

## Fix

Admin overrides (general, explore, title generation, compaction,
advisor) and personal overrides (root, general, explore) become
organization-scoped rows in two new typed tables,
`chat_organization_model_overrides` and `chat_user_model_overrides`.
Composite foreign keys on `(organization_id, model_config_id)` make
cross-organization model references unrepresentable. The migration
deletes the legacy serialized `site_configs`/`user_configs` override
keys instead of migrating them; admins and users re-select models in the
new organization-scoped settings.

New endpoints live under
`/api/experimental/organizations/{organization}/chats/model-overrides`
(admin defaults) and
`/api/experimental/organizations/{organization}/members/{user}/chats/model-overrides`
(personal overrides); the legacy deployment-level routes are removed.
chatd resolves overrides via the chat's organization, restoring the
hard-failure contract for configured-but-unusable title generation
overrides. The deployment-level `AllowUsers` switch and advisor runtime
limits stay deployment-scoped.

The Coder Agents settings UI gains a per-organization "Defaults &
overrides" page under AI Settings > Models, and the personal overrides
page gains an organization selector for users in multiple organizations.
Advisor telemetry now reports per-organization override rows.

**BREAKING CHANGE**: Existing deployment-level overrides are dropped,
not migrated. Every organization starts with no overrides after upgrade,
and model selections must be re-applied in the new settings. The legacy
deployment-level override endpoints are removed.

Closes https://linear.app/coder/issue/CODAGT-872

> [!NOTE]
> Xum (AI agent) authored this PR on behalf of @ibetitsmike.

<!-- xum-attribution: model=claude-opus-4-6 thinking=high -->

---------

Co-authored-by: Ethan Dickson <ethan@coder.com>
2026-08-24 11:20:26 +01:00
Ethan aefc761fcf feat!: enforce organization scope for chat models (#27959)
## Summary

Move chat model management and runtime availability to
organization-scoped API routes. The API now identifies the organization
in every model-management operation, applies chat model RBAC and ACLs
within that organization, and returns only models the caller can use
there.

Depends on #27958.

## Problem

Chat model configurations now belong to organizations, but the existing
experimental management and discovery APIs are deployment-shaped. Their
routes do not identify an organization, SDK methods do not accept an
organization ID, and item operations address a model configuration by ID
alone.

That contract cannot safely expose organization-owned model
configurations. It also does not provide the organization-specific
provider information, authorization behavior, ACL management, or
recovery guidance needed by the settings and chat clients later in the
stack.

## Fix

Add organization-scoped chat model endpoints under:

```text
/api/experimental/organizations/{organization}/chats/models
```

The new API provides:

- Organization-scoped model listing and creation.
- Model get, update, and delete operations scoped by both organization
and model ID.
- Model ACL read and update operations.
- Organization-scoped runtime availability and provider status.
- Redacted provider descriptors for model-management clients.
- Chat model RBAC and row ACL enforcement for writes, runtime discovery,
and Chatd model selection.
- Recovery guidance when a chat's organization has no usable local
model.

Historical chats can retain a model reference owned by another
organization, but runtime selection uses an authorized local default
when one is available. Foreign, inaccessible, disabled, and otherwise
unusable models are not exposed as usable selections for the
organization.

The existing collection and availability routes remain as
default-organization compatibility endpoints in this layer:

```text
GET  /api/experimental/chats/model-configs
POST /api/experimental/chats/model-configs
GET  /api/experimental/chats/models
```

These compatibility routes do not provide access to models in
non-default organizations. The previous unscoped item update and delete
routes are removed because item operations must include the owning
organization.

## Breaking change

This changes the experimental HTTP API and Go SDK contracts for chat
model management.

Clients must migrate model-management and availability requests to the
organization-scoped routes and provide an organization identifier. In
the Go SDK, the following methods now require an organization ID:

- `ChatModelAvailability`
- `ChatModels`
- `CreateChatModel`
- `UpdateChatModel`
- `DeleteChatModel`

`ChatModels` also changes its return value from `[]ChatModel` to
`OrganizationChatModelsResponse`, which includes the organization models
and the redacted provider descriptors required by management clients.
New item and ACL methods likewise require both the organization ID and
model ID.

The default-organization compatibility routes reduce the immediate
impact for collection and availability callers, but they do not preserve
the previous item API or SDK method signatures. Generated clients and
direct consumers of the experimental API must be updated before adopting
this change.

_This pull request description was generated by Coder Agents._
2026-08-24 11:20:25 +01:00
Ethan f32592ec6b feat!: scope chat model configs to organizations (#27955)
## Summary

Scope chat model configurations and runtime model resolution to
organizations. Each model configuration now belongs to exactly one
organization, each organization can have its own default model, and
Chatd only uses models that belong to the chat's organization.

Depends on #27968.

## Problem

Chat model configurations are currently deployment-wide even though
chats belong to organizations. A default model, explicitly selected
model, or configured model override can therefore reference the same
model configuration from every organization. This prevents model
catalogs and defaults from being isolated by organization and allows a
chat to retain or select a model that its organization does not own.

Existing model configurations also have no organization ownership or
organization-scoped ACL data, and the database enforces a single live
default across the entire deployment.

## Fix

Add required organization ownership and ACL storage to chat model
configurations. The migration assigns every existing configuration to
the default organization, seeds organization-wide read access, and
replaces the deployment-wide default constraint with a per-organization
constraint.

Scope Chatd lookup, admission, fallback, and queue-promotion behavior to
the chat's organization:

- New chats use the usable default model from their organization.
- Explicit model selections from another organization are rejected.
- Deployment or personal overrides that point to a foreign model are not
used for the chat.
- Historical cross-organization model references remain unchanged in the
database, but runtime generation falls back to a usable local default.
- Queue promotion resolves unavailable, disabled, missing, or foreign
stored models to the usable local default and fails when no usable local
default exists.

The existing model configuration management and availability routes
remain compatibility routes for the default organization in this layer.
A later layer in the stack adds organization-scoped management routes
and updates clients to use them.

## Breaking change

This changes chat model behavior from deployment-wide to
organization-scoped. Existing configurations are migrated only to the
default organization; they are not copied into every organization.

As a result:

- A non-default organization no longer inherits the deployment's
existing default model.
- Creating a chat in an organization without a usable local default
fails with `400: No default chat model config is configured.`
- Explicitly selecting a configuration owned by another organization
fails with `400: Model config does not belong to the specified
organization.`
- Existing chats whose stored model belongs to another organization can
generate only if a usable local default is available; otherwise
generation fails.

Deployments using Agents in non-default organizations must configure a
usable default chat model separately for each organization after the
organization-scoped management layers in this stack are available.

_This pull request description was generated by Coder Agents._
2026-08-24 09:30:01 +00:00
Michael Suchacz 9168dbf4d3 refactor(coderd/x/chatd): unify LLM model-call construction behind a resolver (#28145)
Centralizes every LLM client build in chatd behind one resolver and
makes all model calls behave the same, instead of preserving per-flow
quirks.

## Why

`chatd` built LLM clients in five places (standard turn, compaction
override, quickgen, title override, advisor), each repeating config
parsing, route resolution, client construction, and debug wrapping with
slightly different behavior. An earlier iteration of this PR centralized
construction but preserved every historical quirk behind
purpose-specific spec constructors and policy flags. Review concluded
the quirks themselves were the problem.

## What changed

- `Server.resolveModelCall` (`modelcall.go`) is the single pipeline from
a `modelCallSpec` to a ready client plus call metadata. Callers build
spec literals inline; the only spec fields are data: model source (chat
config, explicit config row, or fixed provider/model pair for computer
use), requested reasoning effort, chatd-scoped routing, and build
options.
- Uniform behavior for every call:
- Provider options are always derived. Whole-chat summaries, turn status
labels, and chat-model compaction summaries previously sent none; they
now follow the config like everything else.
- `MaxOutputTokens` always defaults to 32k when the config leaves it
unset. The compaction summary call nulls the cap after copying the
template because it is non-streaming and the Anthropic SDK rejects
non-streaming requests whose cap implies a completion over ten minutes.
- Debug recording always wraps the client when chat debug is enabled,
labeled with the resolved provider/model. The debug-rebuild paths
(`manualTitleDebugSpec`, `quickgenDebugSpec`) are deleted; debug helpers
now only create and finalize run records, since run association flows
through the generate context, not the model.
- Clients are always constructed with the configured model string and
unmutated route, so AI Gateway validation sees the name exactly as
configured. The resolved identity feeds metadata, logs, and debug labels
only.
- `resolvedModelCall.newCall` / `newObjectCall` are the only production
constructors of `fantasy.Call` / `fantasy.ObjectCall`; object calls now
carry the config's sampling parameters.
- Net: -240 lines.

## Validation

- Full `./coderd/x/chatd/...` suite green (PostgreSQL-backed).
- Red-green toggles verified for the three key unifications (option
derivation, token default, summary cap removal).
- ARCHITECTURE.md: prose the refactor made stale is deleted; no new
resolver documentation is added.

> Mux acted on Mike's behalf for this PR.
2026-08-20 20:54:52 +00:00
Michael SuchaczandMathias Fredriksson 443e3b9b80 feat!: org-scope MCP server configs with RBAC (#27942)
Moves `mcp_server_configs` from deployment scope to organization scope
so each organization fully controls the MCP servers its members can use
with Coder Agents.

## Summary

- Migration: adds `organization_id` (NOT NULL, FK) and keeps existing
rows as the default organization's originals with credentials intact.
Other organizations start with no MCP servers and configure their own;
nothing is copied across organizations. Chats outside the default
organization keep any now-cross-organization `mcp_server_ids` entries;
the runtime already ignores IDs that do not resolve in the chat's
organization, so no data rewrite is needed. Slug uniqueness becomes
`(organization_id, slug)`.
- RBAC: new org-scoped `ResourceMCPServerConfig` with regosql converter
and `GetAuthorizedMCPServerConfigs`; org admins get in-org CRUD, org
members get read (replaced by ACL evaluation in the follow-up ACL PR in
this stack).
- API: all config routes nest under the organization, matching
templates: `POST|GET
/api/experimental/organizations/{organization}/mcp-servers` and
`GET|PATCH|DELETE .../mcp-servers/{mcpserverconfig}` (plus
`oauth2/connect`), resolved by a read-only param middleware that
conceals read-denied and cross-organization access as 404. Two routes
stay on the frozen `/api/experimental/mcp/servers/{mcpServer}` block:
the OAuth2 callback (the redirect URI baked into existing AS-side client
registrations) and `oauth2/disconnect`, which must remain reachable by
users removed from the organization so they can still revoke their token
grant.
- Chat runtime: selection validation and generation resolve configs
strictly by IDs, enabled state, and the chat's organization in SQL;
requested duplicates are normalized; invalid or cross-org IDs are
rejected with the precise ID list. IDs already persisted on a chat are
exempt from message-time rejection so disabling a selected server never
blocks sends; generation skips servers that are no longer usable.
- Frontend: API layer and admin settings pages target the new endpoints.
The admin page manages the default organization's servers; the org
picker is tracked separately (CODAGT-714).

- Security hardening from review: OAuth user grants are additionally
bound to `oauth2_revocation_url` (changing it invalidates grants, and a
racing OAuth callback gets 409 instead of recreating a grant).
Stack-wide SSRF protection for MCP config-directed traffic was split
into its own PR at the top of this stack (#28242) to keep this diff
reviewable; this PR keeps main's existing discovery IP-range guard.
OAuth2 auto-discovery now completes before the config row is inserted: a
failed discovery persists nothing, and there is no provisional row that
concurrent updates could race against.

Two follow-up PRs in this stack were split out to keep this diff
reviewable: #28064 completes the swagger annotations for the moved
routes (main already ships these experimental MCP handlers unannotated),
and #28065 carries hardening fixes and regression pins on top of the
cutover.

- Force On enforcement (landed on main mid-review) is org-scoped: the
forced set is read per chat organization
(`GetForcedMCPServerConfigsByOrganization`), so another organization's
`force_on` server never attaches to a chat.

## Breaking changes (experimental API)

The MCP server config endpoints move from the deployment-scoped
`/api/experimental/mcp/servers` block to organization-nested paths:
`POST|GET /api/experimental/organizations/{organization}/mcp-servers`
and `GET|PATCH|DELETE
/api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}`
(plus `oauth2/connect`). The old paths are removed, so API consumers
must supply an organization. Two routes intentionally stay on the frozen
`/api/experimental/mcp/servers/{mcpServer}` block: the OAuth2 callback
(its redirect URI is baked into existing AS-side client registrations)
and `oauth2/disconnect` (must remain reachable by users removed from the
organization). These endpoints are under `/api/experimental`, so no
deprecation window is provided.

## Rolling upgrades

During a rolling deploy, an old replica creating an MCP config can fail
the new `NOT NULL organization_id` constraint until it is upgraded
(reads are unaffected: old binaries' generated queries select their own
column lists). This matches the repo's existing precedent for additive
NOT NULL migrations (000562) and affects only the admin config-create
path in the upgrade window.

Upgrades are expected to run in scheduled maintenance downtime with the
database locked during migration, so the migration ships no
rolling-upgrade compatibility machinery. The down migration deletes
organization-created configs (their chat references are cleaned by the
000510 delete trigger) and restores deployment-wide slug uniqueness.

Part of the MCP org-separation stack (CODAGT-711 -> CODAGT-717 audit ->
CODAGT-712 ACLs -> CODAGT-806 token RBAC).

Closes https://linear.app/codercom/issue/CODAGT-711

UAT: validated end to end on a two-org dogfood deployment, including a
real pre-migration to post-migration upgrade, cross-org isolation
(404s), same-slug-two-orgs, chat selection gating, and a live MCP tool
call through the org-scoped generation path. The migration was later
revised to keep existing rows in the default organization only (no
per-organization copies); that revision is covered by the migration test
suite.

> Mux (AI agent) authored this PR on Mike's behalf.

<!-- mux-attribution: model=claude-fable-5 thinking=high -->

---------

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2026-08-19 18:04:13 +00:00
Ben Potter 8a7e8d9d5b refactor: move chat prompt sanitization into codersdk (#28283)
Exports `SanitizePromptText` (and its helpers) from `codersdk` instead
of `coderd/x/chatd`, so API consumers can sanitize prompt text exactly
the way the server stores it.

## Why

coder/terraform-provider-coderd#412 adds a `coderd_chat_system_prompt`
resource whose `system_prompt` attribute compares values by their
sanitized forms (otherwise the trailing newline from
`file("system-prompt.md")` is perpetual drift, since the server stores
the sanitized value). That currently requires a mirrored copy of the
sanitizer in the provider, which can silently rot. Per review there
([discussion](https://github.com/coder/terraform-provider-coderd/pull/412#discussion_r3801110795)),
the sanitizer should be exported from `codersdk` and imported instead.

## What

- `coderd/x/chatd/sanitize.go` → `codersdk/promptsanitize.go` (pure
move; package + doc-comment note about why it lives in codersdk)
- `coderd/x/chatd/sanitize_test.go` → `codersdk/promptsanitize_test.go`
- Callsites updated (`exp_chats.go`, `chatd.go`, `subagent.go`,
`context_prompt.go`); no wrapper left behind, single source of truth
- No behavior change

> Generated by Coder Agents on behalf of @bpmct
2026-08-19 08:51:25 -05:00
Michael Suchacz f0e6ac64b3 feat: remove native chat usage limits in favor of AI Gateway budgets (#27329)
## Stack Context

This stack makes AI Gateway data and budgets the source of truth for AI
spend controls.

1. Re-back the per-chat cost endpoint with AI Gateway data (#27328,
merged).
2. **This PR:** remove native chat usage limits.
3. Remove native chat cost tracking and its dedicated admin UI (#27330).

## Summary

Removes the native usage-limit API, SDK types, SQL, and chat enforcement
for deployment, user, and group chat limits. Compact AI Gateway budget
indicators remain in the Agents sidebar, user menu, and group settings.
Gateway budget rejections and provider quota failures continue to
classify as usage-limit errors, including a 409 response for synchronous
title generation.

Budget-period labels now use the API's UTC boundaries, so users see the
same dates in every browser timezone. The documentation explains the AI
Gateway replacement, its licensing requirements, and the differences
from native limits.

No schema is dropped in this release. The usage-limit table, index, user
and group columns, constraints, audit mappings, and generated scan
fields remain for mixed-version rolling upgrades. #27600 tracks their
removal after the compatibility window.

## Breaking change

Native day, week, and month chat spend limits are removed and are not
migrated. AI Gateway budgets are month-based, group-scoped with per-user
overrides, and require the AI Gateway entitlement. Deployments without
that entitlement no longer have chat spend enforcement.

> Mux prepared this PR on Mike's behalf.
2026-08-04 11:36:49 +02:00
Michael Suchacz fc24c27dfd fix: reserve chat hook dispatch capacity for running turns (#27656)
## Context

Follow-up fix from live UAT of the merged chat lifecycle hooks stack
(#27430). Its companion UAT fix (#27655) has merged, so this targets
`main` directly.

## Why?

UAT measured a burst of 1,500 concurrent chat creations against a
consumer with 1.2s latency. 255 were admitted and 1,245 got `502
hook_dispatch_failed (over_capacity)`, which is correct fail-closed
behavior. The collateral wasn't: the same burst failed 24 `stop`
dispatches, parking chats that had already been admitted and had already
executed tools. One 256-slot semaphore served every event, so new-work
admission could take every slot and kill turns in flight.

Callers now classify each dispatch as admission or generation, and
admission draws from a 192-slot gate held *before* the shared pool. At
least 64 shared slots stay reachable only by dispatches for work a chat
already admitted. The dispatcher is per `coderd` replica, so these
limits are per replica, not deployment-wide, and the docs say so.

**The caller classifies, not the event type.** Event type isn't a
reliable proxy in either direction: a subagent spawn dispatches
`user_prompt_submit` from inside a running turn, and the edit path
dispatches `session_start` at admission time. `CapacityClassUnset` is
rejected in `Dispatch`, so a new call site fails closed rather than
silently inheriting a share.

**Acquisition order is load-bearing.** Admission takes its own gate
first. Taking a shared slot first would let admissions queued on the
gate occupy the very capacity the reserve protects. `acquireCapacity` is
the only path that takes either pool, so the order can't be bypassed.

## What this does not guarantee

Nothing bounds how many turns generate concurrently, so the 192/64 split
is a judgement call, not a derived ceiling. This stops an *admission*
burst from consuming every slot; it does not make the remainder
sufficient. A large enough generation load can still exhaust the reserve
and error a running chat. The docs say so explicitly rather than
promising a guarantee the code doesn't deliver.

Generation can now take all 256 slots, so generation traffic starves
admission harder than before. That's the intended priority: rejecting a
new prompt is recoverable, ending a turn that already ran tools is not.

## Testing

Red-green proved both new tests. Removing the release-on-failure path
fails `RefusedSharedAcquireReleasesAdmission` deterministically;
removing the expired-deadline check fails
`ExpiredDeadlineRefusesFreeSlot` in 18/30 runs.

That deadline check fixes a real race found in review. `acquire`
previously shared one `time.Timer` across both acquires. Because
`select` picks a ready case at random, an admission dispatch could take
a slot after its capacity deadline had passed. Measured over 300 trials:
135 late acquisitions, worst overshoot 2.1ms. `acquire` now takes an
absolute deadline and refuses an expired one before selecting, which
measures 0/300.

Go: `coderd/x/agenthooks/...` and `coderd/x/chatd/...`, plus `-race
-count=3` on the dispatcher.

> Mux opened this PR on Mike's behalf.
2026-08-03 19:04:54 +02:00
Michael Suchacz c17bed25e0 feat: wire chat lifecycle hooks into chatd (#27429)
Wires chat lifecycle hooks into chatd, gated by the
`agent-lifecycle-hooks` experiment. Part of the lifecycle hooks stack
(#27401, #27428, #27430). See `docs/admin/setup/chat-lifecycle-hooks.md`
for the consumer-facing contract.

## Summary

When a hook URL is configured, chatd dispatches `session_start`,
`user_prompt_submit`, `pre_tool_use`, `post_tool_use`, `pre_compact`,
`post_compact`, and `stop` events to the consumer and applies its
responses.

## Design

- **Stateless**: Coder stores no hook dispatch or decision state.
Delivery is at least once; consumers deduplicate on stable payload
identifiers (chat ID, event type, tool-use ID) and answer duplicates
with the same decision.
- **Admission-time prompt effects**: `user_prompt_submit` dispatches
exactly once per submission (create, send, queue, edit, subagent spawn)
and folds its effects into the stored prompt as typed message parts:
original-or-overridden user parts, then model-only `hook-context`, then
a user-visible `hook-notice`. Hook context is stripped from every
client-facing conversion; hook notices are excluded from model prompts.
The server rejects hook parts in client-submitted content.
- **Tool gating**: `pre_tool_use` allow can override tool input; deny
becomes a synthetic denied tool result, with any returned model context
persisted as a model-only transcript row so it never reaches clients.
The denial text identifies an external policy (the deployment's
lifecycle hook) as the source and marks the decision as persistent, so
the model explains the denial instead of retrying it or misreporting it
as an infrastructure failure.
- **Fail closed**: a dispatch failure rejects the triggering request or
moves the chat to the error state in the same transaction as the
affected step, so a runnable state is never published with unapproved
content.
- **Admission before persistence**: `pre_tool_use` is dispatched for the
calls the model produced, before the assistant message is stored. See
"Staged tool admission" below.
- **Fresh dispatch per tool call**: every non-provider-executed tool
call is decided by its own `pre_tool_use` dispatch; Coder never reuses
an earlier decision on the consumer's behalf. Retries re-dispatch the
same logical event.

## Structure

All hook dispatch flows through one seam: entry points build a
`chathooks.Chat` (chat identity) and a `chathooks.Message` (event
details) and call `Trigger.Trigger`, the only component that talks to
the dispatcher. The integration lives in the `coderd/x/chatd/chathooks`
subpackage, split by responsibility:

- `trigger.go`: the trigger seam; builds the wire envelope per event,
normalizes deny into a typed error, and holds the package's single
enabled-check.
- `effects.go`: pure conversion of hook results into transcript rows and
prompt parts.
- `errors.go`: failure classification (dispatch error messages, denial
mapping, tool-result dispatch-failure scanning).
- `tooluse.go`: the tool-call gate (`pre_tool_use` preflight,
`post_tool_use` payloads, applying admitted input to the step).

Server-bound glue stays in `coderd/x/chatd/hook_server.go`: the
chat-parking dispatch error handlers, the step-commit row insertion
wrappers, and the dynamic post-tool-use state loader, which depends on
chatd validation types.

This PR adopts the `codersdk/x/agenthooks` and
`coderd/x/agenthooks/dispatch` import paths introduced at the tip of
#27401; intermediate commits still reference the pre-move paths and are
not individually buildable.

## Staged tool admission

`pre_tool_use` originally ran at tool execution time, which is after the
assistant message carrying the tool call was already committed. An
`input_override` therefore had to rewrite stored message content in
place. @hugodutka pointed out that chatd treats message content as
immutable, and that the rewrite was a shortcut rather than a
requirement.

It was also a correctness problem in its own right: the rewrite only
updated the database, so the transcript could show one input while a
different one had executed.

The hook now runs before the step is persisted:

```text
provider stream ends (tool calls complete, in memory)
  -> pre_tool_use dispatch per call
  -> ONE transaction: assistant row with admitted inputs, synthetic denials, hook rows
  -> execute
```

The step is inserted once, carrying the input the tool runs with.
`UpdateChatMessageContentByID` and `Tx.UpdateMessageContent` are deleted
from #27428, so message content stays immutable.

Two consequences, both intentional:

- **Clients converge rather than wait.** Tool-call parts still stream
live, so a rewritten call briefly shows the model's proposed input
before the committed message replaces it. The chat store already clears
stream state when an assistant message arrives, so the stored input wins
with no frontend change and no added latency before tool cards appear.
- **A call already in history was already admitted.** Execution consumes
the stored input instead of dispatching a second decision, which keeps
one dispatch and one set of hook effects per call. A consumer policy
change between admission and execution applies to later calls, not to
calls already admitted.

The per-chat debug endpoint still records the provider's original tool
input. Its purpose is to report provider behavior, and it requires an
explicit per-chat debug flag; the invariant here covers the transcript.

## Configuration

Adds `chat-hook-url`, `chat-hook-secret`, `chat-hook-timeout`, and
`chat-hook-enabled` deployment options with startup validation. The
flags are hidden from `coder server --help` while the feature is
experimental; the setup guide documents them.

## Tool input validation

Built-in tool arguments reach a consumer as raw JSON with key spelling
preserved, but the tools decode those bytes with Go, which matches
struct fields case-insensitively and keeps the last match. A policy
reading `path` could therefore authorize one value while the tool
executed another, and a lone case variant such as `{"PATH":"/secret"}`
was invisible to a policy checking for `path`.

Coder now rejects a built-in tool call whose input repeats a key or
spells a schema property with different capitalization, before the
`pre_tool_use` dispatch, so a consumer is never asked to authorize bytes
whose meaning depends on the reader. Rejected calls produce an error
result the model can retry; unambiguous calls in the same batch still
run. A consumer-authored `input_override` is rechecked after the
dispatch and fails the turn closed, because the model cannot correct it.
Dynamic and MCP inputs are excluded because the client and the workspace
agent execute those calls rather than coderd.

Two paths needed more than a schema check. Execution resolves a
deprecated tool name to its canonical tool, so validation resolves
aliases first. The `edit_files` decoder also reads `search` and
`replace`, which its schema does not advertise, so those aliases are now
matched exactly and their case variants ignored.

A hook denial now returns a structured 403 carrying `kind:
"hook_denied"`, mirroring the dispatch-failure response that already
carries its own kind. Without it a client cannot tell a policy decision
apart from a generic failure, and the chat UI titled a denial "Request
failed". Adding a kind needs no migration: `ChatErrorKind` is persisted
only inside the JSONB `chats.last_error` column, whose decoder accepts
unknown kinds.

The hook docs also correct the tool-input convergence window. A batch
dispatches sequentially before the assistant row commits, so the
original input stays visible for a span that scales with the number of
tool calls in the step rather than a single hook timeout.

> This PR was written by Mux, an AI coding agent, on Mike's behalf.
2026-07-29 11:39:12 +00:00
Michael Suchacz e96d8646e2 fix(coderd): give chat message ids an append-order guarantee (#27495)
Chat message ordering was derived from `created_at`, which is `now()`
and therefore the transaction start time. That makes it unusable as an
append-order column for two independent reasons: every row in one
`InsertChatMessages` batch shares a single timestamp, and two concurrent
transactions can commit in the opposite order to the one they started
in.

This PR gives `chat_messages.id` a real append-order guarantee and moves
the history reads onto it.

## Changes

**`InsertChatMessages` had no input-order guarantee.** Callers index the
returned slice by input position. That only worked because PostgreSQL
happens to evaluate the `BIGSERIAL` default in row order. Ids are now
allocated up front and the k-th smallest is assigned to input index k,
so the pairing does not depend on where the column default is evaluated.
Returned rows are explicitly `ORDER BY id`.

**Three history reads now order by `id`.**

| Query | Was | Now |
|---|---|---|
| `GetChatMessagesByChatID` | `created_at ASC` | `id ASC` |
| `GetChatMessagesByRevisionForStream` | `created_at ASC, id ASC` | `id
ASC` |
| `GetLastChatMessageByRole` | `created_at DESC, id DESC` | `id DESC` |

`GetChatMessagesByChatID` paginated by `id` while ordering by
`created_at`, which is incoherent on its own terms.

The other two matter because of who consumes them. The stream query
supplies incremental updates on the same socket that emits a full
`GetChatMessagesByChatID` snapshot on history reset, so once that
snapshot moved to `id` the two disagreed under timestamp skew.
`GetLastChatMessageByRole` returns an id that is then used as an id
cursor, both as `AfterID` when synthesizing tool cancellations and as
`chats.last_read_message_id`, where a stale anchor leaves later
assistant messages permanently unread.

A tie-breaker would not have fixed either one. It only resolves equal
timestamps; leading with `created_at` is the actual defect.

**`GetLastChatMessageByRole` loses its index, so this adds one.** `ORDER
BY created_at DESC, id DESC` could take an ordered scan of
`idx_chat_messages_chat_created`. Nothing in the schema can supply
`ORDER BY id DESC LIMIT 1` for a given `chat_id` and `role`, so the
planner switches to a backward scan of the primary key and filters every
newer row in the table, scanning all of it when the chat has no message
in that role, which is the routine case for a fresh chat. Migration
`000559` adds `(chat_id, role, id DESC) WHERE deleted = false`, the same
shape as the existing `idx_chat_messages_user_prompts`. This matters
because the query is hot: it runs on every stream connect and
disconnect, and once per turn when synthesizing tool cancellations.

`GetChatMessagesForPromptByChatID` has the same defect and is fixed in
the stacked PR, because its compaction boundary change is semantic and
deserves a separate review. Auto-archive stays timestamp-based
deliberately: it measures activity, not order.

Wrapping the insert in a CTE (needed because `INSERT` cannot take `ORDER
BY`) makes sqlc synthesize `InsertChatMessagesRow`. It is structurally
identical to `ChatMessage`, so the call sites use a direct struct
conversion that stops compiling if the two ever diverge.

## Testing

Behavior tests write `created_at` values inverted against id order, so a
reader that leads with `created_at` returns the batch backwards. All
three queries were verified red by reverting the `ORDER BY` and
regenerating: the stream query returned `[3,2,1]` for `[1,2,3]`, and
`GetLastChatMessageByRole` picked id 1 instead of id 3.

`TestInsertChatMessagesOrderContract` asserts against the generated SQL,
covering what a behavior test cannot: PostgreSQL evaluates the id
default in row order anyway, so a batch still looks ordered once the
guarantee is removed.

`TestChatMessagesSequenceCacheIsOne` guards the cross-batch half of the
invariant. Ids follow chat row lock order only while the sequence hands
out one value at a time; sequence cache blocks are per session, so with
a cache above one a backend holding stale cached values can lock second
and still commit lower ids. Bumping a sequence cache is an ordinary
throughput tweak, and it would silently corrupt history order.

The index was checked on a 200k row fixture. Without it, the zero-match
lookup filters all 200,000 rows over 2763 buffers; with it, the plan is
an index scan with both `chat_id` and `role` in the index condition, no
sort node, and 3 buffers.

Note that the within-batch mapping does not depend on the cache size. It
is established by `ROW_NUMBER() OVER (ORDER BY id)` over the allocated
ids, so it holds regardless of `nextval` evaluation order.

## Note on the deleted subagent hand-sort

The subagent history reader's hand-sort stays deleted, but calling it
redundant was imprecise. It sorted by `created_at` then `id`, so it is
only equivalent to `id` ordering when the two agree. When they disagree
the old code selected a different "latest assistant". This is a
deliberate behavior change to match the new invariant, not dead-code
removal.

> Opened by Mux on behalf of Mike.
2026-07-29 07:17:42 +00:00
Michael Suchacz 6159eb4fc5 feat(coderd/x/chatd): add structured error fields to wait_agent error payload (#27478) 2026-07-25 15:30:52 +02:00
Michael Suchacz ff10beb042 fix(coderd/x/chatd): surface child error detail in wait_agent last_error (#27477) 2026-07-24 21:01:12 +02:00
Michael Suchacz 02fd1cc691 feat: allow spawn_agent model and reasoning effort override (#27385) 2026-07-22 18:43:22 +02:00
Michael Suchacz 4ed6fcced7 refactor(coderd): stop storing chat gateway key IDs and drop the columns (#27171)
> Mux is working on behalf of Mike.

## Summary

Stop reading and writing the legacy `api_key_id` columns on chat
messages and queued messages, and drop the columns in the same PR.
Runtime AI Gateway attribution continues to use the per-user synthetic
key introduced by #27170.

With the columns gone, `sqlc` generates `database.ChatMessage` and
`database.ChatQueuedMessage` without `api_key_id`, so no transitional
query scaffolding is needed.

Migration `000548` drops the `api_key_id` columns. #27170 already
removed their foreign keys, so the down migration re-adds nullable text
columns without constraints. Previous column values cannot be restored.

Also moves the model config validation in `CreateChat` above the
message-building work so a disabled or invalid model fails fast. On main
this mattered more: the old ordering minted a synthetic API key before
rejecting the request.

Deploy note: replicas still running the previous release write
`api_key_id` on insert, so chat message inserts on old replicas fail
during the rolling window after the column drop. This was previously
split across two PRs to avoid that window; per review feedback the split
added more churn than it was worth for an experimental surface.

Depends on #27170 (merged).
2026-07-20 19:49:19 +02:00
Mathias Fredriksson b511a68ab0 fix(coderd/x/chatd): clarify wait agent timeout (#27335)
The model-visible timeout schema did not state its five-minute default.
Clarify that waits return on completion and that a timeout leaves the
agent running.
2026-07-20 13:47:25 +03:00
Michael Suchacz 997b5d0843 feat: add synthetic gateway keys (#27170)
> Mux is working on behalf of Mike.

## Summary

Add a per-user synthetic API key for chatd AI Gateway attribution. Chatd
resolves the key from the chat owner, extends it before expiry, and
discards the generated bearer token so the key is never a usable
credential.

There is no mapping table. The key is resolved from `api_keys` by a
deterministic token name (`chatd_<owner_id>_session_token`), mirroring
the provisionerd session token model, with three deltas that chatd
needs:

- **Login type guard**: token names are unvalidated user input, so a
user can create a bearer token with the colliding name. The lookup
excludes `login_type = 'token'` rows, so chatd never picks up (or
extends) a real user token. Synthetic keys are minted with the owner's
login type, which is never `token`.
- **In-place expiry extension instead of delete-and-reinsert**: chat
generations have no stop boundary, and an in-flight generation may have
already delegated the current key ID to aibridged. Extending
`expires_at` keeps the key ID stable forever.
- **Advisory-lock mint**: the unique index on token names is partial
(`WHERE login_type = 'token'`), so nothing DB-enforces uniqueness for
synthetic keys. A per-user advisory lock serializes concurrent mints.

Keys carry a minimal scope (`api_key:read`) as defense in depth; the
delegated gateway path never evaluates scopes and the secret is
discarded at mint.

Migration 000544 removes the foreign keys from the legacy message and
queue `api_key_id` columns while chatd continues stamping them for
rolling compatibility. Stale IDs are tolerated because routing uses
`chats.owner_id`. Individual key deletion, delete-all, and password
reset remove the key without changing chat history or queue versions,
and the next lookup remints it. Suspension does not delete the key;
delegated gateway authorization rejects inactive users at request time.

This is the first PR in a three-PR rollout and must be fully deployed
before #27171.

Refs
https://linear.app/codercom/issue/CODAGT-561/maintain-synthetic-api-key-per-user-per-chat
2026-07-18 20:45:13 +02:00
Michael Suchacz a567f6a89f feat: allow admins to override the chat compaction model (#27151) 2026-07-14 16:57:04 +02:00
Michael Suchacz 6f6d7539c8 feat: remove unused chat statuses pending, paused, and completed (#27064)
The chatd state machine only recognizes `waiting`, `running`, `error`,
`requires_action`, and `interrupting`. Remove the unused `pending`,
`paused`, and `completed` values from the database enum, backend, SDK,
frontend, generated queries, and API docs.

Migration `000543_chat_status_remove_unused` remaps existing `pending`
rows to `running`, remaps `paused` and `completed` rows to `waiting`,
drops the obsolete `idx_chats_pending` index, and recreates
`chats_expanded` around the enum swap. It also removes the dead
`AcquireChats` query and all remaining query literals for the deleted
statuses.

**NOTE**: The enum swap can break chat queries from older replicas
during a mixed-version rollout because they still reference
`'pending'::chat_status`. Chats are experimental, so this PR accepts
that limited rollout window instead of adding a two-release expand and
contract sequence.

> This PR was authored by Mux (AI agent) on Mike's behalf.
2026-07-13 20:28:28 +02:00
Danielle Maywood d66e4d794f feat: add configurable reasoning effort to Coder agents (#26974) 2026-07-09 23:35:12 +01:00
Mathias Fredriksson 047c47495b refactor: drop chat_model_configs provider column (#26877)
The provider type already lives authoritatively in ai_providers.type,
reachable on every active row through ai_provider_id, which the
chat_model_configs_ai_provider_required_when_active CHECK makes
mandatory. The stored provider string was a denormalized copy the system
kept in sync with a startup backfill and no longer needs.

Every surface now derives provider type from the linked ai_providers
row. Telemetry is the one exception: it keeps emitting provider, now
sourced from ai_providers.type via a JOIN, so the BigQuery column and the
Nexus dashboards that read it are unaffected. The experimental HTTP/SDK
response drops provider and makes ai_provider_id required, since those
endpoints return only active configs; consumers resolve provider type
from ai_provider_id and the AI providers listing.

This ships in a single release with no compatibility window: production
reads the table via SELECT *, so a pre-drop binary fails config reads the
moment the column is gone. Operators must scale to zero before upgrading,
and there is no rollback.

Closes CODAGT-599
2026-07-01 15:59:55 +03:00
Danielle Maywood 6b8c38b5a4 fix: gate chat advisor and virtual desktop behind experiments, delete experiments page (#26809) 2026-06-30 21:57:40 +01:00
Mathias Fredriksson 59fcc9c0ad feat: improve sub-agent orchestration tools (#26673)
Tool errors caused orchestrators to abandon spawned agents. Bare error
responses and the close_agent name framed delegation as one-shot: one
transient failure or timeout ended the work, and the orchestrator had no
way to recover or reuse agents.

Renames close_agent to interrupt_agent with a hidden backward-compatible
alias. wait_agent and message_agent return structured payloads instead
of bare errors, so the orchestrator can retry after a timeout, recover
from an error status, or redirect an idle agent. Adds list_agents so
orchestrators can rediscover spawned agents. Adds root-only
orchestration guidance for error recovery.
2026-06-26 13:41:43 +03:00
Kyle Carberry cd56ab9e33 refactor: remove legacy live-read and injected-history chat context paths (#26585)
This PR makes the agent-pushed pinned snapshot
(`chat_context_resources`) the sole source of workspace context for
chats, completing the "Release 5" cleanup. It removes legacy mechanisms
now superseded by the snapshot that agents push over dRPC
(`PushContextState`) and refresh via `chat-context/refresh`.

Removed:

- **Live-read at turn time.** MCP tool discovery, skill live-body reads,
and the instruction/skill history fallback that dialed the workspace on
every turn.
- **Context injected as message history.** The
`persist_workspace_context` generation action and its decision-loop
guard.
- **The legacy write path.** `POST`/`DELETE
/api/v2/workspaceagents/me/experimental/chat-context`, the agentsdk
`AddChatContext`/`ClearChatContext` methods, and the CLI one-shot
writer.
- **The `chats.last_injected_context` column** and all of its plumbing
(migration `000529`, queries, `db2sdk`, `dbauthz`, audit table, and the
frontend `ContextUsageIndicator` fallback).

Subagent context inheritance no longer copies parent context messages;
children now hydrate the parent's pinned `chat_context_resources` on
create, which yields an identical pin for the same workspace and agent.

What stays (still served by the live agent connection, not the
snapshot): `read_skill_file` supporting-file reads, `read_skill`
supporting-file listing, and MCP tool execution.

> [!NOTE]
> Migration `000529` drops `chats.last_injected_context` and recreates
the `chats_expanded` view without it. The down migration restores both.

<details>
<summary>Decision log (D1-D5)</summary>

- **D1 (subagent inheritance):** Re-point inheritance from the legacy
message copy to a pinned hydrate. Children call
`hydrateChatContextOnCreate` instead of copying parent context messages.
- **D2 (`persist_workspace_context`):** Remove the generation action
entirely along with the decision-loop guard it existed to satisfy, since
context is never injected into history anymore.
- **D3 (legacy HTTP + CLI):** Remove the experimental `chat-context`
POST/DELETE endpoints, the agentsdk methods, and the CLI one-shot. The
dRPC push + `chat-context/refresh` replace them.
- **D4 (frontend fallback):** Remove the `last_injected_context`
fallback in `ContextUsageIndicator`; pinned `resources` are the sole
source.
- **D5 (sequencing):** Ship as a single PR rather than a stacked pair.

</details>

---
Coder Agents generated on behalf of @kylecarbs.
2026-06-22 19:26:34 -06:00
Hugo Dutka 4debd23cbb fix: chatd refactor (#26270)
Implements the chatd stabilization RFC.

Combines:
- https://github.com/coder/coder/pull/25908
- https://github.com/coder/coder/pull/25923
- https://github.com/coder/coder/pull/26109
- https://github.com/coder/coder/pull/26110
- https://github.com/coder/coder/pull/26111
- https://github.com/coder/coder/pull/26112
2026-06-12 13:33:12 +02:00
Cian Johnston b278be7361 fix(coderd): enforce api_key_id on user messages at type level (#25729)
- Empty string is valid for `apiKeyID` in paths that genuinely lack a
caller key (e.g. agent-initiated context injection in
`workspaceAgentAddChatContext`). AI Gateway fail-closed check remains
the runtime safety net.
- Context injection paths (`persistInstructionFiles`, compaction) read
the key from `aibridge.DelegatedAPIKeyIDFromContext(ctx)`, set upstream
by `contextWithActiveTurnAPIKeyID`.
- Subagent context copy branches on `copiedRole ==
database.ChatMessageRoleUser` to choose the right append function.

> Generated by Coder Agents
2026-05-27 17:00:23 +01:00
Michael Suchacz 8b1705eb65 feat: route chatd provider traffic through aibridge (#25629)
## Summary

Routes chatd model calls backed by concrete AI Provider rows through the
in-process aibridge transport by default, with deployment options to use
direct provider routing when AI Gateway is disabled or chat AI Gateway
routing is disabled.

- Splits model routing into common, direct provider, and AI Gateway
paths behind a single deployment-mode entry point.
- Builds chatd models through explicit request, route, and options data.
Active API key attribution is passed explicitly instead of being hidden
inside generic model construction.
- For AI Gateway BYOK routes, resolves the user's provider key in chatd,
forwards it through provider-specific auth headers, and sets
`X-Coder-AI-Governance-Token` to the `delegated` marker so aibridge
preserves those headers while still stripping Coder-specific metadata.
- Keeps central provider credentials and deployment fallback credentials
out of forwarded provider auth headers, so AI Gateway central policy
remains authoritative.
- Redacts delegated provider auth from default string formatting to
avoid accidental plaintext logging of user BYOK credentials.
- Covers selected chat models, advisor overrides, title and quickgen
paths, subagent overrides, computer use model selection, and an
integration-style chat turn through the aibridge transport path.
- Persists initiating API key IDs on chat and queued user messages,
including subagent child messages, and fails closed for AI
Gateway-routed model builds without an active key.
- Removes unused `api_key_id` indexes while keeping the persistence
columns and foreign keys.
- Keeps the deployment option available through config and env parsing,
but hides it from CLI help and generated docs.
- Stabilizes the subagent poll fallback test so background CreateChat
processing cannot win the state transition under slower CI environments.

## Tests

- `go test ./coderd/x/chatd -run
'TestAIGatewayProviderAuthForUser|TestAIGatewayProviderAuthRedactsFormatting|TestResolveModelRouteForConfigAIGatewayProviderAuth|TestAIGatewayModelForwardsProviderAuth|TestProcessChat_AIGatewayRoutingUsesDelegatedAPIKey|TestAwaitSubagentCompletion'
-count=1`
- `go test ./coderd/aibridged -run
'TestServeHTTP_DelegatedAPIKey|TestServeHTTP_StripCoderToken' -count=1`
- `git diff --check HEAD~1..HEAD`
- `make lint`

> Mux working on behalf of Mike.
2026-05-26 19:31:52 +00:00
Michael Suchacz de6d62815e fix(coderd): avoid redundant workspace setup (#25615)
GPT-class chat turns could eagerly create workspaces or repeat setup
such as cloning an existing repo because the system prompt framed setup
work as the default path.

This updates chatd prompt guidance and the `create_workspace` tool
description so agents reuse existing chat and workspace context, treat
injected workspace context as already read, avoid recloning present
repositories, and create or start workspaces only when workspace-backed
work is required. Delegated chats now report workspace needs to the
parent instead of trying to create one.

> Mux opened this PR on behalf of Mike.
2026-05-22 14:08:07 +00:00
Michael Suchacz ca1f6b19a2 feat: remove legacy chat provider tables (#25416) 2026-05-22 09:50:01 +02:00
Michael Suchacz 5968c3dac7 feat: use AI provider keys at runtime (#25414) 2026-05-22 02:17:09 +02:00
Michael Suchacz 632dcdb63a feat: add personal chat model overrides (#24715) 2026-05-05 00:57:51 +02:00
Michael Suchacz 0bb09935bc feat: add computer-use provider selection for AI agents (#24772)
Adds a deployment-wide setting to select the computer-use provider
(Anthropic or OpenAI) for AI agents, plus the OpenAI computer-use runner
needed to honor that selection.

The setting is stored in `site_configs` under
`agents_computer_use_provider`, defaults to Anthropic when unset, and is
exposed via experimental GET/PUT endpoints under
`/api/experimental/chats/config/computer-use-provider`. The chatd
computer-use tool now dispatches to either `runAnthropicComputerUse` or
`runOpenAIComputerUse` based on the resolved provider, with
provider-specific result metadata for OpenAI screenshots.

Frontend adds a provider dropdown to the Agents Experiments settings
page nested under the virtual desktop toggle, with disabled state
handling while virtual desktop is off and skeleton loaders while config
queries are in flight.

Hugo and Codex review follow-up:
- Uses shared provider validation and clearer computer-use constant
names.
- Removes stale OpenAI pending-safety-checks commentary.
- Documents why provider result metadata is needed for OpenAI
screenshots.
- Keeps the computer-use subagent visible when provider credentials are
missing, then returns a clear spawn-time configuration error.
- Uses OpenAI's recommended 1600x900 screenshot geometry to preserve the
native 16:9 aspect ratio.
- Moves OpenAI-specific computer-use helpers into
`coderd/x/chatd/chatopenai/computeruse` after rebasing onto the provider
package refactor in `main`.
- Converts OpenAI pixel scroll deltas to Coder desktop wheel-click
amounts.
- Preserves OpenAI pointer modifiers with key down/up desktop actions
and rejects unsupported non-left double-click buttons explicitly.
- Maps OpenAI back/forward side-button clicks to browser navigation key
actions.
- Defaults omitted OpenAI click buttons to left-click.
- Retries mouse release cleanup if the final OpenAI drag release fails.
- Keeps computer-use subagent availability messages stable when provider
config cannot be loaded, while logging the backend error.
- Releases remaining OpenAI modifier keys if a synthetic key-up cleanup
action fails.
- Updates Storybook interaction stories so provider snapshots show the
selected final provider.

> Mux updated this PR description on behalf of Mike.
2026-05-04 20:30:50 +02:00
Michael Suchacz 033ed0bb82 feat: add admin-configurable chat title generation model (#24838)
Adds an admin-configurable deployment-wide setting that controls which
model is used for chat title generation. Admins can pick any enabled
chat model config from the Agents settings page, or leave the setting
unset to keep the existing fast-models-then-chat-model fallback
algorithm.

When a model is selected, both automatic and manual title generation use
only that model, with no silent fallback. When the configured model is
disabled, missing credentials, or otherwise unusable, automatic title
generation skips entirely (best-effort) and manual title regeneration
returns a clear error, so admins notice the misconfiguration instead of
silently routing title traffic through another provider.

## Surface

- New deployment-wide setting stored as a `site_configs` row
(`agents_chat_title_generation_model_override`).
- New experimental endpoint `GET/PUT
/api/experimental/chats/config/model-override/{context}`.
- Frontend: title generation now appears as a third dropdown on the
Agents admin settings page alongside the existing general and explore
context overrides.

## DRY refactors folded in

Title generation is integrated as a third value of the existing
`ChatModelOverrideContext` type alongside `general` and `explore`,
sharing the parameterized HTTP route, SDK methods, generated types, and
frontend API plumbing rather than introducing a parallel surface. The
`Agent` prefix was dropped from the type and route since title
generation is not a delegated agent.

The chatd model-override resolver is also shared.
`resolveConfiguredModelOverride` now takes a `failureMode` parameter:

- Subagent overrides use soft failure: misconfigured overrides are
logged and the parent model is used.
- Title generation uses hard failure: misconfigured overrides return an
explicit error so manual title regeneration surfaces the
misconfiguration and automatic title generation skips instead of
silently falling back.

> Mux is acting on Mike's behalf.
2026-05-04 13:13:00 +02:00
Michael Suchacz 3d90546aae feat: add general subagent model override (#24610)
Adds a deployment-wide admin override for general delegated subagents.

## What changed
- store the general override in `site_configs` and expose it through the
shared `agent-model-override/{context}` API
- apply the general override when spawning delegated general subagents,
while preserving the existing Explore override behavior
- reuse a shared Agents settings form for the general and Explore
override sections

## Validation
- `make gen`
- `go test ./coderd -run 'TestChatModelOverrides'`
- `go test ./coderd/x/chatd -run
'TestSpawnAgent_(GeneralUsesConfiguredModelOverride|GeneralOverrideLogsAndFallsBackWhenCredentialsUnavailable|GeneralOverrideLogsAndFallsBackWhenProviderDisabled)'`
- `pnpm -C site lint:types`
- `pnpm -C site test:storybook --
AgentSettingsAgentsPageView.stories.tsx`
- `make lint`
- `make pre-commit`

> Mux is acting on Mike's behalf.
2026-04-24 12:37:20 +02:00
Michael Suchacz dbcc654d28 feat: snapshot explore subagent tool entitlements (#24638)
Explore sub-agents previously could not use `web_search` or external MCP
tools. `runChat` hard-skipped both for Explore. Lifting those guards
naively would over-grant tools, because a child chat could outlive the
spawning turn's plan-mode filter.

This change persists the spawning parent turn's filtered external MCP
server IDs onto the child Explore chat, and simplifies the Explore
provider-tool filter in `runChat`:

- New `resolveExploreToolSnapshot` helper: computes the child's
inherited external MCP subset by running the parent's configs through
`filterExternalMCPConfigsForTurn` (plan-mode policy) and, if the parent
is itself an Explore child, further narrowing to the parent's own
persisted `MCPServerIDs`. The result is written to the child's
`MCPServerIDs` column at spawn time.
- The existing `mcp_server_ids` column is the sole durable snapshot. No
new chat column is added.
- `runChat` for Explore children: loads MCP tools from the persisted
snapshot, and keeps only `web_search` from provider-native tools (to
block computer-use and other write-style tools, since Explore is
read-only). Whether `web_search` is actually available is a per-model
decision, determined by the current model config, just like a main chat.
- Built-in Explore allowlist is unchanged. Workspace-local MCP remains
excluded for Explore.

Verification: `go build ./...`, `go test ./coderd/x/chatd/... -count=1`,
`make gen` (clean tree), `make lint/emdash`, `go vet`. Deep-review ran
12 reviewers on the feature and 5 on the clarity refactor; CAR reviewed
and approved; a subsequent scope reduction dropped a temporary
`allow_web_search` column in favor of per-model handling.

> Mux is acting on Mike's behalf.
2026-04-23 19:07:38 +02:00
Michael Suchacz f073323c89 refactor: unify subagent spawn behind spawn_subagent (#24535)
Unify the three subagent spawn tools (`spawn_agent`,
`spawn_explore_agent`, `spawn_computer_use_agent`) behind a single
`spawn_subagent` tool keyed by a `subagent_type` discriminant
(`general`, `explore`, `computer_use`). Mirrors the single-entry-point
pattern already used by `task` in mux while keeping `wait_agent`,
`message_agent`, and `close_agent` as separate lifecycle tools.

A new backend subagent definition catalog
(`coderd/x/chatd/subagent_catalog.go`) is the source of truth for tool
description, prompt guidance, availability rules (plan mode,
desktop/Anthropic gating), and child-chat option building.
`spawn_subagent` advertises only the types available in the current
context and validates `subagent_type` server-side; context inheritance
still flows through the existing `createChildSubagentChatWithOptions`
path. `wait_agent`, `message_agent`, and `close_agent` responses now
include a server-derived `subagent_type` so the UI stops inferring
lifecycle state from tool names.

The frontend gets a shared normalization helper
(`site/src/pages/AgentsPage/components/ChatElements/tools/subagentDescriptor.ts`)
that maps either legacy tool names or new `spawn_subagent` args into a
common descriptor (action, variant, icon, fallback copy). Legacy
transcripts still render identically; `Tool.tsx`, `SubagentTool.tsx`,
`ToolLabel.tsx`, `ToolIcon.tsx`, and `messageParsing.ts` now key off the
descriptor instead of hard-coded names. Existing UI copy is preserved
(`Spawning Explore agent...`, `Using the computer...`, computer-use
monitor icon and Open Desktop affordance).

> This PR was opened by Mux working on Mike's behalf.
2026-04-21 14:01:32 +02:00
Ethan ef6969dd70 feat(coderd/x/chatd): agent-created file attachments in chat (#24280)
Agents can already see workspace files and take screenshots, but users could not download those artifacts from chat. This PR adds durable chat attachments to chatd. `attach_file`, explicit `computer` screenshot actions (not the automatic post-action screenshots), and `propose_plan` now fetch bytes over the agent connection, store them in `chat_files`, link them to the chat, and carry attachment metadata in tool responses so `buildAssistantPartsForPersist` can materialize ordinary `type:"file"` assistant parts that the chat file APIs serve.

The same storage helpers are reused for other artifact-producing paths. `wait_agent` recordings and thumbnails are stored as chat files and linked back to the parent chat, with best-effort relinking so parent chats retain those artifacts without leaving orphaned rows when chat-file caps reject links. `storeChatAttachment` wraps insert + link in one transaction, files are capped at 10 MB each and 20 per chat, and serving defaults to `Content-Disposition: attachment` with an explicit inline-safe allowlist.

This PR also consolidates chat-file media policy in `coderd/chatfiles`. Uploads and tool-generated attachments share byte-based MIME detection, SVG blocking, inline-safety rules, and compatible `text/plain` refinement for JSON, CSV, and Markdown. Prompt construction still only inlines synthetic pasted text for model consumption; assistant-created attachments are persisted for the user and intentionally not replayed into later LLM turns.

UI follow-up lives in #24281.

Relates to CODAGT-91
2026-04-20 18:04:35 +10:00
Hugo Dutka db8191277b fix: associate computer use recordings with chats (#24471)
Fixes
[CODAGT-195](https://linear.app/codercom/issue/CODAGT-195/agent-uploaded-recordings-are-missing-chat-file-links-entries).
2026-04-17 13:47:59 +02:00
Michael Suchacz 73b5058923 feat: add Explore mode as subagent-only modality (#24448)
> This PR was authored by Mux on behalf of Mike.

Introduce Explore mode, a read-only subagent modality for delegated
discovery and code investigation.

## What

Adds a `spawn_explore_agent` tool that creates child chats restricted to
read-only operations. An admin can optionally configure a
deployment-wide
model override so Explore subagents use a model optimized for large
context
or reasoning without changing the root chat's model.

### Backend

- New `ChatModeExplore` enum value (migration 000471).
- `spawn_explore_agent` tool definition with read-only allowlist:
`read_file`, `execute`, `process_output`, `read_skill`,
`read_skill_file`.
  Write tools, file editors, and nested subagent spawning are blocked.
- Deployment config storage for the Explore model override
  (`agents_chat_explore_model_override` in `site_configs`).
- Model resolution hierarchy: configured override, then current turn
model,
then global default. Silent fallback with warning log when the override
  becomes unavailable.
- RBAC: `AsChatd` for daemon reads, `ActionRead` and `ActionUpdate` on
  `ResourceDeploymentConfig` for admin API calls.
- Plan mode root chats can use `spawn_explore_agent` for read-only
research,
  matching the planning prompt guidance.
- The Explore override config API now reports malformed saved overrides
as
  "treated as unset" so admins can clear them explicitly.

### Frontend

- `ExploreModelOverrideSettings` component in admin agent behavior
settings.
  Uses `ModelSelector`, handles unavailable model warnings, and supports
  explicit Save and Clear actions.
- Malformed saved overrides show a warning and require an explicit Save
to
  clear, instead of Clear auto-submitting behind the scenes.

### Tests

- Integration: `TestExploreSubagentIsReadOnly` (full spawn flow, tool
  verification, prompt overlay, DB state).
- Unit: tool allowlist tests for explore, plan, and default modes.
- Internal: model override resolution with valid, invalid UUID,
disabled, and
  unconfigured override scenarios.
- RBAC: `dbauthz_test.go` for `GetChatExploreModelOverride` and
  `UpsertChatExploreModelOverride`.
- API: admin set and clear, malformed stored override reporting,
disabled
  model rejection, non-admin denial.
2026-04-17 13:40:17 +02:00
Michael Suchacz 1092093e98 feat: add internal subagent model override wiring (#24399)
> Mux working on behalf of Mike.

## Summary
- add an enabled chat model config lookup by ID for internal callers
- keep `spawn_agent` unchanged while threading an internal model
override through child subagent chat creation
- extend chatd coverage for inherited bindings, plan mode, and internal
override behavior

## Validation
- `go test ./coderd/x/chatd ./coderd/database/dbauthz`
- `make lint`
2026-04-16 17:08:02 +02:00
Dean Sheather 3452ab3166 chore: add client_type field to chats and telemetry (#24342)
Add a `chat_client_type` enum (`ui` | `api`) and `client_type` column to
the `chats` table. The column defaults to `api` for new rows so API
callers don't need to set it explicitly. Existing rows are backfilled to
`ui`.

The field flows through `CreateChatRequest`, `chatd.CreateOptions`,
`InsertChat`, and is returned in the `Chat` response via `db2sdk`.

<details>
<summary>Implementation notes (Coder Agents generated)</summary>

### Changes

**Database migration (000469)**
- New enum `chat_client_type` with values `ui`, `api`.
- New `client_type` column, `NOT NULL DEFAULT 'api'`.
- Backfill: `UPDATE chats SET client_type = 'ui'`.

**SQL query** — `InsertChat` now includes `client_type`.

**SDK** — `ChatClientType` type added; `ClientType` field added to both
`CreateChatRequest` (optional, defaults server-side to `api`) and `Chat`
response.

**Handler** — `postChats` maps the request field (defaulting to `api`)
and passes it through `chatd.CreateOptions`.

**Sub-agent** — Child chats inherit their parent's `client_type`.

**db2sdk** — Maps the database value to the SDK type.

### Decision log
- Default is `api` (not `ui`) so existing API integrations get the
correct value without code changes.
- Backfill sets existing rows to `ui` per requirement.
- Child chats inherit `client_type` from parent rather than defaulting.
</details>
2026-04-16 23:57:05 +10:00
Michael Suchacz 1cf0354f72 feat: add plan mode with restricted tool boundary (#24236)
> This PR was authored by Mux on behalf of Mike.

## Summary
- add persistent plan mode for chats and the chat-specific plan file
flow
- add structured planning tools such as `ask_user_question` and
`propose_plan`
- keep `write_file` and `edit_files` constrained to the chat-specific
plan file during plan turns
- allow shell exploration in plan mode, including subagents, via
`execute` and `process_output`
- block implementation-oriented, provider-native, MCP, dynamic, and
computer-use tools during plan turns
- update the chat UI, tests, and docs for the new planning flow
2026-04-16 11:12:01 +02:00
Ethan e7883d4573 fix(coderd/x/chatd): hoist system prompt fetch out of chat creation transactions (#24369)
## Problem

`resolveDeploymentSystemPrompt` was called inside `InTx` closures in
both `CreateChat` (`coderd/x/chatd/chatd.go`) and
`createChildSubagentChatWithOptions` (`coderd/x/chatd/subagent.go`).
That method uses `p.db` (the root store) internally to call
`GetChatSystemPromptConfig`, which requires a second DB pool checkout
while the transaction already holds one connection.

Under concurrent chat creation load (e.g., the chat scaletest at 4800
chats), this causes pool starvation: every in-flight create holds one
connection and blocks waiting for another, leading to `idle in
transaction` pileups and cascading timeouts across the entire coderd DB
pool — including unrelated background work like prebuild metrics and the
chat acquire loop.

## Fix

Move the `resolveDeploymentSystemPrompt` call before `p.db.InTx(...)` in
both call sites. The system prompt config is a read-only
deployment-level setting that does not need transactional consistency
with the chat insert, so fetching it before the transaction is both safe
and preferable (it also shortens transaction lifetime).

## Backporting

The `CreateChat` instance of this bug is also present on `release/2.32`
(`coderd/x/chatd/chatd.go` line 907). The `subagent.go` instance is not
— the child-subagent-chat creation path with its own `InTx` was added
after the branch cut.

This should be backported, but because this is only in the chat creation
path, and that's not typically hit with a great deal of concurrency in
the real world, I don't think an urgent patch for 2.32 is necessary.

## Lint gap

The existing `InTx` ruleguard rule in `scripts/rules.go` catches direct
outer-store usage (`p.db.GetFoo()`) and passing the outer store as a
function argument inside `InTx` closures, but it explicitly cannot catch
indirect access through receiver methods like
`p.resolveDeploymentSystemPrompt()` — the rule documents this blind spot
at line 273. Catching this class of bug would require interprocedural
analysis (following the callee's body to see if it touches `p.db`),
which is beyond what ruleguard's AST pattern matching can express. We're
considering a lightweight custom `go/analysis` analyzer (similar to
`paralleltestctx`) that does 1-level same-package callee inspection to
detect this pattern. In the meantime, this PR adds guidance to
`AGENTS.md` so AI reviewers can flag the pattern during code review.
2026-04-16 00:13:15 +10:00
Cian Johnston c552f9f281 fix: stop group spend limits from leaking across org boundaries (#24294)
Three SQL queries (`GetUserGroupSpendLimit`,
`ResolveUserChatSpendLimit`, `GetUserChatSpendInPeriod`) aggregated chat
spend limits and usage globally across all organizations. A restrictive
group limit in org A would bleed into org B.

## Changes

- Add `organization_id` parameter to all three SQL queries in
`coderd/database/queries/chats.sql`
- When nil UUID is passed, queries fall back to global behavior
(backward compat for HTTP dashboard endpoints)
- When real org ID is passed, limits and spend are scoped to that
organization
- Thread `organizationID` through `ResolveUsageLimitStatus` →
`checkUsageLimit` → all chatd call sites
- Update dbauthz wrappers for new param structs
- HTTP endpoints (`chatCostSummary`, `getMyChatUsageLimitStatus`) pass
`uuid.Nil` with TODO for future org-scoped UI
- Add `TestResolveUsageLimitStatus_OrgScoped` with 5 test cases covering
org isolation, nil-UUID fallback, spend scoping, and user override
priority

Closes coder/internal#1466

> 🤖
2026-04-14 16:56:17 +01:00
Cian JohnstonandMathias Fredriksson 22062ec52e feat: add organization scoping to chats (#23827)
Fixes https://github.com/coder/internal/issues/1436

* Adds organization_id to chats with backfill (workspace org → user org membership → default org)
* No support yet for ACLs (follow-up issue)
- Cross-org workspace binding rejected (both in `CreateChatRequest` and in `create_workspace` tool
- Adds `OrganizationAutocomplete` to `AgentCreateForm`
- Docs updated with `organization_id` in chats-api.md

> 🤖 Written by a Coder Agent. Reviewed by many humans and many agents.

---------

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2026-04-13 12:31:25 +01:00
Danielle Maywood 38d4da82b9 refactor: send raw typed payloads over chat WebSockets (#24148) 2026-04-10 10:47:30 +01:00
Kyle CarberryandMichael Suchacz 391b22aef7 feat: add CLI commands for managing chat context from workspaces (#24105)
Adds `coder exp chat context add` and `coder exp chat context clear`
commands that run inside a workspace to manage chat context files via
the agent token.

`add` reads instruction and skill files from a directory (defaulting to
cwd) and inserts them as context-file messages into an active chat.
Multiple calls are additive — `instructionFromContextFiles` already
accumulates all context-file parts across messages.

`clear` soft-deletes all context-file messages, causing
`contextFileAgentID()` to return `!found` on the next turn, which
triggers `needsInstructionPersist=true` and re-fetches defaults from the
agent.

Both commands auto-detect the target chat via `CODER_CHAT_ID` (already
set by `agentproc` on chat-spawned processes), or fall back to
single-active-chat resolution for the agent. The `--chat` flag overrides
both.

Also adds sub-agent context inheritance: `createChildSubagentChat` now
copies parent context-file messages to child chats at spawn time, so
delegated sub-agents share the same instruction context without
independently re-fetching from the workspace agent.

<details><summary>Implementation details</summary>

**New files:**
- `cli/exp_chat.go` — CLI command tree under `coder exp chat context`

**Modified files:**
- `agent/agentcontextconfig/api.go` — `ConfigFromDir()` reads context
from an arbitrary directory without env vars
- `codersdk/agentsdk/agentsdk.go` — `AddChatContext`/`ClearChatContext`
SDK methods
- `coderd/workspaceagents.go` — POST/DELETE handlers on
`/workspaceagents/me/chat-context`
- `coderd/coderd.go` — Route registration
- `coderd/database/queries/chats.sql` — `GetActiveChatsByAgentID`,
`SoftDeleteContextFileMessages`
- `coderd/database/dbauthz/dbauthz.go` — RBAC implementations for new
queries
- `coderd/x/chatd/subagent.go` — `copyParentContextFiles` for sub-agent
inheritance
- `cli/root.go` — Register `chatCommand()` in `AGPLExperimental()`

**Auth pattern:** Uses `AgentAuth` (same as `coder external-auth`) —
agent token via `CODER_AGENT_TOKEN` + `CODER_AGENT_URL` env vars.

</details>

> 🤖 Generated by Coder Agents

---------

Co-authored-by: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
2026-04-09 16:33:00 +02:00
Hugo Dutka efb19eb748 feat: agents desktop recording thumbnail backend (#24022)
The agents chat interface displays thumbnails for videos recorded by the
computer use agent. Currently, to display a thumbnail, the frontend
downloads the entire video and shows the first frame. This PR starts
storing a new thumbnail file in the database for every recorded video,
and exposes the file id in the `wait_agent` tool result alongside the
recording file id, so the frontend can fetch just the thumbnail.
2026-04-09 13:47:54 +02:00
Hugo Dutka 17dec2a70f feat: agents desktop recordings backend (#23894)
This PR introduces screen recording of the computer use agent using the
virtual desktop.

- Screen recording is triggered by a `wait_agent` tool call. Recording
is stopped by a successful `wait_agent` tool call or when there hasn't
been any desktop activity for 10 minutes.
- Recordings are handled by the `portabledesktop` cli via the `record`
command. The videos are sped up in periods of inactivity.
- Recordings are saved to the database to the `chat_files` table.
There's a hard limit of 100MB per recording. Larger recordings are
dropped.
- A successful `wait_agent` on a computer use subagent tool call returns
a `recording_file_id`, later allowing the frontend to display the
corresponding video.
2026-04-02 17:23:27 +00:00
Ethan 15f2fa55c6 perf(coderd/x/chatd): add process-wide config cache for hot DB queries (#23272)
## Summary

Adds a process-wide cache for three hot database queries in `chatd` that
were hitting Postgres on **every chat turn** despite returning
rarely-changing configuration data:

| Query | Before (50k turns) | After | Reduction |
|---|---|---|---|
| `GetEnabledChatProviders` | ~98.6k calls | ~500-1000 | ~99% |
| `GetChatModelConfigByID` | ~49.2k calls | ~500-1000 | ~98% |
| `GetUserChatCustomPrompt` | ~46.7k calls | ~1000-2000 | ~97% |

These were identified via `coder exp scaletest chat` (5000 concurrent
chats × 10 turns) as the dominant source of Postgres load during chat
processing.

## Design

Follows the established **webpush subscription cache pattern**
(`coderd/webpush/webpush.go`):
- `sync.RWMutex` + `tailscale.com/util/singleflight` (generic) +
generation-based stale prevention + TTL
- 10s TTL for provider/model config, 5s TTL for user prompts
- Negative caching for `sql.ErrNoRows` on user prompts (the common case
— most users don't set custom prompts)
- Deep-clones `ChatModelConfig.Options` (`json.RawMessage` = `[]byte`)
on both store and read paths

### Invalidation

Single pubsub channel (`chat:config_change`) with kind discriminator for
cross-replica cache invalidation. Seven publish points in
`coderd/chats.go` cover all admin mutation endpoints
(create/update/delete for providers and model configs, put for user
prompts).

_This PR was generated with mux and was reviewed by a human_
2026-03-26 18:04:53 +11:00
Ethan 61e31ec5cc perf(coderd/x/chatd): persist workspace agent binding across chat turns (#23274)
## Summary

This change removes the steady-state "resolve the latest workspace
agent" query from chat execution.

Instead of asking the database for the latest build's agent on every
turn, a chat now persists the workspace/build/agent binding it actually
uses and reuses that binding across subsequent turns. The common path
becomes "load the bound agent by ID and dial it", with fallback paths to
repair the binding when it is missing, stale, or intentionally changed.

## What changes

- add `workspace_id`, `build_id`, and `agent_id` binding fields to
`chats`
- expose those fields through the chat API / SDK so the execution
context is explicit
- load the persisted binding first in chatd, instead of always resolving
the latest build's agent
- persist a refreshed binding when chatd has to re-resolve the workspace
agent
- keep child / subagent chats on the same bound workspace context by
inheriting the parent binding
- leave `build_id` / `agent_id` unset for flows like `create_workspace`,
then bind them lazily on the next agent-backed turn

## Runtime behavior

The binding is treated as an optimistic cache of the agent a chat should
use:

- if the bound agent still exists and dials successfully, we use it
without a latest-build lookup
- if the bound agent is missing or no longer reachable, chatd
re-resolves against the latest build and persists the new binding
- if a workspace mutation changes the chat's target workspace, the
binding is updated as part of that mutation

To avoid reintroducing a hot-path query, dialing uses lazy validation:

- start dialing the cached agent immediately
- only validate against the latest build if the dial is still pending
after a short delay
- if validation finds a different agent, cancel the stale dial, switch
to the current agent, and persist the repaired binding

## Result

The hot path stops issuing
`GetWorkspaceAgentsInLatestBuildByWorkspaceID` for every user message,
which is the source of the DB pressure this PR is addressing. At the
same time, chats still converge to the correct workspace agent when the
binding becomes stale due to rebuilds or explicit workspace changes.
2026-03-26 17:22:38 +11:00