mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
fe4a73f8ab1b784e8af6f197c20122d7993270bc
2675
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fe4a73f8ab |
docs: note user secret target uniqueness and delivery boundary (#27532)
Two gaps in the user secrets guide, both independent of any pending secrets work. ## Changes **Target uniqueness (`Update a secret`, `File secrets`).** The guide teaches `coder secret update api-key --file ""` but never states that env names and file paths are unique per user, so the conflict you get when setting a target back has no explanation in the docs. The `File secrets` paragraph also read as though duplicate file paths are tolerated with a warning; an identical path is rejected outright, while two *different* paths resolving to the same location are both accepted and produce the agent warning. Both indexes are partial (`WHERE env_name != ''` / `WHERE file_path != ''`), so a cleared target really is released for another secret to claim — now stated. **Delivery boundary (`How your secrets reach a workspace`).** Coder controls where a secret is delivered, not whether it is still valid. Nothing done in Coder revokes a credential a workspace already received, so an exposed credential is rotated at its source. This applies to deletion as much as to clearing a target. No new headings, no changes to the existing tables or callouts. > This PR was generated by Coder Agents on behalf of @dylanhuff-at-coder. --------- Co-authored-by: Nick Vigilante <nickvigilante@users.noreply.github.com> |
||
|
|
ba4779fc87 |
docs: lead with env vars in admin docs and add configuration reference (#26824)
## What & why Admin/setup docs lead with `coder server --flag` examples, but most operators configure Coder through `CODER_*` environment variables (system service, container, or Helm chart). There is no single page mapping a setting to its env var, CLI flag, YAML key, and default, so searching the docs for an env var name such as `CODER_PG_CONNECTION_URL` returns nothing. This adds a generated configuration reference and begins shifting admin docs to lead with the environment-variable form. ## Changes - **Generated configuration reference** (`docs/admin/setup/configuration-reference.md`): a searchable, per-setting list of every visible deployment option. Each option is a heading (grouped and nested by serpent group) followed by its description and the environment variable, CLI flag, YAML key, and default that apply to it. Generated from `codersdk.DeploymentValues` so it stays in sync. - **Generator + `make gen` wiring** (`scripts/configdocgen/`): new binary plus a Makefile target and `GEN_FILES` entry, mirroring the existing `clidocgen` / `auditdocgen` pattern. Output is host-independent (same env normalization as `clidocgen`). - **Demo conversion** (`docs/admin/users/github-auth.md`): inverted to lead with the `/etc/coder.d/coder.env` env-var form; the CLI-flag form becomes a closing note that links to the reference. H2 slugs preserved. - **Style guide** (`.claude/docs/DOCS_STYLE_GUIDE.md`): documents the env-var-first convention for admin/setup docs. - **Navigation**: manifest entry under Administration → Setup, plus a TIP callout on the setup index. ## Risk Docs + gen pipeline only; no runtime change. The page is regenerated by `make gen`; the `gen` and `check-docs` CI checks pass. ## Follow-up Several other admin pages still lead with flag walls. Recommend sweeping them incrementally in separate PRs rather than expanding scope here. <details> <summary>Implementation notes (provenance, conflict resolution, verification)</summary> - Continues prior work by @aslilac and @bpmct from the `kayla/docs-env-vars-first` branch. Both original commits are cherry-picked here with authorship preserved. - Rebased onto current `main`. Resolved two `Makefile` conflicts where `main` had since added the `feature-stages.md` gen target at the same locations; kept both targets (union) in `GEN_FILES`, `gen/mark-fresh`, and the recipe block. - The original branch's checked-in page predated recent `codersdk.DeploymentValues` changes, so it was **regenerated** against current `main` (adds `CODER_SCIM_USE_LEGACY`, the `Networking / Cluster` section with `CODER_CLUSTER_HOST`, `CODER_BOUNDARY_LOG_RETENTION`, and the AI Gateway description rename). The `gen` CI check enforces this stays current. - Fixed flag-link anchors for short-form flags (`--config`, `--log-filter`): the generator derives the anchor from `FlagShorthand` to match `clidocgen`'s heading (e.g. `#-l---log-filter`). - `linkspector` ignores the AWS Bedrock base URL that appears as an illustrative `<region>` placeholder in an option description, consistent with the existing `openai.com` ignore patterns. </details> <details> <summary>Configuration reference layout (2026-07-08 update)</summary> Reworked the reference from a wide table into a nested, per-setting list so it fits without horizontal scrolling and stops repeating the group name in every heading: - **List, not table.** Each option renders as a heading, its description, and a bullet list of only the configuration methods that apply to it (non-applicable methods are omitted instead of shown as `-`). - **Nested sections.** Sections nest by the serpent group hierarchy, so `Email / Email Authentication` becomes `Email` (h2) with an `Email authentication` (h3) subsection instead of a redundant flat title. - **Shorter, sentence-case headings.** The redundant group prefix is stripped from each option name and the remainder is lowercased to sentence case, preserving acronyms and mixed-case tokens (`URL`, `TLS`, `OAuth2`, `GitHub`) plus a small proper-noun allowlist (`Coder`, `Terraform`, `Honeycomb`, `Anthropic`, `Bedrock`, ...). Example: `AI Gateway Send Actor Headers` becomes `Send actor headers`. - **Deprecated options** sort to the end of each section and lead with an emphasized **Deprecated** marker. Headings stay clean (no `(deprecated)` suffix) so their anchors remain stable. - **Section intros** render from a group's `Description` when the source defines one (e.g. DERP); no hand-maintained prose or links are introduced. All transformations run in pure Go at `make gen` time (no AI at generation time). Generation is idempotent, and `markdownlint` and `golangci-lint` both pass. </details> --- 🤖 Opened by Coder Agents on behalf of @nickvigilante. Continues work by @aslilac and @bpmct. --------- Co-authored-by: Kayla (via Coder Agents) <kayla@coder.com> Co-authored-by: Coder Agents <noreply@coder.com> Co-authored-by: Ben Potter <me@bpmct.net> |
||
|
|
ee7e7ecb74 |
docs: add a glossary to the reference section (#27165)
Adds a reference glossary at `docs/reference/glossary.md` that defines
the Coder-specific terms and product names readers encounter across the
docs, and registers it in `docs/manifest.json` under **Reference**.
The page prioritizes the term collisions around "agent": it
disambiguates Coder Agents (the AI product), the workspace agent (the
in-workspace daemon), and the `coder_agent` Terraform resource with an
`[!IMPORTANT]` callout and cross-referenced entries. Every definition
was checked against the current docs, and every internal link is a
relative path that resolves in-repo.
Decision log and verification notes
**Scope of this PR**
- Creates the glossary page (`docs/reference/glossary.md`) and its
manifest entry.
- Adds glossary drift-prevention guidance in response to review feedback
(see **Follow-up from review** below).
- Out of scope (tracked separately): `Glossary: ` cross-link callouts
across the docs IA, the search-ranking boost, the workspace-daemon
rename decision, and the automated glossary lint guard
([DOCS-604](https://linear.app/codercom/issue/DOCS-604)).
**Follow-up from review (@bpmct)**
Ben flagged the risk that terms get introduced, renamed, or deprecated
without the glossary keeping up. Addressed in commit
|
||
|
|
fc24c27dfd |
fix: reserve chat hook dispatch capacity for running turns (#27656)
## Context Follow-up fix from live UAT of the merged chat lifecycle hooks stack (#27430). Its companion UAT fix (#27655) has merged, so this targets `main` directly. ## Why? UAT measured a burst of 1,500 concurrent chat creations against a consumer with 1.2s latency. 255 were admitted and 1,245 got `502 hook_dispatch_failed (over_capacity)`, which is correct fail-closed behavior. The collateral wasn't: the same burst failed 24 `stop` dispatches, parking chats that had already been admitted and had already executed tools. One 256-slot semaphore served every event, so new-work admission could take every slot and kill turns in flight. Callers now classify each dispatch as admission or generation, and admission draws from a 192-slot gate held *before* the shared pool. At least 64 shared slots stay reachable only by dispatches for work a chat already admitted. The dispatcher is per `coderd` replica, so these limits are per replica, not deployment-wide, and the docs say so. **The caller classifies, not the event type.** Event type isn't a reliable proxy in either direction: a subagent spawn dispatches `user_prompt_submit` from inside a running turn, and the edit path dispatches `session_start` at admission time. `CapacityClassUnset` is rejected in `Dispatch`, so a new call site fails closed rather than silently inheriting a share. **Acquisition order is load-bearing.** Admission takes its own gate first. Taking a shared slot first would let admissions queued on the gate occupy the very capacity the reserve protects. `acquireCapacity` is the only path that takes either pool, so the order can't be bypassed. ## What this does not guarantee Nothing bounds how many turns generate concurrently, so the 192/64 split is a judgement call, not a derived ceiling. This stops an *admission* burst from consuming every slot; it does not make the remainder sufficient. A large enough generation load can still exhaust the reserve and error a running chat. The docs say so explicitly rather than promising a guarantee the code doesn't deliver. Generation can now take all 256 slots, so generation traffic starves admission harder than before. That's the intended priority: rejecting a new prompt is recoverable, ending a turn that already ran tools is not. ## Testing Red-green proved both new tests. Removing the release-on-failure path fails `RefusedSharedAcquireReleasesAdmission` deterministically; removing the expired-deadline check fails `ExpiredDeadlineRefusesFreeSlot` in 18/30 runs. That deadline check fixes a real race found in review. `acquire` previously shared one `time.Timer` across both acquires. Because `select` picks a ready case at random, an admission dispatch could take a slot after its capacity deadline had passed. Measured over 300 trials: 135 late acquisitions, worst overshoot 2.1ms. `acquire` now takes an absolute deadline and refuses an expired one before selecting, which measures 0/300. Go: `coderd/x/agenthooks/...` and `coderd/x/chatd/...`, plus `-race -count=3` on the dispatcher. > Mux opened this PR on Mike's behalf. |
||
|
|
7bd9f5ec93 |
fix: correct authorization header spelling in api docs (#27721)
Corrects the misspelled `Authorizaiton` Swagger header name to `Authorization` in the source annotation and checked-in generated API documentation. This prevents generated API specs and SCIM examples from documenting the wrong HTTP header name. |
||
|
|
df1c0f9710 |
feat: show what a chat lifecycle hook changed (#27655)
## Stack Context Follow-up fixes from live UAT of the merged chat lifecycle hooks stack (#27430). Two PRs: 1. **This PR**: make hook effects visible and correctly attributed in the transcript. 2. [`mike/chat-hooks-uat/dispatch-capacity`]: reserve dispatch capacity so an admission burst can't fail running turns. ## Why? UAT found three ways the transcript misrepresented what a lifecycle hook did. All three are user-visible and share the same surface (`chathooks/effects.go`, `codersdk.ChatMessagePart`, the conversation timeline), so they're reviewed together. **A prompt `input_override` silently discarded attachments.** `ComposeUserPromptContent` replaced the entire submitted part list with one text part, dropping `file` and `file-reference` parts along with their `chat_file_links`. The user saw their attachments vanish with no explanation. The override now replaces submitted *text* parts only and preserves non-text parts in order. A consumer that wants to block attachments uses `deny`, which is the documented mechanism for refusing a submission. **Every user-visible `system` row was labelled "Lifecycle hook".** The timeline keyed the notice off `role === "system"`. That was correct only by accident, because the hook `user_message` was the sole client-visible system row. The backend now emits the notice as a typed `hook-notice` part and the timeline renders on that, so a future system row can't be mislabelled as a policy notice. **Nothing marked a tool call the hook had rewritten.** A consumer could replace tool input via `input_override` and the transcript showed the rewritten input as if the model had produced it. `ChatMessagePart` gains `hook_rewritten`, set from `preflight.Overrides` on the same path that already carries `ToolCallCreatedAt`, and the tool row renders a "Modified by policy" badge. `ToolCall.PolicyProvider` renders the badge itself, at four wrap sites: the `Tool` dispatch wrapper, the `ReadFilesTool` aggregate and its per-file rows, and `ReadFileTimelineBlock` (grouped and single `read_file` rows bypass `Tool`). Renderer props do not include the flag; descendants consume it through the provider context. The badge is emitted by the provider rather than by the shared header because several renderer branches return early without one, including the auth-required `execute` card, a completed `ask_user_question`, and an empty question payload. Those branches would drop the attribution with no type or runtime error, and the gap is not greppable: every renderer file contains a header somewhere, only individual branches do not. Emitting at the provider removes the possibility instead of enumerating the cases. A rewritten call is wrapped in a group labelled by its badge, so one rewritten file inside a merged read is attributed on its own rather than inheriting the group's badge. `HeaderButton` still appends the policy wording to an explicit `ariaLabel`, since an explicit `aria-label` replaces the name computed from descendants. Provider-executed calls are excluded from attribution. Hooks never see them, and duplicate tool-call ID rejection deliberately skips them, so a reused ID would otherwise mark a provider-executed call as policy-rewritten. ## Testing Go: `coderd/x/chatd/...`, `coderd/x/agenthooks/...`, `codersdk/...`, and `coderd -run 'Hook|Chat'`. Frontend: `tsc` plus every `AgentsPage` story; the only failures are `MCP Tool Completed` and `Scroll To Bottom Button Works With Inverse Scroll`, both of which fail on trunk. A registry-wide story asserts every registered renderer shows the badge, verified against three inverted toggles: removing the badge, hiding it with `display:none`, and skipping the provider for one renderer (which names that renderer). Storybook also covers the rewritten subagent spawn, a completed empty question payload, a non-hook system message, and a failed `read_file` guarding the accessible name. > Mux opened this PR on Mike's behalf. |
||
|
|
4245e4e378 |
feat: expose dynamic client registration in deployment settings (#27480)
Adds the admin-controlled OAuth2 Dynamic Client Registration setting landed by #27316 (`GET`/`PUT /api/v2/oauth2-provider/settings`) to the OAuth2 Applications deployment settings page, since it was previously only reachable via the API or `coder oauth2-provider dcr enable|disable`. The page is now tabbed, **Applications** and **Settings**, so DCR has a home that further OAuth2 settings can share (an Initial Access Token setting is a likely next one). The active tab is backed by a `tab` search param, so `?tab=settings` links straight to it, and an unpermitted deep link falls back to **Applications** rather than selecting nothing. On the Settings tab, DCR renders as a titled section with a description, an `Enabled` badge when active, and an Enable/Disable button. Enabling opens a confirmation dialog, since it lets any OAuth2 client self-register against the deployment without prior admin approval (RFC 7591). Disabling is immediate, no confirmation. The control is a button rather than a switch on design feedback: a switch reads as an immediate on/off flip, which conflicts with a confirmation dialog standing in front of it, and it left the only explanation of the risk inside a dialog that disappears. A button carries the confirmation step without misrepresenting what a click costs, the always-visible description explains the setting on the page, and the `Enabled` badge gives the active state a persistent indicator. The layout follows Tracy's mockup on `tj/oauth2-apps-pagination`; the apps-table pagination work that shares that branch is deliberately not included here. Visibility and editability are gated on the same `ResourceDeploymentConfig` RBAC checks the endpoint itself enforces (`viewDeploymentConfig` / `editDeploymentConfig`), not a separate hardcoded check. The view takes the settings values as one optional `settings` prop, absent when the viewer lacks `viewDeploymentConfig`, so "cannot view" is the shape of the prop rather than a flag the caller keeps consistent with the values beside it, and the tab is not rendered at all. Closes https://github.com/coder/coder/issues/27432 ## Where this sits in the request path ```mermaid sequenceDiagram autonumber actor Admin participant View as OAuth2AppsSettingsPageView<br/>(Tabs + Enable/Disable + Dialog) participant Page as OAuth2AppsSettingsPage<br/>(React Query) participant S as coderd Note over Page: On mount Page->>S: GET /api/v2/oauth2-provider/settings S-->>Page: { dynamic_client_registration_enabled } Page-->>View: settings: { dynamicClientRegistrationEnabled, canEdit, ... } Note over Admin,View: Admin opens the Settings tab and enables DCR Admin->>View: click "Enable" View->>View: open confirmation dialog<br/>(no request sent yet) Admin->>View: click Confirm View->>Page: settings.onDynamicClientRegistrationChange(true) Page->>S: PUT /api/v2/oauth2-provider/settings<br/>{dynamic_client_registration_enabled: true} S-->>Page: 200 OK (audited) Page->>S: GET /api/v2/oauth2-provider/settings (refetch) S-->>Page: { dynamic_client_registration_enabled: true } Page-->>View: section shows the "Enabled" badge and a Disable button Note over Admin,View: Admin disables DCR Admin->>View: click "Disable" View->>Page: onDynamicClientRegistrationChange(false)<br/>(no dialog, disable is immediate) Page->>S: PUT ... {dynamic_client_registration_enabled: false} S-->>Page: 200 OK (audited) ``` ## Files changed All 10 files are hand-written; nothing in this PR is `make gen` output. | File | What changed | |---|---| | `site/src/api/api.ts` | New `getOAuth2ProviderSettings`/`putOAuth2ProviderSettings` methods, thin typed wrappers around the two endpoints #27316 added to `main`. | | `site/src/api/api.test.ts` | Covers both methods against the request they issue and the error they propagate. | | `site/src/api/queries/oauth2.ts` | A `getSettings` query and a `putSettings` mutation that invalidates the settings key on success. Both the app and settings keys now derive from a shared `oauth2ProviderKey` constant. | | `site/src/api/queries/oauth2.test.ts` | 4 tests: the key nesting, both delegations, and that a successful update invalidates the settings key without touching app queries. | | `.../OAuth2AppsSettingsPage.tsx` | Wires query and mutation into the page and passes the settings values down as one object, or omits it entirely without `viewDeploymentConfig`. The apps error stays its own prop, since the view gates the applications empty state on it. | | `.../OAuth2AppsSettingsPageView.tsx` | `Tabs` splitting Applications from Settings. The settings tab distinguishes loading, failed, and a value the server omitted rather than rendering nothing, and the header's "Add application" action is scoped to the applications tab. | | `.../OAuth2AppsSettingsPageView.stories.tsx` | 14 stories, covering the tab wiring, both permission boundaries, the header action's scope, and the settings tab's loading, fetch-error, update-error, and value-omitted states. | | `.../DynamicClientRegistrationSetting.tsx` | The section itself: heading, description including what disabling does not undo, `Enabled` badge, a permission explanation when the viewer cannot edit, and one button that confirms only in the enable direction. | | `.../DynamicClientRegistrationSetting.stories.tsx` | 11 stories, including focus surviving an in-flight request and the dialog ignoring a value that changes underneath it. | | `docs/admin/integrations/oauth2-provider.md` | Adds the web UI route to the DCR section, which previously enumerated only the CLI and the management API. | ## Suggested review order Follows the direction data actually flows, from the raw HTTP call up to the rendered section. 1. **`site/src/api/api.ts`**: the two new methods. Confirms they match the `codersdk.OAuth2ProviderSettings` shape #27316 landed and sit next to the existing OAuth2 app methods they mirror. 2. **`site/src/api/queries/oauth2.ts`**: the query/mutation pair. The mutation's `onSuccess` → `invalidateQueries` is the one detail worth double-checking: it's what makes the on-screen state catch up with what was just saved, rather than trusting the PUT payload. 3. **`OAuth2AppsSettingsPage.tsx`**: the container. Check the two separate permission gates (`viewDeploymentConfig` on the query's `enabled` option, `editDeploymentConfig` on the button's editability) match the RBAC the backend enforces. 4. **`OAuth2AppsSettingsPageView.tsx`**: the tabs and the settings tab's four states. The `settings` prop being optional is what hides the tab; the error inside the tab is deliberately separate from the page-level `error`, which gates the applications empty state. 5. **`DynamicClientRegistrationSetting.tsx`**: the section. Two things worth reading closely: the enable path opens the dialog while the disable path calls straight through, and lacking permission uses the native `disabled` attribute while an in-flight request uses `aria-disabled`, so a keyboard user is not blurred mid-flip. 6. **The two story files**: read last, as they exercise everything above without a real server. The dialog stories query `canvasElement.ownerDocument.body` rather than `canvasElement`, since the dialog renders into a portal attached to `<body>`. ## Deliberately not in this PR - **ENG-3116**: the applications list cannot distinguish self-registered clients from admin-created ones. Surfacing that needs a new field on `codersdk.OAuth2ProviderApp`, which is an API addition this PR does not need. - **ENG-3118**: reusing the shared `EnabledBadge` and `SettingsHeader` primitives for this section. Both hinge on what the mockup intends, and the badge in particular is a visible change either here or on the four other pages that share it. ## Screenshots Default (disabled): <img width="1676" height="497" alt="image" src="https://github.com/user-attachments/assets/cfa60266-8678-410e-9577-16ef474491e3" /> Enabling (confirmation dialog): <img width="1661" height="558" alt="image" src="https://github.com/user-attachments/assets/a7d54fdd-f65d-4fec-9ed9-3bfdcfdae5be" /> Enabled: <img width="1666" height="559" alt="image" src="https://github.com/user-attachments/assets/d39251c2-771c-4608-81c2-dda151b35c3d" /> --------- Co-authored-by: Tracy Johnson <tracy@coder.com> |
||
|
|
07073024ee |
docs: update 2.35 latest release to v2.35.3 (#27511)
Updates the release calendar in `docs/install/releases/index.md` so the 2.35 mainline row points at the latest patch release `v2.35.3` instead of `v2.35.2`. > [!NOTE] > This PR was generated by Coder Agents on behalf of @mtojek. --------- Co-authored-by: Nick Vigilante <nickvigilante@users.noreply.github.com> |
||
|
|
8886a5749a |
feat: add network calls list to AI session threads API (#27425)
The AI session threads API returned only a network call *summary* (total/blocked counts + top domains). This adds the per-call list so the session detail can render individual Agent Firewall network calls. `ListAIBridgeSessionNetworkCalls` reuses the same sequence-number windowing as the existing summary and includes all protocols. The list is exposed as `network_call_logs` on the threads response and is capped server-side at 100 rows. The summary (`network_calls.total`/`blocked`) remains authoritative for whole-session totals: the list length and its blocked count equal the summary only when a session has at most 100 calls, and are truncated beyond that. ### PR map (merge strictly bottom-up) This change is a 4-PR stack. Each PR depends on all the ones below it, so merge in this exact order: 1. #27417 — backend network summary 2. #27418 — frontend summary rows 3. #27425 — backend per-call list `network_call_logs` 4. #27426 — frontend network-calls panel Refs AIGOV-464 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3f1973f45c |
docs: document AI Gateway cost controls (#27643)
### Description Adds documentation for AI Governance Cost Control, including how administrators configure budgets, how effective groups are resolved, how enforcement works, and where spend reporting is available. ### Changes - Replace the placeholder cost control page with a full admin guide - Document deployment settings, group budgets, user overrides, and effective group resolution - Explain estimated spend, unpriced models, notifications, enforcement, and spend reporting - Add migration guidance for Coder Agents Cost Control - Add screenshots for group budgets and user overrides Closes [AIGOV-476](https://linear.app/codercom/issue/AIGOV-476/add-documentation-for-ai-bridge-cost-controls). > [!NOTE] > Initially generated by Coder Agents, modified and reviewed by @ssncferreira |
||
|
|
3f3fd1c4d7 |
feat: show network request summary on AI session detail card (#27418)
Frontend for the AI session network summary. Adds Network calls, Blocked network requests, and Top domains rows to the Session summary card on the individual AI session detail page, driven by the network fields on the session threads response. Renders "Disabled" when network monitoring was not active and "No activity" when there were no calls. Covered by Storybook stories for each state. ### PR map (merge strictly bottom-up) This change is a 4-PR stack. Each PR depends on all the ones below it, so merge in this exact order: 1. #27417 — backend network summary 2. #27418 — frontend summary rows 3. #27425 — backend per-call list `network_call_logs` 4. #27426 — frontend network-calls panel Refs AIGOV-463 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Cian Johnston <cian@coder.com> |
||
|
|
841a1765f7 |
feat: add network calls summary to AI session threads API (#27417)
Backend for the AI session network summary. Exposes total/blocked
network calls and top destination domains on the session threads
endpoint (`GET /api/v2/ai-gateway/sessions/{id}`).
Total and blocked reuse the existing Agent Firewall aggregation from the
sessions list query, so the numbers match the sessions table. Top
domains are a new server-side aggregation
(`GetAIBridgeSessionTopDomains`) over boundary logs, using the same
interception-window correlation. There is no network-error state,
matching the current data model.
Frontend consuming these fields is in a separate stacked PR.
### PR map (merge strictly bottom-up)
This change is a 4-PR stack. Each PR depends on all the ones below it,
so merge in this exact order:
1. #27417 — backend network summary (base `main`)
2. #27418 — frontend summary rows (base #27417)
3. #27425 — backend per-call list `network_call_logs` (base #27418)
4. #27426 — frontend network-calls panel (base #27425)
Refs AIGOV-463
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cian Johnston <cian@coder.com>
|
||
|
|
95a2c2ba02 |
feat: back the per-chat cost endpoint with AI Gateway data (#27328)
## Stack Context
This stack removes native chat cost tracking and native chat usage
limits, making the AI Gateway the single source of AI spend data and
budget enforcement.
1. **This PR:** re-back the per-chat cost endpoint with AI Gateway data.
2. Remove native chat usage limits end to end, rewiring the sidebar
indicator to gateway spend.
3. Remove native chat cost tracking end to end, deleting the
Analytics/Spend cost UI.
## What?
`GET /api/experimental/chats/{chat}/cost` summed
`chat_messages.total_cost_micros`, which native chat cost tracking
maintained. It now aggregates AI Gateway interception data instead, and
has no native fallback.
- New `GetAIBridgeChatCost` query, authorized through the root chat so
members can read their own chat's cost without gaining access to raw
interception rows.
- Response fields renamed: `priced_message_count` -> `request_count`,
`unpriced_messages_having_usage_count` -> `unpriced_request_count`.
- The chat summary sidebar keys its cost cache by root chat, and hides
the cost row where the AI Gateway is off or unlicensed. The root cost is
invalidated when a chat leaves an active status and when a generated
title lands, since title generation bills its own gateway request.
`GetChatModelUsageCostByChatID` and the rest of native cost tracking are
untouched here; PR 3 removes them.
## Why?
Native cost tracking duplicates what the AI Gateway already records, and
the two disagree. Repointing the endpoint first means the cost UI keeps
working while the native implementation is deleted later in the stack.
Two behaviour changes follow from gateway semantics and are intentional:
- **Requests, not messages.** The gateway records interceptions, so
counts are requests. Title-generation traffic now counts.
- **Whole-tree totals.** The gateway records the *spawning* chat's ID as
the interception session ID, so a subagent's requests are attributed to
its immediate parent, not always the root. Only a whole chat tree can be
summed, so the query resolves the root and aggregates the tree, and
every chat in a tree reports the same total. Native returned per-subtree
totals.
## Attribution and counting semantics
The aggregate groups token usage per interception before counting, so
the reported numbers are per request even though a request records one
usage row per provider response:
- `RequestCount` counts finished `Coder Agents` interceptions in the
tree, including unpriced ones.
- `UnpricedRequestCount` counts requests with at least one usage row the
gateway could not price. It is a subset of `RequestCount`.
- `TotalCostMicros` omits only unpriced usage, so a partially priced
request still contributes its priced portion. The sidebar therefore says
`Excludes unpriced usage from N request(s)` rather than claiming whole
requests were dropped.
A recorded cost of zero is a free request, not an unpriced one. Usage
without an effective group is excluded, matching what never reached
`ai_user_daily_spend`.
## Authorization
Reads go through `ExtractChatParam` plus `ResourceChat`, with no
cost-specific RBAC widening. `TestGetChatCost/MemberCanReadOwnChat`
covers a scoped `agents-access` member reading their own chat's cost,
and `MemberCannotReadOtherUsersChat` still asserts 404 for a non-owner.
Plain members without `agents-access` cannot create or read chats at
all, so they never reach this endpoint.
## Known limitation
AI Gateway data has its own retention period, 60 days by default and
configured independently of chat retention, so spend for requests older
than that is no longer reported. A chat whose gateway records have all
been purged reports zero cost, which is indistinguishable from genuinely
free usage under this contract. The endpoint documents the caveat;
#27330 documents it on the Spend Management page.
In-flight interceptions are excluded, since cost is only known once the
response is recorded. A chat's cost therefore lags the active turn by
one request.
## Rebase note
Rebased onto `main` after #27579 removed the `ai-gateway-cost-control`
experiment. The per-chat cost row is now gated on the `aibridge` feature
alone, matching how #27579 degated the other cost-control surfaces.
> Mux prepared this PR on Mike's behalf.
|
||
|
|
54d5eb7ec2 |
feat: add hourly hb_agent_runtime_v1 usage events for Coder Agent runtime (#27312)
closes CODAGT-839 closes CODAGT-843 closes CODAGT-773 ## Summary Adds a new heartbeat usage event type, `hb_agent_runtime_v1`, measuring the total agent-loop runtime of Coder Agents (chats) per UTC hour, plus a reconciler that generates one event per hour with self-healing backfill over a trailing 7-day window. Events flow to Tallyman through the existing publisher unchanged. This measures the new Coder Agents (the `chats` tables), not the deprecated Tasks counted by `dc_managed_agents_v1`. Independent of #27508, which fixes the dead ai-seats cron registration. Both PRs carry the identical `usage_event` create permission hunk for the usage-publisher subject (this feature's generator and the ai-seats cron each need it for heartbeat inserts), so they can land in either order and the overlap merges cleanly. > [!WARNING] > **Do not include this in a release until Tallyman accepts `hb_agent_runtime_v1`.** The publisher marks permanently rejected events as done-forever, and the generator then sees those buckets as complete locally, so their usage would be silently and permanently lost. ## Details Each event's payload is `{"runtime_ms": N}`: the sum of `chat_messages.runtime_ms` for messages created in the hour bucket `[H, H+1)`, across all chats (sub-agents, API-created, archived, and soft-deleted messages included). Events use deterministic IDs (`hb_agent_runtime_v1:<bucket start>`) with `created_at` set to the bucket start, so concurrent replicas race safely via `ON CONFLICT (id) DO NOTHING` without locking, and daily rollups attribute backfilled hours to the correct day. Idle hours produce zero-valued events. A bucket becomes eligible 5 minutes after it closes; hours missing for longer than the 7-day window are forfeited, which can only undercount. Note that this makes `usage_events.created_at` explicitly the *event occurrence time* rather than the row insertion time; the two only diverge for backfilled events. It already behaved as the occurrence timestamp (it drives the daily rollup day and is shipped to Tallyman/Metronome as the event timestamp), and the migration now documents this with a `COMMENT ON COLUMN`, which also surfaces as a Go doc comment on `UsageEvent.CreatedAt`. The new `usage.Generator` runs unconditionally in enterprise builds; the `publish_usage_data` license flag continues to gate egress only, so air-gapped deployments still fill their local ledger. The `aggregate_usage_event()` trigger sums `runtime_ms` per day into `usage_events_daily` (unlike `hb_ai_seats_v1`, which takes the daily max). `InsertHeartbeatUsageEvent` now takes an explicit `createdAt` so generators can backfill historical buckets; the cron passes `clock.Now()` to preserve its existing behavior. ## Tallyman follow-up <details> <summary>Prompt for the Tallyman-repo agent</summary> > **Task**: Add support for the new Coder usage event type `hb_agent_runtime_v1` so Tallyman accepts, validates, and forwards it to Metronome. > > **Background**: coder/coder PR (this PR) adds hourly heartbeat events measuring Coder Agent runtime. Events arrive via the existing `/api/v1/events/ingest` endpoint with: `event_type: "hb_agent_runtime_v1"`, `event_data: {"runtime_ms": <int64 >= 0>}`, deterministic `id` of the form `hb_agent_runtime_v1:2026-07-15_14:00:00` (UTC hour bucket start), and `created_at` set to the bucket start (may be up to ~8 days in the past due to backfill; within Metronome's 34-day dedup window). Zero-value events are normal (idle hours). > > **Work**: > 1. Update Tallyman's vendored/imported `coderd/usage/usagetypes` (or equivalent) to the coder/coder commit that adds `UsageEventTypeHBAgentRuntimeV1` and `HBAgentRuntime`. > 2. Ensure ingestion validation accepts the type (`Valid()` switches) and rejects negative `runtime_ms`. > 3. Ensure Metronome forwarding maps the event with transaction ID derived from the event `id` as for existing types, passing `runtime_ms` through as the property for a SUM-aggregated billable metric ("Coder Agent Hours" = `SUM(runtime_ms) / 3,600,000`). > 4. Do NOT permanently reject unknown-but-well-formed future `hb_*` types if avoidable; at minimum confirm current behavior for unknown types (temporary vs permanent rejection) and report it. > 5. Tests: ingest accept/validate, dedup by ID, Metronome payload mapping. > > **Constraint**: this must be deployed to tallyman-prod **before** any coder/coder release containing the event generator; coderd treats permanent rejections as terminal per event. </details> |
||
|
|
18128b7b52 |
docs: add standalone AI Gateway docs (#27592)
Documents standalone AI Gateway deployment, Gateway key authentication, monitoring, and the updated embedded vs standalone topology in the AI Gateway docs. --------- Co-authored-by: Cian Johnston <cian@coder.com> |
||
|
|
3deecb481e |
chore: remove ai-gateway-cost-control experiment flag (#27579)
## Description Closes [AIGOV-443](https://linear.app/codercom/issue/AIGOV-443/remove-ai-gateway-cost-control-experiment-flag-once-feature-is-stable). The AI Gateway cost control feature is planned for GA on the upcoming release, so this removes the `ExperimentAIGatewayCostControl` experiment and all of its gating. The cost control API endpoints remain gated by the `FeatureAIBridge` license feature (the AI Governance add-on), so this only drops the experiment layer. ## Changes - **`codersdk/deployment.go`**: remove the `ExperimentAIGatewayCostControl` const, its `DisplayName()` case, and its `ExperimentsKnown` entry. - **`enterprise/coderd/coderd.go`**: remove the `httpmw.RequireExperiment(...)` gating from the AI cost control routes. They keep `RequireFeatureMW(codersdk.FeatureAIBridge)`. Affected endpoints: - `GET /organizations/{organization}/groups/ai/spend` - `GET /organizations/{organization}/groups/{groupName}/members/ai/spend` - `GET /organizations/{organization}/ai/spend/export` - `GET /groups/{group}/members/ai/spend` - `GET /groups/{group}/ai/spend` - `GET/PUT/DELETE /users/{user}/ai/budget/override` and `GET /users/{user}/ai/spend` - **`enterprise/coderd/aibridge_test.go`**: drop the experiment from test setup and remove the now-obsolete `RequiresExperiment` negative-path tests. - **Frontend (`site/src/...`)**: remove the `ai-gateway-cost-control` experiment checks from the cost control UI (Groups pages, user dropdown) and their stories/mocks. The feature is now driven solely by the `aibridge` feature visibility. - **Generated**: regenerated `coderd/apidoc/*`, `docs/reference/api/schemas.md`, and `site/src/api/typesGenerated.ts`. ## Out of scope The dogfood `CODER_EXPERIMENTS` config lives in a separate infra repo, not `coder/coder`. Leaving `ai-gateway-cost-control` there is harmless: unknown experiment values are logged as `"ignoring unknown experiment"` at startup and otherwise ignored, so no ordering dependency or breakage. That cleanup can be a follow-up. <details> <summary>Implementation notes</summary> - Verified how unknown experiments are handled in `coderd/coderd.go` `ReadExperiments`: unknown values produce a warning log and are inert, so removing the definition before the dogfood config is updated is safe. - Noticed the group `ai/budget` routes (`/groups/{group}/ai/budget`) were already gated only by `FeatureAIBridge`, never by the experiment. After this change all cost control routes are uniformly feature-gated, resolving that inconsistency. - Removed an obsolete `RequiresExperiment` subtest in `TestUserAISpendStatus` that only asserted a 403 from the experiment gate; with the gate gone it would no longer be blocked pre-RBAC. </details> --- _This PR was created by Coder Agents on behalf of @ssncferreira._ |
||
|
|
d6a5c8e9f8 |
refactor: make user AI budget and spend endpoints consistent (#27611)
## Description
Makes the user AI cost control endpoints consistent.
## Changes
- Replaces the flat `spend_limit_micros` and `limit_source` fields on
`GET /users/{user}/ai/spend` with a nested `effective_budget`, reusing
the type behind `group_budget`. The flat pair made it possible to encode
a limit without a source.
- Renames `AIGroupBudget` to `AIBudgetLimit`, since it also carries
`user_override` limits and is no longer group-specific. The type name is
not part of the wire format.
- Moves `/users/{user}/ai/budget` to `/users/{user}/ai/budget/override`.
The endpoint only ever managed the per-user override, which the type,
the handlers, and the operation IDs all already said; the path was the
only place that didn't.
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
|
||
|
|
e71249a821 |
fix: ai cost control cap configurable AI spend limit (#27640)
## Problem A configured AI spend limit was only validated as `gte=0`, with no upper bound. The group spend query multiplies the per-member limit by the number of attributed members, so a large enough limit overflows `bigint` and fails the whole query, returning an error for every group in the request rather than just the misconfigured one. ## Changes - Add `MaxAISpendLimitMicros`, $1,000,000 per member per budget period. - Reject group budgets and per-user overrides above the maximum with a 400 naming the limit. - Bound both budget forms in the UI so they show the valid range before submitting. Follow-up https://github.com/coder/coder/pull/27589#discussion_r3668956350 Depends on https://github.com/coder/coder/pull/27589 > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira |
||
|
|
4987afada7 |
docs: present AI Governance as included with Premium (#27545)
## Summary AI Governance is now included with Premium licenses instead of being sold as a separate per-user add-on. This updates `docs/` to describe the new packaging, removes "Add-On" from AI Governance references, and refreshes the editions architecture diagram. ## Changes - **`docs/ai-coder/ai-governance.md`**: title is now "AI Governance"; rewrote the licensing statements (previously "a separate, per-user license... not included with a Premium subscription and must be purchased separately") to state it is included with Premium. The usage-pool section now attributes the shared Agent Workspace Build pool to Premium deployments. - **Repeated admonition (28 files under `ai-coder/agent-firewall/` and `ai-coder/ai-gateway/`)**: replaced "requires the AI Governance Add-On / as of Coder v2.32, deployments without the add-on..." with "is part of AI Governance, which is included with a Premium license." The v2.32 add-on gate no longer applies; the gate is now Premium vs. Community. - **`docs/ai-coder/index.md`, `security.md`, `tasks.md`, `usage-data-reporting.md`, `admin/licensing/index.md`, `install/releases/esr-2.29-2.34-upgrade.md`, `ai-gateway/ai-gateway-proxy/setup.md`, `ai-gateway/clients/claude-code.md`**: reworded add-on references to Premium inclusion. - **`docs/manifest.json`**: nav title "AI Governance Add-On" → "AI Governance", updated two descriptions, and swapped the 25 `"state": ["ai governance add-on"]` badges to `["premium"]` so the sidebar badge reads "Premium" instead of "AI Governance Add-On". - **`docs/images/single-region-architecture.png`**: refreshed the diagram in the **Community and Premium editions** tab on [Architecture](https://coder.com/docs/admin/infrastructure/architecture). Also deleted the unreferenced `single-region-architecture.svg` copy. ## Follow-ups outside this PR - The `"ai governance add-on"` doc-state badge is defined in `coder/coder.com` (`src/utils/docs/state.ts`). After this merges, no manifest entry uses that key, so it becomes dead config and can be removed there. - `enterprise/coderd/license/license.go:564-572` still warns admins that "The AI Governance add-on is required to use AI Gateway." That backend string will contradict these docs once shipped. ## Verification - `pnpm run lint-docs`: 0 errors across 504 files - `make lint/emdash`: clean - Vale on the changed Markdown files: 0 errors; remaining warnings are pre-existing gerund headings on untouched lines - `docs/manifest.json` validated as JSON - Confirmed the deleted SVG had no references anywhere in the repo --- PR generated with Coder Agents on behalf of @mattvollmer. |
||
|
|
c17bed25e0 |
feat: wire chat lifecycle hooks into chatd (#27429)
Wires chat lifecycle hooks into chatd, gated by the `agent-lifecycle-hooks` experiment. Part of the lifecycle hooks stack (#27401, #27428, #27430). See `docs/admin/setup/chat-lifecycle-hooks.md` for the consumer-facing contract. ## Summary When a hook URL is configured, chatd dispatches `session_start`, `user_prompt_submit`, `pre_tool_use`, `post_tool_use`, `pre_compact`, `post_compact`, and `stop` events to the consumer and applies its responses. ## Design - **Stateless**: Coder stores no hook dispatch or decision state. Delivery is at least once; consumers deduplicate on stable payload identifiers (chat ID, event type, tool-use ID) and answer duplicates with the same decision. - **Admission-time prompt effects**: `user_prompt_submit` dispatches exactly once per submission (create, send, queue, edit, subagent spawn) and folds its effects into the stored prompt as typed message parts: original-or-overridden user parts, then model-only `hook-context`, then a user-visible `hook-notice`. Hook context is stripped from every client-facing conversion; hook notices are excluded from model prompts. The server rejects hook parts in client-submitted content. - **Tool gating**: `pre_tool_use` allow can override tool input; deny becomes a synthetic denied tool result, with any returned model context persisted as a model-only transcript row so it never reaches clients. The denial text identifies an external policy (the deployment's lifecycle hook) as the source and marks the decision as persistent, so the model explains the denial instead of retrying it or misreporting it as an infrastructure failure. - **Fail closed**: a dispatch failure rejects the triggering request or moves the chat to the error state in the same transaction as the affected step, so a runnable state is never published with unapproved content. - **Admission before persistence**: `pre_tool_use` is dispatched for the calls the model produced, before the assistant message is stored. See "Staged tool admission" below. - **Fresh dispatch per tool call**: every non-provider-executed tool call is decided by its own `pre_tool_use` dispatch; Coder never reuses an earlier decision on the consumer's behalf. Retries re-dispatch the same logical event. ## Structure All hook dispatch flows through one seam: entry points build a `chathooks.Chat` (chat identity) and a `chathooks.Message` (event details) and call `Trigger.Trigger`, the only component that talks to the dispatcher. The integration lives in the `coderd/x/chatd/chathooks` subpackage, split by responsibility: - `trigger.go`: the trigger seam; builds the wire envelope per event, normalizes deny into a typed error, and holds the package's single enabled-check. - `effects.go`: pure conversion of hook results into transcript rows and prompt parts. - `errors.go`: failure classification (dispatch error messages, denial mapping, tool-result dispatch-failure scanning). - `tooluse.go`: the tool-call gate (`pre_tool_use` preflight, `post_tool_use` payloads, applying admitted input to the step). Server-bound glue stays in `coderd/x/chatd/hook_server.go`: the chat-parking dispatch error handlers, the step-commit row insertion wrappers, and the dynamic post-tool-use state loader, which depends on chatd validation types. This PR adopts the `codersdk/x/agenthooks` and `coderd/x/agenthooks/dispatch` import paths introduced at the tip of #27401; intermediate commits still reference the pre-move paths and are not individually buildable. ## Staged tool admission `pre_tool_use` originally ran at tool execution time, which is after the assistant message carrying the tool call was already committed. An `input_override` therefore had to rewrite stored message content in place. @hugodutka pointed out that chatd treats message content as immutable, and that the rewrite was a shortcut rather than a requirement. It was also a correctness problem in its own right: the rewrite only updated the database, so the transcript could show one input while a different one had executed. The hook now runs before the step is persisted: ```text provider stream ends (tool calls complete, in memory) -> pre_tool_use dispatch per call -> ONE transaction: assistant row with admitted inputs, synthetic denials, hook rows -> execute ``` The step is inserted once, carrying the input the tool runs with. `UpdateChatMessageContentByID` and `Tx.UpdateMessageContent` are deleted from #27428, so message content stays immutable. Two consequences, both intentional: - **Clients converge rather than wait.** Tool-call parts still stream live, so a rewritten call briefly shows the model's proposed input before the committed message replaces it. The chat store already clears stream state when an assistant message arrives, so the stored input wins with no frontend change and no added latency before tool cards appear. - **A call already in history was already admitted.** Execution consumes the stored input instead of dispatching a second decision, which keeps one dispatch and one set of hook effects per call. A consumer policy change between admission and execution applies to later calls, not to calls already admitted. The per-chat debug endpoint still records the provider's original tool input. Its purpose is to report provider behavior, and it requires an explicit per-chat debug flag; the invariant here covers the transcript. ## Configuration Adds `chat-hook-url`, `chat-hook-secret`, `chat-hook-timeout`, and `chat-hook-enabled` deployment options with startup validation. The flags are hidden from `coder server --help` while the feature is experimental; the setup guide documents them. ## Tool input validation Built-in tool arguments reach a consumer as raw JSON with key spelling preserved, but the tools decode those bytes with Go, which matches struct fields case-insensitively and keeps the last match. A policy reading `path` could therefore authorize one value while the tool executed another, and a lone case variant such as `{"PATH":"/secret"}` was invisible to a policy checking for `path`. Coder now rejects a built-in tool call whose input repeats a key or spells a schema property with different capitalization, before the `pre_tool_use` dispatch, so a consumer is never asked to authorize bytes whose meaning depends on the reader. Rejected calls produce an error result the model can retry; unambiguous calls in the same batch still run. A consumer-authored `input_override` is rechecked after the dispatch and fails the turn closed, because the model cannot correct it. Dynamic and MCP inputs are excluded because the client and the workspace agent execute those calls rather than coderd. Two paths needed more than a schema check. Execution resolves a deprecated tool name to its canonical tool, so validation resolves aliases first. The `edit_files` decoder also reads `search` and `replace`, which its schema does not advertise, so those aliases are now matched exactly and their case variants ignored. A hook denial now returns a structured 403 carrying `kind: "hook_denied"`, mirroring the dispatch-failure response that already carries its own kind. Without it a client cannot tell a policy decision apart from a generic failure, and the chat UI titled a denial "Request failed". Adding a kind needs no migration: `ChatErrorKind` is persisted only inside the JSONB `chats.last_error` column, whose decoder accepts unknown kinds. The hook docs also correct the tool-input convergence window. A batch dispatches sequentially before the assistant row commits, so the original input stays visible for a span that scales with the number of tool calls in the step rather than a single hook timeout. > This PR was written by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
0b4095085e |
fix: report combined member limit in group AI spend (#27589)
## Problem The organization groups page showed each group's AI budget as the group's per-member limit, so the total it displayed was effectively group members × group budget. That ignores per-user budget overrides charged to the group, so a group where one member has an override reported a limit that doesn't match what its members can actually spend. ## Changes - Add `total_spend_limit_micros` to the organization groups AI spend payload, the combined budget of the members attributed to the group, with each member's override replacing their share. - Return `null` for the total when the group has no budget, since its members spend without a cap. - Both the organization groups and single group spend endpoints report the new field, as they share the same query. - Use the total as the denominator on the groups page AI budget column. Depends on #27568 |
||
|
|
06ceb4253d |
feat: add agent runtime hour license claims and entitlement feature (#27459)
Licenses can now carry three agent runtime hour claims:
`agent_runtime_hours_allocation`, `agent_runtime_hours_limit_soft`, and
`agent_runtime_hours_limit_hard` (unit: hours). They surface as the new
usage-period feature `agent_runtime_hours` in `GET
/api/v2/entitlements`, where `limit` carries the allocation and the new
optional `soft_limit` / `hard_limit` fields on `codersdk.Feature` carry
the thresholds.
Invalid combinations reject the entire license via `validateClaims`
(both at upload and when computing entitlements for stored licenses):
soft/hard without allocation, negative allocation, soft outside `0 <=
soft < allocation`, or `hard < allocation`.
Soft and hard limits are not comparison inputs in `Feature.Compare`;
they ride along with whichever license wins (newest `iat`, existing
behavior). None of the three claim names is a feature name, so old
servers ignore them via the existing unknown-claim tolerance, protecting
rollout of licenses minted with the new claims.
The claim name constants defined in `enterprise/coderd/license` are the
canonical contract for `github.com/coder/license` (X1).
Part of
[CODAGT-837](https://linear.app/codercom/issue/CODAGT-837/a1-agent-runtime-license-claims-and-entitlement-feature).
Blocks B4 (usage wiring + warnings), C1 (hard-limit admission gate), F1
(licenses page), A4 (managed-agent coexistence), X1 (licensor).
Out of scope, handled by follow-up issues: `Actual` usage wiring,
threshold warnings, admission gating, premium defaults, and FE surfacing
beyond regenerated types.
<details>
<summary>Implementation plan and decision log</summary>
## Decisions (confirmed by jaayden, 2026-07-23)
1. **Claim names / unit:**
- `agent_runtime_hours_allocation` - allocation (unit: hours, int64)
- `agent_runtime_hours_limit_soft` - soft limit
- `agent_runtime_hours_limit_hard` - hard limit
- None of the three claim names is itself a `FeatureName`; all three map
to the single new usage-period feature `agent_runtime_hours`
(`FeatureAgentRuntimeHours`), mirroring how `managed_agent_limit_soft`
mapped onto `managed_agent_limit`. Old servers therefore ignore all
three claims via the `FeatureNamesMap` check.
2. **Reject-license.** Invalid claim combinations reject the whole
license via `validateClaims` (upload returns 400 via
`ParseClaimsIgnoreNbf`; already-stored licenses produce an `Invalid
license ... parsing claims` entitlements error and contribute nothing).
## Design notes
- `codersdk.Feature` had a `SoftLimit` field until
|
||
|
|
fbac602456 |
feat!: add admin-controlled dynamic client registration toggle (#27316)
`POST /oauth2/register` (RFC 7591 Dynamic Client Registration) has exactly one gate today: `ExperimentOAuth2`, a static, process-lifetime flag that wraps the entire `/oauth2/*` route tree as an all-or-nothing switch. That flag is scheduled for removal at GA, which would leave DCR with zero admin control at all once it is gone. Add a persistent, DCR-specific `oauth2_dcr_enabled` deployment setting, independent of the experiment system, so admin control over DCR survives GA. `POST /oauth2/register` checks the flag and rejects new registrations with an RFC 7591-shaped `403` when disabled; discovery metadata (`GET /.well-known/oauth-authorization-server`) conditionally omits `registration_endpoint`. A new audited `GET`/`PUT /api/v2/oauth2-provider/settings` endpoint lets an owner toggle it live, no restart required. The setting defaults to disabled, matching the canonical design proposal; disabling only stops new self-registrations, clients that already registered continue to authorize and exchange tokens normally. Address issue described in [ENG-3056](https://linear.app/codercom/issue/ENG-3056/oauth2-dcr-admin-configurable-enabledisable). ## Where this sits in the request path ```mermaid sequenceDiagram autonumber participant A as Admin participant S as coderd participant DB as site_configs<br/>(oauth2_dcr_enabled) participant C as OAuth2/MCP Client Note over A,S: Admin toggles DCR (new) A->>S: PUT /api/v2/oauth2-provider/settings<br/>{dynamic_client_registration_enabled: false} S->>S: authorizeContext(ActionUpdate, ResourceDeploymentConfig) S->>DB: UPSERT oauth2_dcr_enabled = false S-->>A: 200 OK (audited) Note over C,S: Client discovery + registration afterward C->>S: GET /.well-known/oauth-authorization-server S->>DB: GetOAuth2DCREnabled (system ctx, every request, no cache) DB-->>S: false S-->>C: 200 metadata, registration_endpoint omitted C->>S: POST /oauth2/register S->>DB: GetOAuth2DCREnabled (system ctx, every request, no cache) DB-->>S: false S-->>C: 403 invalid_request,<br/>"Dynamic client registration is disabled" Note over C,S: A client that registered before the change is unaffected C->>S: GET /oauth2/authorize?client_id=... Note over S: no DCR-enabled check on this path S-->>C: 200 (proceeds normally) C->>S: PUT/DELETE /oauth2/clients/{client_id} (RFC 7592 self-management) Note over S: no DCR-enabled check on this path either S-->>C: 200 (proceeds normally) ``` ## Files changed: manual vs. generated Reviewers should focus on the **manual** files. The **generated** ones are `make gen` output that follows mechanically from the manual changes and don't need direct review. <details> <summary><b>Manual files (26)</b> — click to expand, grouped the same way as "Suggested review order" below</summary> **1. Database** | File | What changed | |---|---| | `coderd/database/queries/siteconfig.sql` | New `GetOAuth2DCREnabled`/`UpsertOAuth2DCREnabled` query pair on the existing generic `site_configs` table. No schema change. | | `coderd/database/dbauthz/dbauthz.go` | RBAC check (`rbac.ResourceDeploymentConfig`) on the two new query methods; extends the `subjectSystemOAuth2` system-actor role with read-only `ResourceDeploymentConfig` access, needed so the public discovery/registration endpoints can read the flag via `dbauthz.AsSystemOAuth2`. | | `coderd/database/dbauthz/dbauthz_test.go` | RBAC assertion coverage for `GetOAuth2DCREnabled`/`UpsertOAuth2DCREnabled` in the method-coverage test suite. | **2. Request gating (the actual feature)** | File | What changed | |---|---| | `coderd/oauth2provider/registration.go` | The actual gate: `CreateDynamicClientRegistration` reads the flag first and returns an RFC 7591-shaped `403` when disabled (defaults disabled if never configured). | | `coderd/oauth2provider/registration_test.go` | New unit test, `TestCreateDynamicClientRegistration_DCREnabled`: calls the handler directly (no HTTP server), covering enabled / explicitly disabled / never-configured. | | `coderd/oauth2provider/metadata.go` | `GetAuthorizationServerMetadata` conditionally omits `registration_endpoint` from discovery metadata when DCR is disabled. | | `coderd/oauth2provider/metadata_test.go` | New unit test, `TestGetAuthorizationServerMetadata_DCREnabled`: same three states, for the discovery handler. | **3. Admin settings endpoint** | File | What changed | |---|---| | `codersdk/oauth2.go` | New `OAuth2ProviderSettings` SDK type plus `Client.OAuth2ProviderSettings`/`PutOAuth2ProviderSettings` methods. | | `coderd/oauth2.go` | New `oauth2ProviderSettings`/`putOAuth2ProviderSettings` admin handlers (audited via `audit.InitRequest`); updates the `GetAuthorizationServerMetadata` call site to pass `api.Database`. | | `coderd/coderd.go` | Registers `GET`/`PUT /api/v2/oauth2-provider/settings`. | | `coderd/oauth2_provider_settings_test.go` | New test file: admin `GET`/`PUT` round-trip, default-disabled-before-any-`PUT`, and `403` for a non-owner on both `GET` and `PUT`. | **4. Audit wiring** | File | What changed | |---|---| | `coderd/database/types.go` | New `database.OAuth2ProviderSettings` audit-only struct (mirrors `NotificationsSettings`). | | `coderd/audit/diff.go` | Adds the new struct to the `Auditable` type union. | | `coderd/audit/request.go` | Adds the new struct to all four dispatch switches (`ResourceTarget`, `ResourceID`, `ResourceType`, `ResourceRequiresOrgID`). | | `codersdk/audit.go` | New API-facing `ResourceTypeOAuth2ProviderSettings` constant and its `FriendlyString` case. | | `enterprise/audit/table.go` | Field-level audit action map (`ActionTrack`/`ActionIgnore`) for the new struct. | | `coderd/database/migrations/000546_audit_oauth2_provider_settings.up.sql` | Adds `oauth2_provider_settings` to the `resource_type` Postgres enum, required for the audit wiring above (`resource_type` is a real enum, not a Go-only value). | | `coderd/database/migrations/000546_audit_oauth2_provider_settings.down.sql` | No-op (`ALTER TYPE ... ADD VALUE` can't be reverted). | **5. Test-suite ripple from the disabled-by-default flip** | File | What changed | |---|---| | `coderd/oauth2provider/oauth2providertest/helpers.go` | New shared test helper, `EnableDCR`, since DCR now defaults to disabled and many pre-existing tests need it turned on to register a client. | | `coderd/oauth2_test.go` | Adds `TestOAuth2DynamicClientRegistrationDisabled` (registers a client, disables DCR, verifies new registration is rejected while the existing client's self-management, authorize, and token exchange all keep working); calls `EnableDCR` in every pre-existing test that registers a client. | | `coderd/oauth2_error_compliance_test.go` | Calls `EnableDCR` in every test that registers a client, so RFC-error-format assertions aren't masked by the new disabled-by-default gate. | | `coderd/oauth2_metadata_validation_test.go` | Same: `EnableDCR` added to every registration-dependent test. | | `coderd/oauth2_security_test.go` | Same. | | `coderd/oauth2provider/validation_test.go` | Same (near-duplicate of `oauth2_metadata_validation_test.go` in a different package). | | `coderd/oauth2provider/provider_test.go` | Same. | | `coderd/mcp/mcp_e2e_test.go` | Same, for the MCP end-to-end dynamic-registration flow test. | </details> <details> <summary><b>Generated files (12)</b> — from <code>make gen</code>, no need to review directly</summary> `coderd/apidoc/docs.go`, `coderd/apidoc/swagger.json`, `coderd/database/dbmetrics/querymetrics.go`, `coderd/database/dbmock/dbmock.go`, `coderd/database/dump.sql`, `coderd/database/models.go`, `coderd/database/querier.go`, `coderd/database/queries.sql.go`, `docs/admin/security/audit-logs.md`, `docs/reference/api/enterprise.md`, `docs/reference/api/schemas.md`, `site/src/api/typesGenerated.ts`. </details> ## Suggested review order ### 1. Database Establishes the persisted setting and its RBAC rule; everything else builds on `GetOAuth2DCREnabled`/`UpsertOAuth2DCREnabled`. 1. `coderd/database/queries/siteconfig.sql` — the two new queries. Same boolean-encoding pattern as the existing `oauth2_github_default_eligible` key right above them in the same file. 2. `coderd/database/dbauthz/dbauthz.go` — the RBAC wrapper for those two queries, plus the `subjectSystemOAuth2` role extension (search this file for `ResourceDeploymentConfig`, it appears in both spots). 3. `coderd/database/dbauthz/dbauthz_test.go` — asserts the RBAC checks from (2) actually fire. ### 2. Request gating (the actual feature) Where `POST /oauth2/register` and discovery metadata change behavior. 1. `coderd/oauth2provider/registration.go` — the primary gate. Read this first; it's the feature. 2. `coderd/oauth2provider/registration_test.go` — its new unit test, exercising the gate's three states directly against the handler. 3. `coderd/oauth2provider/metadata.go` — the same gating pattern applied to the discovery `GET` endpoint. 4. `coderd/oauth2provider/metadata_test.go` — its new unit test. ### 3. Admin settings endpoint How an owner flips the setting live. 1. `codersdk/oauth2.go` — the `OAuth2ProviderSettings` SDK type and `Client` methods first; this is the public contract everything below implements against. 2. `coderd/oauth2.go` — the `GET`/`PUT` handlers themselves. 3. `coderd/coderd.go` — route registration, to see where those handlers get wired in. 4. `coderd/oauth2_provider_settings_test.go` — round-trip and permission tests. ### 4. Audit wiring Plumbing required so step 3's `PUT` is auditable; mechanical except for (3). 1. `coderd/database/types.go` — the audit-only struct; everything else in this layer exists to plumb it through. 2. `coderd/audit/diff.go` — adds it to the `Auditable` type union (the compiler enforces this one). 3. `coderd/audit/request.go` — the four dispatch switches; the one part of this layer worth reading closely. 4. `codersdk/audit.go` — the API-facing resource type constant. 5. `enterprise/audit/table.go` — the field-action map. 6. `coderd/database/migrations/000546_audit_oauth2_provider_settings.{up,down}.sql` — read last; a consequence of needing a new `resource_type` enum value for (1)-(5), not a design decision of its own. ### 5. Test-suite ripple from the disabled-by-default flip 1. `coderd/oauth2provider/oauth2providertest/helpers.go` — the new `EnableDCR` helper. Read first to understand the fix pattern before seeing it applied repeatedly. 2. `coderd/oauth2_test.go` — next, since it also contains the new `TestOAuth2DynamicClientRegistrationDisabled`, not just `EnableDCR` call sites. 3. The rest, in any order, they're mechanical repeats of the same one-line addition: `coderd/oauth2_error_compliance_test.go`, `coderd/oauth2_metadata_validation_test.go`, `coderd/oauth2_security_test.go`, `coderd/oauth2provider/validation_test.go`, `coderd/oauth2provider/provider_test.go`, `coderd/mcp/mcp_e2e_test.go`. ## Explicitly out of scope Per the design proposal: rate limiting on `POST /oauth2/register` (tracked separately), retroactively affecting already-registered clients when DCR is disabled (this only gates new self-registration), and an Initial Access Token requirement (a separate, follow-up ticket). |
||
|
|
efbf802319 |
feat: add bulk secret import upload to Add secret dialog (PLAT-240) (#26725)
Adds a file dropzone to the create branch of the Add secret dialog (final PR in the PLAT-240 stack, after #26723 and #26724). The browser reads the file, derives the format from the extension (`.env`/`.json`/`.yaml`/`.yml`), and imports via `POST /secrets/batch`; per-entry backend errors surface in an alert and the success toast flags secrets imported without an env name. Storybook play stories and vitests cover the flow. Also documents the upload flow in `docs/user-guides/user-secrets.md`. Closes https://linear.app/codercom/issue/PLAT-240 > Reviewed and updated by Coder Agents on behalf of @dylanhuff-at-coder. |
||
|
|
0b2a6cac78 |
feat: add coder secret import for bulk secret files (#27534)
Adds `coder secret import <file>` to bulk-import dotenv, JSON, or YAML secrets through the existing batch API. The command infers the format from the extension or accepts `--input-format`, supports non-interactive stdin, validates files locally before upload, and warns when imported keys cannot be injected as environment variables. Reviewed and updated by Coder Agents on behalf of @dylanhuff-at-coder. |
||
|
|
1a6a8be96c |
feat: log tailnet tunnels to the connection log (#27423)
Co-authored-by: Chris DiGiamo <cd@anthropic.com> Co-authored-by: Chris DiGiamo <cdigiamo@anthropic.com> |
||
|
|
09a69e624a |
feat: search users by display name (#27398)
Free-text member search previously matched only username and email, so typing a person's display name returned no results even though the UI shows the display name as the primary label. This broadens the free-text `@search` filter to also match `users.name`. The change is in three queries: `GetUsers`, `PaginatedOrganizationMembers`, and `GetGroupMembersByGroupIDPaginated`. This covers every server-filtered surface: the Users page, the Organization Members page, the Group Members page, and the `UserAutocomplete` / `WorkspaceUserAutocomplete` pickers (which query `GetUsers` with `q`). The org member picker (`MemberAutocomplete`) filters client-side via cmdk, so display name is added to its `keywords`. Explicit filters (`name:`, `username`/`email`) and pagination counts are unchanged; the group members count still comes from the filtered `COUNT(*) OVER()` in the same query. Refs DEVEX-484 Refs DEVEX-565 <details> <summary>Implementation plan</summary> ## Problem Member search (both the global Users page and the Organization Members page) matches only on `username` and `email`. It does not match on the user's display name (`users.name`), even though the Organization Members table shows `name` as the primary title. So typing a person's full name in the search box returns nothing. Today a bare search term (`alice`) is routed to the SQL `@search` filter, which only checks `email`/`username`. Display name is only matched if the user explicitly types `name:alice`, which is undiscoverable. ## Design decision Include `name` in the free-text `@search` condition in the affected SQL queries. A bare term then matches `email OR username OR name`, using the same case-insensitive substring `ILIKE` already in place. This keeps the existing explicit `name:` filter working. Tradeoff: this broadens the meaning of free-text `search` globally (anything using these queries now also matches display name). This is the intended behavior, confirmed against DEVEX-565 (display name search in the user picker). ## Affected files Backend: - `coderd/database/queries/users.sql` (`GetUsers`) - `coderd/database/queries/organizationmembers.sql` (`PaginatedOrganizationMembers`) - `coderd/database/queries/groupmembers.sql` (`GetGroupMembersByGroupIDPaginated`) - `coderd/database/queries.sql.go` regenerated via `make gen` Frontend: - `site/src/components/UserAutocomplete/UserAutocomplete.tsx` (add `name` to client-side cmdk keywords) Tests: - `coderd/coderdtest/users.go` (shared `UsersFilter` helper): added a `DisplayNameSearch` case and extended search-based expectations to include `name`. Exercised by `TestGetUsersFilter`, `TestGetOrgMembersFilter`, and `TestGetGroupMembersFilter`. Docs: - `docs/admin/users/index.md`: documented that free-text search matches username, email, and display name. ## Frontend surface coverage | Surface | Sends | Backend | Query | |---|---|---|---| | Users page | `q` | `GET /users` | `GetUsers` | | Organization Members page | `q` | paginated members | `PaginatedOrganizationMembers` | | Group Members page | `q` | `groupMembers` | `GetGroupMembersByGroupIDPaginated` | | User pickers (server-filtered) | `q` | `GET /users` | `GetUsers` | | Org member picker (client-filtered) | local cmdk | n/a | keyword change | ## Out of scope - Trigram/similarity (fuzzy) matching; keeps `ILIKE` substring semantics. - Sort/pagination ordering (still `LOWER(username)`). </details> --- _Created by Coder Agents on behalf of @aqandrew._ |
||
|
|
85984ff142 |
feat: add enable/disable support for user secrets (#27537)
Users can now disable a secret to stop it from being injected into workspaces without deleting it, and re-enable it later. Disabled secrets stay visible and editable everywhere they already appear. An enabled secret must have at least one injection target; a secret with no target can be stored only while disabled. Existing target-less secrets are migrated to disabled to preserve current behavior. Support spans the REST API, SDK, CLI, dashboard, and audit log. |
||
|
|
3c61a9a939 |
chore(docs): update release docs for v2.34.7 (#27591)
Automated docs update for v2.34.7 release. Created by `releasetui`. |
||
|
|
be226409b8 | fix: delete the unused ChatMessagePart.Signature field (#27588) | ||
|
|
8ea2586189 |
feat: add chat lifecycle hook dispatch backend (#27401)
Adds the chat lifecycle hook wire contract and dispatch plumbing, first PR of the lifecycle hooks stack (followed by #27428, #27429, #27430). - `codersdk/x/agenthooks`: event and response wire types, JWT creation and verification with the shared secret (HS256, request body digest, expiry and not-before freshness checks), and an HTTP handler helper so consumers only implement the events they use. The `codersdk/x` location marks the consumer SDK as experimental. - `coderd/x/agenthooks/dispatch`: a stateless dispatcher that signs and posts hook events, enforces a concurrency cap under one configured timeout that bounds both the capacity wait and both post attempts, retries one connection failure with the same JWT, sends a distinctive `coderd-agenthooks/<version>` User-Agent, and records Prometheus metrics. Delivery is at least once; consumers own durable decision state, audit records, and deduplication keyed by the stable payload identifiers. Nothing is persisted by Coder. - Response bodies decode strictly: unknown fields, duplicate JSON keys (including inside `input_override`), and trailing data fail the dispatch closed as protocol errors instead of silently reading as allow. - `coderd/util/xnet`: shared timeout and connection error classification used by the dispatcher retry logic. Transient HTTP/2 stream aborts count as connection errors, so the documented single retry also applies to h2 consumers, which is the shape Go's default transport negotiates against any TLS consumer. Deterministic protocol failures stay terminal. Only the struct form of a stream error is matched, because `net/http` bundles its own HTTP/2 types and `h2_error.go` bridges only that shape. - `scripts/agenthooks-server`: a reference consumer that logs events and demonstrates consumer-owned pre-tool decision deduplication. It requires an explicitly configured JWT audience rather than deriving one from the request, and its startup output names the mode it is running in so an operator can see that the example policy flags need `-log-only=false`. - `scripts/apitypings`: generate TypeScript types for the hook wire contract. Dispatch failures log without the error's stack frames, since a failed dispatch is an expected, operator-visible condition. Nothing dispatches these events yet; chatd wiring lands in #27429. > This PR was written by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
ed37483ff7 |
feat: add group AI spend endpoint (#27568)
## Description
Adds `GET /api/v2/groups/{group}/ai/spend`, returning the AI spend limit
and aggregate spend for a single group over the current budget period.
The period is derived from the deployment's configured budget period
rather than being caller-specified, matching the other AI spend
endpoints.
## Changes
- Add the `groupAISpend` handler and route, gated by the
`aigateway-cost-control` experiment and the `AIBridge` feature.
- Reuse the existing `GetOrganizationGroupsAISpend` query with a single
group ID, so no new query or authorization path is introduced.
- Add the `GroupAISpend` codersdk type and client method.
Closes
https://linear.app/codercom/issue/AIGOV-475/implement-apiv2groupsgroupaispend
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
|
||
|
|
e96e7cfec2 |
docs(docs): add AI Gateway cost controls placeholder page (#27570)
## Summary Adds a placeholder "Cost Controls" page under AI Gateway in the docs, plus its `manifest.json` navigation entry. This is a stub with a title only; the full content will be written in a follow-up. Relates to [AIGOV-476](https://linear.app/codercom/issue/AIGOV-476/add-documentation-for-ai-bridge-cost-controls). Related to [internal slack thread](https://codercom.slack.com/archives/C096PFVBZKN/p1785150528587409). ## Changes - Add `docs/ai-coder/ai-gateway/cost-controls.md` placeholder page - Register the page in `docs/manifest.json` under AI Gateway (after Monitoring) --- > [!NOTE] > This PR was generated with Coder Agents. |
||
|
|
c3895ff9c0 |
feat: add CSV export for AI spend data (#27491)
## Description
Adds `GET /api/v2/organizations/{organization}/ai/spend/export`,
returning `text/csv` with per-user, per-group, per-model, per-provider
aggregated AI spend. The data is built from the raw AI Gateway token
usage tables rather than the `ai_user_daily_spend` rollup, but stays
consistent with it: spend is attributed through the token usage's
effective group and bucketed by the token usage `created_at`, the same
values the daily rollup derives from.
The period defaults to the current UTC month, narrowed to the configured
AI Gateway retention window when the month begins before retained data
does. Explicit `period_start`/`period_end` params must be provided
together, are interpreted as UTC, and may span at most 31 days. Unlike
the default period, an explicit period that begins before the retention
window is rejected rather than narrowed. Every row echoes the applied
bounds, so a narrowed window is visible in the export.
The endpoint requires organization-level admin permissions.
## Changes
- Add the `ExportOrganizationAISpend` query aggregating
`aibridge_token_usages` joined to `aibridge_interceptions`, scoped to
the organization via the effective group, resolving the username, group
name, and organization name alongside their IDs.
- Add the `exportOrganizationAISpend` handler and route, gated by the
`aigateway-cost-control` experiment and the `AIBridge` feature,
returning the CSV in a single response.
- Add the `ExportOrganizationAISpend` codersdk client method.
- Require organization-wide `ResourceGroupMember` read, since the export
aggregates every user in the organization. The per-row filter stays in
`dbauthz` as defence in depth.
- Escape leading formula characters in the free-text columns, so a model
or provider name recorded from an intercepted request cannot be
evaluated when the CSV is opened in a spreadsheet.
- Add an index on `aibridge_token_usages (effective_group_id,
created_at)`, which the period and group predicates otherwise cannot
use.
Closes
https://linear.app/codercom/issue/AIGOV-293/add-csv-export-for-ai-spend-data
> [!NOTE]
> Generated by Coder Agents on behalf of @ssncferreira
|
||
|
|
c351280a37 |
feat: add Prometheus metrics for AI Governance cost control (#27490)
## Description Adds Prometheus metrics for AI budget cost control, emitted by the aibridged server under the `cost_control` subsystem (full names are prefixed `coder_ai_gateway_`). - `blocked_requests_total` (counter) — labels: `group_id` - `blocked_users` (gauge) — labels: `group_id` - `unpriced_requests_total` (counter) — labels: `provider`, `model` - `enforcement_duration_seconds` (histogram) — labels: `outcome` ## Changes - Add `GetOverBudgetUsersPerGroup` query (plus dbauthz/dbmetrics/dbmock wiring) to count over-budget users per effective group. - Add a background collector that refreshes the `blocked_users` gauge on an interval, started only when Prometheus is enabled. - Wire `Metrics` through the aibridged server, coderd API, `cli/server.go`, and the enterprise AI gateway handler; recording is nil-safe when metrics are unset. Closes https://linear.app/codercom/issue/AIGOV-296/add-prometheus-metrics-for-cost-control > [!NOTE] > Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira |
||
|
|
bfcfb71860 |
fix: show 'Unset' for missing providers in AI models list (#27400)
## Summary
Frontend-only fixes for the `/ai/settings/models` page:
1. **Provider column displays "Unset"** with an info tooltip when a
model's provider has been deleted, instead of "N/A".
2. **Models without a usable provider display as "Disabled"** in the
list, regardless of the stored `enabled` flag. Covers both missing
(soft-deleted) and disabled providers.
3. **Save button re-enabled when only the provider changes** on the edit
page (previously the button stayed disabled because provider changes
lived outside the formik state).
## Scope
Frontend only. The DB constraint
`chat_model_configs_ai_provider_required_when_active` already prevents a
non-deleted model from having a NULL `ai_provider_id`; CODAGT-709
addresses the server-side cascade when a provider is deleted.
## Changes
- `ModelsPageView.tsx`: two `useMemo` maps (`hasProviderByModelId`,
`providerEnabledByModelId`) passed to `ModelRow`.
- `ModelRow.tsx`: `isEffectivelyEnabled = model.enabled && hasProvider
&& providerEnabled`. When `hasProvider` is false, renders "Unset" with a
standard `InfoIcon` tooltip.
- `ModelForm.tsx`: `canSubmit` OR's in `hasProviderChange` so the save
button enables when only the provider dropdown changes.
- `ModelRow.stories.tsx`: four stories covering baseline,
missing-provider (with tooltip assertion), disabled-provider, and
disabled-model paths.
- `ModelsPageView.stories.tsx`: `OrphanedModelShowsUnset` feeds an
orphaned model through the real derivation (map-miss + `?? false`),
matching the production shape produced by `deriveProviderStates`.
`DisabledProviderModelsStillListed` now asserts the "Disabled" badge.
- `ModelForm.stories.tsx`: `EditUpdateEnabledOnProviderChange` asserts
the save button is enabled when the selected provider differs from the
model's stored provider.
- `testFixtures.ts`: `mockOrphanedModel` fixture representing the
deleted-provider case.
Diff: 7 files, 240 insertions, 9 deletions.
> 🤖 This PR was updated with Coder Agents.
|
||
|
|
1ab4ed8db5 |
feat: exclude AI Bridge usage from AI Governance seat counting (#27280)
Under the new `ai-gateway-seat-exclusion` experiment, AI Bridge usage stops counting toward AI Governance seats. ## Seat recording Under the experiment, `RecordInterception` no longer records `ai_seat_state` usage for the initiator: AI Gateway access is licensed by the AI Governance add-on rather than per seat. This experiment is independent of `workspace-capable-licensing` (#27279) so the two licensing behaviors can be enabled separately. Task workspace builds still claim AI Governance seats. ## Manual verification Verified live on a dev deployment (provider chained to dev.coder.com's gateway, model `gpt-5.6-luna`): with the experiment off, the first bridge request from each identity type (admin, plain member, service account) wrote an `ai_seat_state` row (`aibridge` reason); with it on, requests recorded interceptions but left seat state untouched — no new rows, and existing rows' `last_used_at` did not advance. Part of the gateway-accounts feature. ## Stack Part 2 of the gateway-accounts stack: 1. **#27279**: permission-based license seat counting. Behind the `workspace-capable-licensing` experiment and gated on the AI Governance add-on, `user_limit` counts only users the RBAC engine authorizes to create workspaces. 2. **This PR**: stops AI Bridge usage from claiming AI Governance seats under the new `ai-gateway-seat-exclusion` experiment. 3. ~~**#27281**: adds a `use_shared` capability precondition for workspace ACL grants, so workspace sharing is ineffective for (and rejected toward) users without workspace capabilities, evaluated live on every authorization.~~ This will be done in follow-up work when we have time to look into the performance impact. Related but independent: **#27278** hides the Workspaces page create CTAs for users without workspace-create permission. |
||
|
|
6c102cc3f3 |
feat: count only workspace-capable users toward license seats (#27279)
Adds permission-based license seat counting behind the
`workspace-capable-licensing` experiment. When the experiment is enabled
and a valid license carries the AI Governance add-on, the `user_limit`
feature counts only active users the RBAC engine authorizes to create a
workspace, instead of every active user. Users without workspace-create
capability ("gateway accounts", e.g. AI-Gateway-only users) no longer
consume seats.
## How it works
- A new `GetActiveUsersAuthorizationRoles` bulk query returns effective
roles (implied member roles, org default member roles) and group
memberships for every seat-eligible user (active, not deleted, not
system, not a service account), matching `GetActiveUserCount` semantics.
- `license.CountWorkspaceCapableUsers` evaluates `workspace.create`
against the any-organization object form, which covers site-wide grants,
membership grants, and org-scoped bans in one check. Evaluation is
deduplicated on a sha256 of each user's canonical subject JSON (a fixed
sentinel user ID, sorted deduplicated roles and groups), so cost scales
with unique subjects rather than user count, and every subject field
participates in both the evaluation and the key.
- The AI Governance add-on is only known after license claims are
parsed, so `Entitlements()` passes a lazy `WorkspaceCapableUserCountFn`
(following the `ManagedAgentCountFn` precedent) and
`LicensesEntitlements` resolves it when a validated add-on is present.
Each license's `user_limit` claim becomes a candidate pair of limit and
counting mode, the most favorable pair is selected (see Behavior notes),
and the selected pair's limit, entitlement, and count become the
`user_limit` feature's terms; the warnings read the same values.
`license.Entitlements` gains `logger`, `authorizer`, and `experiments`
parameters.
- All custom roles are prefetched in a single query before evaluation
(new exported `rolestore.PrefetchCustomRoles`), and each count emits one
Info log line (capable count, eligible active users, unique subjects,
elapsed) whose presence identifies the counting mode. The count is
bounded by a 60s timeout.
## Behavior notes
- Without the experiment or without the add-on, the legacy
`GetActiveUserCount` path is unchanged.
- When the mode is active, the over-limit and expired-limit warnings say
"workspace-capable users" instead of "active users", since that is what
was counted.
- With multiple licenses, each license's `user_limit` claim forms a
candidate pair of limit and counting mode (workspace-capable for add-on
licenses, all active users otherwise), and the most favorable pair is
enforced: a pair satisfied by its own count wins over any unsatisfied
one, then higher entitlement, then higher limit. One license's limit is
never combined with another license's counting mode, so a small add-on
license can neither borrow a bigger non-add-on limit nor suppress it.
- Licenses in their grace period still gate the count; it reverts to the
legacy count only on hard expiry. While the add-on exists only on
grace-period licenses, a warning tells admins the counting mode will
revert and states the legacy active-user count they will then be
measured by.
- Count errors (database failures, timeout) abort the entitlements
computation, matching the legacy count's error semantics: the refresh
fails and the caller keeps the previous entitlements rather than a
silently different count. One exception: a stored role string that fails
to parse is logged and treated as not workspace-capable instead of
failing the refresh, since authorization fails closed on such roles
anyway.
- The experiment is deliberately not in `ExperimentsSafe`.
Part of the gateway-accounts feature; no behavior changes for
deployments without the experiment.
## Stack
Part 1 of the gateway-accounts stack. Each PR builds on the previous:
1. **#27279 (this PR)**: permission-based license seat counting. Behind
the `workspace-capable-licensing` experiment and gated on the AI
Governance add-on, `user_limit` counts only users the RBAC engine
authorizes to create workspaces.
2. **#27280**: adds the `organization-ai-gateway-access` org role
carrying the AI Bridge interception permissions (extracted from the
member floors, backfilled into org default roles by migration) and
enforces it at AI Gateway authentication; bridge usage stops claiming AI
Governance seats under the experiment.
3. ~~**#27281**: gates workspace ACL grants on matching member-level
capability (each granted action only takes effect while the recipient
holds that action in the org), so workspace sharing is ineffective for
(and rejected toward) users without workspace capabilities, evaluated
live on every authorization.~~ Tabled — excluded from the
gateway-accounts MVP.
Related but independent: **#27278** hides the Workspaces page create
CTAs for users without workspace-create permission.
## Benchmarks
`BenchmarkCountWorkspaceCapableUsers` (in `usercount_bench_test.go`, run
manually with `go test ./enterprise/coderd/license/ -bench
BenchmarkCountWorkspaceCapableUsers -benchtime 5x -run '^$'` — never
executed by CI) measures the count across user-scale and role-diversity
shapes:
| Scenario | Users | ~Unique subjects | per count |
|---|---|---|---|
| Uniform | 1k | 4 | 8.5ms |
| Uniform | 10k | 4 | 71ms |
| Uniform | 50k | 4 | 344ms |
| ManyOrgs (100 orgs) | 10k | ~200 | 112ms |
| CustomRoles (1000 org-scoped roles) | 10k | ~1000 | 168ms |
| UniquePairs (every user a distinct subject) | 10k | ~10,000 | 2.66s |
Summary:
- **Row-side cost is ~7µs per user, linear** (role parsing, subject
canonicalization, and sha256 per row). The bulk query + subject dedupe
handles 50k users in ~350ms; extrapolated 100k ≈ 0.7s. A non-issue at
the 10-minute refresh cadence.
- **Unique subjects are the dominant axis at ~0.26ms each** (role
expansion + one any-organization rego evaluation per subject). The
worst-case scenario — every user a distinct subject — costs ~2.7s at 10k
users, extrapolating to ~13s at 50k.
- **Realistic deployments sit near the cheap rows.** Subject diversity
tracks orgs × role/group combinations, not user count; only per-user
custom roles or per-user org-membership patterns approach the worst
case.
- Caveat encountered while building the harness: the roles query's plan
depends on accurate table statistics. With stale stats (e.g. right after
a bulk user import, before autovacuum ANALYZEs), the planner picks a
nested-loop plan that re-runs the aggregation per user row — a ~300×
regression (1.08s for 1k users). Fresh statistics restore the hash-join
plan; the harness ANALYZEs after seeding, so the numbers above reflect
the healthy plan.
|
||
|
|
00d134ebfd | chore: remove classic parameter UI (#25014) | ||
|
|
51ac968d5a |
feat: wire up Template Builder session telemetry endpoint (#27124)
`TemplateBuilderSession` telemetry types and telemetry-server ingestion were added in earlier PRs (#25082, coder/coder-telemetry-server#41), but no code ever produced session events. This adds the missing producer. **Backend**: `POST /api/v2/templatebuilder/sessions` reports wizard entry and compose completion events directly via `api.Telemetry.Report()`, using the same inline pattern as `NetworkEvents` and `UserTailnetConnections`. No database migration or `createSnapshot()` changes needed. RBAC requires `policy.ActionCreate` on `ResourceTemplate.AnyOrganization()`, matching the compose endpoint. **Frontend**: The template builder wizard fires `wizard_entry` on page mount and `compose_completion` on create success or failure. A client-generated session ID (UUID) correlates the two events for the same wizard visit, enabling precise funnel analysis and abandonment detection in BigQuery. Duration is tracked via `Date.now()` in the wizard state. Closes https://linear.app/codercom/issue/DEVEX-599 <details> <summary>Implementation plan</summary> ## Root Cause Analysis The DEVEX-599 ticket diagnosis suggested missing DB tables, queries, and `eg.Go` blocks. That diagnosis assumes the DB-backed periodic snapshot path is required. It is not. Investigation shows two telemetry reporting patterns in the codebase: 1. **DB-backed periodic snapshots** (`createSnapshot()` with `eg.Go` blocks): Used for durable entities like workspaces, templates, users. 2. **Direct inline reporting** (`api.Telemetry.Report(&telemetry.Snapshot{...})`): Used for ephemeral events like `NetworkEvents`, `UserTailnetConnections`, `CLIInvocations`. Template builder sessions are ephemeral events, so the direct inline reporting pattern is the correct fit. ## Backend Changes - `codersdk/templatebuilder.go`: `TemplateBuilderSessionRequest` type with `SessionID`, `EventType` enum, `TemplateBuilderSession()` client method - `coderd/coderd.go`: Route registration in `/templatebuilder` group - `coderd/templatebuilder_handler.go`: Handler with RBAC check, request validation, session ID fallback, and inline telemetry report - `coderd/templatebuilder_handler_test.go`: Tests for wizard entry, compose completion, invalid event type, disabled feature, and member RBAC rejection ## Frontend Changes - `site/src/api/api.ts`: `recordTemplateBuilderSession` API method - `site/src/api/queries/templateBuilder.ts`: React Query mutation - `site/src/pages/TemplateBuilder/wizardState.ts`: `sessionId` and `enteredAt` fields, `createWizardState()` factory for per-mount initialization - `site/src/pages/TemplateBuilder/TemplateBuilderPageView.tsx`: `sessionId` prop, `useReducer` initializer form - `site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx`: Telemetry calls for wizard entry (on mount) and compose completion (on create success/failure) </details> > 🤖 Generated by Coder Agents --------- Co-authored-by: Coder Agent <agent@coder.com> |
||
|
|
025ded0536 | docs: remove beta labels from user secrets (#27510) | ||
|
|
92d45a0411 |
docs: document SCIM 2.0 handler opt-in and legacy flag (#27469)
Documents the SCIM 2.0 handler introduced in #25572 and how to opt in. Adds a "SCIM 2.0 handler" subsection to the SCIM section of `docs/admin/users/oidc-auth/index.md`: - The handler follows RFC 7644 and supports user provisioning/deprovisioning and user listing. - Opt in with `CODER_SCIM_USE_LEGACY=false` (also `--scim-use-legacy` / `scimUseLegacy`); requires a server restart. - Behavior notes: delete/deactivate suspends (never hard-deletes), reactivation goes through dormant, usernames are immutable. - Notes it will eventually become the default behavior. Behavior details were verified against `enterprise/coderd/scimroutes.go`, `enterprise/coderd/scim/`, and the `SCIM Use Legacy` option in `codersdk/deployment.go`. `make lint/markdown` and `make lint/emdash` pass. --- Generated by Coder Agents on behalf of @Emyrk. --------- Co-authored-by: Nick Vigilante <nickvigilante@users.noreply.github.com> |
||
|
|
dba45cede7 |
fix: remove 403 from key failover and cooldown on 401 (#27419)
## Problem When a key returned 401 or 403, the pool marked it permanently unavailable for the lifetime of that in-memory pool. This is bad UX: a transient auth failure or a briefly-misconfigured key could take a key out of rotation until the operator either restarted Coder or reconfigured the key (even re-saving the same working value). ## Changes - **403 removed from key failover**: it's a per-request authorization failure, not a key-level problem, so it's surfaced to the caller as-is without marking the key or failing over. - **401 now applies a temporary cooldown** (like 429) so the key recovers on its own instead of staying blocked. - When every key is in an auth-failure cooldown, the pool reports a `502` with no `Retry-After`, but the keys still recover automatically once the cooldown elapses. Closes https://linear.app/codercom/issue/AIGOV-421/ai-gateway-a-quarantined-centralized-key-never-recovers-without-a Closes https://linear.app/codercom/issue/AIGOV-533/403s-misclassifying-keys-as-permanently-down-in-ai-gateway > [!NOTE] > Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira |
||
|
|
6f2011af88 |
feat: add chat summary tab in the right sidebar and per-chat cost endpoint (#26649)
Stacked on #26657 (the persisted whole-chat summary backend). Base branch is `chat-summary-62j9`; review/merge that first. Adds a reusable `ChatSummary` component. The summary text is the persisted whole-chat summary (`chat.summary`) introduced by #26657. It is generated asynchronously and may be `null` until the first summary is produced, in which case the popover renders a muted empty state. Live updates arrive via that PR's `chat_summary_change` watch event, which is already merged into the chat caches. Cost is served by a new per-chat endpoint, `GET /api/experimental/chats/{chat}/cost`, which rolls up assistant-message cost across a chat's root and child (subagent) chats and is authorized like the other `{chat}` routes (read on the chat, 404 otherwise). Visual and interaction coverage lives in `ChatSummary.stories.tsx` and `ChatSummaryPopover.stories.tsx` (including populated-summary, empty-state, and cost-loading cases). --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
0f1eafa17e |
docs(docs/admin): document wildcard hostname suffixes (#27482)
Documents wildcard hostname suffixes such as `*-apps.example.com`, which the existing application hostname parser and Helm chart already support. Explains the generated application hostname and the DNS and TLS wildcard required for each supported form. Also adds the suffix form to the installation summary. Validated with the repository's documentation linters and pre-commit hook, the hostname-pattern unit test, and an end-to-end workspace application on Coder v2.35.2. |
||
|
|
3cf97ff8e7 | fix: show selected owner's external auth when creating a workspace (#26653) | ||
|
|
d5a3963167 |
feat: add bulk user secret import endpoint and SDK client (PLAT-240) (#26724)
Adds `POST /api/v2/users/{user}/secrets/batch` and
`codersdk.Client.ImportUserSecrets` to import env, JSON, or YAML secrets
atomically. The endpoint validates each entry, rolls back the full batch
on conflicts or limits, omits secret values from responses and audit
logs, and imports keys that cannot be injected as environment variables
with an empty `env_name`.
Part of the [PLAT-240 bulk secret import
stack](https://linear.app/codercom/issue/PLAT-240). Reviewed and updated
by Coder Agents on behalf of @dylanhuff-at-coder.
|
||
|
|
73af2ca632 |
docs: audit and fix manifest.json page descriptions for SEO (#27267)
## What Audit and fix the page `description` fields in `docs/manifest.json` so each one is accurate, unique, and follows meta-description SEO best practices, targeting 70-155 characters. Tracking: DOCS-576 ## Why Many manifest descriptions were terse (243 of 272 hand-maintained descriptions were under 70 characters), a few reused another page's description (copy/paste errors), and one just repeated its own title. These feed the per-page `<meta name="description">` on coder.com/docs, so they matter for search snippets and click-through. ## What changed The manifest diff is +244 / -244 lines, touching only `description` string values (0 structural lines changed). A second commit regenerates one downstream file (see Generated file below). - **Fixed 5 copy/paste errors** where a page reused another page's description: - `admin/monitoring/index.md` (had Security's text) - `admin/monitoring/metrics.md` (had Logs' text) - `admin/templates/template-permissions.md` (had "Creating Templates" text) - `admin/networking/stun.md` (had Port Forwarding's text) - `admin/provisioners/manage-provisioner-jobs.md` (had the provisioners index text) - **Fixed `reference/index.md`**, whose description merely repeated the title "Reference". - **Corrected wording**: "Coderd API" to Coder REST API; "VSCode" to VS Code; dropped the `&` shorthand on the AI Gateway index per the docs style guide. - **Corrected accuracy**: the AI landing page listed outdated example agents (GPT-Code, OpenDevin, SWE-Agent); it now references agents used elsewhere in the docs (Claude Code, Aider). - **Expanded terse descriptions** into the 70-155 range with active-voice, front-loaded phrasing. ## Generated file `docs/install/releases/feature-stages.md` is generated by `scripts/release/docs_update_feature_stages.sh`, which copies the beta pages' manifest descriptions verbatim into the beta-features table. Three rows (MCP Server, JetBrains Toolbox, Coder Agents) update to match the new descriptions; User secrets is unchanged. Regenerated with `make gen` so the generated-files check stays clean. ## Scope / exclusions Auto-generated reference subtrees are intentionally left untouched, since `make gen` rebuilds them from source and would revert hand edits (and fail the generated-files check): - `Reference > Command Line` children, from `scripts/clidocgen` (each command's `Short` help) - `Reference > REST API` children, from `scripts/apidocgen` - `Reference > Agent API` children The section index nodes themselves (Reference, REST API, Command Line, Agent API) are hand-maintained and are in scope. ## Validation - `docs/manifest.json` is valid JSON; diff touches only `description` values. - All 272 in-scope descriptions are now 70-155 characters, with 0 duplicates across distinct pages. - No double quotes, backslashes, em/en dashes, or `&` / `<` / `>` in descriptions. - Biome 2.4.10 (`scripts/biome_format.sh`) is a no-op on the result. - `scripts/check_emdash.sh` passes. > This PR was created with AI assistance (Coder Agents). |
||
|
|
5bafbace8e |
docs: add What's next? carve-out to the Learn more style rule (#27163)
## What Adds a **What's next?** carve-out to the **Learn more, not Next steps** rule in the docs style guide (`docs/.style/style-guide/word-choice.md`). The existing `## Learn more, not Next steps` heading, its two rationales, and the ban on **Next steps** are unchanged, so the `#learn-more-not-next-steps` anchor is preserved. A new `### Sequenced tutorials: What's next?` subsection lets a tutorial in an ordered series point to the single next tutorial, and the enforcement note now clarifies that the planned `Coder.LearnMore` rule flags **Next steps**, not **What's next?**. ## Why **What's next?** and **Learn more** do different jobs: - **What's next?** carries the reader along a defined sequence: the single next tutorial. - **Learn more** stays optional related reading, such as feature or reference pages. The **What's next?** phrasing also avoids the "steps" mobility metaphor, so the inclusive-language reason for banning **Next steps** still holds. The merged Quickstart "Customize your template" series (#26712) already uses **What's next?** sections, so this codifies the pattern those pages adopted. ## Implementation plan and decision log - Keep `## Learn more, not Next steps` (preserves the anchor and the core ban). - Add `### Sequenced tutorials: What's next?` after the Learn more Do/Don't examples: a tutorial in an ordered series may add a **What's next?** section pointing to the single next tutorial, placed above **Learn more**, written as a short sentence with the link. - Add a **Do** example showing **What's next?** above **Learn more**. - Update the closing note to: *Enforced by `Coder.LearnMore` (planned). The planned rule flags Next steps, not What's next?.* Decisions: - Subsection, not a new top-level rule, keeps the shared rationale and the `#learn-more-not-next-steps` anchor intact. - The planned Vale rule must flag **Next steps** but allow **What's next?**, so the note calls that out explicitly to prevent a future false positive. - Diff scope: only the Learn more section changes (21 insertions, 1 deletion); no other rules are touched. --- Generated by Coder Agents on behalf of @nickvigilante. |
||
|
|
66a55e1ebd |
feat(docs/.style): enable Coder.GerundHeading (#25502)
## Summary Adds `Coder.GerundHeading`, a `warning`-level Vale rule that flags headings and titles whose first word ends in `-ing` (a gerund or present participle used as a verb form, like `Installing` or `Configuring`). Task headings read better in the imperative (`Install Coder`); concept headings read better as nouns (`Installation`). The choice is context-dependent, so the rule is a `warning`: it annotates without blocking CI. The style-guide section this rule enforces already lives on `main` at [`capitalization-and-punctuation.md#no-gerund-leading-headings`](https://github.com/coder/coder/blob/main/docs/.style/style-guide/capitalization-and-punctuation.md#no-gerund-leading-headings). This PR adds the matching rule and nothing else: the net diff is a single file. ## What's in this PR - `docs/.style/styles/Coder/GerundHeading.yml` (new). Heading-scoped `existence` rule, anchored regex `^[A-Z][a-z]+ing\b`, `level: warning`. - `exceptions:` mirror the style guide's **Exceptions** section: `-ing` words that name a feature, category, or attribute (`Logging`, `Monitoring`, `Networking`, `Tracing`, `Troubleshooting`, `Pricing`, `Billing`, ...) plus words that only look like gerunds (`Bring`, `String`, ...). This branch was rebuilt onto `main`'s restructured `docs/.style/` (the single `style-guide.md` became a `style-guide/` directory and Vale moved into `ci.yaml`), which is why the diff is now just the rule. ## Scope: rule only The rule ships as a `warning`, so it surfaces the existing `-ing` task headings (~200) as advisory annotations rather than failing CI. De-gerunding those headings (imperative rewrites plus internal anchor fixes) is a corpus-wide content change and lands in a dedicated follow-up PR, tracked separately. Splitting keeps this PR to the rule and keeps the content churn reviewable on its own. <details> <summary>Decision log</summary> **`existence` + `scope: heading`, not `sequence` + `tag: VBG`.** Vale's POS-tagging sequence rules are hardcoded to sentence scope and never reach heading text, so a `VBG` sequence rule fires on paragraphs and stays silent on H1-H6. Google's and Microsoft's heading rules all use the existence+regex pattern; this rule follows it. **Exceptions align to the committed style guide, not the original branch design.** The first draft of this rule intentionally left concept-noun gerunds (`Logging`, `Monitoring`, ...) in the flagged set. Since then, `main`'s style guide declared exactly those as non-violations. The rule now excepts them so the rule and the guide agree. An excepted first word is allowed everywhere, which is a deliberate precision trade-off for a first-word regex: `Monitoring Coder` (a task) is not flagged, but the standalone concept heading `Monitoring` stays clean. **Severity = warning.** The imperative-vs-noun choice is judgment-bound, which is the case the `warning` tier exists for: strong guidance, legitimate human-judgment exceptions, no CI block. **Verification.** `make lint/prose` loads the rule cleanly; the excepted words (`Troubleshooting`, `Monitoring`, `Networking`, `Logging`, `Contributing`, `Styling`, `Scaling`, `Routing`, `Pricing`, `Billing`, `Tracing`) each produce zero findings. </details> --- *Opened via Coder Agents on @nickvigilante's behalf.* |