mirror of
https://github.com/coder/coder.git
synced 2026-10-31 02:43:21 +08:00
5e0710de922475df6196a4320be2161b698fde2a
15921
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5e0710de92 |
chore: refresh AI model price book (#28365)
## Price book changes 61 models added, 14 models removed, 51 models changed. <details> <summary>Added</summary> - azure/claude-mythos-5 - azure/claude-opus-4-7 - azure/kimi-k2.7-code - bedrock/global.openai.gpt-5.6-luna - bedrock/global.openai.gpt-5.6-sol - bedrock/global.openai.gpt-5.6-terra - bedrock/xai.grok-4.6 - copilot/gemini-3.7-flash - copilot/grok-4.6 - copilot/kimi-k3 - copilot/mai-code-1.1-flash - google/gemini-3.7-flash - openrouter/bytedance-seed/seed-2-1-turbo - openrouter/bytedance-seed/seed-2.0-code - openrouter/deepseek/deepseek-v4-pro-0813 - openrouter/dots-studio/dots-3-note-preview:free - openrouter/google/gemini-3.7-flash - openrouter/inclusionai/ling-3.0-flash - openrouter/liquid/lfm-2.5-2.6b:free - openrouter/meta/muse-glimmer-30b - openrouter/meta/muse-spark-1.2 - openrouter/nvidia/nemotron-3.5-lightning - openrouter/nvidia/nemotron-3.5-lightning:free - openrouter/qwen/qwen3.8-2.4t-a95b - openrouter/qwen/qwen3.8-27b - openrouter/sakana/sakana-namazu - openrouter/upstage/solar-pro4 - openrouter/x-ai/grok-4.6 - openrouter/z-ai/glm-5.2:free - openrouter/z-ai/glm-5.3 - openrouter/~z-ai/glm-latest - vercel/alibaba/qwen3.8-2.4t-a95b - vercel/alibaba/qwen3.8-27b - vercel/deepseek/deepseek-v4-pro-0813 - vercel/google/gemini-3.7-flash - vercel/inclusionai/ling-3.0-flash - vercel/meta/muse-glimmer-30b - vercel/meta/muse-spark-1.2 - vercel/meta/muse-spark-1.2-contributor - vercel/nvidia/nemotron-3.5-lightning - vercel/openai/gpt-4.1-fast - vercel/openai/gpt-4.1-mini-fast - vercel/openai/gpt-4.1-nano-fast - vercel/openai/gpt-4o-fast - vercel/openai/gpt-4o-mini-fast - vercel/openai/gpt-5-fast - vercel/openai/gpt-5-mini-fast - vercel/openai/gpt-5.1-thinking-fast - vercel/openai/gpt-5.2-fast - vercel/openai/gpt-5.3-codex-fast - vercel/openai/gpt-5.4-fast - vercel/openai/gpt-5.4-mini-fast - vercel/openai/gpt-5.5-fast - vercel/openai/gpt-5.6-luna-fast - vercel/openai/gpt-5.6-sol-fast - vercel/openai/gpt-5.6-terra-fast - vercel/openai/o3-fast - vercel/openai/o4-mini-fast - vercel/sakana/namazu - vercel/xai/grok-4.6 - vercel/zai/glm-5.3 </details> <details> <summary>Removed</summary> - anthropic/claude-opus-4-1 - anthropic/claude-opus-4-1-20250805 - google/gemini-2.0-flash - google/gemini-2.0-flash-lite - google/gemini-3-pro-preview - openrouter/ai21/jamba-large-1.7 - openrouter/inclusionai/ling-3.0-flash:free - openrouter/mancer/weaver - openrouter/openai/gpt-5.3-chat - vercel/anthropic/claude-opus-4.1 - vercel/inclusionai/ling-3.0-flash-free - vercel/openai/gpt-5.1-instant - vercel/openai/gpt-5.3-chat - vercel/zai/glm-4.6v </details> <details> <summary>Changed</summary> - azure/gpt-5.6-luna - azure/gpt-5.6-sol - azure/gpt-5.6-terra - bedrock/openai.gpt-5.6-sol - openrouter/deepseek/deepseek-chat-v3-0324 - openrouter/deepseek/deepseek-v3.1-terminus - openrouter/deepseek/deepseek-v4-flash - openrouter/deepseek/deepseek-v4-flash-0731 - openrouter/deepseek/deepseek-v4-pro - openrouter/google/gemini-3.6-flash - openrouter/google/gemma-4-31b-it - openrouter/gryphe/mythomax-l2-13b - openrouter/minimax/minimax-m2.5 - openrouter/minimax/minimax-m2.7 - openrouter/mistralai/mistral-small-3.2-24b-instruct - openrouter/moonshotai/kimi-k2.5 - openrouter/moonshotai/kimi-k2.6 - openrouter/moonshotai/kimi-k2.7-code - openrouter/openai/gpt-5.6-luna - openrouter/openai/gpt-5.6-luna-pro - openrouter/openai/gpt-5.6-sol - openrouter/openai/gpt-5.6-sol-pro - openrouter/openai/gpt-5.6-terra - openrouter/openai/gpt-5.6-terra-pro - openrouter/openai/gpt-oss-120b - openrouter/qwen/qwen-plus-2025-07-28:thinking - openrouter/qwen/qwen2.5-vl-72b-instruct - openrouter/qwen/qwen3-14b - openrouter/qwen/qwen3-235b-a22b-2507 - openrouter/qwen/qwen3-30b-a3b - openrouter/qwen/qwen3-coder-30b-a3b-instruct - openrouter/qwen/qwen3-vl-235b-a22b-thinking - openrouter/qwen/qwen3-vl-30b-a3b-instruct - openrouter/qwen/qwen3.5-35b-a3b - openrouter/qwen/qwen3.6-27b - openrouter/qwen/qwen3.6-35b-a3b - openrouter/tencent/hy3-preview - openrouter/thinkingmachines/inkling - openrouter/thinkingmachines/inkling-small - openrouter/z-ai/glm-5 - openrouter/z-ai/glm-5.2 - openrouter/~deepseek/deepseek-v4-flash-latest - openrouter/~google/gemini-flash-latest - openrouter/~moonshotai/kimi-latest - openrouter/~openai/gpt-latest - openrouter/~x-ai/grok-latest - vercel/deepseek/deepseek-v4-flash - vercel/deepseek/deepseek-v4-pro - vercel/google/gemini-3.6-flash - vercel/openai/gpt-5.6-sol - vercel/zai/glm-5.2 </details> ## Review notes Regenerated by `make gen/aibridge-prices` from the live [models.dev](https://models.dev) catalog. Both artifacts come from one snapshot, so they ship together: - `coderd/aibridge/prices/data/prices.json` - `site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/knownModelsGenerated.json` These are customer-visible cost numbers taken from upstream data, so this PR is never merged automatically. The summary above lists what moved; check the diff for exact figures before approving. Opened automatically by the [aigateway-prices-refresh workflow](https://github.com/coder/coder/actions/runs/32352560765). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
f5d42a868e |
refactor(coderd): add related-data selection to workspace queries (#28302)
This is the first of three PRs implementing lite `codersdk.Workspace` responses ([GRU-82](https://linear.app/codercom/issue/GRU-82), [RFC](https://app.notion.com/p/coderhq/Lite-Workspace-API-Requests-3aad579be5928060b5e7cc65c539717f)). A fully populated workspace fans out into a large tree of DB queries (template, latest build, provisioner job + queue position, resources, metadata, agents, apps, statuses, scripts, log sources, template version). Most callers do not need most of that data, and one of these queries, `GetProvisionerJobsByIDsWithQueuePosition`, is consistently the top resource consumer in scale tests. This PR adds the internal plumbing to load only a selected subset of that tree. It introduces a `workspaceRelated` selection type and threads it through `workspaceData` and `workspaceBuildsData` down to the individual query call sites, skipping any query whose data was not selected. The selection is a pointer tree that mirrors the parent/child relationships between the objects: branch nodes are pointers (non-nil when selected) and leaf nodes are bools. Modeling it this way makes selecting a child without its parent unrepresentable, which matches how loading works (a parent must be queried to learn its children's identifiers), so there is no normalization step to forget. No behavior change: every existing caller passes a fully populated selection (`allWorkspaceRelated` / `allLatestBuildRelated`), so responses are identical. Wiring the HTTP `include_related` query parameter (Phase 2) and narrowing individual callers (Phase 3) come in follow-up PRs. Two notes on the current mapping: - `queue_position` is modeled as its own node, but only one job query exists and it always computes the ranking, so selecting `job` runs the full query today. Splitting off a cheaper queue-position-free job query is deferred (it needs a new SQL query + `make gen`). - The workspace-level `LatestAppStatus` is gated on the `latest_build.resources.agents.apps.statuses` node. It is queried directly by workspace ID, but a caller selecting it also pulls in the build chain as ancestors. Happy to decouple this if preferred. <details> <summary>Implementation plan</summary> # Lite Workspace API — Phase 1 Plan Add an internal configuration structure to the Coderd method that fetches a workspace with its related data (`workspaceData` /`workspaceBuildsData`), thread it down to the SQL query call sites, and skip ("squash") queries that are not requested. Server-internal only. No HTTP query-string parsing (Phase 2) and no `wsrelated` struct tags or field nil-ing (Phase 3). ## Scope - In: config struct mapping the RFC related-data hierarchy; threading through `workspaceData` and `workspaceBuildsData`; gating every related-data query; comprehensive unit tests with a mocked `database.Store` (gomock/dbmock). - Out: HTTP `include_related` parsing, codersdk changes, `wsrelated` tags, docs autogen, and changing `convertWorkspaces` skip-on-missing-data behavior. All existing callers pass an "include all" config, so Phase 1 is a behavior- preserving refactor. ## Design New file `coderd/wsrelateddata.go` with a pointer tree that mirrors the RFC hierarchy. Branch nodes are pointers (present when non-nil); leaf nodes are bools. Because a child cannot be expressed without its parent, the RFC "includes all ancestors" rule is a structural invariant of the type: an illegal selection is unrepresentable, so no normalization pass exists or is needed. ```go type workspaceRelated struct { Template bool LatestBuild *latestBuildRelated } type latestBuildRelated struct { Job *jobRelated Resources *resourcesRelated TemplateVersion bool } type jobRelated struct{ QueuePosition bool } type resourcesRelated struct { Metadata bool Agents *agentsRelated } type agentsRelated struct { Apps *appsRelated Scripts bool LogSources bool } type appsRelated struct{ Statuses bool } ``` Helpers: - `allWorkspaceRelated()` / `allLatestBuildRelated()` construct a fully populated tree. Passed by all current callers so behavior is unchanged. - Zero value (nil subtree) includes nothing but the workspace itself. - `(*latestBuildRelated).appStatuses()` is a nil-safe accessor for the deep `apps.statuses` leaf so call sites descend the tree without a guard chain. Rationale for the pointer tree over a flat struct plus `withAncestors()`: the invariant holds by construction (illegal state is unrepresentable) instead of relying on a normalization step a caller must remember. `workspaceBuildsData` takes the `*latestBuildRelated` subtree directly. ### Query gating `workspaceData(ctx, workspaces, cfg)`: - `GetTemplatesWithFilter` gated on `Template`. - `GetLatestWorkspaceBuildsByWorkspaceIDs` gated on `LatestBuild`; when off, skip `workspaceBuildsData` entirely (empty builds). - `GetLatestWorkspaceAppStatusesByWorkspaceIDs` (workspace-level LatestAppStatus) gated on `AppStatuses`. `workspaceBuildsData(ctx, builds, cfg)` gates each stage: - jobs (`GetProvisionerJobsByIDsWithQueuePosition`) + eligible provisioner daemons gated on `Job`. Only one job query exists and it always computes the queue position, so `QueuePosition` does not gate a separate query yet; it is modeled for hierarchy/Phase 2 completeness. A cheaper job query without the window-function ranking is a worthwhile follow-up but is out of scope for a simple Phase 1 (needs new SQL + sqlc gen). - resources gated on `Resources`; metadata on `Metadata`; agents on `Agents`; apps on `Apps`; app statuses on `AppStatuses`; scripts on `Scripts`; log sources on `LogSources`; template versions on `TemplateVersion`. Downstream `convertWorkspaceBuilds` already tolerates empty slices, so squashing queries yields zero/nil fields without extra work. ## Testing (comprehensive, mocked database.Store) Internal tests (`coderd/wsrelateddata_internal_test.go`) build `&API{Options: &Options{Database: dbmock.NewMockStore(ctrl)}}` and call `workspaceData` directly. gomock is strict, so setting EXPECT only on permitted queries proves the rest are squashed. Cases: include-all (every query runs); zero value (no related queries run); each single node (template only, latest_build only, job, resources, metadata, agents, apps, statuses, scripts, log_sources, template_version, workspace-level app statuses); the empty-resources short-circuit; and the constructors plus the nil-safe `appStatuses()` accessor at every depth. ## Steps 1. Add `coderd/wsrelateddata.go` (selection tree, constructors, `appStatuses`). 2. Thread the selection through `workspaceData` and `workspaceBuildsData`; gate queries by descending the tree. 3. Update the callers to pass `allWorkspaceRelated()` / `allLatestBuildRelated()`. 4. Add unit tests; run `make fmt`, `make lint`, targeted `go test`. ## Resolved decisions 1. Pointer tree over a flat struct + normalizer, so the ancestor rule is a structural invariant. 2. Cheaper queue-position-free job query deferred to a follow-up. </details> --- *Generated by Coder Agents on behalf of @spikecurtis.* |
||
|
|
2529a54cfc |
chore(docs): update release docs for v2.36.1 (#28357)
Automated docs update for v2.36.1 release. Created by `releasetui`. |
||
|
|
e5deb57079 |
feat(site): show total agent hours consumption on the licenses page (#27953)
<img width="1195" height="165" alt="Screenshot 2026-08-19 at 5 31 17 PM" src="https://github.com/user-attachments/assets/d1e49e5f-1af6-4221-87fe-ae40997f0ae7" /> Adds a "Total agent hours" consumption panel to the Licenses page, surfacing the `agent_runtime_hours` entitlement introduced by the #27983 / #27984 / #27985 stack. The new `TotalAgentHoursCard` renders full width directly above the Agent Workspace Builds panel. It shows a usage bar with Used/Limit labels and an info tooltip next to the title. The bar color and tooltip copy track the same thresholds the backend uses for license warnings: green below the soft limit, orange once usage reaches the soft limit, and red once usage reaches the purchased allocation (values at or beyond the threshold count as reached, matching `appendAgentRuntimeHoursWarning`). When a soft limit exists, the tooltip states the actual soft-limit percentage computed from `soft_limit / limit`. The panel is hidden entirely when the feature is disabled, which is how the backend reports a zero-hour allocation. The usage period dates from `Feature.usage_period` render above the bar (start left, end right), matching the Agent Workspace Builds panel, so admins can tell which license period the consumption covers. The backend always attaches a period to an enabled feature; a missing or unparsable one just omits the dates row. The CODAGT-855 link to the per-user usage view is deferred: that view (F3, CODAGT-861) and the API it needs (F2, CODAGT-857) are not built yet, so there is no route to target. The link lands with F3. An enabled feature with `limit` omitted is the unlimited allocation (license claim `-1`, coder/license#49): the card shows a full neutral bar with "Limit: Unlimited" and the tooltip without threshold copy, mirroring `SeatUsageBarCard`'s unlimited state. The invalid-limits error is kept for negative limits, which can only come from a decoding bug. Storybook stories cover the green/orange/red states, the capped over-allocation bar, missing usage data, the missing usage period, the hidden state, the unlimited state, the invalid-limit error state, and each tooltip message via play interactions. The page view story verifies the panel renders above Agent Workspace Builds. Usage renders with exactly one decimal (e.g. `435.8`, `1,000.0`), derived from #27985's `actual_ms` field and floored to tenths with integer math. The same floored value drives the reached states, so the displayed number and the bar color flip at the same instant; a fraction past the allocation now trips the red state (`1,000.1 >= 1,000`), pinned by the `ReachedAllocationByFraction` story. The limit and hard cap labels stay whole because the license claims are whole hours. Stacked on #27985. closes [CODAGT-855](https://linear.app/codercom/issue/CODAGT-855/f1-runtime-hours-consumption-on-the-licenses-page). |
||
|
|
676b9bb06b | fix: only create workspace agents for start builds (#28276) | ||
|
|
64f26ef387 |
chore: add supabase icon (#28327)
Adds the Supabase icon to the built-in icons for use with external auth and modules. - Adds `site/static/icon/supabase.svg` (official Supabase logo) - Registers icon in `site/src/theme/icons.json` The icon is full-color with good contrast on both light and dark backgrounds, so no special handling in `externalImages.ts` is needed. |
||
|
|
5be2fa7974 |
feat(site/src/pages/AgentsPage/components/ChatsSidebar): move PR icon to diff line, center unread dot, persist kebab (#28326)
Three chat-sidebar row updates cherry-picked from #28317 (that PR stays open as the full riff), plus one removal: - **PR icon moves to the diff line.** The linked pull request icon (open / draft / merged / closed) no longer replaces the status icon when the chat is idle. It renders on the second line next to the `+additions -deletions` stats, in both the sidebar tree and the chat search results, regardless of chat status. - **Unread dot alignment.** The unread dot is centered in a box matching the kebab's width, so it sits on the same axis as the actions trigger instead of shifting when the hover swap happens. - **Persistent kebab on the selected chat.** The row actions trigger stays visible on the active chat (no hover required, reachable on touch devices); the timestamp is hidden there. Other rows keep the hover swap. - **Subagent count badge removed.** The `N` + bot icon added to the metadata line in #28234 no longer renders, making room for the PR icon and diff stats. The "Show subagents" menu toggle and expand behavior are unchanged. Status and PR icons carry `role="img"` with accessible labels ("Idle", "Pull request merged", ...). <details> <summary>Implementation notes</summary> - `statusConfig.ts`: `getChatDisplayConfig(chat)` now returns the status icon plus a separate `prIcon` config (icon, className, accessible label) instead of letting the PR icon take precedence over the status. Labels added for all status and PR states. - `ChatTreeNode.tsx` / `ChatSearchResults.tsx`: render `prIcon` on the second line before the line stats. - Kebab persistence reuses the existing `isActiveChat` value: `opacity-100` on the trigger, `hidden` on the timestamp for the active row. - `ChatTreeNode.tsx`: the on-row subagent count badge (`childIDs.length` + `BotIcon`) is removed; the `SubagentsMenuToggle` story only asserts the menu toggle, so it still passes. - Not included from #28317: the subagent status icon variants (`CheckCheckIcon` / `SubagentsLoaderIcon`, the `hasSubagents` param, and the `WithSubagentStatusIcons` story). Validation: - `pnpm run lint` (biome, tsc, knip, circular deps, compiler): pass - Story tests: 96 passed (4 files) including the updated `WithPRStateIcons` label assertions and the new `ActiveChatKebabPersistent` story - Unit suite for the sidebar area: 65 passed - `make pre-commit` (via git hook): pass </details> _Generated by Coder Agents on behalf of @tracyjohnsonux._ |
||
|
|
7ba2e6e22a |
fix: add distributed protection for external auth refresh (#27479)
Before refreshing, the caller has to put a lease on the row to prevent any other replicas from trying to refresh using the same token. Callers unable to get a lease enter a holding pattern waiting for the results. Also use this same lease query to re-read the link. This more or less functions the same as the re-read we used to have after a refresh failure, except it also allows us to avoid making the failed request in the first place. The downside is that we have to refetch the link every time even if the caller just fetched it, but since we have to make a database call to set the lease anyway... |
||
|
|
24b83dc727 |
feat: add organization picker to MCP server settings pages (#28150)
Adds an organization picker to the MCP server settings pages so
multi-org deployments can manage every organization's MCP servers
through the UI, not just the default organization's. Closes CODAGT-714
(scoped to MCP servers only; model configs stay deployment-wide until
CODAGT-709 lands).
## Stack Context
Top of the CODAGT-711/717/712/806 stack: MCP server configs are
org-scoped with RBAC, audit logging, per-config ACLs, and owner-scoped
user tokens. This layer makes the admin UI org-aware, resolving the
"default org only" limitation documented in the bottom PR.
## Summary
- The MCP servers list and add pages resolve the working organization
from an `org` search param (falling back to the default org), and render
an organization picker built on the existing `OrganizationAutocomplete`.
The picker only appears when more than one organization is visible, so
single-org deployments see no change.
- Selecting an organization rewrites the search param, refetches the
list for that org, and threads through to the add page so creation posts
to the selected organization.
- The update page now loads the server by ID (new organization-scoped
`GET /api/experimental/organizations/{organization}/mcp-servers/{id}`
frontend binding) instead of listing the default org and searching
client-side, so servers from any organization can be edited. Post-save
navigation returns to the owning organization's list via the server's
`organization_id`.
- Removes the now-unused `getDefaultOrganizationId` helper.
- Review hardening: organization display labels are disambiguated with
the unique org name when effective display names collide (picker
options, trigger text, and accessible name). Access is no longer
restricted to deployment admins: the pages, sidebar item, and nav entry
accept org-scoped `mcp_server_config` permissions, the picker lists only
organizations the user can view, content and affordances (add, edit,
delete) are gated on the selected organization's permissions, and
deployment admins keep full access everywhere.
## Testing
Storybook interaction stories cover: picker hidden with one org,
switching orgs refetches and swaps list contents, add posting to the
URL-selected org, add posting after an in-form org switch (asserting
typed form state survives the switch), and update loading by ID.
Red-green verified by breaking the search-param resolution and observing
the new stories fail.
> Mux acted on Mike's behalf for this PR.
|
||
|
|
8203b2ebaf | feat!: hide Coder Tasks behind the enable-ai-tasks flag (#28008) | ||
|
|
a3eb1ee930 |
fix: gate MCP user tokens on owner-scoped personal RBAC (#27945)
Re-gates every `mcp_server_user_tokens` dbauthz method from `rbac.ResourceDeploymentConfig` onto owner-scoped personal actions, mirroring the `external_auth_links` prior art. These are per-user credential rows, not deployment config, and the old gate forced every call site to elevate with `dbauthz.AsSystemRestricted`. ## Summary - Reads gate on `ActionReadPersonal` and writes on `ActionUpdatePersonal` against the token owner (`MCPServerUserToken.RBACObject()` derives from `user_id`). The id-keyed refresh and refresh-failure mutations resolve the owner via a `GetMCPServerUserTokenByID` prefetch inside dbauthz; the mutation queries cannot retarget a row's owner, so there is no fetch-authorize race. - All seven token-path `AsSystemRestricted` elevations in `coderd/mcp.go` and all four in chatd are removed along with their `//nolint:gocritic` markers; handlers use the request actor. - The daemon-wide chatd subject keeps only `ActionReadPersonal` on `ResourceUser`. Background token refresh and failure persistence use a new per-user `AsChatdTokenOwner` subject holding only user-level `ActionUpdatePersonal` scoped to the token owner (both mutations resolve the row via the underlying store, so the subject needs no read grant), and chatd never holds site-wide personal-write access. `TestAsChatd` pins the complete `ResourceUser` action set and `TestMCPServerUserTokensAuth` exercises both subjects against the real authorizer. - New strict-authorizer test (`rbac.NewAuthorizer` against real rows): owner passes get/upsert/delete, a stranger fails, and an org admin of another org fails. No user-visible behavior change. Stacked on #27944. Completes the MCP org-separation stack. Closes https://linear.app/codercom/issue/CODAGT-806 UAT: regression pass on a dogfood instance: OAuth2 connect redirect, member list/status with redaction, self-scoped disconnect (cannot touch another user's token), end-to-end chat MCP tool invocation, and lower-stack audit/ACL smoke checks. > Mux (AI agent) authored this PR on Mike's behalf. <!-- mux-attribution: model=claude-fable-5 thinking=high --> |
||
|
|
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 -->
|
||
|
|
299e72ad30 |
feat: audit MCP server config changes (#27943)
Adds enterprise audit logging for MCP server config create, update, and
delete, with strict secret redaction. MCP configs hold credentials
(OAuth2 client secrets, API keys, custom headers), so admin changes to
them need an audit trail.
## Summary
- `enterprise/audit/table.go` gains an `MCPServerConfig` entry
enumerating every column: `oauth2_client_secret`, `api_key_value`, and
`custom_headers` are `ActionSecret` (never appear in diffs); dbcrypt
`*_key_id` bookkeeping, IDs, and timestamps are ignored; the remaining
config fields, including the endpoint URL fields, are tracked so
auditors can see which endpoints a config points at.
- Type registration in `coderd/audit` (diff, request, resource target
with org attribution), `codersdk/audit.go`, and a `resource_type` enum
migration.
- Handlers wire `audit.InitRequest`: create records `New`; update and
delete record `Old` from the param middleware before the
write-authorization check, so a readable-but-not-writable caller
produces an audited 403 while read-denied callers stay concealed as
unaudited 404s.
- Tests: create/update/delete audit entries, write-denied and
delete-denied 403 auditing, cross-org concealment producing zero
entries, and a serializer-level regression test proving none of the
three secret classes can reach a serialized diff.
- Review round: MCP config audit entries link to
`/ai/settings/mcp-servers/{id}`, audit table comments are trimmed per
review, and a fault-injection test pins that a config row surviving a
failed post-discovery credential update still gets its creation audit
entry.
Stacked on #27942 (org-scoped MCP configs). Part of the MCP
org-separation stack.
Closes https://linear.app/codercom/issue/CODAGT-717
UAT: verified on a trial-licensed dogfood instance: audit entries for
the full CRUD lifecycle with correct actor/org/target, redacted secrets
in the update and OAuth2 create diffs, and a full plaintext scan of the
audit dump finding zero secret leaks.
> Mux (AI agent) authored this PR on Mike's behalf.
<!-- mux-attribution: model=claude-fable-5 thinking=high -->
|
||
|
|
29c129e2d0 | fix(site): fix price threshold display and debounce model price lookup (#28298) | ||
|
|
f7a0de7a11 |
fix: harden org-scoped MCP config chat gating, updates, and visibility (#28065)
Hardens the org-scoped MCP server config surface from #27942 with fixes and regression pins that are independent of the core cutover: - Keep chats sendable after a selected MCP server is disabled (persisted selections are exempt from message-time rejection). - Keep cached MCP selections usable after a background refetch error (gate the composer on missing data, not `isSuccess`). - Merge PATCH updates onto the current row so unset fields are not clobbered. - Give auditors the full management view of MCP configs their audit logs reference (site and org auditor roles). - Regression pins: frozen OAuth2 callback path, cross-org concealment for item routes, OAuth callback token binding, and disconnect responses indistinguishable from nonexistent config IDs. - Storybook: semantic textbox queries in MCP loading stories; docs note that the MCP settings page needs deployment access. ## Stack context Part of the MCP org-separation stack (CODAGT-711 org scope -> apidocs -> hardening -> CODAGT-717 audit -> CODAGT-712 ACLs -> CODAGT-806 token RBAC). Split out of #27942 to keep the core cutover reviewable; each change here builds on the org-scoped routes and chat gating introduced below it. > Mux (AI agent) authored this PR on Mike's behalf. <!-- mux-attribution: model=claude-fable-5 thinking=high --> |
||
|
|
dd43574990 |
refactor: remove AI add-on badge, seat column, and add-on wording leftovers (#28004)
Removes all "AI add-on" labeling from the UI, plus two leftover "AI
Governance add-on" wording items from the licensing repackaging (AI
Governance is now included with Premium).
- Remove the **AI add-on** badge from the group settings AI budget
section and the AI Governance add-on card on the Licenses page.
- Remove the **AI add-on** column (check icon showing AI seat
consumption) from the Users and Organization Members tables, along with
its help popover, the `AISeatCell` component, and the
`shouldShowAISeatColumn` entitlement helper.
- Update related Storybook stories.
- Drop "add-on" from the AI cost control route comments in
`enterprise/coderd/coderd.go` and the `coder exp ai-model-prices`
prerequisite in `docs/ai-coder/ai-gateway/cost-controls.md` (leftovers
not covered by #28075/#28077/#28268).
The `has_ai_seat` API field is left intact.
<details>
<summary>Removal scope</summary>
Badge usages removed:
- `site/src/pages/GroupsPage/GroupSettingsPageView.tsx`
-
`site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/AIGovernanceAddOnCard.tsx`
AI seat column plumbing removed:
- `site/src/pages/UsersPage/UsersTable.tsx` and `UsersPage.tsx`
- `site/src/pages/OrganizationSettingsPage/OrganizationMembersTable.tsx`
and `OrganizationMembersPage.tsx`
- `site/src/modules/users/AISeatCell.tsx` (deleted)
- `AiAddonHelpPopover` in `site/src/modules/users/UserHelpPopovers.tsx`
- `shouldShowAISeatColumn` in
`site/src/modules/dashboard/entitlements.ts`
Add-on wording leftovers:
- `enterprise/coderd/coderd.go`: 7 route comments (comments only, no
behavior change)
- `docs/ai-coder/ai-gateway/cost-controls.md`: model prices CLI
prerequisite
</details>
---
🤖 This pull request was generated by Coder Agents on behalf of
@tracyjohnsonux.
|
||
|
|
f2bc9ab1f5 |
docs: complete swagger annotations for organization-scoped MCP routes (#28064)
Adds the missing swagger annotations for the eight organization-scoped MCP server config routes introduced in #27942 and checks in the regenerated API artifacts (`coderd/apidoc`, `docs/reference/api`). No behavior changes: 58 hand-written annotation lines, the rest is generated output. ## Stack context Part of the MCP org-separation stack (CODAGT-711 org scope -> apidocs -> hardening -> CODAGT-717 audit -> CODAGT-712 ACLs -> CODAGT-806 token RBAC). Split out of #27942 to keep the core cutover reviewable; these routes live under `/api/experimental`, where main already ships several MCP handlers without annotations, so the base PR is consistent with existing precedent until this lands. Closes nothing on its own; documentation completion for CODAGT-711. > 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>
|
||
|
|
7ff1278ab3 |
feat: track Junie as an AI Gateway client (#28266)
## Problem Junie CLI works with AI Gateway, but every Junie session is recorded as `Unknown`. Junie sends `User-Agent: Junie:SNAPSHOT` (observed directly from the CLI), which `GuessClient` does not match, so usage, spend, and audit views cannot attribute Junie traffic. There were also no docs for pointing Junie at AI Gateway. ## Fix - Detect the observed `junie:` user-agent prefix and add the `Junie` client value. - Add the Junie icon so sessions render with a logo instead of the fallback question mark. - Add a Junie client page and list it in the compatibility table. - Add `Roo Code` and `Charm Crush` to the documented `client` filter values. Both were already implemented but missing from the list. ## Supported configurations | Provider | API type | Endpoint | |-----------|-------------------|--------------------------| | OpenAI | `OpenAIResponses` | `/openai/v1/responses` | | Anthropic | `Anthropic` | `/anthropic/v1/messages` | BYOK works on both through `extraHeaders` with `X-Coder-AI-Governance-Token`. Preview: https://coder.com/docs/@matifali-junie-ai-gateway-client/ai-coder/ai-gateway/clients/junie > 🤖 This PR was created with the help of Coder Agents, and needs a human review. 🧑💻 |
||
|
|
1bb0978361 |
fix(site): centralize UUID generation and fix non-secure crash (#28285)
Fixes OSS-38: `crypto.randomUUID is not a function` when accessing Coder over plain HTTP. ### Root cause `crypto.randomUUID` is only defined in secure contexts (HTTPS or localhost). It is `undefined` when Coder is served over plain HTTP. The `uuid` package's `v4()` short-circuits to `crypto.randomUUID()` when it is truthy, and in the production bundle that path compiles down to a direct `crypto.randomUUID()` call, so `uuidv4()` throws over HTTP. Observed crash (Template Builder, in the `useMemo` that mints the telemetry session ID): ``` TypeError: crypto.randomUUID is not a function Rt/h<@.../assets/TemplateBuilderPage-*.js useMemo@... ``` The same `uuidv4()` path backed the Terminal reconnection token and the Agent chat tab IDs / reconnection tokens, so those flows were affected too. ### Fix Add a single `generateUUID` helper in `site/src/utils/random.ts` that derives the UUID from `crypto.getRandomValues` (the one `Crypto` member available in insecure contexts) and only calls `crypto.randomUUID` when it actually exists. Route all client-side UUID generation through it, so every call site is fixed at once and future ones stay safe. ### Changes | Change | Route | Human tested/verified | |---|---|---| | `utils/random.ts` — new `generateUUID()` helper | N/A | N/A | | `TemplateBuilderPage.tsx` — telemetry session ID | `/templates/new/builder` | [x] | | `TerminalPage.tsx` — reconnection token | `/:username/:workspace/terminal` | [x] | | `AgentChatPageView.tsx` — tab IDs + reconnection tokens | `/agents/:agentId` | [ ] | | `useChatDraftAttachments.ts` — draft client IDs | `/agents/:agentId` | [x] | | `TaskApps.stories.tsx` — mock app IDs (Storybook) | `/tasks/:username/:taskId` | [ ] | | `WorkspaceSharingIndicator.stories.tsx` — mock actor IDs (Storybook) | `/workspaces` | [ ] | | Removed unused `uuid` / `@types/uuid` direct deps | N/A | N/A | `uuid` is still pinned via a `pnpm.overrides` entry because `mermaid`/`streamdown` depend on it transitively. ### Tests - `random.test.ts` covers a valid UUID, uniqueness, and the insecure-context path (with `crypto.randomUUID` stubbed to `undefined`). - `TerminalPage.test.tsx` now mocks `#/utils/random` instead of the `uuid` module. ### Validation - `make fmt`, `biome check`, `tsc --noEmit` clean - `vitest run` green: `random.test.ts` (6), `TerminalPage.test.tsx` (16), `useChatDraftAttachments.test.ts` (17), both changed stories (12) - Reproduced the original crash over HTTP on a production build, top frame `uuidv4()` in Template Builder. --- _This PR was generated by Coder Agents on behalf of @phorcys420._ |
||
|
|
6107cfe918 |
fix(enterprise/coderd/license): remove AI Governance required banner (#28268)
## Summary The AI Governance add-on is now included in all Premium licenses, so the transitional license banner warning that AI Bridge / AI Gateway requires the AI Governance add-on is no longer accurate. This removes that warning throughout Coder. The banner text this removes: > The AI Governance add-on is required to use AI Gateway. Please reach out to your account team or sales@coder.com to learn more. ## Changes - Remove the AI Bridge soft warning block in `LicensesEntitlements`. - Remove the now-unused `hasExplicitAIBridgeEntitlement` tracking that only fed that warning. - Remove `TestAIBridgeSoftWarning` and the vestigial warning assertions/comments that pinned the removed behavior. The license banner surfaces backend entitlement warnings verbatim, so removing the warning here removes it from the dashboard banner. No frontend changes are needed; the warning string existed only in the backend. ## Testing - `go build ./enterprise/coderd/license/...` - `go vet ./enterprise/coderd/license/...` - `go test ./enterprise/coderd/license/` (license entitlement + AI Governance addon suites) --- *This PR was created by Coder Agents on behalf of @jcjiang.* |
||
|
|
e003014ff8 |
docs: remove space in GitHub-style callout markers [DOCS-680] (#28305)
## What Normalize the one GitHub-style callout marker that had a space after `!` to the canonical tight form. - `docs/user-guides/workspace-access/jetbrains/gateway.md`: `> [! WARNING]` -> `> [!WARNING]` A full-corpus scan confirmed this was the only spaced marker. ## Why GitHub alerts, the hosted docs (coder.com), and the offlinedocs Fumadocs renderer all require the tight `> [!WARNING]` form. The spaced form rendered as literal `[! WARNING]` text instead of a styled alert, so this is a genuine rendering fix on GitHub and coder.com, not an offline-pipeline accommodation. Surfaced by the offlinedocs Fumadocs migration review (CRF-60 on #27390). Tracked separately from the mixed-case uppercasing fix (DOCS-681), per request. Linear: [DOCS-680](https://linear.app/codercom/issue/DOCS-680/remove-the-space-after-in-github-style-callout-markers-across-docs) > This PR was created with AI assistance (Coder Agents). |
||
|
|
2ee6f459b2 |
docs: uppercase mixed-case GitHub-style callout markers [DOCS-681] (#28304)
## What Uppercase the mixed-case GitHub-style callout markers in the docs corpus to the canonical all-caps form. - `docs/install/rancher.md`: `> [!Important]` -> `> [!IMPORTANT]` - `docs/user-guides/shared-workspaces.md`: `> [!Important]` -> `> [!IMPORTANT]` - `docs/user-guides/workspace-access/index.md`: `> [!Note]` -> `> [!NOTE]` A full-corpus scan confirmed these were the only mixed-case markers. ## Why GitHub alerts, the hosted docs (coder.com), and the offlinedocs Fumadocs renderer all require all-caps alert types (`[!NOTE]`, `[!IMPORTANT]`, and so on). The mixed-case form rendered as literal text instead of a styled alert (on some pages directly next to a correctly-rendered all-caps alert), so this is a genuine rendering fix on GitHub and coder.com, not an offline-pipeline accommodation. Surfaced by the offlinedocs Fumadocs migration review (CRF-60 on #27390). Tracked separately from the space-after-`!` fix (DOCS-680), per request. Linear: [DOCS-681](https://linear.app/codercom/issue/DOCS-681/uppercase-mixed-case-github-style-callout-markers-across-docs) > This PR was created with AI assistance (Coder Agents). |
||
|
|
023f61626d |
feat: default AI Gateway sessions list to a 24h time range (#28256)
The AI Gateway sessions list page takes 5-13 seconds to load because `ListAIBridgeSessions` scans all `aibridge_interceptions` rows (~1M on dogfood) when no time filter is set. This PR defaults the list to the last 24 hours of sessions, reducing the scan by roughly two orders of magnitude without any backend or migration changes: the `started_after`/`started_before` filters already exist end-to-end in SQL, searchquery, and the API. The default range is held in component state (not the URL) and merged into every query payload including prefetches, so the unbounded query never runs on page load. A time range filter in the filter bar lets users override the window explicitly, which doubles as a forensic tool. Picking a range writes quoted RFC 3339 timestamps into the existing filter query. There are no presets and no unbounded "all time" mode, so the fast path is the only path. Existing "All sessions"/"My sessions" presets reset the filter query, which resets the time window to the default 24 hours; that is intentional. Also makes the five filter triggers a uniform width and left-aligns the new picker so the search input keeps room on wide viewports. Depends on #28255 (the DateTimeRangeFilter component) and #28254 (filterQuery serialization). Part of [AIGOV-580](https://linear.app/codercom/issue/AIGOV-580/ai-gateway-sessions-page-takes-5-10-seconds-to-load) --- _Generated by Coder Agents on behalf of @johnstcn._$ --- **Stack:** #28254 (filterQuery fix) \u2192 #28255 (component) \u2192 #28256 (sessions page) |
||
|
|
05699c4d55 |
feat: automate weekly AI model price book refresh (#28146)
Example of generated PR: https://github.com/coder/coder/pull/28228 ## Summary The price book is regenerated by hand with `make gen/aibridge-prices`, so keeping it current depends on someone remembering to run it. Newly launched models stay unpriced and changed prices stay stale until the next manual run. This adds a weekly workflow that regenerates both artifacts from live [models.dev](https://models.dev) data and opens a pull request when the output changes. For scale, a run against `main` today produced 32 models added, 11 removed, and 43 repriced. ## What it does - Runs every Thursday at 09:00 UTC, plus `workflow_dispatch`. Exits without opening anything when regeneration produces no diff. - Reuses one branch and one pull request, force-pushing each week, so at most one refresh PR is open and it always carries the newest snapshot. - Ships `prices.json` and `knownModelsGenerated.json` together, since both come from a single upstream snapshot. - Never merges automatically. Prices are customer-visible cost numbers, so every change goes through human review. - Announces failures in Slack. The generator fails by design when upstream drops a model pinned in `overrides.jq` or curated in `curation.json`, and a silently red weekly cron would defeat the point. `scripts/aibridgepricesdiff` renders the PR body summary: counts, then the models added, removed, and repriced. Exact figures stay in the diff rather than being restated in the body. It is deterministic and unit tested rather than AI-generated, so the model lists cannot drift from the file. `CODEOWNERS` covers the two generated artifacts, which are exactly the files a refresh PR touches. ## Verification Ran on this branch via a temporary `push` trigger, since `schedule` and `workflow_dispatch` only fire from the default branch. Confirmed end to end: mise and pnpm setup, generation from live models.dev, change detection, summary and body assembly, commit under the bot identity, branch creation, force-push on the second run, and Slack delivery. `gh pr create` and `gh pr edit` remain unverified: both runs hit `HTTP 503` from `api.github.com/graphql` during a GitHub major outage. The push path itself succeeded, so the credential and permissions are proven. Also verified locally: `go test ./scripts/aibridgepricesdiff/`, `make lint/actions/actionlint`, `make lint/actions/zizmor`, and `make pre-commit`. ## Notes - The PR is opened with `secrets.CDRCI_GITHUB_TOKEN`. PRs opened with the default `GITHUB_TOKEN` do not trigger workflow runs, which would leave the refresh without CI signal on a file that feeds cost calculation. - Requires the `AIGATEWAY_PRICES_SLACK_WEBHOOK` repository secret. The notification step fails loudly if it is unset, since by then the refresh has already failed and nobody would be told. - The workflow installs pnpm dependencies because catalog generation formats its output with biome; without `site/node_modules` the formatting step silently no-ops and an unformatted file would be committed. - A refresh that lands after a release branch is cut can reach that release through the existing cherry-pick label, so no separate release-cut trigger is included. The label is not applied automatically; that stays a reviewer decision. - Identifiers keep the `aibridge` spelling where they name real paths and make targets (`coderd/aibridge/prices`, `make gen/aibridge-prices`, `scripts/aibridgepricesgen`). The workflow itself is named `aigateway-prices-refresh`, and prose says AI Gateway per the style guide. - The issue also asked for code owners on the generator. Only the generated artifacts are owned here, since those are the files a refresh PR touches. Ownership of `curation.json` and `overrides.jq` can be added separately if human edits to them should request review. <details> <summary>Implementation plan and decision log</summary> ### Goal Refresh `make gen/aibridge-prices` output on a schedule and open a human-reviewed PR when it changes, so releases never ship a stale price book. ### Schedule Weekly, Thursdays 09:00 UTC, leaving Thursday, Friday, and Monday for review before Tuesday releases. A monthly run was considered first. Release ships Tuesday morning and the freeze is the last Tuesday before EOM, so a monthly run on the 19th or 20th collapses to 1-2 usable business days in roughly 10 of 132 months: February, when the release Tuesday is the 22nd and the run day falls on a weekend. Measured across 2026-2036: | Cron day | Worst case usable business days | Months with 2 or fewer | Median | |---|---|---|---| | 20th | 1 | 10 | 6 | | 19th | 1 | 10 | 6 | | 16th | 3 | 0 | 6 | | Weekly Thursday | n/a, always a full week | 0 | n/a | The median is 6 days for every candidate, so moving to weekly costs nothing and removes the bad tail. The shipped book is never more than 7 days stale. ### Branch strategy One fixed branch, force-pushed, rather than a new branch and PR per run. `prices.json` is a full regeneration from the current upstream snapshot, not an incremental patch, so two open refresh PRs are the same file at two points in time and the older one is strictly wrong. A single PR makes merging a stale snapshot impossible, keeps reviewer load at one PR, and avoids a full CI run per week per stale PR. ### Summary generation Deterministic Go tool, not an AI summary. An LLM summarizing a JSON diff can drop or invent a row, and would be non-reproducible run to run. A jq implementation was considered and rejected as effectively untestable. The body lists which models moved; the diff in the Files tab remains the source of truth for figures. An earlier version rendered full price tables with percentage deltas. That duplicated the diff, so it was reduced to model lists. ### Token `secrets.CDRCI_GITHUB_TOKEN`, the cdrci machine user already used for bot-authored PRs in `release.yaml` and cited for exactly this reason in the commented-out `update-flake` job in `ci.yaml`. A dedicated GitHub App scoped to this repo would be tighter, but needs org admin to create. ### Commit identity `github-actions[bot]` with its numeric noreply address, matching `backport.yaml` and `cherry-pick.yaml`. An invented address would leave refresh commits with an unlinked author. ### Alerting A dedicated webhook secret rather than reusing `CI_FAILURE_SLACK_WEBHOOK`. The repo already splits alerting per domain (docs, security, dependabot each have their own webhook), and the CI-failure channel carries a user mention plus `vars.BLINK_CI_FAILURE_PROMPT` that feeds an automated triage flow this payload does not belong in. GitHub's built-in email notification was considered and rejected as the primary channel: for scheduled workflows it goes to a single user, the one who last modified the cron syntax, and cannot be routed to a team. ### Out of scope Triggering a refresh when a release branch is cut. The existing cherry-pick label already gets a late refresh into a release, so a separate mechanism is unnecessary. </details> --- Closes [AIGOV-578](https://linear.app/codercom/issue/AIGOV-578/automate-updates-to-the-shipped-ai-model-price-book). Authored by Coder Agents on behalf of @evgeniy-scherbina. |
||
|
|
7268cada94 |
docs: remove JetBrains Fleet references (#28301)
## Summary The `jetbrains-fleet` module was removed from [registry.coder.com](https://registry.coder.com) and `coder/registry`, but the docs still pointed users at Fleet and at the now-dead module page. ## Changes - Removed `docs/user-guides/workspace-access/jetbrains/fleet.md` and its screenshot - Removed the Fleet entry from `docs/manifest.json` - Dropped Fleet from the supported IDE lists in `jetbrains/index.md` and `workspace-access/index.md` - Glossary: replaced the Fleet link with Toolbox - Contributing guide: replaced the dead `jetbrains-fleet` registry link with `jetbrains` ## Validation - No remaining Fleet references in `docs/` (the only match left is Tailscale's "global fleet of DERP relays") - `docs/manifest.json` parses, `pnpm run lint-docs` reports 0 errors Preview: https://coder.com/docs/@docs-remove-jetbrains-fleet/user-guides/workspace-access/jetbrains > [!NOTE] > `site/static/icon/fleet.svg` and its entry in `site/src/theme/icons.json` are intentionally left in place. Removing the icon would break existing templates that reference that path. > The deleted page will 404 until a redirect is added in the website repo. > 🤖 This PR was created with the help of Coder Agents, and needs a human review. 🧑💻 |
||
|
|
46c0dba1e9 | fix: standardize EmptyState typography (#27720) | ||
|
|
71b5bc398f |
docs: fix broken callout on the AI Gateway Monitoring page (#28303)
The callout on this page is broken, as flagged by Atif. This PR moves the callout out of the `<details>` block to avoid the broken formatting. Fixes DOCS-679 <!-- If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting. --> |
||
|
|
fc6d6babfa |
docs: add markdown_url front matter so the About page links to /docs.md (#28281)
The docs About page renders at `/docs/about`, but its Markdown source is the docs root `README.md`, whose Markdown twin is `/docs.md`. The path-derived mapping (`/docs/about` -> `/docs/about.md`) points the "Copy page / view as Markdown" affordance and the sitewide `rel="alternate"` link at a URL that 404s for this page. Add a `markdown_url: /docs.md` front-matter override to `docs/README.md`. It is inert for rendering and for `/docs.md` itself (the body still begins at `# About`); it only populates the front matter the docs site reads to advertise the correct Markdown alternate. Reader side: coder/coder.com#1013 (DOCS-638). Neither change breaks without the other, so they can merge in either order. Refs DOCS-678 > This PR was created with AI assistance (Coder Agents). |
||
|
|
c2bb62446d |
fix(coderd): remove per-chat system prompt limit (#28294)
Per-chat system prompts longer than 10,000 characters are rejected during chat creation. Remove that validation so chat creation accepts longer system prompts within the existing request-body limit. |
||
|
|
31e95f7096 | docs: fix prebuilt-workspaces example syntax and defaults (#28088) | ||
|
|
8a7e8d9d5b |
refactor: move chat prompt sanitization into codersdk (#28283)
Exports `SanitizePromptText` (and its helpers) from `codersdk` instead of `coderd/x/chatd`, so API consumers can sanitize prompt text exactly the way the server stores it. ## Why coder/terraform-provider-coderd#412 adds a `coderd_chat_system_prompt` resource whose `system_prompt` attribute compares values by their sanitized forms (otherwise the trailing newline from `file("system-prompt.md")` is perpetual drift, since the server stores the sanitized value). That currently requires a mirrored copy of the sanitizer in the provider, which can silently rot. Per review there ([discussion](https://github.com/coder/terraform-provider-coderd/pull/412#discussion_r3801110795)), the sanitizer should be exported from `codersdk` and imported instead. ## What - `coderd/x/chatd/sanitize.go` → `codersdk/promptsanitize.go` (pure move; package + doc-comment note about why it lives in codersdk) - `coderd/x/chatd/sanitize_test.go` → `codersdk/promptsanitize_test.go` - Callsites updated (`exp_chats.go`, `chatd.go`, `subagent.go`, `context_prompt.go`); no wrapper left behind, single source of truth - No behavior change > Generated by Coder Agents on behalf of @bpmct |
||
|
|
ddf2d33665 |
docs: update Tallyman Agent Time reporting (#28275)
Moves Coder Agents usage reporting into the Licensing & Usage page and updates the documentation to describe Agent Time and the `hb_agent_runtime_v1` Tallyman payload. Removes the superseded Usage Data Reporting page and its manifest entry, and removes the obsolete AI Governance link to that page. PR generated with Coder Agents |
||
|
|
97cb722fb8 |
fix: show no budget instead of unlimited for empty group AI budget (#27993)
An empty per-member AI budget on the group settings page previously displayed "unlimited budget" with a "Members in this group have no spending cap." alert. Unlimited spend only applies when the everyone group is the sole group, so this messaging was misleading everywhere else. ## Changes - Empty budget now shows "This group has **no budget** set. View docs" with no info alert. "View docs" links to [Effective group resolution](https://coder.com/docs/ai-coder/ai-gateway/cost-controls#effective-group-resolution) using the versioned `docs()` helper. - Removed the "Members in this group have no spending cap." alert entirely. - Input placeholder changed from `unlimited` to `no budget`. - Updated the `AIBudgetUncapped` story expectations to match. ## Unchanged - Entering an explicit `$0` still shows the "A $0 limit disables AI access for this group." alert, as before. - Saving with an empty field still sends `null` to the budget API; backend semantics are untouched. Story tests pass: `pnpm test:storybook src/pages/GroupsPage/GroupSettingsPageView.stories.tsx` (7/7). --- *This PR was generated by Coder Agents on behalf of @tracyjohnsonux.* |
||
|
|
da99941099 |
feat(site): add shared DateTimeRangeFilter component (#28255)
A text-expression datetime range picker with From/To inputs accepting
`now`, a clock time (current day), a date (midnight), or a date with a
clock time. Invalid text gets inline errors, underspecified expressions
resolve on blur, and out-of-order boundaries clamp against the other
one. The trigger derives a concise label ("Last 24 hours", "Apr 10",
"Aug 11 - Today", "Apr 17 - 19").
Placed in `site/src/components/DateTimeRangeFilter/` so other pages can
reuse it. Expression parsing and trigger-label derivation live in
`timeRange.ts` next to the component; both are generic over `Date`
pairs. Depends on #28254 for the shared `filterQuery` serialization
helpers.
Part of
[AIGOV-580](https://linear.app/codercom/issue/AIGOV-580/ai-gateway-sessions-page-takes-5-10-seconds-to-load)
---
_Generated by Coder Agents on behalf of @johnstcn._$
---
**Stack:** #28254 (filterQuery fix) \u2192 #28255 (component) \u2192
#28256 (sessions page)
|
||
|
|
63641b98c8 |
fix: treat a missing serve endpoint as a fatal dial error (#27864)
Adds 404 as a terminal error for establishing DRPC connection. A standalone AI Gateway pointed at a coderd that does not expose `/api/v2/ai-gateway/serve` gets a 404, which the connect loop classified as transient and retried forever. Redialing cannot fix a missing endpoint. 404 now is treated as terminal handshake failure. `--url` is expected to point directly at coderd, so a 404 from an intermediary is not distinguished. Refs https://linear.app/codercom/issue/AIGOV-320/write-connection-tests --- Generated with Coder Agents. |
||
|
|
8405bbb26c | feat(site): show best-effort model pricing on the model form (#28290) | ||
|
|
34e95c46bf |
docs: note JetBrains client attribution in AI Gateway (#28296)
Noticed while testing Junie that JetBrains AI Assistant sends `User-Agent: ktor-client`, the stock Ktor default, so AI Gateway records these sessions as `Unknown`. Admins who enable Gateway for governance won't see their JetBrains attributed in the sessions view or the `client` filter. Documents the behavior on the JetBrains client page. Nothing to fix on our side, matching `ktor-client` would misattribute any other Ktor-based application. |
||
|
|
a441b03d70 |
feat: add yaml config option to standalone AI gateway (#28258)
`coder ai-gateway start` now accepts a `--config` / `-c` flag (and `CODER_CONFIG_PATH`) to load configuration from a YAML file. |
||
|
|
059546e92a |
ci: skip oversized flake-check selections (#28293)
Skip the targeted Go flake check when a change selects more than 100 tests, and update `whichtests` to the version that supports this limit. Large selections usually come from broad or mechanical changes rather than a narrowly introduced flaky behavior. Repeating hundreds of tests substantially increases runtime and runner resource usage while providing less focused signal; regular CI still exercises the affected test suite across its normal jobs and repetitions. |
||
|
|
1fbd029754 |
fix(provisioner/terraform): sync version constants with the shipped binary (#27871)
`provisioner/terraform/install.go` still pins `TerraformVersion` to 1.14.5 and `maxTerraformVersion` to 1.14.9, but the shipped binary has been Terraform 1.15.5 since v2.35.0. `scripts/Dockerfile.base`, `install.sh`, `mise.toml`, `mise.lock`, and `flake.nix` all track 1.15.5; only the Go constants were left behind, which makes the `NOTE: Keep this in sync with ...` comments on those constants false. Two user-visible effects on the same release: - Provisioner daemons that have a system Terraform log `installed terraform version newer than expected, you may experience bugs installed_version=1.15.5 max_version=1.14.9` on every startup. The check at [`provisioner/terraform/serve.go#L122-L126`](https://github.com/coder/coder/blob/79723db2d23b6b941e405a8d3c2200206e7e4cb4/provisioner/terraform/serve.go#L122-L126) is warn-only, so the 1.15.5 binary is used anyway and the warning is pure noise. - Hosts *without* a system Terraform fall through to `Install()` at [`serve.go#L105`](https://github.com/coder/coder/blob/79723db2d23b6b941e405a8d3c2200206e7e4cb4/provisioner/terraform/serve.go#L105), which downloads `TerraformVersion`. So the same Coder version provisions with 1.15.5 in some environments and 1.14.5 in others. This sets `TerraformVersion` to 1.15.5 and `maxTerraformVersion` to 1.15.9, keeping the existing `.9` convention that auto-allows patch releases. `minTerraformVersion` is unchanged. No other file needed updating; everything else already tracks 1.15.5. This is a consistency fix, not a CVE remediation. The divergence was already identified as [CRF-2 in the review of #27183](https://github.com/coder/coder/pull/27183#pullrequestreview-4685322854) (`provisioner/terraform/install.go:25`), which noted that leaving the constants behind means "a maintainer who trusts it could 'resync' install.sh/Dockerfile.base back to 1.14.5". That PR was closed for unrelated reasons; this change addresses only the drift. Refs #27183 |
||
|
|
ddcffd8248 | docs: remove inaccurate Agent Firewall filesystem protection claim (#28286) | ||
|
|
663f41ffa9 |
feat: derive OAuth2 client type from token_endpoint_auth_method (#28043)
Adds an OAuth2 client type (public vs confidential, RFC 7591 §2) derived from the requested auth method instead of hardcoded confidential. The type is stored and guarded here, but no endpoint enforces on it yet; public behavior at the token endpoint follows in the next PR in the stack. - Client type is derived once and reused by both registration and redirect URI validation, so they can't disagree - IsPublic() fails closed: an unrecognized or missing value reads as confidential - RFC 7592 update (PUT) now rejects moving a client between public and confidential (400) instead of silently flipping it when the auth method is omitted - Discovery still doesn't advertise "none"; follows once the token endpoint honors it ### Behavior by client shape `client_type` is derived from `token_endpoint_auth_method` at POST and pinned at PUT. RFC 7592 GET/PUT authenticate with the registration access token, not the client secret, so neither endpoint reads a secret. | Registered with | Stored `client_type` / method | GET reports | PUT that flips the method | |------------------------------------|---------------------------------------|-----------------------|-----------------------------------------| | omitted, or `client_secret_basic` | `confidential` / `client_secret_basic` | `client_secret_basic` | `none` → 400 `invalid_client_metadata` | | `none` (new) | `public` / `none` | `none` | `client_secret_*` → 400 `invalid_client_metadata` | | `none` (before this PR) | `confidential` / `none` | `none` | either → 200, type stays `confidential` | - PUT still replaces every other RFC 7591 field. `client_type` is the only pinned one; the method may move within a type (`client_secret_basic` ↔ `client_secret_post`). - Row 3 is the only shape where the two columns disagree. The guard fires only on a method change that crosses the type line, so those clients keep managing themselves instead of being locked out of their own configuration endpoint. - The token endpoint does not consult `client_type` yet, so every client still authenticates with a secret and registration still issues one. Split out of #27873, second in the stack (on top of #28041). Refs https://linear.app/codercom/issue/ENG-3029/oauth2-support-public-client |
||
|
|
4d13bef74d |
feat(site): add Install Coder Desktop item to UserDropdown (#28244)
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell. Resolves [DEVEX-722](https://linear.app/codercom/issue/DEVEX-722/add-install-coder-desktop-button-to-userdropdown). Adds an **Install Coder Desktop** item to `UserDropdown`, sitting directly above the existing **Install CLI** option. Because `UserDropdownContent` is shared, it appears in both the navbar dropdown and the agents-page sidebar footer. ### Screenshots macOS/Windows shots are captured with `navigator.platform` overridden accordingly; the Linux shot is the real workspace platform, where the Coder Desktop item is hidden. Install Coder Desktop uses a monitor glyph, and Install CLI now uses a terminal glyph (it was a monitor on `main`). | `main` (before) | This PR · macOS/Windows | This PR · Linux / iPad OS | | --- | --- | --- | |  |  |  | ### Behaviour - **Platform-gated visibility:** shown only on macOS and Windows (the platforms Coder Desktop ships for), hidden on Linux/other. The Linux client ([coder/coder-desktop-linux](https://github.com/coder/coder-desktop-linux)) is experimental and not advertised yet, so it stays hidden there. - **Links to the docs** (`https://coder.com/docs/user-guides/desktop`) rather than a single install method. The docs page is the canonical hub covering Homebrew, WinGet, manual release downloads, and source, and it stays current without frontend changes. The `coder-desktop-*` repo READMEs don't enumerate install methods; they defer to these same docs. - **No installed-detection:** consistent with the Install CLI item, we don't try to detect whether Coder Desktop is already present. ### Changes - `site/src/utils/platform.ts`: add `isWindows()` and `supportsCoderDesktop()`. - `UserDropdownContent.tsx`: render the platform-gated item (`MonitorIcon`) and switch Install CLI to `TerminalIcon`. - Unit tests for the platform helpers and Storybook coverage for the dropdown item (present + correct href on macOS/Windows, absent on Linux/iPadOS). Implementation plan & decision log **Ticket open questions and decisions** 1. *Shown to all users or only supported platforms?* Only supported platforms (macOS, Windows); hidden elsewhere. The Linux client is experimental and not advertised yet, so hiding it there is intentional. 2. *Where should it link?* The Coder Desktop docs page. Users install via many methods (brew, winget, releases, source); the docs are the single hub that lists them all and are maintained in-repo. The GitHub release pages / repo READMEs only cover downloads and defer back to the docs. 3. *Hide when already installed?* No. There is no reliable browser-side way to detect a native app install, and it mirrors how Install CLI behaves. **Implementation** - Extend `site/src/utils/platform.ts` with `isWindows()` and `supportsCoderDesktop()` (reusing the existing `isMac()`). - In `UserDropdownContent.tsx`, conditionally render an `Install Coder Desktop` `DropdownMenuItem` (external link, `MonitorIcon`, opens in a new tab) above `Install CLI` when `supportsCoderDesktop()` is true. - Tests: `platform.test.ts` (OS detection via `vi.stubGlobal`) and `UserDropdown.stories.tsx` (visibility + href per platform, mocking platform detection with `spyOn`). Left as a **draft** pending review, let me know when you'd like it opened. |
||
|
|
60722bb653 | fix: link workspaces empty state to template builder (#28280) | ||
|
|
71e95a3611 |
feat(coderd/tracing): correlate request logs and spans by client_session_id (#27671)
## What Adds `client_session_id` correlation to coderd's HTTP request handling, per the [Connection log collection and correlation RFC](https://www.notion.so/coderhq/Connection-log-collection-and-correlation-36ed579be5928025a56cd11fe58661fb). Clients attach a per-session correlation ID to every API request via W3C baggage using the `client_session_id` key. This change makes coderd's tracing middleware read that baggage member and: - add `client_session_id` to the **per-request log context** so all logs for a request (and the handlers it calls) can be correlated by a single ID, and - set `client_session_id` as a **span attribute** when tracing is enabled. Per RFC requirement 6.1, the value is added to the log context **even when tracing is disabled** (the middleware previously returned early when no tracer provider was configured, so baggage was never read). The `client_session_id` is validated as a 32-character hexadecimal string (a 16-byte value, per RFC requirement 1) to guard against logging arbitrary client-controlled baggage values. ## Scope This is `DEVEX-659` and is intentionally limited to the coderd tracing middleware. It is the first piece of a stack: the web terminal client change (`DEVEX-663`) that generates and sends the `client_session_id` will be stacked on top of this PR. No client currently sends `client_session_id` baggage, so this change is a no-op until the client work lands. ## Testing - `coderd/tracing`: new unit tests cover `validSessionID`, baggage extraction (`sessionIDFromHeaders`), and the middleware end to end, asserting `client_session_id` lands on the log context with tracing enabled **and** disabled, is exposed as a span attribute when tracing is enabled, and that absent/malformed baggage is ignored. - Existing `Test_Middleware` route-matching behavior is unchanged. <details> <summary>Design notes / decision log</summary> - **Where the value is read:** the existing `tracing.Middleware` runs high in the coderd middleware stack (`coderd/coderd.go`), before request-id and request-logger middleware, and already matches the `/api`, `/api/**`, app proxy, and external-auth routes. Reading baggage here means the `client_session_id` is on the context before the request logger and handlers run, so it flows into all downstream `slog` calls that use the request context. This mirrors the existing `request_id` pattern in `httpmw.AttachRequestID` (`slog.With(ctx, ...)` + span attribute). - **Works when tracing is off:** the middleware now gates only on the route matcher, extracts baggage and adds `client_session_id` to the log context for all matched routes, and only then branches on whether a tracer is configured. When a tracer is present, `client_session_id` is additionally set as a span attribute. - **Explicit baggage propagator:** extraction uses `propagation.Baggage{}` directly rather than the global text map propagator, so it does not depend on the global propagator being configured (also makes it deterministic in tests). - **Validation:** only a 32-char hex string is accepted (lower or upper case). Malformed values are dropped rather than logged, preventing log/attribute pollution from arbitrary client-supplied baggage. - **Out of scope for this PR (tracked elsewhere):** client generation/sending of `client_session_id` (`DEVEX-663`, web terminal), the equivalent agent-side middleware (RFC 6.2), `connection_logs.client_session_id` (RFC 12), and additional connection state-change logging (RFC 7-13). </details> --- _Opened by Coder Agents on behalf of @aqandrew._ |
||
|
|
821d91fabd | fix: log tailnet tunnel authorization decisions (#27819) | ||
|
|
b3f05a23fc |
feat(site/src/pages/AgentsPage): surface subagents in the chat sidebar (#28234)
## What Makes subagents more discoverable in the agent chat sidebar, without disturbing the existing status icon or layout. - **Parent chat rows show a subagent count** with a bot icon (e.g. `3 🤖`) at the start of the metadata line, next to the diff stats. Only shown when a chat has subagents; uses lucide `BotIcon`, the same icon this codebase already uses to denote agents. Passive indicator (the leading status icon, timestamp, and kebab are untouched). - **The chat actions menu gains a "Show subagents (N)" / "Hide subagents" toggle**, grouped directly under "Rename chat" in both the kebab (⋮) and the right-click context menu. It expands/collapses the row's subagents and is only rendered when the chat has subagents. Expansion still uses the existing sidebar expand state, so the hover chevron, the menu toggle, and the count all stay in sync. ## Notes / decisions - Subagent children are capped at depth 1, so the count and toggle only appear on parent chats. - The menu toggle lives in the shared `ChatActionsMenuItems`, so it appears in both the kebab and the right-click menu; the top-bar kebab intentionally does not pass the props, so it stays hidden there. - The on-row `N 🤖` indicator is currently passive (clicking the row opens the chat as before). Can be made a click-to-expand control if wanted. ## Testing Validated in Storybook (`ChatsSidebar` stories) with 0 TypeScript errors. Added a `SubagentsMenuToggle` story with a `play` function asserting the label swaps between "Show subagents (3)" and "Hide subagents" and that children expand. Existing `ChatsSidebar.test.tsx` does not assert menu contents and is unaffected. --- *This PR was generated by Coder Agents on behalf of @tracyjohnsonux.* |
||
|
|
72a6c8ae72 | fix(site): stop the new-turn scroll snap on agent chat (#28213) | ||
|
|
166d92ba73 |
fix: bound request body size on JSON API endpoints (#28168)
## Summary `httpapi.Read` decoded request bodies with no size limit, so a single request could allocate memory without bound. This adds a 4 MiB default ceiling, leaves the endpoints that legitimately need more explicitly exempted, and counts the rejections so a limit set too tight is visible. This is the first of three PRs split out of #28048, covering the endpoints that answer in `codersdk.Response` shape. The OAuth2 decode paths (RFC 6749, RFC 7591) and the SCIM ones (RFC 7644) answer in their own error shapes and follow in separate PRs, along with the lint rule that pins the invariant. Closes PLAT-463. Remediates SEC-416 (CWE-770, CVSS 7.5) and SEC-392. ## Problem `httpapi.Read` calls `json.NewDecoder(r.Body).Decode(value)` with no ceiling, and no middleware in the chain bounds body size. The exposure is pre-authentication: login, OTP, and first-user creation all read a body before any authorization decision is reached. The existing rate limiter bounds request *rate*, which is orthogonal to the memory a single admitted request may consume. ## Fix `Read` is split into `Read` and `ReadLimit`. `ReadLimit` wraps `r.Body` in an `http.MaxBytesReader` and keeps the existing decode and validate logic; `Read` delegates to it with a new `DefaultMaxRequestBodyBytes` of 4 MiB, which covers the 124 remaining non-test callers at a single site. `http.MaxBytesReader` composes as tightest-wins, so the handlers that pre-wrapped their own bodies pass their limit to `ReadLimit` rather than wrapping, and each keeps its previous ceiling byte for byte. That matters most for the bulk secrets import at `8 * MaxSecretsFileBytes`: an unconditional wrap inside `Read` would have silently halved it to the default. `TestImportUserSecretsBodyLargerThanDefaultLimit` is the regression guard for that specific failure, and `TestMaxBytesReaderNesting` pins the composition behavior the whole requirement rests on. Every rejection site calls `httpapi.RecordRequestBodyLimit`, which names the limit that tripped on the request's existing log line and marks the request so `coderd_api_requests_too_large_total{reason="request_body"}` counts body rejections apart from the 413s coderd answers for other causes, such as agent log storage overflow. A limit set too tight for a legitimate payload therefore surfaces without waiting for a user report. The limit is a constant rather than a deployment option: an operator raising it to unblock something would reopen the vulnerability as configuration, where a security scan will not find it. A legitimate 413 is answered with a targeted `ReadLimit` on that endpoint. ## Behavior change `POST /api/v2/files` now answers 413 rather than 400 when a request body exceeds `HTTPFileMaxBytes`. It installed that bound already but reported the rejection as a read failure, which leaked the stdlib `http: request body too large` string through `Detail` and kept the largest limit in the tree off the metric. The separate 413 for an oversized expanded archive is unchanged. The task log snapshot endpoint now answers 413 rather than 400 when its 64 KiB cap is exceeded. Routing it through `ReadLimit` also changes its decode-failure message from "Failed to decode request payload." to "Request body must be valid JSON.", which is what every other endpoint answers. Its tests are updated to match both. `coderd_api_requests_too_large_total` is new, so there is no existing query to migrate. It counts the 413s coderd answers, labeled `method`, `path`, and `reason`. `reason="request_body"` is a rejection by one of the limits above; `reason="other"` is a 413 that has nothing to do with body size, such as agent log storage overflow. ## Reading this The commits are ordered to be read in sequence. Commits 1 and 2 are the security fix; commits 3 to 5 are the observability consequences, and commit 3 is the one that touches dashboards. Commit 7 documents the limit on the REST API reference index. Commits 6 and 8 add and revert an exhaustive `@Failure 413` annotation pass, which buried the fix under its regenerated swagger, and cancel out. |