mirror of
https://github.com/coder/coder.git
synced 2026-09-22 13:10:21 +08:00
2f879910af7d0017736ea35bfa0f34eef557dd66
4246
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2f879910af |
fix(coderd): harden oauth2 redirect validation (#27274)
Closes DEVEX-604 Hardens `redirect` URL handling in the OAuth2/OIDC/external-auth callback flows so redirects are always reduced to a safe, relative path local to the application. Previously a redirect value with an opaque scheme (e.g. `javascript:...`) or a path with multiple leading slashes (e.g. `///evil.com`) could survive sanitization mostly intact. Also de-duplicates the previously copy-pasted `uriFromURL` helper (now exported `httpmw.URIFromURL`) so there's a single implementation shared by `coderd/userauth.go`, `coderd/externalauth.go`, and `coderd/httpmw/oauth2.go`. <details> <summary>Context</summary> Addresses a low-severity finding reported via a pentest disclosure: the redirect sanitizer used `url.Parse(...).RequestURI()`, which doesn't reject non-hierarchical (opaque) URLs and doesn't collapse extra leading slashes, so crafted `redirect` values could partially survive sanitization. </details> This PR was authored by a Coder Agent on behalf of @aslilac. |
||
|
|
3c7a1d33e3 |
feat: add persisted whole-chat summary with background generation (#26657)
Adds a persisted whole-chat summary that backs the chat summary popover. A new nullable `chats.summary` column is populated in the background after a successful root-chat turn and pushed to clients via a new `chat_summary_change` watch event (distinct from `summary_change`, which is bound to `last_turn_summary`), so the popover reads `chat.summary` straight off the loaded `Chat` with no extra query. This is the data source for the popover and per-chat cost UI built in #26649; the popover can consume `chat.summary` once this lands (the field is nullable, so merge order does not matter). ## How it works - **Generation** runs in the existing successful-turn finalize hook, detached from the request so the user's turn is never blocked. A cadence gate generates the first summary after one completed turn, then regenerates every three turns, using the `chats.summary_generated_at` freshness marker. Generation reads compaction-aware history, renders it to a bounded plain-text transcript (short transcripts are skipped), and asks for a 1-3 sentence summary via structured output. Failures never clear an existing summary. - **Staleness** is guarded by `history_version` (mirroring `last_turn_summary`), so a background write racing a newer turn loses while worker lifecycle transitions cannot reject a fresh write. - **Model selection** uses the chat's configured model. ## Deferred to follow-ups - **Cost accounting**: the `chat_messages.cost_source` discriminator and summary/title usage recording were removed from this PR so summary persistence is not blocked by hidden accounting rows advancing `history_version`. Title usage recording stays on main's `InsertChatMessages` path. - **Model override**: deployment-wide summary generation model selection is split into #26803; the base feature always uses the chat model. ## Notes - Migration `000540` adds `chats.summary` and `chats.summary_generated_at`, and recreates `chats_expanded` to expose the new columns. - Root chats only; shared viewers pick up the summary on their next refetch (live watch events are owner-only). Refs #26649 --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
b9fad66214 |
refactor: authorize AI budget reads against the user resource directly (#27443)
Replaces the `GetUserByID` read used as an authz check in the AI budget-resolution queries with a targeted `authorizeContext` against the user resource. Same RBAC decision, one fewer db query per resolution step. Follow-up to https://github.com/coder/coder/pull/27364#discussion_r3632577802. > [!NOTE] > Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira |
||
|
|
c23f2c0223 |
feat: fall back to the Everyone group for AI spend attribution (#27364)
## Description Previously, a user with no per-user override and no membership in a budgeted group had no effective group, so their AI spend was attributed nowhere and was, therefore, untracked. This change falls back to the organization's Everyone group when no override or group budget applies. Since every user in an organization is implicitly a member of that org's Everyone group, spend is now attributed and tracked for any user with organization membership. A user with no organization membership resolves to no group, so their daily spend is not incremented and a warning is logged. The fallback is unlimited, so enforcement is unaffected: only override and group budgets can block requests. For users in multiple organizations, an existing budget on any Everyone group is still chosen by the "highest" policy; when none is budgeted, the fallback prefers the default org, then orders by organization name. ## Changes - Add `ResolveUserEffectiveGroup` and the `GetUserEveryoneFallbackGroup` query: resolve override → group budget → Everyone group fallback. - Attribute token-usage spend and the user AI spend endpoint via the fallback, so unbudgeted users resolve to their Everyone group instead of null. - Update `GetGroupMembersAISpend` to surface the Everyone fallback as the effective group. - Update `GetHighestGroupAIBudgetByUser` to break ties by organization name then group name, keeping multi-org resolution deterministic and consistent with the fallback. - For multi-org users with no budget anywhere, the fallback picks the Everyone group deterministically: prefer the default org, then order by organization name. Closes https://linear.app/codercom/issue/AIGOV-509/fall-back-to-the-everyone-group-for-spend-attribution > [!NOTE] > Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira |
||
|
|
02fd1cc691 | feat: allow spawn_agent model and reasoning effort override (#27385) | ||
|
|
99e740bdb7 |
feat: add user secrets file parser and shared validator (PLAT-240) (#26723)
Part of the **PLAT-240** bulk secret import stack: this PR adds the `codersdk` parser and shared create-secret validator used by the follow-up batch endpoint and UI PRs. `ParseSecretsFile` parses `.env`, `.json`, and `.yaml` files into `CreateUserSecretRequest` entries in source order, with size, count, duplicate-key, structure, and malformed-input checks. `ValidateCreateUserSecretRequest` now backs the single-create handler too, so create validation has one SDK-level implementation. Part of https://linear.app/codercom/issue/PLAT-240 > This PR was generated by Coder Agents on behalf of @dylanhuff-at-coder. |
||
|
|
d77aa3bca3 |
test(coderd): make TestTemplateVersionDryRun/ImportNotFinished deterministic (#27386)
Closes PLAT-334 / [coder/internal#1221](https://github.com/coder/internal/issues/1221). The subtest asserts HTTP 425 while the import job is unfinished, but it ran a real provisioner daemon. Any failure in an early import phase (init, parse, update job) sets `CompletedAt`, which is all `postTemplateVersionDryRun` checks, so the endpoint could return 201 and flake. Run the subtest without a provisioner daemon: the job is never acquired, stays pending, and the 425 is deterministic. > Generated by Coder Agents on behalf of @Emyrk. |
||
|
|
2b2a5c963a | Revert "fix(coderd): explain default GitHub app org visibility on login rejection" (#27388) | ||
|
|
48e9bb3391 |
fix(coderd): explain default GitHub app org visibility on login rejection (#27374)
## Problem On a fresh deployment with no custom GitHub OAuth app, Coder falls back to the default Coder-managed GitHub app. That app can only see organization memberships in organizations where it has been installed. If `CODER_OAUTH2_GITHUB_ALLOWED_ORGS` is set but the app isn't installed in the allowed organizations, the membership list comes back empty and every login, including the first admin login, is rejected with a bare "You aren't a member of the authorized Github organizations!" with no hint about the actual cause. This leaves fresh deployments in an apparently broken state. ## Fix * Append a remediation hint to the login rejection when the default provider is configured, pointing at the [app installation page](<https://github.com/apps/coder/installations/select_target>) and at configuring a custom GitHub OAuth app. * Log a startup warning when the default provider is combined with `CODER_OAUTH2_GITHUB_ALLOWED_ORGS`, listing the allowed orgs and the install URL. * Document the installation requirement next to the `CODER_OAUTH2_GITHUB_ALLOWED_ORGS` step in the GitHub auth docs. Access-control behavior is unchanged; the org check still rejects logins as before, it just explains why and how to fix it. ## Testing * New `TestUserOAuth2Github/NotInAllowedOrganizationDefaultProvider` asserts the hint appears when `DefaultProviderConfigured` is set; the existing `NotInAllowedOrganization` subtest asserts it does not leak into the custom-app path. Fixes coder/coder#17752 |
||
|
|
a9a1dcc65d |
feat: add network calls column to AI sessions table (#27269)
Add a "Total/blocked network calls" column to the AIBridge sessions table. Update `ListAIBridgeSessions` query to calculate network called made and blocked per session. See query plan [here](https://explain.dalibo.com/plan/54355c90b165ggb4). --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
aa89801ee5 |
fix(coderd/x/chatd/chatadvisor): textualize advisor prompt tool exchanges (#27059)
Closes CODAGT-592.
## Problem
The advisor tool sometimes fails with the opaque error `advisor produced
no text output`. Live reproduction against `claude-sonnet-4-6` showed
the cause: `BuildAdvisorMessages` forwards the parent conversation's raw
`tool_use`/`tool_result` blocks into the nested advisor call, which
defines no tools. The nested model imitates the forwarded pattern and
spends its turn committing to a tool call it cannot make (captured
reasoning from a failing run: "The user wants me to make another tool
call to the advisor about writing a poem about cucumbers."), so the step
ends with reasoning-only or empty content and no advice. Because each
chat step currently rebuilds the advisor runtime and snapshot
(CODAGT-593), the second advisor call in a run reliably sees the first
call's exchange, which is why the first call succeeds and later ones
fail.
## Fix
- `BuildAdvisorMessages` rewrites tool activity as plain-text notes:
assistant tool-call parts are removed and folded, together with their
matching result, into a single user-role note of the form `[The parent
agent ran the X tool with input {...}. Result: ...]`. No raw tool blocks
and no bare call lines reach the tool-less nested request. This also
removes the provider requirement that `tool_result` blocks pair with a
`tool_use`, so results orphaned by window truncation are kept as notes
instead of dropped.
- The `advisor produced no text output` error now appends the finish
reason and content-part kinds, e.g. `advisor produced no text output
(finish_reason=stop; parts: reasoning=1)`, so field reports distinguish
tool-call mimicry, reasoning-only turns, and truncation.
Validated live by driving the production `RunAdvisor` path against
`claude-sonnet-4-6` through the dev.coder.com AI gateway: the failing
scenario went from 3/3 errors to 6/6 genuine advice (with and without
extended thinking), with the control scenario unaffected.
Related: CODAGT-593 (per-step advisor runtime recreation, addressed
separately) and CODAGT-742 (advisor tool call design).
<details>
<summary>Investigation and validation details</summary>
### Reproduction
A CLI prototype constructed the exact conversation snapshot the
generation preparer hands the advisor tool and called the real
`chatadvisor.NewRuntime` / `Runtime.RunAdvisor` / `BuildAdvisorMessages`
/ `chatloop.GenerateAssistant` chain against live `claude-sonnet-4-6`,
with a stream-teeing model wrapper capturing what `runner.go` discards
(finish reason, part kinds, reasoning text).
| Scenario (snapshot contents) | Thinking | Before fix | After fix |
|---|---|---|---|
| control: call #1 state, no prior advisor exchange | on | 3/3 advice |
2/2 advice |
| repro: call #2 state, prior advisor `tool_use`/`tool_result` pair
forwarded | on | 3/3 `advisor produced no text output` | 3/3 genuine
advice |
| repro | off | 2/3 same error, 1/3 degenerate advice ("I'll ask the
advisor...") | 3/3 genuine advice |
Every failing response was a tiny thinking block, zero text, zero
tool-call stream parts, finish reason `stop`; the model's own reasoning
text showed it deciding to "make the second tool call" in a request with
`tools=0`. The refunded `remaining_uses: 1200` in the failing
tool-result JSON matches the original issue screenshot.
### Decision log
- Tool exchanges are folded into a single user-role note per call/result
pair. A first attempt rendered assistant-authored `[tool call:
name(input)]` text lines plus separate result messages; live runs then
returned the literal `[tool call: advisor(...)]` line as the advice 6/6
times. The bare assistant call line is itself an imitable pattern, so no
assistant-authored tool artifact may survive the handoff. The folded
user-role note produced 6/6 genuine advice.
- An assistant message that carried only tool calls is dropped entirely;
the folded notes preserve the information.
- `dropOrphanToolMessages` was removed: without raw tool blocks there is
no provider pairing constraint, and an orphaned result note retains
context value.
- A reasoning-budget-starvation hypothesis (thinking budget consuming
`MaxOutputTokens`) did not reproduce on `claude-sonnet-4-6`; the model
adapts thinking length to the cap. The enriched error would identify
such cases on other models via `finish_reason=length`.
- CODAGT-593 (persisting the advisor runtime across steps) is
intentionally not addressed here; it shrinks the priming window but the
handoff fix is what removes the failure mode.
</details>
---
*This PR was generated by Coder Agents on behalf of @ThomasK33 (Linear
agent session for CODAGT-592).*
|
||
|
|
9bd4cf2a2a |
test: use NATS in coderdtest by default (#27343)
Closes GRU-70 Enables NATS as the pubsub for `coderdtest` unless specifically overwritten by the test case. |
||
|
|
3227cac217 |
feat: add manual chat compaction via /compact (#27081)
Adds a user-triggered `/compact` action for Coder Agents chats: typing
`/compact` in the composer (or picking it from the `/` trigger menu)
summarizes the conversation so far to free up context window space.
## How it works
- New `POST /api/experimental/chats/{chat}/compact` endpoint
(owner-only, RBAC `ActionUpdate`, excluded from the public API reference
via `x-apidocgen skip`). It marks the chat with a durable one-shot
`chats.compaction_requested_at` signal and moves it `waiting -> running`
via a new `RequestCompaction` state transition; no message row is
inserted. AI Gateway attribution needs no per-request key: generation
preparation resolves the owner's synthetic API key (#27170) like any
other turn.
- `RequestCompaction` hands off chat ownership (clears
`worker_id`/`runner_id`) so a worker acquisition hint is published;
since the transition changes no history, the previous runner could
otherwise miss the request under reordered pubsub delivery.
- The background chat worker picks the chat up like any other turn. A
pending manual request takes precedence over turn completion in the
generation decision, and forces compaction even below the automatic
threshold (and when compaction is disabled via threshold=100). The
commit step consumes the request marker in the same transaction; any
transition that ends the turn clears stale markers.
- The summary triplet reuses the automatic-compaction path, now tagged
with a `source` (`automatic` | `manual`) that is plumbed through
streamed progress parts, persisted tool JSON, and the UI label
("Summarized (manual)").
- Validation order: busy chats reject with 409 (state-machine conflict),
empty/already-compacted chats with 409 "nothing to compact", archived
chats with 400; the owner usage-limit check runs last so no-op requests
surface the specific conflict instead of a limit error.
- Web UI: the `/` trigger menu now has a built-in "Commands" group
listing `/compact`; submit intercepts exactly `/compact` and calls the
endpoint instead of sending a message. A personal or workspace skill
named `compact` takes precedence over the built-in command; while skill
collisions are still resolving, an exact `/compact` submission is
blocked with a retryable hint instead of leaking as message text.
History and queued-message edits are never intercepted. After
compaction, the context usage indicator resets to its unknown state
until the next assistant response reports fresh usage, instead of
showing the stale pre-compaction number.
- codersdk: `ExperimentalClient.CompactChat`.
Worker-path execution (rather than compacting synchronously in the
handler) reuses the existing lock fencing, live "Summarizing..."
streaming, retry accounting, restart resilience, and debug-run
observability. Rationale documented in `coderd/x/chatd/ARCHITECTURE.md`.
## Testing
- State machine: transition-matrix coverage for `RequestCompaction`,
marker lifecycle tests (carried by lease renewals/queue appends, cleared
by terminal transitions, consumed by commit), ownership handoff +
acquisition hint assertions.
- Worker: decision-ordering and forced-compaction unit tests;
active-server end-to-end test (manual compact below threshold produces a
`source=manual` summary, returns to `waiting`, no assistant follow-up;
busy chat rejected).
- API: success, archived, non-owner, RBAC-denied, empty-chat, no-daemon
cases; usage-limit ordering (at-limit owners still get
state/nothing-to-compact conflicts for no-op requests, with marker
rollback).
- Frontend: Storybook play tests for the Commands menu group, submit
intercept, skill-name collision, queued-edit passthrough, and
manual/automatic tool rendering; unit tests for command availability
resolution and the post-compaction context usage reset.
> This PR was created by Mux, an AI coding agent, working on Mike's
behalf.
|
||
|
|
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). |
||
|
|
a9fdf87a2f |
feat: add GET /groups/{group}/members/ai/spend (#27130)
## Description
Adds `GET /api/v2/groups/{group}/members/ai/spend?user_ids=...` (also available org-scoped at `/api/v2/organizations/{org}/groups/{groupName}/members/ai/spend`) to return per-member AI spend attributed to a group, along with each member's effective budget group and the applied spend limit when the queried group is their effective budget source.
In the UI, this endpoint is used alongside the existing `/api/v2/groups/{group}/members` endpoint. AI spend data is kept separate from that endpoint so that:
- Different concepts stay on different endpoints: identity (group members) vs. cost control (spend). Cost control is an additional feature layered on top of groups/orgs.
- Callers that don't need spend information don't pay for its computation.
UI flow:
1. Request `/api/v2/groups/{group}/members` → returns the group's members.
2. Request `/api/v2/groups/{group}/members/ai/spend?user_ids=...` with the IDs from step 1.
**Note:** Only current members of the queried group are returned. `spend_limit_micros` and `limit_source` are populated only when the queried group is the member's effective budget source (its own limit or a user override). `effective_group_id` is null when the member's budget resolves to a group in another organization, since an organization is treated as a tenant boundary.
<img width="2880" height="1904" alt="image" src="https://github.com/user-attachments/assets/33ed395d-d1a3-4b46-bb04-c8d3f41c8886" />
## Changes
- Add `codersdk.GroupMembersAISpend` and `GroupMemberAISpend` types, reusing the shared `AISpendPeriodWindow`.
- Add `GetGroupMembersAISpend` SQL query with a dbauthz per-row filter that mirrors `GET /api/v2/groups/{group}/members`.
- Add handler and routes under `/groups/{group}/members/ai/spend` (and the org-scoped alias) with a required `user_ids` query param (cap 100). Callers with more than 100 members are expected to batch across multiple requests.
- Add codersdk client method.
- Tests: dbauthz, raw SQL, endpoint, and role-access.
Closes https://linear.app/codercom/issue/AIGOV-471/backend-group-members-endpoint-with-members-spend
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
|
||
|
|
2adc8f5272 |
feat: add GET /organizations/{org}/groups/ai/spend (#27123)
## Description
Adds `GET /api/v2/organizations/{org}/groups/ai/spend?group_ids=...` to return per-group AI spend and configured limits for a set of groups in an organization.
In the UI, this endpoint is used alongside the existing `/api/v2/organizations/{org}/groups` endpoint. AI spend data is kept separate from that endpoint so that:
- Different concepts stay on different endpoints: identity (groups) vs. cost control (spend). Cost control is an additional feature layered on top of groups/orgs.
- Callers that don't need spend information don't pay for its computation.
UI flow:
1. Request `/api/v2/organizations/{org}/groups` → returns the organization's groups.
2. Request `/api/v2/organizations/{org}/groups/ai/spend?group_ids=...` with the IDs from step 1.
The groups endpoint from 1) is currently not paginated, but if pagination is added later, this design keeps the two responses in sync. This spend endpoint intentionally takes `group_ids` rather than paginating on its own, since it depends on the group set from step 1. Pagination could be added in the future, especially for Cost Control-focused pages.
<img width="2880" height="1460" alt="image" src="https://github.com/user-attachments/assets/ea83b74d-6a4f-45a6-af2f-1024e019da07" />
## Changes
- Add `codersdk.OrganizationGroupsAISpend` and `OrganizationGroupAISpend` types, plus a shared `AISpendPeriodWindow` embedded in the spend response.
- Add `GetOrganizationGroupsAISpend` SQL query with a dbauthz per-row filter that mirrors `GET /organizations/{org}/groups`.
- Add handler and route under `/organizations/{organization}/groups/ai/spend` with a required `group_ids` query param (cap 100). Callers with more than 100 groups are expected to batch across multiple requests.
- Add codersdk client method.
- Tests: dbauthz, raw SQL, endpoint, and role-access.
Closes https://linear.app/codercom/issue/AIGOV-466/backend-organization-groups-endpoint-with-groups-spend
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
|
||
|
|
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. |
||
|
|
54fa4a087e |
chore: wire quartz.Clock into Acquirer (#27291)
- Wires quartz.Clock into provisionerdserver.Acquirer - Allows overriding Acquirer in coderd.Options - Updates existing tests to use an Acquirer driven by a quartz.Mock Before this change `enterprise/coderd/prebuilds` package tests would take ~60-70s to run. After this change, it's down to ~10s. > Generated by Coder agents, massaged by this human. |
||
|
|
9f4ddea571 |
feat: revoke MCP server OAuth grants at the provider on disconnect (#27300)
Closes [CODAGT-805](https://linear.app/codercom/issue/CODAGT-805/revoke-oauth-grants-at-the-source-for-mcp-servers). The experimental MCP server OAuth2 disconnect endpoint previously deleted only the stored token row, leaving the grant active at the OAuth provider. This PR adds provider-side token revocation while keeping local disconnect independent of provider availability. ## Changes - Add `mcp_server_configs.oauth2_revocation_url` in migration `000547`. The value can be configured manually, discovered from RFC 8414 metadata, and managed through the MCP server settings UI. Non-admin responses redact it with the other OAuth2 fields. - Revoke the refresh token first through the RFC 7009 endpoint, then fall back to the access token only for `unsupported_token_type`. Public clients send `client_id`; confidential clients use `client_secret_basic`. - Delete the local token transactionally before best-effort provider revocation. Callers without a token receive the same response for hidden and nonexistent config IDs, and provider failures return a generic warning without exposing provider response bodies. - Require HTTPS revocation endpoints except for HTTP loopback URLs. Redirects must preserve the POST and remain on the configured origin. Redirect errors omit provider-controlled paths and query strings so reflected token material cannot enter logs. - Treat `200 OK` and `204 No Content` as completed revocations. `202 Accepted` remains a failure because it does not confirm completion. - Prevent an in-flight refresh from recreating a token deleted by disconnect. Refresh persistence now uses an optimistic update keyed by token ID and `updated_at`; only the OAuth callback can create a token row. Refresh conflicts reload the current row or clear in-memory auth when disconnect deleted it. - Return `{token_revoked, token_revocation_error}` from disconnect, while retaining SDK compatibility with the legacy `204` response. The UI surfaces provider revocation failures as warning toasts. - Document revocation endpoint discovery, HTTPS requirements, and best-effort disconnect behavior. No token or no configured revocation URL returns `token_revoked: false` without an error, so disconnect remains idempotent. > Updated by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
33fcc9de68 |
fix(coderd/x/chatd): drop stale APIKeyID from CreateOptions test literal (#27331)
> Mux is working on behalf of Mike. Closes coder/internal#1622 (ENG-3061). ## Problem `main` is broken: the chatd test package fails to compile, taking down `lint`, `test-go-pg`, `test-go-pg-17`, and `test-go-race-pg`. This was a semantic merge conflict between two individually green PRs: - #27170 removed `APIKeyID` from `chatd.CreateOptions` (chatd now mints a synthetic gateway key from the chat owner). - #27070 branched before that merge and added the `CreateChatProviderDisabledRejected` test, which sets `APIKeyID` in a `CreateOptions` literal. Its CI ran against the old base and passed. Merged together: `unknown field APIKeyID in struct literal of type CreateOptions`. ## Fix Two lines in the test: - Drop the stale `APIKeyID` field from the `CreateOptions` literal. - Create the chat owner with `dbgen.User` instead of a random `uuid.New()`. On current `main`, `CreateChat` resolves the owner's synthetic API key before the model-config recheck, so a nonexistent owner fails with `sql: no rows` instead of reaching the `ErrInvalidModelConfigID` assertion. ## Validation - `go build ./...` - `go test ./coderd/x/chatd/...` (full package, passes) - pre-commit hooks (lint/go, lint/ts) |
||
|
|
9b3af629cd | fix: hide and reject chat models from disabled AI providers (#27070) | ||
|
|
5f5efa49da |
fix: stop sending adaptive thinking to pre-4.6 Anthropic models (#27314)
## Problem
A chat model config with `reasoning_effort` set on a pre-4.6 Anthropic
model (for example `claude-haiku-4-5`) fails every generation with HTTP
400 `adaptive thinking is not supported on this model`, surfaced in chat
as "Anthropic returned an unexpected error." The fantasy Anthropic
provider always serialized effort as `thinking: {type: "adaptive"}` plus
`output_config.effort`, a shape only Claude 4.6+ accepts.
## Changes
- Bump the coder/fantasy pin to include coder/fantasy#47: the provider
now converts effort into `{type: "enabled", budget_tokens}` on models
older than Claude 4.6, with the budget derived from the call's
`max_tokens` (aibridge-mirroring ratios, 1024-token API floor; below the
floor thinking is omitted, which keeps small-budget calls like title
generation working). Adaptive-capable models keep the current shape, and
Opus 4.5 keeps `output_config.effort` alongside the derived budget since
it supports effort without adaptive thinking. Models older than Claude
3.7 predate extended thinking, so effort sends no thinking at all there.
`minimal` is normalized to `low`, `xhigh` falls back to `max` on
adaptive models that predate the xhigh tier (Claude 4.7+); effort `none`
disables thinking, including an explicit `thinking: {type: "disabled"}`
on Claude 5+ models that otherwise run adaptive thinking by default. The
Bedrock provider wraps the Anthropic one, so both are covered, and
Vertex-style `@date` model IDs parse correctly.
- `TestActiveServer_CompactionModelOverride` previously codified the
buggy shape (asserting `output_config.effort` sent to
`claude-3-5-haiku-latest`). The summary-routing subtest is now a
three-case table: pre-thinking override models (Claude 3.5) expect no
thinking, legacy budget-thinking ones (Haiku 4.5) expect enabled
thinking with the derived budget, adaptive-capable ones still expect
`output_config.effort`.
- New regression test `TestActiveServer_AnthropicModelReasoningEffort`:
a `claude-haiku-4-5` config with `reasoning_effort` produces enabled
thinking with the derived budget and no `output_config` on the wire, and
a `claude-sonnet-5` config with effort `none` sends an explicit thinking
disable.
- `chattest.AnthropicRequest` gains a `Thinking` field so tests can
assert the thinking config.
- One-sentence note in the chatd ARCHITECTURE reasoning-effort section.
No chatd production code changes: `ApplyReasoningEffort` keeps setting
`Effort`, which is now valid for every Anthropic model.
## Validation
- `go test ./coderd/x/chatd/...` passes (19 packages).
- Fork PR validated separately: full fantasy test suite plus new
provider unit tests (version gating incl. Vertex/Bedrock IDs, budget
derivation, floor behavior, normalization, effort `none` incl. Claude 5+
disable, Opus 4.5 effort preservation, sampling-param stripping),
golangci-lint clean.
Closes
[CODAGT-812](https://linear.app/codercom/issue/CODAGT-812/reasoning-effort-on-pre-46-anthropic-models-fails-generations-with).
> This PR was authored by Mux, an AI coding agent, acting on Mike's
behalf.
|
||
|
|
46d1823c0a |
feat: add workspace skills to agent chat slash menu (#25600)
> This Pull Request was updated by Mux working on behalf of Mike. Adds workspace skills to the agent chat slash menu, sourced entirely from the chat's pinned context resources (the single-chat GET response the page already fetches), the same inventory `read_skill` resolves from. No new API endpoint is introduced. Personal entries insert `/name`, or `/personal/name` when the name collides with a workspace skill or the chat's pinned context has not resolved yet; workspace entries insert `/workspace/name`. Qualified aliases stay searchable even when the displayed trigger is bare. Before a chat binds a workspace (new chat form, or a selected but unbound workspace), the menu lists personal skills only. Sending a message invalidates the chat detail query, and chatd broadcasts a context watch event when a first-turn bind pins the chat, so the menu picks up newly pinned context without a reload. Makes `UpdateChatWorkspaceBinding` a no-op when the requested workspace/build/agent binding is unchanged, preserving `updated_at` so chat list ordering and watch events stay stable. Includes regression coverage for the no-op binding guard, pinned-context skill mapping, collision qualification, and skills menu behavior. Refs [CODAGT-474](https://linear.app/codercom/issue/CODAGT-474/ux-improvements-for-coder-agents) (skills autocompleting in the editor). |
||
|
|
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 |
||
|
|
1ac106255b | feat: add Anthropic 1M context window toggle for Agents model configs (#27257) | ||
|
|
3dd9265fa6 |
feat: add UI option to disconnect OAuth2 MCP credentials (#27299)
Closes [CODAGT-804](https://linear.app/codercom/issue/CODAGT-804/add-ui-option-to-revoke-oauth-mcp-credentials). Users could authenticate with an OAuth2 MCP server from the chat input, but there was no UI to disconnect those per-user credentials. The backend endpoint (`DELETE /api/experimental/mcp/servers/{id}/oauth2/disconnect`) already existed. ## Changes - Connected OAuth2 MCP rows in the chat input plus menu now show a disconnect icon button next to the enable switch. It opens a confirmation dialog; confirming calls the disconnect endpoint, shows a toast, and refetches MCP configs so the row reverts to the `Auth` button without a reload. - New `disconnectMCPServerOAuth2` API client method and react-query mutation that invalidates `mcp-server-configs`. - Storybook interaction tests: control visibility per auth state, cancel makes no API call, confirm calls the endpoint once, failed disconnect keeps the dialog open. - Hardened `TestMCPServerConfigsOAuth2Disconnect`: seeded tokens flip `auth_connected`, disconnect only removes the calling user's token, and repeat disconnect stays idempotent. The endpoint removes the token stored in Coder; it does not revoke the upstream OAuth grant, so the UI copy says "disconnect" rather than "revoke". Validated with the targeted Go test, Storybook tests (51 passed), tsc, biome, the react-compiler check, and a manual dogfood run (seeded token, disconnect/cancel/reconnect flows verified in the UI). > This PR was authored by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
101aee8ee0 |
refactor: use Options struct in aibridgedserver.NewServer (#27200)
Refactor `aibridgedserver.NewServer` to take an `Options` struct instead of a long list of positional arguments. Follow-up to review feedback in https://github.com/coder/coder/pull/27117#discussion_r3571535760 |
||
|
|
f7481c5d08 |
feat: Add full text search over chat messages (#27126)
Closes CODAGT-721 Closes CODAGT-722 Closes CODAGT-723 Closes CODAGT-724 Closes CODAGT-725 This PR adds the database and API pieces necessary to support full-text chat message search. - Adds required chat schema for full-text search - Adds dbpurge job to populate search_tsv in the background - Adds `search` parameter to GetChats query - Adds `search` filter to `searchquery.Chats` - Wires chat search filter into chats API > Implemented by Coder Agents, reviewed and tested by a human. |
||
|
|
3ddf7d3baa |
fix: stop the template builder build progress bar from looping (#27276)
## Summary The template builder's "Building your template" loader had a progress bar that animated 0→100% every 5s with an infinite repeat, so it visibly restarted over and over while a template built. It looked broken and was frustrating to watch. This replaces the looping fill with a single ease-out fill that decelerates toward 90% and holds until the request resolves and the loader unmounts. Since the loader is intentionally indeterminate and no progress is streamed to the browser, this also removes the now-dead `onUpdate` callback plumbing from the backend `waitForProvisionerJob` (its only caller passed `nil`). Resolves DEVEX-593. https://github.com/user-attachments/assets/6d5ec04e-9f97-4864-bd18-e1e75055f079 ## Commits - `refactor(coderd): drop unused onUpdate callback from waitForProvisionerJob` - `fix(site/src/pages/TemplateBuilder): stop build progress bar from looping` ## Testing - `go build ./coderd/` passes with the reduced `waitForProvisionerJob` signature. - Biome clean on the changed frontend file. - Storybook: `pages/TemplateBuilder/BuildingTemplateLoader` shows the bar fill once and hold, with no restart. <details> <summary>Implementation plan</summary> # DEVEX-593: Stop the build progress bar from looping repeatedly ## Problem While the template builder composes and imports a template, the FE shows `BuildingTemplateLoader`. Its progress bar animates from 0% to 100% over 5s with `repeat: Number.POSITIVE_INFINITY`, so it visibly restarts over and over. Users report this looks broken and is frustrating to watch while waiting. ## Decision (scope) Minimal fix only: **stop the loop**, plus remove the now-dead `onUpdate` plumbing from the backend. Since the UI is intentionally indeterminate and no progress signal is streamed, the callback serves no purpose and should be deleted rather than left as dead code. ### Why not "real sync" now - `POST /api/v2/templatebuilder/compose/template` is a single blocking request. It composes, bundles, inserts the provisioner job, then calls `waitForProvisionerJob(jobCtx, provisionerJob.ID, nil)` and only responds once the job completes. - The `onUpdate` callback runs server-side only. Nothing is streamed to the browser during the wait, so the FE has no progress signal to bind to. - A provisioner job exposes no numeric percentage. Only status transitions (`pending -> running -> succeeded`) and coarse log stages (`init/plan/graph/apply`) exist. Real sync would require converting the endpoint to a streaming protocol (SSE/WebSocket) plus FE rework, which is disproportionate for this 1-point ticket. ### Keep polling (do not switch to pubsub-block) The wait could technically block instead of poll: on completion `CompleteJob` publishes `ProvisionerJobLogsNotifyMessage{EndOfLogs: true}` on the job logs notify channel, so we could subscribe and wait for that message with the context timeout as a fallback. We deliberately do not do that here: correctness would require subscribe-before-completion plus an initial DB completion check to avoid a race, and Postgres LISTEN/NOTIFY is at-most-once (can drop under load), so a poll fallback would still be needed. The existing backoff poll (100ms -> 200ms -> 500ms -> 1s) is simple and robust for a short-lived synchronous request. ## Approach Replace the looping fill with a single, non-repeating ease-out fill that decelerates and approaches (but never reaches) ~90%, holding there until the request resolves and the loader unmounts. This reads as continuous forward progress for an unknown-duration operation and never restarts. The floating-icon animation is intentional ambient motion and is not in scope. ## Out of scope - Any behavioral change to how the endpoint waits (it still blocks on the job). - Streaming real job progress to the browser. - Changes to the floating-icon animation. </details> --- Generated by Coder Agents. |
||
|
|
e489092154 |
feat: handle revoked OAuth grants for MCP servers gracefully (#27264)
Closes [CODAGT-792](https://linear.app/codercom/issue/CODAGT-792/handle-revoked-oauth-grants-for-mcp-servers-gracefully). When a user revokes an upstream OAuth grant for an MCP server used by Coder Agents, Coder kept treating the cached token as valid: `invalid_grant` refresh failures were logged and swallowed, the dead bearer token kept being attached, the list endpoints re-attempted the refresh on every call, and the UI kept showing the server as authenticated. ## Changes Backend, mirroring the `external_auth_links` prior art: - New migration adds `mcp_server_user_tokens.oauth_refresh_failure_reason`. `UpsertMCPServerUserToken` clears it, so completing the OAuth flow again recovers the row. - New `MarkMCPServerUserTokenRefreshFailure` query records the failure and clears all token material, guarded by an `updated_at` optimistic lock so a stale failure never clobbers a concurrently refreshed token (on a lock miss the winner's row is used). - `mcpclient.IsPermanentRefreshError` classifies `*oauth2.RetrieveError` codes: only `invalid_grant` and `bad_refresh_token` are permanent. Client/config errors (`invalid_client`, `unauthorized_client`, ...) stay transient for the user row since reconnecting cannot fix them. - chatd token refresh and the MCP list/get endpoints persist permanent failures, return cleared tokens for the in-flight request, and skip provider calls for already-failed rows. - `buildAuthHeaders` no longer attaches an Authorization header for failed tokens, so chat degrades by omitting that server's tools instead of sending a dead bearer. API and UI: - No new API surface. A permanently failed token simply reports `auth_connected: false`, so the existing "Auth" button and "Not authenticated" tooltip appear and the user re-runs the same OAuth flow to recover. An earlier revision added an `auth_status` enum (`connected` / `not_connected` / `reconnect_required`) with a dedicated "Reconnect" button; it was collapsed to keep the API minimal since both states lead to the identical re-auth action. Out of scope (follow-up): typed 401-on-connect detection and forced refresh. mcp-go exposes no stable typed 401 signal in the static-header path, so a revocation while the access token still looks valid locally stays undetected until expiry triggers a refresh. ## Testing - Unit and integration tests: classifier, chatd refresh paths (permanent/transient/race/persist-failure), API endpoints (revoked, transient, no-retry caching, re-auth recovery, stale-lock), dbauthz, dbcrypt, migrations. - Dogfood UAT against a dev instance with a mock IdP returning `invalid_grant`: revoked grant detected on refresh and persisted once (no repeated IdP calls), chat with the revoked server selected completes with the server's tools omitted, and re-auth restores the connected state. > This PR was authored by Mux, working on Mike's behalf. |
||
|
|
f997afa220 |
feat(coderd/x/chatd/chatloop): retain user constraints in compaction summaries (#27230)
Compaction summaries drop or soften user-stated constraints, corrections, and prohibitions, so post-compaction assistants repeat behavior the user already corrected. Add a summary prompt bullet that instructs the summarizer to quote them, treat them as standing until revoked, and attribute rules to their true source instead of defaulting to the user. Validated offline on unseen human chats: holdout P1 delta +0.175 (arbitrated), 13/18 cases improve. This improves per-compaction retention only; it does not address deep-chain correction loss. |
||
|
|
21d08241e9 |
fix(coderd/x/chatd): recover timed out agents (#27254)
Closes CODAGT-802 Coder Agents only escalated failed workspace dials when the agent had connected and later disconnected. An agent that never connected and had already exceeded its `connection_timeout` stayed on the soft retry error indefinitely, so a chat could keep attempting tools against an unhealthy workspace. To fix, we'll classify the latest agent after a failed dial and return stop/start recovery guidance when its status is `timeout`. Agents still connecting, including templates with `connection_timeout = 0`, keep the existing retryable behaviour. ## Before <img width="843" height="229" alt="image" src="https://github.com/user-attachments/assets/d659a376-c8d4-4983-b7d7-d1a699770dfb" /> ## After <img width="848" height="250" alt="image" src="https://github.com/user-attachments/assets/26b88f72-f67c-4d9e-87b1-51d701b7352f" /> |
||
|
|
de716f89dc |
fix: normalize path before rate-limit bucket keying (#27273)
Coder's rate limiter keyed its bucket on the raw, un-normalized request path (`httprate.KeyByEndpoint` reads `r.URL.Path` directly). The router's `singleSlashMW` already collapses redundant slashes so a request like `/api/v2/users//validate-password` reaches the same handler as the canonical path, but it never touched `r.URL.Path`, so the rate limiter saw a different key and let a client bypass a limit it had already hit just by respelling the URL. `keyByNormalizedEndpoint` replaces `KeyByEndpoint` and runs `path.Clean` on `r.URL.Path` before using it as the key, so equivalent paths share one bucket. Includes a unit test at the key-function level and an integration test (`TestRateLimitPathNormalization`) that reproduces the bypass against a real server. Fixes CDM-02-003 (Cure53). Refs https://github.com/coder/security-disclosures/issues/166. |
||
|
|
15da504cf9 |
fix: remove excess calls to prepareSQLFilter for workspace and template endpoints (#27248)
|
||
|
|
4d4cbd07e6 |
fix: prevent concurrent token refreshes (#26530)
This can cause bad refresh token errors, since it can only be used once. Looks like there was an attempt to fix this by checking the database after a failed refresh, but of course this depends on the first request having updated the database in time, so both that and this fix are required to fully solve. |
||
|
|
d0982e3cc7 |
fix(coderd/templatebuilder): prompt for DigitalOcean base variables (#27268)
## Summary The DigitalOcean template builder base declared Terraform `variable` blocks for `project_uuid` and `ssh_key_id` that the template builder never filled. `project_uuid` was required with no default, so the build broke with no way to supply a value from the wizard (DEVEX-591). This brings the DigitalOcean base to parity with the GCP bases fixed in #27015: - Declare `project_uuid` (required) and `ssh_key_id` (optional, default `0`) in `base.json` so the wizard prompts for them on the first step. - Inject the entered values via `default = {{ .Variables.* }}` in `main.tf.tmpl`, keeping the existing `variable` blocks and validation. - Drop the `sensitive` flags. The variable-injection path (`mergeBaseVariables`, `DefaultBaseRenderContext`, and the snapshot test helper) skips sensitive variables, so a sensitive base variable renders empty. A project UUID / SSH key ID are not secrets. - Update the README now that the values are prompted rather than manually edited. - Regenerate the `digitalocean-linux.tf.golden` snapshot. ## Testing - `go test ./coderd/templatebuilder/` <img width="1048" height="616" alt="Screenshot 2026-07-15 at 12 44 42 PM" src="https://github.com/user-attachments/assets/89e334ca-e904-4387-9264-6ed1614a40ba" /> <details> <summary>Audit of all template builder bases for unfilled variables</summary> | Base | Variable status | Verdict | |------|-----------------|---------| | aws-linux | no HCL `variable` blocks; provider env auth | OK | | aws-windows | same | OK | | azure-linux | same | OK | | **digitalocean-linux** | `project_uuid` (required, no default) + `ssh_key_id`; absent from `base.json` | **Fixed here** | | docker | `docker_socket` has `default = ""`; `container_image` via `{{ .Variables }}` + declared | OK | | gcp-linux | fixed in #27015 | OK | | gcp-windows | fixed in #27015 | OK | | kubernetes | `namespace` (required), `use_kubeconfig`, `container_image` all via `{{ .Variables }}` + declared | OK | | scratch | no variables | OK | DigitalOcean was the only broken base; all others either have safe defaults or already declare their variables. **Mechanism note:** `base.json` `variables[]` drives the first-step prompts and values are injected as HCL literals via `{{ .Variables.<name> }}` (strings quoted, numbers/bools raw; supported types: string, number, bool). Sensitive/computed variables are intentionally skipped everywhere the injection map is built, so they cannot currently be injected. That is why the `sensitive` flags were removed here. </details> --- *This PR was generated by Coder Agents on behalf of @jeremyruppel.* |
||
|
|
3b72a3e5dd |
test(coderd/rbac): add many-orgs authorization benchmark (#27270)
<!-- Authored with Coder Agents on behalf of @Emyrk --> Adds `BenchmarkRBACManyOrgs` to measure `Authorize`, `Prepare` (partial evaluation), and `Prepare`+`CompileToSQL` as a subject's org-membership count grows (1, 5, 10, 50, 100 orgs). - Written to evaluate the org set-membership rewrite in #27244, where partial-eval cost scales with org count. - Subject uses pre-expanded cached roles (`WithCachedASTValue`), member + per-org `organization-member` roles, `ScopeAll`; authorizer has no cache so each iteration measures a real evaluation. Results comparing `main` vs #27244 are posted on that PR. <sub>Coder Agents on behalf of @Emyrk.</sub> |
||
|
|
cc11c8a536 |
feat: surface model content-filter refusals as a blocked chat error (#27118)
Blocked turns from a provider's content filter (Anthropic's `refusal` stop reason with empty content) previously ended silently on the "Thinking" spinner. They now end as a terminal `content_filter` error that renders as a "Response blocked" message with the provider's category and explanation. <img width="888" height="335" alt="image" src="https://github.com/user-attachments/assets/cef85a59-4091-4e62-9d45-1eb06748db48" /> Closes CODAGT-611 Follow-ups will involve implementing fallbacks, but this alone is pretty important |
||
|
|
8eaf4f507b |
feat: generate the known-models catalog and aigateway prices (#27146)
- Regenerates `prices.json` from models.dev. The seeder only upserts, so existing deployments keep delisted models. - Generate the frontend known-models catalog instead of hand-writing it. `make gen/aibridge-prices` fetches models.dev once - Moved patches to model definitions to separate `overrides.jq` which handles both `claude-sonnet-4-5` 200k context and 'aliasing' Fable 5 as Mythos 5. - Editorial choices of selection, order, aliases, and reasoning defaults live in `curation.json`. - Adds golden join tests with one error case per validation, a no-network drift test comparing curation to the checked-in artifact, and pinned invariants for the Anthropic thinking-mode split (the wrong side returns HTTP 400) and the sonnet-4-5 context pin. Adding a model is now one `curation.json` entry plus `make gen/aibridge-prices`, assuming it is present on models.dev. > This PR was authored by Coder Agents on Cian's behalf. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
0207a9824f |
fix: enforce max body size on CSP violation report endpoint (#27243)
The `/api/v2/csp/reports` endpoint is unauthenticated and CSRF-exempt, since it's the browser's `report-uri` target, and decoded request bodies with no size limit. This let an attacker post arbitrarily large JSON bodies to force unbounded heap allocation and OOM the server (Cure53 CDM-02-007). Wraps the request body in `http.MaxBytesReader` before decoding and returns 413 when the limit is exceeded, matching the existing convention used by `files.go`, `aitasks.go`, and `exp_chats.go`. Fixes: https://github.com/coder/security-disclosures/issues/171 |
||
|
|
d99ed85db8 | fix: fix test flake in nats subscribe cleanup (#27238) | ||
|
|
61e52532c0 |
docs: wrap placeholder syntax in backticks in CLI help and swagger annotations (#27194)
## Problem
Generated reference docs (`docs/reference/cli/*`,
`docs/reference/api/*`) contained raw placeholder and JSON syntax that
came straight from Go CLI help strings and swagger annotations. HTML
renderers treat the angle-bracket tokens (`<team-slug>`, `<uuid>`,
`<KEY>`, etc.) as unknown tags and drop them, so readers see
broken/half-missing text today. The same strings also break MDX parsing.
## Fix
Wrap the placeholder/JSON syntax in backticks **at the source** (Go help
strings and swagger annotation comments), then `make gen`. Rendered docs
now show the placeholders as inline code instead of dropping them.
### Source changes
| File | Placeholder wrapped | Surfaces in |
|------|--------------------|-------------|
| `codersdk/deployment.go` | `` `<organization-name>/<team-slug>` `` |
`cli/server.md`, `coder --help`, settings UI |
| `codersdk/deployment.go` | `` `CODER_AI_GATEWAY_PROVIDER_<N>_*` ``, ``
`CODER_AI_GATEWAY_PROVIDER_<N>_<KEY>` `` | `api/schemas.md` |
| `cli/tokens.go` | `` `<type>:<uuid>` `` | `cli/tokens_create.md`,
`coder --help` |
| `coderd/aitasks.go` | `` `owner:<…>` ``, `` `organization:<…>` ``, ``
`status:<status>` `` | `api/tasks.md` |
| `coderd/exp_chats.go` | `` `pr_status:<…>` `` and sibling filter
tokens | `api/chats.md` |
| `coderd/provisionerdaemons.go`, `coderd/provisionerjobs.go` | ``
`{'tag1':'value1','tag2':'value2'}` `` | `api/organizations.md`,
`api/provisioning.md` |
Everything else in the diff (`coderd/apidoc/*`, `docs/reference/**`,
`*.golden`, `site/src/api/typesGenerated.ts`) is `make gen` output.
## Reviewer notes (the "considered pass" from the ticket)
- **Product-visible:** this changes `coder server --help` and `coder
tokens create --help` output, and the `server-config.yaml` reference
comment. Backticks in terminal help are literal but read fine as
placeholder markers.
- **Settings UI:** the `deployment.go` `Description` also renders in the
deployment settings page. If that field is not Markdown-rendered,
literal backticks will show there. Happy to drop the `deployment.go`
change if you'd rather keep the UI text clean and fix `server.md`
another way.
- **Out of scope here:** `docs/reference/cli/agent-firewall.md`
(`<host>`/`<glob>`) is generated from the external
`github.com/coder/boundary` module, not this repo. It needs an upstream
fix + module bump; not included in this PR.
<details>
<summary>Implementation notes / decision log</summary>
- Scope taken from DOCS-551: source-level backtick pass for generated
reference docs only. Hand-written Markdown fixes are tracked separately
(companion ticket).
- Swagger `@Param` descriptions are Go comments, so the existing `\|`
pipe-escaping in the chats `q` filter is preserved inside the new
backticks (still required for the Markdown table cell to render `|`).
- Verified after `make gen`: generated docs render placeholders as code
spans, table pipes intact; `gofmt` clean; changed Go packages build; no
emdash/endash introduced.
- Deliberately left the `AIProviderConfig` type-level doc comment
untouched because it does not surface in any generated doc (kept the
diff to doc-feeding comments).
</details>
Linear: DOCS-551
---
_Opened by Coder Agents on behalf of @nickvigilante._
---
## Evidence: placeholders dropped on the live docs site
Verified **2026-07-14** against the live site (`coder.com/docs`, i.e.
`main`, pre-merge) by loading each affected page in headless Chrome and
reading the post-hydration DOM (confirmed identical in the raw page
payload). Each simple `<token>` placeholder is parsed as an **empty
custom HTML element**, so the browser renders nothing for it and the
placeholder text disappears from the page.
### What readers see today (before this PR)
| Page (live) | Source Markdown | Rendered on the live site |
|-------------|-----------------|---------------------------|
| [`cli/server`](https://coder.com/docs/reference/cli/server) — OAuth2
GitHub Allowed Teams | `Structured as: <organization-name>/<team-slug>.`
| `Structured as: /.` |
|
[`cli/tokens_create`](https://coder.com/docs/reference/cli/tokens_create)
— `--allow` | `Repeatable allow-list entry (<type>:<uuid>, e.g.
workspace:1234-...).` | `Repeatable allow-list entry (:, e.g.
workspace:1234-...).` |
| [`api/tasks`](https://coder.com/docs/reference/api/tasks) — `q` | `...
status:<status>` | `... status:` (nothing after the colon) |
| [`api/schemas`](https://coder.com/docs/reference/api/schemas) —
AIBridgeConfig (`anthropic`/`bedrock`/`openai`) |
`CODER_AI_GATEWAY_PROVIDER_<N>_*` | `CODER_AI_GATEWAY_PROVIDER__*` |
| [`api/schemas`](https://coder.com/docs/reference/api/schemas) —
AIBridgeConfig (`providers`) | `CODER_AI_GATEWAY_PROVIDER_<N>_<KEY>` |
`CODER_AI_GATEWAY_PROVIDER__` |
[`api/chats`](https://coder.com/docs/reference/api/chats) (`q`) drops
five tokens the same way — `title:<substring>`, `diff_url:<url>`,
`pr:<number>`, `pr_title:<text>`, and the trailing `title:<value>`. The
live parameter description reads (note the dangling `title:`,
`diff_url:`, `pr:`, `pr_title:`):
```text
Search query. Supports title: (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status:<draft|open|merged|closed> as repeated or comma-separated values, source:<created_by_me|shared_with_me>, diff_url: (quote values containing colons), pr: (exact PR number match), repo:<owner/repo> (case-insensitive substring match against git remote origin or URL), pr_title: (case-insensitive PR title substring). Bare terms are not supported; use title: for title filtering.
```
<details>
<summary>Raw rendered DOM from the live site (headless Chrome,
post-hydration)</summary>
```html
<!-- reference/cli/server -->
Structured as: <organization-name>/<team-slug>.</team-slug></organization-name>
<!-- reference/cli/tokens_create -->
Repeatable allow-list entry (<type>:<uuid>, e.g. workspace:1234-...).</uuid></type>
<!-- reference/api/tasks : only status:<status> drops; the /-containing tokens are escaped and survive -->
Search query for filtering tasks. Supports: owner:<username/uuid/me>, organization:<org-name/uuid>, status:<status></status>
<!-- reference/api/schemas : anthropic / bedrock / openai rows -->
Deprecated: Use Providers with indexed CODER_AI_GATEWAY_PROVIDER_<n>_* env vars instead.</n>
<!-- reference/api/schemas : providers row -->
Providers holds provider instances populated from CODER_AI_GATEWAY_PROVIDER_<n>_<key> env vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.</key></n>
```
The parser auto-inserts closing tags
(`</team-slug></organization-name>`) and lowercases the tag name (`<N>`
becomes `<n>`), leaving `__` where `<N>_` used to be. Every wrapped
placeholder renders correctly as inline code on the [docs preview for
this
branch](https://coder.com/docs/@vigilante%2Fdocs-551-backtick-placeholder-syntax-in-generated-reference-docs-cli/reference/cli/server).
</details>
### Accuracy note — cases that do *not* drop on live
These render fine today, so they are **not** evidence of dropping (the
PR still wraps them for consistency / MDX-safety):
-
[`api/organizations`](https://coder.com/docs/reference/api/organizations)
and
[`api/provisioning`](https://coder.com/docs/reference/api/provisioning):
`{'tag1':'value1','tag2':'value2'}` renders verbatim — curly braces are
not an HTML tag.
- Tokens containing `/` or `|` are escaped by the renderer and stay
visible (as literal `<...>`): `<username/uuid/me>`, `<org-name/uuid>`,
`<owner/repo>`, `<draft|open|merged|closed>`,
`<created_by_me|shared_with_me>`. Backticks still improve their
readability, but they were never dropped.
|
||
|
|
3126306598 |
feat: add --aigateway-proxy-target flag (#27122)
Adds `--aigateway-proxy-target` option to `deploymentGroupAIGatewayProxy` that defines URL to which intercepted requests should be forwarded to. Forward URL used to be hardcoded to `coderAPI.AccessURL` pointing to embedded Gateway. With addition of standalone AI Gateway this needs to be configurable. Renamed `aibridgeproxyd.Server.coderAccessURL` and `coderAccessPort` -> `gatewayURL` and `gatewayPort` + option to better reflect reality. |
||
|
|
a567f6a89f | feat: allow admins to override the chat compaction model (#27151) | ||
|
|
0f55c283f1 | fix: use backend-selected chat agent for desktop, git, terminal (#26959) | ||
|
|
55b06f14a3 | feat: allow overriding advisor reasoning effort (#27196) | ||
|
|
d0e67a74d5 |
chore: report Coder Agents experiments in telemetry (#27042)
Closes CODAGT-352
This adds the Coder Agents experiments (virtual desktop with computer
use, and the advisor) to telemetry, so they finally show up in each
deployment snapshot. Everything stays inside `coderd/telemetry/`.
## Shape received by the telemetry server
The experiments are reported as a single `agents_experiments` field on
the deployment record, alongside the other config-derived deployment
fields. Its value is one JSON blob with one top-level key per
experiment:
```json
{
"virtual_desktop": {
"enabled": false,
"computer_use": {"provider": "anthropic", "provider_source": "default"}
},
"advisor": {"enabled": true, "max_uses_per_run": 5, "max_output_tokens": 4096, "provider": "openai", "model": "gpt-5.2"}
}
```
When the advisor falls back to the chat model, either because no
override is set or because the configured override is inactive (its
config or provider was deleted or disabled), the provider and model
carry a sentinel instead:
```json
"advisor": {"enabled": true, "max_uses_per_run": 5, "max_output_tokens": 4096, "provider": "advisor_reuse_chat_model", "model": "advisor_reuse_chat_model"}
```
- `virtual_desktop.enabled` and `advisor.enabled` track the
`chat-virtual-desktop` and `chat-advisor` deployment experiments, not
the stored config. We ignore the stored advisor `enabled` flag on
purpose: since #26809 the runtime gates on the experiment, and the
stored flag ends up permanently true for any deployment that ever opened
the settings form.
- Computer use sits under `virtual_desktop` rather than as its own
top-level key because it isn't a separate experiment; the same
`chat-virtual-desktop` flag gates both the desktop and the computer-use
provider. `provider_source` says whether an admin picked the provider
(`configured`) or we fell back to the default (`default`).
- `advisor.provider` is the `ai_providers` type (e.g. `openai`,
`anthropic`, `azure`) and `advisor.model` is the configured model
string. Two sentinels stand in when there's no concrete value:
`advisor_reuse_chat_model` when the advisor has no active override and
falls back to the chat model (matching the runtime), and `unknown` when
we genuinely couldn't tell, e.g. a query failed or the stored config
wouldn't parse.
- `advisor.max_uses_per_run` and `advisor.max_output_tokens` are clamped
to 0 before reporting, matching how the API normalizes these values on
read.
## Why this shape
Putting the data on the deployment record keeps it next to the other
config-derived fields, and leaves `telemetry_items` as a faithful mirror
of the `telemetry_items` table rather than a place we inject synthetic
rows. Adding or removing an experiment is a one-line edit to the
`agentsExperiments` registry. The `agents_experiments` field itself
never changes; only the JSON inside it does. The field is `omitempty`,
so older Coder versions that don't emit it are distinguishable from a
real absence, and when an experiment isn't reported in a snapshot its
JSON path is simply missing, so queries can tell "not reported" apart
from a real `false`.
One key holding one JSON blob is also easier to query than many separate
fields. Because everything lives in one blob, a question like "of the
deployments running the desktop, how many changed the computer-use
provider?" is one query with no join:
```sql
SELECT
JSON_VALUE(agents_experiments, '$.virtual_desktop.computer_use.provider_source') AS src,
COUNT(*) AS deployments
FROM deployments
WHERE JSON_VALUE(agents_experiments, '$.virtual_desktop.enabled') = 'true'
GROUP BY src
```
|
||
|
|
535c775f2a |
fix(coderd): serialize chat model config default election with advisory lock (#27114)
Closes CODAGT-736 Concurrent chat model config writes on a deployment with no default all elect themselves default: at READ COMMITTED neither transaction sees the other's uncommitted default, so both self-promote and `idx_chat_model_configs_single_default` rejects the loser as a spurious 409. The coderd Terraform provider hits this routinely, since a single `terraform apply` creates or deletes many configs in parallel by design. The fix serializes the election with a transaction-scoped advisory lock: the create, update, and delete handlers run their default election inside a transaction that first takes `pg_advisory_xact_lock` on a dedicated `LockIDChatModelConfigDefault`, so elections run one at a time and the index is never contended. The partial unique index stays in place as the schema-level invariant, and the existing 409 mapping remains as a backstop for any writer that bypasses the lock. We considered a singleton pointer table (one row holding a `model_config_id` FK, making a second default unrepresentable), which would remove the race outright, but it needs a migration, new queries, dbauthz rules, and handler/read-path rework. Not proportionate for an experimental endpoint. |
||
|
|
0c3c65d85b |
fix: stabilize latest workspace app status ordering (DEVEX-381) (#27041)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell. Closes [DEVEX-381](https://linear.app/codercom/issue/DEVEX-381/flake-test-tasksendwaitsforworkingappstate). Follow-up to #25648 and #25858, which addressed a different symptom of the same test. ## Symptom ``` task_send_test.go:348: context expired while waiting for trap: context deadline exceeded --- FAIL: Test_TaskSend/WaitsForWorkingAppState (26.02s) ``` Windows-only, on `test-go-pg (windows-2022)`. Reported four times since #25648 landed (2026-06-02, 2026-06-10, 2026-07-01). ## Root cause The test: 1. `setupCLITaskTest` inserts `workspace_app_status(state=idle)` at the end of setup. 2. `WaitsForWorkingAppState` then inserts `workspace_app_status(state=working)` before starting the CLI. 3. Both are persisted via `dbtime.Now()`, which rounds to microseconds. Windows `time.Now()` resolution is coarser than that (often ~1 ms or worse), so back-to-back calls frequently round to the same microsecond. 4. `GetLatestWorkspaceAppStatusesByWorkspaceIDs` has no tiebreaker: ```sql ORDER BY workspace_id, created_at DESC ``` Its sibling `GetLatestWorkspaceAppStatusByAppID` already uses `ORDER BY created_at DESC, id DESC` for exactly this reason. When the two rows collide, Postgres picks either. 5. On the failing runs, the query returned the `idle` row. `waitForTaskIdle` saw idle on the first poll, returned nil, `TaskSend` proceeded, and the CLI completed successfully in ~5 s. 6. But the test was blocked at `resetTrap.MustWait(ctx)` waiting for a **second** `ticker.Reset` that never happened. `WaitLong = 25s` elapsed, line 348 failed. CI log confirms the sequence: only one `Ticker.Reset(5s)` is caught, then `Ticker.Stop([]) call, matched 0 traps` (from `defer ticker.Stop()`), then the trap wait times out. This is the same class of flake Spike documented in #15923 and #21332 ("Windows in particular doesn't have high-resolution timers"), just hidden behind a SQL `ORDER BY`. ## Fix Two changes: 1. **`coderd/database/queries/workspaceapps.sql`**: add an `id DESC` tiebreaker to `GetLatestWorkspaceAppStatusesByWorkspaceIDs`, matching `GetLatestWorkspaceAppStatusByAppID`. Makes the query deterministic when `created_at` collides. 2. **`cli/task_test.go` / `cli/task_send_test.go`**: add a `withoutInitialAppStatus()` option to `setupCLITaskTest` and use it from `WaitsForWorkingAppState`. The test now inserts a single `working` row, so the collision cannot happen in the first place. Belt-and-braces with change 1. Comments in both places reference DEVEX-381 and #21332 so the next agent doesn't have to re-derive this. ## Verification - `go test ./cli -run 'Test_TaskSend' -count=1`: all 12 subtests pass, `WaitsForWorkingAppState` completes in ~5.6 s (was ~16 s previously due to a longer poll loop). - Stress: 20 sequential runs of `WaitsForWorkingAppState` on Linux, race-enabled binary, all pass in ~5.5 s each. - `go test ./coderd -run 'AppStatus|Task' -count=1` passes. - `go vet ./coderd/database/... ./cli/...` clean. - `make lint/emdash` clean. - `gofmt` clean. Not reproducible on Linux (real time between the two patches is orders of magnitude larger than microsecond); the Windows path is fixed by making the ordering deterministic and by not creating the collision in the first place. <details> <summary>Implementation plan & decision log</summary> ### Investigation 1. Pulled the failing job log for run `28483879823/job/84428355669`. 2. Traced the mock-clock trap sequence: one `NewTicker` and exactly one `Ticker.Reset(5s)` were caught, then `Ticker.Stop([]) call, matched 0 traps` fires (the `defer ticker.Stop()` on `waitForTaskIdle` return). This proves `waitForTaskIdle` returned after a single poll, not that the trap machinery hung. 3. The command exited with `<nil>` (`clitest.go:299: command "coder task send" exited with error: <nil>`) and a `POST /send` completed in 5.4 s. So the CLI succeeded; the test's own trap wait is what timed out. 4. The only `waitForTaskIdle` return-nil paths are `Active + CurrentState.State in {Idle, Complete, Failed}` and `Active + CurrentState == nil past 30s grace`. First observation of nil cannot be past 30s. So `TaskByID` must have returned `State == Idle`. 5. Traced `TaskByID` → `taskGet` → `workspaceData` → `GetLatestWorkspaceAppStatusesByWorkspaceIDs`. Found the missing tiebreaker; the sibling query one line above (`GetLatestWorkspaceAppStatusByAppID`) already had it. 6. Confirmed the two `PATCH /app-status` calls in the Windows log happened at `00:26:13.077` and `00:26:13.093`, well within Windows timer resolution. 7. Confirmed `dbtime.Now()` rounds to microseconds; Windows `time.Now()` doesn't have that precision, so `Round(time.Microsecond)` on two calls close together frequently produces equal values. ### Prior art from Spike - #15923: loosened `HeartbeatPeriod * 9/10` to `3/4` for Windows. - #21332: switched `assert.After` to `assert.NotBefore` because timestamps can equal on Windows. Both explicitly cite "Windows doesn't always have high-resolution timers available." ### Considered alternatives - **Only fix the test.** Works today but leaves the SQL query non-deterministic; another test that relies on `GetLatestWorkspaceAppStatusesByWorkspaceIDs` could hit the same collision. - **Only fix the SQL query.** Would give a stable answer but not necessarily the *right* one. If both patches share a `created_at`, `id DESC` picks whichever UUID sorted higher, still random with respect to insertion order. - **Make `dbtime.Now()` monotonic per process.** Cleanest at the source, but affects every timestamp in the database and has broader implications than a targeted flake fix. Going with both the query fix (defense in depth, matches existing pattern) and the test fix (eliminates the collision at the source) is the smallest change that closes the flake and hardens the query. ### Rejected commit-message scopes Changes touch both `cli/` and `coderd/database/`, so per AGENTS.md the scope is omitted for the cross-cutting commit and PR title. </details> |
||
|
|
63ec93a7ce |
feat: add AWS Bedrock mantle endpoint to AI Gateway (#26745)
Implements https://linear.app/codercom/issue/AIGOV-213/add-bedrock-provider # AWS Bedrock mantle support in AI Gateway ## Summary Add support for the AWS Bedrock **mantle** endpoint (`bedrock-mantle.{region}.api.aws/anthropic/v1/messages`) to AI Gateway. Mantle serves Claude through the native Anthropic Messages API. We model it as a `protocol` field on the existing Bedrock provider settings (`invoke-model` default, or `mantle`) rather than as a new provider type, and we treat mantle as a pure passthrough: SigV4-sign and forward, no body translation. ## Background Claude on AWS Bedrock is reachable through two endpoints, each speaking exactly one wire protocol: 1. **InvokeModel** (existing): `bedrock-runtime.{region}.amazonaws.com`. Model id in the URL path, request translated into Bedrock's InvokeModel format, responses returned as a binary AWS eventstream. This is what AI Gateway already supported for Bedrock. 2. **Mantle** (this doc): `bedrock-mantle.{region}.api.aws/anthropic/v1/messages`. Native Anthropic Messages API: model in the body, plain SSE streaming. ## Why a `protocol` field, not a new provider type The alternative is to model mantle as its own `ai_provider_type` (`bedrock-mantle`) alongside `bedrock`. I chose the `protocol` field instead for two reasons: 1. Mantle reads more like a protocol of Bedrock than a separate provider. It is the same AWS account, credentials, region, and IAM, reached over a different wire protocol and host. One Bedrock provider with two protocols (`invoke-model` default and `mantle`) models that more organically than two provider types. 2. It avoids a database migration. The `protocol` field lives in the settings JSON blob (empty resolves to `invoke-model`, so existing providers are unaffected), whereas a new type means an enum value and the `ALTER TYPE ... ADD VALUE` migration that goes with it. ## Why passthrough, not translation The client already emits Bedrock-legal requests in mantle mode: ```sh export CLAUDE_CODE_USE_MANTLE=1 export CLAUDE_CODE_SKIP_MANTLE_AUTH=1 export ANTHROPIC_BEDROCK_MANTLE_BASE_URL=https://<coder>/api/v2/aibridge/<provider-name> ``` So the gateway just forwards the body and SigV4-signs it (service `bedrock-mantle`), and skips all the InvokeModel body-translation (model remap, thinking conversion, beta-flag allowlist, field stripping). This keeps the mantle path thin and avoids a second copy of translation logic to maintain. ## Consequences - Protocol-dependent fields: `model` / `small_fast_model` are used by InvokeModel but ignored by mantle (the client sends the model), and `base_url` is required for mantle but optional for InvokeModel. Validation is protocol-aware. - No central model control on mantle: because it is a passthrough, the operator cannot pin the model. - `region` and the `base_url` host must name the same region (the SigV4 scope must match the endpoint); a mismatch surfaces as `Credential should be scoped to a valid region`. ## Draft UI <img width="1100" height="579" alt="image" src="https://github.com/user-attachments/assets/37bab46d-8958-4a96-9f47-1fef3493e1b6" /> ## Follow-up PRs: - https://github.com/coder/coder/pull/27156 |