mirror of
https://github.com/coder/coder.git
synced 2026-11-01 21:13:14 +08:00
d014d4c372fb7dfdb3b0ea00a9935fec9b9528f9
28
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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._ |
||
|
|
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. |
||
|
|
7ca7c30f40 |
feat: add group and user ACLs to MCP server configs (#27944)
Adds group and user ACLs to org-scoped MCP server configs so
organizations can restrict specific MCP servers to subsets of members,
mirroring the template ACL pattern.
## Summary
- Migration adds `group_acl`/`user_acl` JSONB columns (nested `{"<id>":
{"permissions": [...]}}` shape) and seeds every existing config with its
organization's Everyone group read entry (the Everyone group ID equals
the org ID), so member access is unchanged by default. Creation seeds
the same entry.
- The blanket org-member read grant from the base PR is replaced by ACL
evaluation: Rego requires org membership for every ACL grant, and
`GetAuthorizedMCPServerConfigs` compiles ACL-aware SQL filters.
- New `ActionShare` (org admins) gates `GET|PATCH
/api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl`
(nested under the organization like the rest of the config surface).
PATCH validates principals against the config's organization, merges
sparse updates under a row lock, stamps `updated_by`/`updated_at`, and
is audited as a Write with `Old` captured before authorization. A config
deleted concurrently between the middleware fetch and the locked
re-fetch is concealed as 404, matching the update and delete handlers.
ACL columns are tracked in the audit table.
- ACL management is available in all editions (no enterprise
entitlement), documented in the MCP servers page. Revoking an ACL does
not retro-strip already-selected configs from existing chats; new
selection is blocked at chat create.
- Force On respects the ACL: the forced set is loaded as the chat owner,
so a `force_on` server whose ACL denies the owner never attaches at
create, send, or generation time.
- No rolling-upgrade machinery: upgrades run in scheduled maintenance
downtime, so the migration only backfills existing rows; the API sets
the Everyone read grant explicitly on every insert.
Stacked on #27943. Part of the MCP org-separation stack.
Closes https://linear.app/codercom/issue/CODAGT-712
UAT: verified on a dogfood instance with two members and a custom group:
Everyone-seed default visibility, group grant with Everyone removal
(non-member loses list/fetch/selection), foreign-principal rejection,
non-admin share denial (audited 403 / concealed 404), user_acl restore,
and audited ACL diffs.
> Mux (AI agent) authored this PR on Mike's behalf.
<!-- mux-attribution: model=claude-fable-5 thinking=high -->
|
||
|
|
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>
|
||
|
|
7724ee281a |
feat: defer MCP tool schemas behind a find_tools search (#28225)
## Summary When the `mcp-tool-search` experiment is enabled, chatd stops inlining connected MCP tool schemas into every generation. It instead exposes a built-in `find_tools` tool whose description carries a compact catalog of the deferred tools, and only ships full JSON schemas for tools the model has activated by searching or by calling them directly. Closes [CODAGT-760](https://linear.app/coder/issue/CODAGT-760). ## Problem Tool-heavy agent configurations (GitHub, Linear, Notion, and dev-tooling MCP servers) inline over 100k tokens of tool schema definitions into every generation. Initial uncached requests reached ~216k tokens with time-to-first-token close to nine minutes, while the model typically invokes only a handful of tools per turn. ## How it works - `decideMCPToolSearch` defers external and workspace `.mcp.json` MCP tools whenever the experiment is enabled. Native, dynamic, provider, skill, and transport tools are never deferred. - `find_tools` embeds a server-grouped catalog in its tool description (degrading to names-only, then counts-only, then a constant-size summary past a context-scaled size cap) and scores keyword matches across tool names, descriptions, parameter schemas, and server metadata. Queries can scope to one server with a `server:` prefix, and exact `names` arguments always activate. - Activation state is ephemeral: it is re-derived each generation from surviving chat history (`find_tools` results and direct calls to deferred tools), so activations naturally lapse when compaction summarizes them away. Aggregate activated schema weight is capped at 10% of the context window, shedding the least recently activated schemas first; `find_tools` shares that budget across parallel calls in one step. No new persistence. - Deferred tools stay registered for execution, so the model can call a cataloged tool directly without searching first; the schema is activated for subsequent steps. - Fail-open: the experiment being disabled, an empty candidate set, or an MCP tool named `find_tools` all disable deferral, leaving today's behavior byte-identical on the wire. - Prometheus counters/histograms track `find_tools` calls, matches, activations, and deferred token weight. - The conversation timeline renders `find_tools` calls with a collapsed search summary and expandable match list, falling back to the generic renderer on malformed payloads. ## Validation - Unit tests for the catalog, matcher, experiment-gated decision, and activation derivation; end-to-end chatd generation tests covering search-then-call, direct-call activation, experiment-off wire parity, compaction lapse, and subagent tool gating. - Storybook interaction tests for the timeline rendering and malformed-payload fallback. - Remote dogfood UAT on dev.coder.com passed: deferral with a real MCP server and Anthropic model, direct calls without prior search, activation persistence across turns, experiment-off parity, and clean UI/console. > Disclosure: Mux (AI agent) authored this PR on Mike's behalf. |
||
|
|
2e5353bde7 |
feat: add built-in Browser tab for agent-browser (#27910)
Adds a built-in Browser tab to the Agents page right panel, alongside the built-in Terminal and Desktop tabs, when the chat's bound agent has an app with the well-known slug `agent-browser`. The tab shows only while the app is embeddable and its health is `healthy` (or `disabled`, for templates without a healthcheck), so it appears and disappears live as the daemon comes up or goes down. The iframe stays mounted across tab switches to preserve session state. To avoid duplicates, the generic Add Tab menu and persisted workspace-app tabs now exclude the `agent-browser` app. Detection uses the existing `coder_app` slug and healthcheck signals already present in the workspace data model. The workspace watch handler compares the agent app fields the chat UI consumes, so health transitions propagate without re-render churn on every heartbeat. On the backend, the chat `execute` tool now exports `AGENT_BROWSER_SESSION=<chat id>` on every process it starts. agent-browser resolves its default session from that variable, so browser automation from each chat lands in its own isolated session (named by the chat id in the embedded dashboard) instead of a shared default browser. > Mux created this PR on Mike's behalf. |
||
|
|
91d3027498 | fix(coderd): enforce Force On MCP server policy on the backend (#27990) | ||
|
|
404bb2f663 |
refactor(coderd/x/chatd): own provider option construction in one function (#27705)
Stacked on #27704. Provider option conversion and reasoning effort injection both create OpenAI option structs, so each of the four call sites had to pair them in the right order and pick the same transport for each. `chatprovider.ProviderOptionsForCall` now owns both steps, and the two helpers it wraps are unexported. The advisor, main generation, compaction override, and quickgen paths each collapse to one call. The ARCHITECTURE section on transport selection is updated to match: `ProviderOptionsForCall` is described as the only entry point in `chatprovider` that builds provider options for a call, delegating OpenAI conversion to `chatopenai.ProviderOptionsFromChatConfig`, and it now records that the quickgen turn status label and chat summary paths deliberately send no provider options. > Mux prepared this PR on Mike's behalf. |
||
|
|
0e16e356b0 |
refactor(coderd/x/chatd): read the OpenAI transport from the model (#27704)
Stacked on #27703. Provider option conversion, reasoning effort injection, and file part acceptance each recomputed the OpenAI wire format from `(provider, modelID, override)`. They now read it from `chatprovider.Model`, so a decision cannot drift from the client it was built for. `ProviderOptionsFromChatConfig` takes a `Transport`, `ApplyReasoningEffort` takes a `Model`, and `AcceptsFilePartMediaType` becomes a `Model` method. `UsesResponsesAPI` and `UsesResponsesOptions` are deleted. The override extraction is unexported and reachable only from `ModelFromConfig`, which now takes the model's `ChatModelOpenAIConfig` directly, removing the six scattered extractions at call sites. That also resolves the computer-use mismatch. The computer-use model is a hardcoded default with no config row of its own: its client was built without an override while request preparation applied the chat model's. Preparation now reads the computer-use model's own transport, so the two agree without one model's client settings following a different model. Passing the chat model's `openai_config` into the computer-use client would have made them agree on the wrong value. `TestModelTransportConsumersAgree` pins the invariant in one test: the HTTP path the client actually hits, the concrete provider option struct type, the type created by reasoning effort, and text/image file acceptance. > Mux prepared this PR on Mike's behalf. |
||
|
|
b5c9e8e471 |
refactor(coderd/x/chatd): carry the resolved OpenAI transport on a model wrapper (#27703)
Stacked on #27683. The OpenAI wire format is decided when the client is built, then thrown away, so downstream sites recompute it from `(provider, modelID, override)`. Any disagreement fails silently: the SDK type-asserts the concrete provider options struct and discards every OpenAI option, and text attachments are dropped because Responses natively accepts only images and PDFs. This adds `chatprovider.Model`, which pairs a fantasy client with the transport resolved from that client's own identity. Its fields are unexported and only the constructor sets the transport, deriving it from the client, so no caller can pick a transport that disagrees with the client it wraps. `chatopenai.Transport`'s zero value is invalid and panics when read rather than defaulting to a wire format, following the existing precedent for construction invariants. `Model` is threaded through construction, the resolve paths, and the four struct fields that store a model for later request preparation. Terminal call sites keep taking `fantasy.LanguageModel` and receive `LanguageModel()`, which avoids a new package edge from `chatloop` and `chatadvisor` into `chatprovider`. No decisions move yet. The consumers still recompute the transport, and `UsesResponsesAPI` now delegates to `TransportFor` so the two agree by construction. #27704 makes the consumers read it from the model. > Mux prepared this PR on Mike's behalf. |
||
|
|
c6cee10e8b |
feat: add per-model OpenAI Responses API toggle (#27683)
chatd hardcoded `WithUseResponsesAPI()`, so the provider SDK's static known-model list decided whether an OpenAI model spoke the Responses API or Chat Completions. A model absent from that list silently fell back to Chat Completions until the fantasy fork was patched. This exposes the SDK's `WithResponsesAPIFunc` hook as a per-model setting, `openai_config.use_responses_api`, stored in the existing `chat_model_configs.options` JSONB. Unset keeps the known-model list, `true` forces Responses, `false` forces Chat Completions. There is no migration. It sits in a new construction-time `openai_config` section rather than in `provider_options.openai` because it selects the API when the client is built, while `provider_options` holds per-request parameters. That placement is also load-bearing: a config setting only this field would otherwise materialize an OpenAI request-options struct and turn on provider-side response storage, since `Store` defaults to true there. Three places independently decided the transport and would silently disagree with the client actually built: | Site | Effect when it disagrees | | --- | --- | | `ModelFromConfig` | the transport being overridden | | `AcceptsFilePartMediaType` | text attachments dropped, since Responses natively accepts only images and PDFs | | `UsesResponsesOptions` | the SDK type-asserts the concrete options struct, so every OpenAI option is discarded | They share one predicate here, `chatopenai.UsesResponsesAPI`, with the override threaded to each. The rest of the stack removes that threading by resolving the transport once and carrying it. Compaction overrides and the quickgen debug model built clients without `ConfigOptions`, so they now pass it and pick up both this setting and the existing Anthropic beta headers. The toggle also makes transport-conditional option handling admin-switchable, so two hardening changes ride along. `ServiceTierFromChat` now maps every tier the codersdk enum advertises (`auto`, `default`, `flex`, `scale`, `priority`); it previously returned nil for `default` and `scale`, so flipping a model to Responses silently dropped a configured `service_tier` that the API accepts (fantasy forwards the value unchanged). And a new `TestProviderOptionsTransportParity` pins, per `provider_options.openai` field, which transport honors it, against a table in ARCHITECTURE.md, so a field honored on one transport and silently ignored on the other fails the test unless recorded as intentional. Review rounds also caught two lifecycle gaps around the new field. `isZeroChatModelCallConfig` now inspects `OpenAIConfig`, so a stored options blob whose only setting is this toggle survives into GET/list responses instead of reading as `model_config: null`; `TestIsZeroChatModelCallConfigCoversEveryField` sets each config field in isolation and fails if any field is invisible to the zero check. And the model editor's update path sends an explicit empty `model_config` when an edit clears the last field, since an omitted property preserves the stored options server-side; covered by the `EditClearingLastOptionSendsEmptyConfig` story. Azure keeps following the known-model list, because the Azure provider exposes no equivalent hook. The model editor renders Azure with the OpenAI option schema, so instead of shipping a visible but inert control, the option schema generator gains a `providers` struct tag that it emits as `visible_for_providers`. Gating uses the raw provider type rather than the alias table, so the control appears only for openai-typed providers. No hand-written frontend field: the editor renders it from the generated schema. Closes https://linear.app/codercom/issue/CODAGT-874/add-completionsresponses-api-toggle-in-model-editor > Mux prepared this PR on Mike's behalf. |
||
|
|
6f2011af88 |
feat: add chat summary tab in the right sidebar and per-chat cost endpoint (#26649)
Stacked on #26657 (the persisted whole-chat summary backend). Base branch is `chat-summary-62j9`; review/merge that first. Adds a reusable `ChatSummary` component. The summary text is the persisted whole-chat summary (`chat.summary`) introduced by #26657. It is generated asynchronously and may be `null` until the first summary is produced, in which case the popover renders a muted empty state. Live updates arrive via that PR's `chat_summary_change` watch event, which is already merged into the chat caches. Cost is served by a new per-chat endpoint, `GET /api/experimental/chats/{chat}/cost`, which rolls up assistant-message cost across a chat's root and child (subagent) chats and is authorized like the other `{chat}` routes (read on the chat, 404 otherwise). Visual and interaction coverage lives in `ChatSummary.stories.tsx` and `ChatSummaryPopover.stories.tsx` (including populated-summary, empty-state, and cost-loading cases). --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
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 |
||
|
|
a567f6a89f | feat: allow admins to override the chat compaction model (#27151) | ||
|
|
d66e4d794f | feat: add configurable reasoning effort to Coder agents (#26974) | ||
|
|
b21e0717d5 |
feat: remove chat chain mode (#26980)
Removes OpenAI Responses "chain mode" from chatd. Closes CODAGT-445.
- Deletes `chatopenai/responses.go` (chain detection, activation, prompt filtering, response ID extraction) and its tests.
- Deletes the `ChainBroken` classification in `chaterror` and the chatloop retry bookkeeping that disabled chain mode mid-generation.
- Drops the `chain_broken` label from the `coderd_chatd_stream_retries_total` metric.
- Stops reading and writing `chat_messages.provider_response_id`
- Deletes the dead `ClearChatMessageProviderResponseIDsByChatID` query. Dropping the column is a follow-up migration.
- Deletes three chatloop hooks no caller sets (`ReloadMessages`, `DisableChainMode`, `PrepareMessages`), the dead `const AgentChatContextSentinelPath`, and stale chain-mode comments.
🤖 Generated by Coder Agents on behalf of @johnstcn.
|
||
|
|
4936ff9808 |
refactor: deprecate AIGatewayRoutingEnabled, remove direct chat routing (#26862)
This PR removes the now-dead direct-routing code: - Deletes the direct routing implementation. - Collapses the resolvedModelRoute discriminated union into aiGatewayModelRoute. - Removes the dead providerKeys cascade. - Deletes the preferredShortTextCandidates quickgen function. - Simplifies the advisor override error handling. - Deprecates the AIGatewayRoutingEnabled deployment option. It is now a no-op so as to not break existing deployments on upgrade. Once direct routing was gone, the AI Gateway became mandatory for chat, which surfaced gaps in how the product behaves with the gateway disabled: - Exposes ai-gateway-enabled to the frontend via embedded page metadata. - Disables the chat composer via the existing AgentSetupNotice when the gateway is disabled, for both new and existing chats. - Fixes nil/typed-nil chatDaemon panics on startup and shutdown when gateway is disabled. - Fixes chat WebSocket from retrying the still-gated stream endpoint forever when the gateway is disabled. |
||
|
|
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 |
||
|
|
6b8c38b5a4 | fix: gate chat advisor and virtual desktop behind experiments, delete experiments page (#26809) | ||
|
|
74b8f10d4e |
fix(coderd/x/chatd): drop foreign provider-executed tools on model switch (#26555)
Drops provider-executed tool history (calls and results) from assistant rows whose producing provider ID differs from the target turn's provider ID, before the prompt is built. Same-provider history is left untouched, so normal `web_search` replay is unaffected. - When a model config has an `AIProviderID`, use this as identity so two providers of the same type (e.g. two `openai-compat` providers at different base URLs) are correctly distinguished. Falls back to the normalized provider type name. - Sanitization runs at the `database.ChatMessage` row level in `prepareGeneration`. The `chatloop` pre-request and reload paths are untouched. - Foreign provider-executed results are dropped and not converted to text. - Unknown origin (unresolvable `ModelConfigID`) fails closed (strip). - Adds tests for the pure `stripForeignProviderExecutedToolRows`. - Adds unit tests for `modelConfigProviderIdentity`. _This pull request was created by Coder Agents on behalf of @johnstcn._ |
||
|
|
7cf6a4d304 |
fix(coderd/x/chatd): convert file attachment that would otherwise be dropped (#26556)
fix(coderd/x/chatd): inline text attachments that providers would drop Text-family file attachments (e.g. application/json) sent to providers that reject them as file parts were silently dropped with a CallWarning the user never saw. Convert them to TextPart at prompt build when the target provider would drop that media type, so the model sees the content while the stored file part (chip, download, history) is unchanged. Provider acceptance is keyed on model.Provider() (the fantasy transport identity) to correctly handle aibridge routing remapping. OpenAI distinguishes Responses vs Chat Completions via IsResponsesModel. Only text/plain, text/markdown, text/csv, and application/json are ever decoded; binary content is never touched. Inlined content is sent in full with no truncation, matching how a provider that accepts the media type natively would receive the file. |
||
|
|
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. |
||
|
|
78c5ab96c9 |
feat(coderd/x/chatd): serve workspace MCP tools and read_skill from pinned context (#26581)
This wires the last two consumer-side gaps of the agent-pushed workspace
context refactor. coderd already hydrates each chat's pinned context
(`chat_context_resources`), and `resolveTurnWorkspaceContext` already
prefers the pin for instruction files and skill metadata. This change
extends that preference to workspace MCP tools and the `read_skill`
body.
Workspace MCP tools are now built from the chat's pinned `mcp_server`
resources instead of a live `ListMCPTools` pull.
`resolveWorkspaceMCPTools` prefers the pin and falls back to live
discovery for chats whose agent has not reported context yet, gated the
same way as the instruction/skills pin: the pin wins whenever the chat
has any pinned rows, so a workspace with no MCP servers contributes no
tools rather than resurrecting stale ones. Because the agent reports
tool names unprefixed, each tool is re-prefixed to the
`{server}__{tool}` form and the pushed JSON Schema is split into
`properties` and `required` so the result matches what live discovery
produced. Calls still proxy through the workspace agent connection; the
snapshot carries tool definitions, not a way to execute them.
`read_skill` now serves a workspace skill's `SKILL.md` body from the
pinned snapshot (`SkillMeta.Meta`) instead of dialing the agent, so a
pinned chat keeps returning the same instructions even when the
workspace is unreachable. The supporting-file list stays a best-effort
live lookup, since the snapshot carries only the meta file per the agent
push contract.
The legacy live paths remain as the fallback for agents that have not
pushed context; RFC Release-5 cleanup of those paths is out of scope
here.
<details>
<summary>Implementation plan and decisions</summary>
### Background
The agentcontext refactor is mostly shipped across earlier PRs (#25983,
#26526, #26533, #26577, #26570, #26573): the agent resolves instruction
files, skills, and MCP servers into a snapshot, pushes it via
`PushContextState`, and coderd hydrates each chat's pinned context
(`chat_context_resources`). This PR closes the two remaining
consumer-side gaps.
### Key facts established from the code
- Pushed MCP tool names are **unprefixed** (`mcprunner` stores
`tool.Name`); the agent MCP proxy and `CallMCPTool` expect the
`{server}__{tool}` form (`agentmcp.ToolNameSep`). The pinned path
reconstructs the prefix for execution, matching the model-facing names
the legacy path produced.
- Legacy `agentmcp` sets `MCPToolInfo.Schema = InputSchema.Properties`
and `Required = InputSchema.Required` separately. The pushed
`input_schema` is the full JSON Schema object, so the pinned builder
extracts `properties` and `required` to match that shape.
- `SkillMetaBody.meta` is the verbatim SKILL.md. The supporting-file
list is **not** in the snapshot, so it is fetched live on demand
(best-effort).
- Gating mirrors `resolveTurnWorkspaceContext`: the pin wins when the
chat has any pinned rows; otherwise the live path is used.
### Changes
1. `chattool/skill.go`: add `SkillMeta.Meta []byte`; extract
`listSkillFiles` from `LoadSkillBody`; in `readWorkspaceSkillBody`, when
`Meta` is present, parse the body from it without dialing and list files
best-effort, else use the legacy live read.
2. `context_prompt.go`: populate `SkillMeta.Meta` in
`contextResourcesToPrompt`; add `workspaceMCPToolInfosFromResources`
(pinned `mcp_server` rows to `[]workspacesdk.MCPToolInfo` with prefixed
names and split properties/required) and `splitMCPInputSchema`.
3. `chatd.go`: add `pinnedWorkspaceMCPTools` (build tools from the pin,
ok-gated) and `resolveWorkspaceMCPTools` (pin-first, fall back to
`discoverWorkspaceMCPTools`).
4. `generation_preparer.go`: call `resolveWorkspaceMCPTools` instead of
`discoverWorkspaceMCPTools`.
### Tests
- `chattool/skill_test.go`: read_skill serves the pinned body without
dialing, lists files via LS, and still returns the body when the
workspace is unreachable.
- `context_prompt_internal_test.go`: `SkillMeta.Meta` is populated;
`workspaceMCPToolInfosFromResources` prefixing/properties/required/skip
behavior; `pinnedWorkspaceMCPTools` ok-gating and fallback dispatch.
</details>
---
*Opened by Coder Agents on behalf of @kylecarbs.*
|
||
|
|
46916bf899 |
feat(coderd/x/chatd): consume the pinned chat context in prompt generation (#26558)
## What `prepareGeneration` now builds the system-prompt instruction block and workspace skills from a chat's **pinned context copy** (`chat_context_resources`, populated in #26438) instead of re-scanning per-turn history, when the chat has a pinned copy. This is the first production reader of the pin. Selection is **presence-based, no experiment**: a chat with pinned rows builds its prompt from the pin; a chat without them falls back to the existing per-turn history path. The two paths are mutually exclusive, so older agents that never report context keep their current behavior and the per-turn pull stays as the fallback. ## How - `contextResourcesToPrompt` maps the protojson resource bodies (instruction files and skills) into the instruction block and skill metadata, skipping non-OK statuses, non-prompt body kinds, and malformed bodies (the malformed count is logged so a proto/encoding regression cannot silently drop context). - `pinnedWorkspaceContext` reads the pin and reports `ok=false` (history fallback) when there are no pinned rows; read errors propagate. The bound agent only decorates the instruction header with OS and directory, so the pin still resolves when the workspace is unreachable. - `resolveTurnWorkspaceContext` dispatches between the pinned and history paths; `prepareGeneration` calls it. ## Testing - `go test ./coderd/x/chatd/` for `TestContextResourcesToPrompt`, `TestPinnedWorkspaceContext` (incl. `...FromHydratedPin` against real Postgres), and `TestResolveTurnWorkspaceContext`: pass. - `make gen` (no drift), `golangci-lint`, `gofmt`, emdash scan, and `go build`/`go vet` on `./coderd/x/chatd/...`: all clean. ## Scope This is the foundational backend slice split from #26466 (the full-stack staging PR). It changes no API surface, schema, proto, or generated files. The remaining pieces land as follow-ups in dependency order: 1. `ChatContext` drift/diff API (`resources` + `changes`, `ContextDetail`). This also extracts the body decoders inlined here so they are shared with the diff path. 2. Context-ring drift indicator, changes dialog, and refresh (UI). 3. In-workspace `coder exp chat context` source CRUD and `refresh` (CLI). <details> <summary>Why this is the first split</summary> The coderd hydration, the `PUT /chats/{id}/context` refresh endpoint (#26389), the `chat_context_resources` table (#26430), and the copy-into-pin logic (#26438) are already merged, as is the agent-side push (#26526, #26533). Consuming the pin in prompt building is the step #26438 explicitly deferred, and it is the bottom of the remaining dependency stack: the drift/diff API, the UI indicator, and the CLI are only meaningful once the chat actually builds its prompt from the pin. Keeping it presence-based means it is independently revertable and leaves the per-turn pull intact as a fallback, matching the RFC's Release 3 rollout. The files are taken verbatim from the reviewed #26466 boundary commit (before the diff-API work began), so the deep-review feedback already applied there (CRF-1 through CRF-10) is preserved. </details> --- *This PR was created by Coder Agents on behalf of @kylecarbs.* Split from #26466. |
||
|
|
21a2652343 |
fix(coderd/x/chatd): show correct provider and clean detail for Bedrock errors (#26338)
## Problem
Bedrock errors (routed through aibridge) showed the wrong provider
("Anthropic ...")
and a doubly-wrapped detail string instead of the clean message.
## Fix (chatd only)
- **Provider label:** thread the configured provider to error
classification via
`GenerateAssistantOptions.ErrorProvider`. Transport provider still
drives prompt
prep, sanitization, and metric labels (unchanged).
- **Detail:** unwrap the SDK transport wrapper (`METHOD "URL": NNN
{body}`) to surface
the inner message; handles top-level `{"message":...}` and nested
`{"error":{"message":...}}`.
## Notes
- Surfacing a top-level `message` now applies to all providers
(intentional; nested wins when both present).
- The advisor path keeps the transport label; accepted as-is (it returns
`err.Error()`, not the classification).
- Fixes display in chatd only; the wrapper originates in aibridge (out
of scope here).
🤖 Generated by Coder Agents.
|
||
|
|
86bdedb0a9 |
fix(coderd/x/chatd): dont send web notifs on subagent completion (#26379)
The chat refactor mistakenly started sending web push notifications on subagent completion. This PR fixes that. Addresses [CODAGT-624](https://linear.app/codercom/issue/CODAGT-624/subagent-completion-sends-web-push-notifications). |
||
|
|
3cde346cbb |
fix(coderd/x/chatd): fix compaction still over limit check (#26377)
Addresses [CODAGT-620](https://linear.app/codercom/issue/CODAGT-620/session-can-get-stuck-at-compaction-with-request-failed). We have logic that checks whether message compaction still leaves the chat over the context limit. We want to abort if it does - if we didn't, we'd get into an endless compaction loop. The check's logic was faulty. This PR changes fixes it. The new flow is: 1. In iteration 1, a chat runner commits a message compaction summary. 2. In iteration 2, the runner submits the newly compacted conversation to the LLM provider in order to generate the next message. 3. In iteration 3, 4, 5, etc., if the conversation needs compaction, the runner looks up the configured context limit and the first assistant message after the last compaction summary. It compares the context usage on that message with the context limit. If the usage is over the limit, it returns an error. |
||
|
|
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 |