mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
0b4095085ef182d6aefb57cfd5fbc7546ac2868e
1490
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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). |
||
|
|
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> |
||
|
|
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. |
||
|
|
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
|
||
|
|
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 |
||
|
|
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.
|
||
|
|
ce4ee923c2 |
feat: notify users when AI spend crosses the budget threshold (#27346)
Implements: https://linear.app/codercom/issue/AIGOV-289/notify-users-and-admins-on-budget-warning-and-limit-reached Notify users when their AI spend crosses a budget threshold for their effective group. Two thresholds are covered: a warning at 85%, and a limit-reached notification at 100%. Detection runs on the post-response path, right after the interception's cost is added to the user's daily spend. It reads the user's AI spend on the same transaction where token usage is recorded and AI daily spend is incremented, and derives the pre-interception total by subtracting this interception's cost. In case of `oldSpend < threshold && newSpend >= threshold` - notification is sent. A single interception that crosses both thresholds enqueues both notifications. Detection and delivery are best-effort: a failure is logged and never fails usage recording. The payload uses only stable values (the threshold percentage and the spend limit, not the exact spend), so duplicate enqueues are deduplicated by the notification system. The two templates are added via migration and appear in each user's notification settings under the "AI Budget" group. Admin notifications (owners and user admins) are a follow-up: #27415. ## Screenshots: <img width="1102" height="252" alt="image" src="https://github.com/user-attachments/assets/62291510-09ca-4cdf-a1f5-4bdc11a1db4b" /> <img width="466" height="384" alt="image" src="https://github.com/user-attachments/assets/030460ff-6fe2-4d59-b247-3550c543ef30" /> --------- Co-authored-by: Cian Johnston <cian@coder.com> |
||
|
|
5770085435 |
fix: add prefix to standalone metrics (#27526)
Adds `coder_ai_gateway_` to standalone Gateway metics to match embedded case. |
||
|
|
3c7a1d33e3 |
feat: add persisted whole-chat summary with background generation (#26657)
Adds a persisted whole-chat summary that backs the chat summary popover. A new nullable `chats.summary` column is populated in the background after a successful root-chat turn and pushed to clients via a new `chat_summary_change` watch event (distinct from `summary_change`, which is bound to `last_turn_summary`), so the popover reads `chat.summary` straight off the loaded `Chat` with no extra query. This is the data source for the popover and per-chat cost UI built in #26649; the popover can consume `chat.summary` once this lands (the field is nullable, so merge order does not matter). ## How it works - **Generation** runs in the existing successful-turn finalize hook, detached from the request so the user's turn is never blocked. A cadence gate generates the first summary after one completed turn, then regenerates every three turns, using the `chats.summary_generated_at` freshness marker. Generation reads compaction-aware history, renders it to a bounded plain-text transcript (short transcripts are skipped), and asks for a 1-3 sentence summary via structured output. Failures never clear an existing summary. - **Staleness** is guarded by `history_version` (mirroring `last_turn_summary`), so a background write racing a newer turn loses while worker lifecycle transitions cannot reject a fresh write. - **Model selection** uses the chat's configured model. ## Deferred to follow-ups - **Cost accounting**: the `chat_messages.cost_source` discriminator and summary/title usage recording were removed from this PR so summary persistence is not blocked by hidden accounting rows advancing `history_version`. Title usage recording stays on main's `InsertChatMessages` path. - **Model override**: deployment-wide summary generation model selection is split into #26803; the base feature always uses the chat model. ## Notes - Migration `000540` adds `chats.summary` and `chats.summary_generated_at`, and recreates `chats_expanded` to expose the new columns. - Root chats only; shared viewers pick up the summary on their next refetch (live watch events are owner-only). Refs #26649 --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
468b1a27a3 |
fix: remove standalone AI Gateway http listener dependency on loading providers (#27303)
Fixes an issue where the standalone AI Gateway waited for the initial provider load before starting its HTTP server. HTTP serving now starts independently of provider synchronization. `/healthz` becomes available when the HTTP server starts, while `/readyz` requires an active DRPC connection and completed initial provider load. Enables the Helm chart's startup and liveness probes by default because liveness no longer depends on provider loading. |
||
|
|
c23f2c0223 |
feat: fall back to the Everyone group for AI spend attribution (#27364)
## Description Previously, a user with no per-user override and no membership in a budgeted group had no effective group, so their AI spend was attributed nowhere and was, therefore, untracked. This change falls back to the organization's Everyone group when no override or group budget applies. Since every user in an organization is implicitly a member of that org's Everyone group, spend is now attributed and tracked for any user with organization membership. A user with no organization membership resolves to no group, so their daily spend is not incremented and a warning is logged. The fallback is unlimited, so enforcement is unaffected: only override and group budgets can block requests. For users in multiple organizations, an existing budget on any Everyone group is still chosen by the "highest" policy; when none is budgeted, the fallback prefers the default org, then orders by organization name. ## Changes - Add `ResolveUserEffectiveGroup` and the `GetUserEveryoneFallbackGroup` query: resolve override → group budget → Everyone group fallback. - Attribute token-usage spend and the user AI spend endpoint via the fallback, so unbudgeted users resolve to their Everyone group instead of null. - Update `GetGroupMembersAISpend` to surface the Everyone fallback as the effective group. - Update `GetHighestGroupAIBudgetByUser` to break ties by organization name then group name, keeping multi-org resolution deterministic and consistent with the fallback. - For multi-org users with no budget anywhere, the fallback picks the Everyone group deterministically: prefer the default org, then order by organization name. Closes https://linear.app/codercom/issue/AIGOV-509/fall-back-to-the-everyone-group-for-spend-attribution > [!NOTE] > Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira |
||
|
|
8a3fb04510 |
feat: add Helm chart for standalone AI Gateway (#27256)
Adds the `coder-ai-gateway` Helm chart for deploying the Coder AI Gateway as a standalone Kubernetes workload. Adds the coder-ai-gateway Helm chart for deploying the Coder AI Gateway as a standalone Kubernetes workload. The chart supports AI Gateway keys from an existing Secret or environment configuration, Coder connectivity through CODER_URL, listener and Coder-facing TLS, and optional Service, Ingress, and Gateway API HTTPRoute resources. Integrates the chart with existing Helm build, lint, golden generation, release artifact, Helm repository, and OCI publishing workflows. |
||
|
|
a9a1dcc65d |
feat: add network calls column to AI sessions table (#27269)
Add a "Total/blocked network calls" column to the AIBridge sessions table. Update `ListAIBridgeSessions` query to calculate network called made and blocked per session. See query plan [here](https://explain.dalibo.com/plan/54355c90b165ggb4). --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
9bd4cf2a2a |
test: use NATS in coderdtest by default (#27343)
Closes GRU-70 Enables NATS as the pubsub for `coderdtest` unless specifically overwritten by the test case. |
||
|
|
3227cac217 |
feat: add manual chat compaction via /compact (#27081)
Adds a user-triggered `/compact` action for Coder Agents chats: typing
`/compact` in the composer (or picking it from the `/` trigger menu)
summarizes the conversation so far to free up context window space.
## How it works
- New `POST /api/experimental/chats/{chat}/compact` endpoint
(owner-only, RBAC `ActionUpdate`, excluded from the public API reference
via `x-apidocgen skip`). It marks the chat with a durable one-shot
`chats.compaction_requested_at` signal and moves it `waiting -> running`
via a new `RequestCompaction` state transition; no message row is
inserted. AI Gateway attribution needs no per-request key: generation
preparation resolves the owner's synthetic API key (#27170) like any
other turn.
- `RequestCompaction` hands off chat ownership (clears
`worker_id`/`runner_id`) so a worker acquisition hint is published;
since the transition changes no history, the previous runner could
otherwise miss the request under reordered pubsub delivery.
- The background chat worker picks the chat up like any other turn. A
pending manual request takes precedence over turn completion in the
generation decision, and forces compaction even below the automatic
threshold (and when compaction is disabled via threshold=100). The
commit step consumes the request marker in the same transaction; any
transition that ends the turn clears stale markers.
- The summary triplet reuses the automatic-compaction path, now tagged
with a `source` (`automatic` | `manual`) that is plumbed through
streamed progress parts, persisted tool JSON, and the UI label
("Summarized (manual)").
- Validation order: busy chats reject with 409 (state-machine conflict),
empty/already-compacted chats with 409 "nothing to compact", archived
chats with 400; the owner usage-limit check runs last so no-op requests
surface the specific conflict instead of a limit error.
- Web UI: the `/` trigger menu now has a built-in "Commands" group
listing `/compact`; submit intercepts exactly `/compact` and calls the
endpoint instead of sending a message. A personal or workspace skill
named `compact` takes precedence over the built-in command; while skill
collisions are still resolving, an exact `/compact` submission is
blocked with a retryable hint instead of leaking as message text.
History and queued-message edits are never intercepted. After
compaction, the context usage indicator resets to its unknown state
until the next assistant response reports fresh usage, instead of
showing the stale pre-compaction number.
- codersdk: `ExperimentalClient.CompactChat`.
Worker-path execution (rather than compacting synchronously in the
handler) reuses the existing lock fencing, live "Summarizing..."
streaming, retry accounting, restart resilience, and debug-run
observability. Rationale documented in `coderd/x/chatd/ARCHITECTURE.md`.
## Testing
- State machine: transition-matrix coverage for `RequestCompaction`,
marker lifecycle tests (carried by lease renewals/queue appends, cleared
by terminal transitions, consumed by commit), ownership handoff +
acquisition hint assertions.
- Worker: decision-ordering and forced-compaction unit tests;
active-server end-to-end test (manual compact below threshold produces a
`source=manual` summary, returns to `waiting`, no assistant follow-up;
busy chat rejected).
- API: success, archived, non-owner, RBAC-denied, empty-chat, no-daemon
cases; usage-limit ordering (at-limit owners still get
state/nothing-to-compact conflicts for no-op requests, with marker
rollback).
- Frontend: Storybook play tests for the Commands menu group, submit
intercept, skill-name collision, queued-edit passthrough, and
manual/automatic tool rendering; unit tests for command availability
resolution and the post-compaction context usage reset.
> This PR was created by Mux, an AI coding agent, working on Mike's
behalf.
|
||
|
|
a9fdf87a2f |
feat: add GET /groups/{group}/members/ai/spend (#27130)
## Description
Adds `GET /api/v2/groups/{group}/members/ai/spend?user_ids=...` (also available org-scoped at `/api/v2/organizations/{org}/groups/{groupName}/members/ai/spend`) to return per-member AI spend attributed to a group, along with each member's effective budget group and the applied spend limit when the queried group is their effective budget source.
In the UI, this endpoint is used alongside the existing `/api/v2/groups/{group}/members` endpoint. AI spend data is kept separate from that endpoint so that:
- Different concepts stay on different endpoints: identity (group members) vs. cost control (spend). Cost control is an additional feature layered on top of groups/orgs.
- Callers that don't need spend information don't pay for its computation.
UI flow:
1. Request `/api/v2/groups/{group}/members` → returns the group's members.
2. Request `/api/v2/groups/{group}/members/ai/spend?user_ids=...` with the IDs from step 1.
**Note:** Only current members of the queried group are returned. `spend_limit_micros` and `limit_source` are populated only when the queried group is the member's effective budget source (its own limit or a user override). `effective_group_id` is null when the member's budget resolves to a group in another organization, since an organization is treated as a tenant boundary.
<img width="2880" height="1904" alt="image" src="https://github.com/user-attachments/assets/33ed395d-d1a3-4b46-bb04-c8d3f41c8886" />
## Changes
- Add `codersdk.GroupMembersAISpend` and `GroupMemberAISpend` types, reusing the shared `AISpendPeriodWindow`.
- Add `GetGroupMembersAISpend` SQL query with a dbauthz per-row filter that mirrors `GET /api/v2/groups/{group}/members`.
- Add handler and routes under `/groups/{group}/members/ai/spend` (and the org-scoped alias) with a required `user_ids` query param (cap 100). Callers with more than 100 members are expected to batch across multiple requests.
- Add codersdk client method.
- Tests: dbauthz, raw SQL, endpoint, and role-access.
Closes https://linear.app/codercom/issue/AIGOV-471/backend-group-members-endpoint-with-members-spend
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
|
||
|
|
2adc8f5272 |
feat: add GET /organizations/{org}/groups/ai/spend (#27123)
## Description
Adds `GET /api/v2/organizations/{org}/groups/ai/spend?group_ids=...` to return per-group AI spend and configured limits for a set of groups in an organization.
In the UI, this endpoint is used alongside the existing `/api/v2/organizations/{org}/groups` endpoint. AI spend data is kept separate from that endpoint so that:
- Different concepts stay on different endpoints: identity (groups) vs. cost control (spend). Cost control is an additional feature layered on top of groups/orgs.
- Callers that don't need spend information don't pay for its computation.
UI flow:
1. Request `/api/v2/organizations/{org}/groups` → returns the organization's groups.
2. Request `/api/v2/organizations/{org}/groups/ai/spend?group_ids=...` with the IDs from step 1.
The groups endpoint from 1) is currently not paginated, but if pagination is added later, this design keeps the two responses in sync. This spend endpoint intentionally takes `group_ids` rather than paginating on its own, since it depends on the group set from step 1. Pagination could be added in the future, especially for Cost Control-focused pages.
<img width="2880" height="1460" alt="image" src="https://github.com/user-attachments/assets/ea83b74d-6a4f-45a6-af2f-1024e019da07" />
## Changes
- Add `codersdk.OrganizationGroupsAISpend` and `OrganizationGroupAISpend` types, plus a shared `AISpendPeriodWindow` embedded in the spend response.
- Add `GetOrganizationGroupsAISpend` SQL query with a dbauthz per-row filter that mirrors `GET /organizations/{org}/groups`.
- Add handler and route under `/organizations/{organization}/groups/ai/spend` with a required `group_ids` query param (cap 100). Callers with more than 100 groups are expected to batch across multiple requests.
- Add codersdk client method.
- Tests: dbauthz, raw SQL, endpoint, and role-access.
Closes https://linear.app/codercom/issue/AIGOV-466/backend-organization-groups-endpoint-with-groups-spend
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
|
||
|
|
54fa4a087e |
chore: wire quartz.Clock into Acquirer (#27291)
- Wires quartz.Clock into provisionerdserver.Acquirer - Allows overriding Acquirer in coderd.Options - Updates existing tests to use an Acquirer driven by a quartz.Mock Before this change `enterprise/coderd/prebuilds` package tests would take ~60-70s to run. After this change, it's down to ~10s. > Generated by Coder agents, massaged by this human. |
||
|
|
9f4ddea571 |
feat: revoke MCP server OAuth grants at the provider on disconnect (#27300)
Closes [CODAGT-805](https://linear.app/codercom/issue/CODAGT-805/revoke-oauth-grants-at-the-source-for-mcp-servers). The experimental MCP server OAuth2 disconnect endpoint previously deleted only the stored token row, leaving the grant active at the OAuth provider. This PR adds provider-side token revocation while keeping local disconnect independent of provider availability. ## Changes - Add `mcp_server_configs.oauth2_revocation_url` in migration `000547`. The value can be configured manually, discovered from RFC 8414 metadata, and managed through the MCP server settings UI. Non-admin responses redact it with the other OAuth2 fields. - Revoke the refresh token first through the RFC 7009 endpoint, then fall back to the access token only for `unsupported_token_type`. Public clients send `client_id`; confidential clients use `client_secret_basic`. - Delete the local token transactionally before best-effort provider revocation. Callers without a token receive the same response for hidden and nonexistent config IDs, and provider failures return a generic warning without exposing provider response bodies. - Require HTTPS revocation endpoints except for HTTP loopback URLs. Redirects must preserve the POST and remain on the configured origin. Redirect errors omit provider-controlled paths and query strings so reflected token material cannot enter logs. - Treat `200 OK` and `204 No Content` as completed revocations. `202 Accepted` remains a failure because it does not confirm completion. - Prevent an in-flight refresh from recreating a token deleted by disconnect. Refresh persistence now uses an optimistic update keyed by token ID and `updated_at`; only the OAuth callback can create a token row. Refresh conflicts reload the current row or clear in-memory auth when disconnect deleted it. - Return `{token_revoked, token_revocation_error}` from disconnect, while retaining SDK compatibility with the legacy `204` response. The UI surfaces provider revocation failures as warning toasts. - Document revocation endpoint discovery, HTTPS requirements, and best-effort disconnect behavior. No token or no configured revocation URL returns `token_revoked: false` without an error, so disconnect remains idempotent. > Updated by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
101aee8ee0 |
refactor: use Options struct in aibridgedserver.NewServer (#27200)
Refactor `aibridgedserver.NewServer` to take an `Options` struct instead of a long list of positional arguments. Follow-up to review feedback in https://github.com/coder/coder/pull/27117#discussion_r3571535760 |
||
|
|
e489092154 |
feat: handle revoked OAuth grants for MCP servers gracefully (#27264)
Closes [CODAGT-792](https://linear.app/codercom/issue/CODAGT-792/handle-revoked-oauth-grants-for-mcp-servers-gracefully). When a user revokes an upstream OAuth grant for an MCP server used by Coder Agents, Coder kept treating the cached token as valid: `invalid_grant` refresh failures were logged and swallowed, the dead bearer token kept being attached, the list endpoints re-attempted the refresh on every call, and the UI kept showing the server as authenticated. ## Changes Backend, mirroring the `external_auth_links` prior art: - New migration adds `mcp_server_user_tokens.oauth_refresh_failure_reason`. `UpsertMCPServerUserToken` clears it, so completing the OAuth flow again recovers the row. - New `MarkMCPServerUserTokenRefreshFailure` query records the failure and clears all token material, guarded by an `updated_at` optimistic lock so a stale failure never clobbers a concurrently refreshed token (on a lock miss the winner's row is used). - `mcpclient.IsPermanentRefreshError` classifies `*oauth2.RetrieveError` codes: only `invalid_grant` and `bad_refresh_token` are permanent. Client/config errors (`invalid_client`, `unauthorized_client`, ...) stay transient for the user row since reconnecting cannot fix them. - chatd token refresh and the MCP list/get endpoints persist permanent failures, return cleared tokens for the in-flight request, and skip provider calls for already-failed rows. - `buildAuthHeaders` no longer attaches an Authorization header for failed tokens, so chat degrades by omitting that server's tools instead of sending a dead bearer. API and UI: - No new API surface. A permanently failed token simply reports `auth_connected: false`, so the existing "Auth" button and "Not authenticated" tooltip appear and the user re-runs the same OAuth flow to recover. An earlier revision added an `auth_status` enum (`connected` / `not_connected` / `reconnect_required`) with a dedicated "Reconnect" button; it was collapsed to keep the API minimal since both states lead to the identical re-auth action. Out of scope (follow-up): typed 401-on-connect detection and forced refresh. mcp-go exposes no stable typed 401 signal in the static-header path, so a revocation while the access token still looks valid locally stays undetected until expiry triggers a refresh. ## Testing - Unit and integration tests: classifier, chatd refresh paths (permanent/transient/race/persist-failure), API endpoints (revoked, transient, no-retry caching, re-auth recovery, stale-lock), dbauthz, dbcrypt, migrations. - Dogfood UAT against a dev instance with a mock IdP returning `invalid_grant`: revoked grant detected on refresh and persisted once (no repeated IdP calls), chat with the revoked server selected completes with the server's tools omitted, and re-auth restores the connected state. > This PR was authored by Mux, working on Mike's behalf. |
||
|
|
fa4c7c5700 |
chore: fix replicasync to function for NATS even if no DERP relay address is set (#27235)
<!-- If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting. --> Fixes `replicasync` so that it keeps peers even if they don't set a RelayAddress. This allows NATS to function even if you are not running a DERP relay on the primay Coderd instances. Also renames some replicasync functions to make it clear they are for DERP. |
||
|
|
4d4cbd07e6 |
fix: prevent concurrent token refreshes (#26530)
This can cause bad refresh token errors, since it can only be used once. Looks like there was an attempt to fix this by checking the database after a failed refresh, but of course this depends on the first request having updated the database in time, so both that and this fix are required to fully solve. |
||
|
|
61e52532c0 |
docs: wrap placeholder syntax in backticks in CLI help and swagger annotations (#27194)
## Problem
Generated reference docs (`docs/reference/cli/*`,
`docs/reference/api/*`) contained raw placeholder and JSON syntax that
came straight from Go CLI help strings and swagger annotations. HTML
renderers treat the angle-bracket tokens (`<team-slug>`, `<uuid>`,
`<KEY>`, etc.) as unknown tags and drop them, so readers see
broken/half-missing text today. The same strings also break MDX parsing.
## Fix
Wrap the placeholder/JSON syntax in backticks **at the source** (Go help
strings and swagger annotation comments), then `make gen`. Rendered docs
now show the placeholders as inline code instead of dropping them.
### Source changes
| File | Placeholder wrapped | Surfaces in |
|------|--------------------|-------------|
| `codersdk/deployment.go` | `` `<organization-name>/<team-slug>` `` |
`cli/server.md`, `coder --help`, settings UI |
| `codersdk/deployment.go` | `` `CODER_AI_GATEWAY_PROVIDER_<N>_*` ``, ``
`CODER_AI_GATEWAY_PROVIDER_<N>_<KEY>` `` | `api/schemas.md` |
| `cli/tokens.go` | `` `<type>:<uuid>` `` | `cli/tokens_create.md`,
`coder --help` |
| `coderd/aitasks.go` | `` `owner:<…>` ``, `` `organization:<…>` ``, ``
`status:<status>` `` | `api/tasks.md` |
| `coderd/exp_chats.go` | `` `pr_status:<…>` `` and sibling filter
tokens | `api/chats.md` |
| `coderd/provisionerdaemons.go`, `coderd/provisionerjobs.go` | ``
`{'tag1':'value1','tag2':'value2'}` `` | `api/organizations.md`,
`api/provisioning.md` |
Everything else in the diff (`coderd/apidoc/*`, `docs/reference/**`,
`*.golden`, `site/src/api/typesGenerated.ts`) is `make gen` output.
## Reviewer notes (the "considered pass" from the ticket)
- **Product-visible:** this changes `coder server --help` and `coder
tokens create --help` output, and the `server-config.yaml` reference
comment. Backticks in terminal help are literal but read fine as
placeholder markers.
- **Settings UI:** the `deployment.go` `Description` also renders in the
deployment settings page. If that field is not Markdown-rendered,
literal backticks will show there. Happy to drop the `deployment.go`
change if you'd rather keep the UI text clean and fix `server.md`
another way.
- **Out of scope here:** `docs/reference/cli/agent-firewall.md`
(`<host>`/`<glob>`) is generated from the external
`github.com/coder/boundary` module, not this repo. It needs an upstream
fix + module bump; not included in this PR.
<details>
<summary>Implementation notes / decision log</summary>
- Scope taken from DOCS-551: source-level backtick pass for generated
reference docs only. Hand-written Markdown fixes are tracked separately
(companion ticket).
- Swagger `@Param` descriptions are Go comments, so the existing `\|`
pipe-escaping in the chats `q` filter is preserved inside the new
backticks (still required for the Markdown table cell to render `|`).
- Verified after `make gen`: generated docs render placeholders as code
spans, table pipes intact; `gofmt` clean; changed Go packages build; no
emdash/endash introduced.
- Deliberately left the `AIProviderConfig` type-level doc comment
untouched because it does not surface in any generated doc (kept the
diff to doc-feeding comments).
</details>
Linear: DOCS-551
---
_Opened by Coder Agents on behalf of @nickvigilante._
---
## Evidence: placeholders dropped on the live docs site
Verified **2026-07-14** against the live site (`coder.com/docs`, i.e.
`main`, pre-merge) by loading each affected page in headless Chrome and
reading the post-hydration DOM (confirmed identical in the raw page
payload). Each simple `<token>` placeholder is parsed as an **empty
custom HTML element**, so the browser renders nothing for it and the
placeholder text disappears from the page.
### What readers see today (before this PR)
| Page (live) | Source Markdown | Rendered on the live site |
|-------------|-----------------|---------------------------|
| [`cli/server`](https://coder.com/docs/reference/cli/server) — OAuth2
GitHub Allowed Teams | `Structured as: <organization-name>/<team-slug>.`
| `Structured as: /.` |
|
[`cli/tokens_create`](https://coder.com/docs/reference/cli/tokens_create)
— `--allow` | `Repeatable allow-list entry (<type>:<uuid>, e.g.
workspace:1234-...).` | `Repeatable allow-list entry (:, e.g.
workspace:1234-...).` |
| [`api/tasks`](https://coder.com/docs/reference/api/tasks) — `q` | `...
status:<status>` | `... status:` (nothing after the colon) |
| [`api/schemas`](https://coder.com/docs/reference/api/schemas) —
AIBridgeConfig (`anthropic`/`bedrock`/`openai`) |
`CODER_AI_GATEWAY_PROVIDER_<N>_*` | `CODER_AI_GATEWAY_PROVIDER__*` |
| [`api/schemas`](https://coder.com/docs/reference/api/schemas) —
AIBridgeConfig (`providers`) | `CODER_AI_GATEWAY_PROVIDER_<N>_<KEY>` |
`CODER_AI_GATEWAY_PROVIDER__` |
[`api/chats`](https://coder.com/docs/reference/api/chats) (`q`) drops
five tokens the same way — `title:<substring>`, `diff_url:<url>`,
`pr:<number>`, `pr_title:<text>`, and the trailing `title:<value>`. The
live parameter description reads (note the dangling `title:`,
`diff_url:`, `pr:`, `pr_title:`):
```text
Search query. Supports title: (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status:<draft|open|merged|closed> as repeated or comma-separated values, source:<created_by_me|shared_with_me>, diff_url: (quote values containing colons), pr: (exact PR number match), repo:<owner/repo> (case-insensitive substring match against git remote origin or URL), pr_title: (case-insensitive PR title substring). Bare terms are not supported; use title: for title filtering.
```
<details>
<summary>Raw rendered DOM from the live site (headless Chrome,
post-hydration)</summary>
```html
<!-- reference/cli/server -->
Structured as: <organization-name>/<team-slug>.</team-slug></organization-name>
<!-- reference/cli/tokens_create -->
Repeatable allow-list entry (<type>:<uuid>, e.g. workspace:1234-...).</uuid></type>
<!-- reference/api/tasks : only status:<status> drops; the /-containing tokens are escaped and survive -->
Search query for filtering tasks. Supports: owner:<username/uuid/me>, organization:<org-name/uuid>, status:<status></status>
<!-- reference/api/schemas : anthropic / bedrock / openai rows -->
Deprecated: Use Providers with indexed CODER_AI_GATEWAY_PROVIDER_<n>_* env vars instead.</n>
<!-- reference/api/schemas : providers row -->
Providers holds provider instances populated from CODER_AI_GATEWAY_PROVIDER_<n>_<key> env vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.</key></n>
```
The parser auto-inserts closing tags
(`</team-slug></organization-name>`) and lowercases the tag name (`<N>`
becomes `<n>`), leaving `__` where `<N>_` used to be. Every wrapped
placeholder renders correctly as inline code on the [docs preview for
this
branch](https://coder.com/docs/@vigilante%2Fdocs-551-backtick-placeholder-syntax-in-generated-reference-docs-cli/reference/cli/server).
</details>
### Accuracy note — cases that do *not* drop on live
These render fine today, so they are **not** evidence of dropping (the
PR still wraps them for consistency / MDX-safety):
-
[`api/organizations`](https://coder.com/docs/reference/api/organizations)
and
[`api/provisioning`](https://coder.com/docs/reference/api/provisioning):
`{'tag1':'value1','tag2':'value2'}` renders verbatim — curly braces are
not an HTML tag.
- Tokens containing `/` or `|` are escaped by the renderer and stay
visible (as literal `<...>`): `<username/uuid/me>`, `<org-name/uuid>`,
`<owner/repo>`, `<draft|open|merged|closed>`,
`<created_by_me|shared_with_me>`. Backticks still improve their
readability, but they were never dropped.
|
||
|
|
3126306598 |
feat: add --aigateway-proxy-target flag (#27122)
Adds `--aigateway-proxy-target` option to `deploymentGroupAIGatewayProxy` that defines URL to which intercepted requests should be forwarded to. Forward URL used to be hardcoded to `coderAPI.AccessURL` pointing to embedded Gateway. With addition of standalone AI Gateway this needs to be configurable. Renamed `aibridgeproxyd.Server.coderAccessURL` and `coderAccessPort` -> `gatewayURL` and `gatewayPort` + option to better reflect reality. |
||
|
|
ad29777cb2 | feat: NATS mTLS pubsub implementation (#26902) | ||
|
|
6580cdcf7f |
refactor: use AI budget period from deployment config (#27117)
## Description
Read the AI budget period from the deployment config on both the RPC server and the `/users/{user}/ai/spend` endpoint, instead of hardcoding `month`. Drops the `period_start` RPC parameter that was incorrectly introduced in #26915: the period should have been derived from the deployment config from the start.
## Changes
- Add `codersdk.NewAIBudgetPeriodFromString`, mirroring `NewAIBudgetPolicyFromString`.
- `aibridgedserver.Server` takes a `quartz.Clock`, reads `BudgetPeriod` from the deployment config at construction, and computes the period window inside `IsBudgetExceeded`.
- Remove `period_start` from `IsBudgetExceededRequest` and stop sending it from the daemon.
- The `userAISpendStatus` endpoint reads the period from `AIBridgeConfig.BudgetPeriod` instead of hardcoding month.
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
|
||
|
|
d66e4d794f | feat: add configurable reasoning effort to Coder agents (#26974) | ||
|
|
bab8ce9d41 |
feat: setup logging, tracing and metrics in standalone AI Gateway (#27068)
Adds logging, tracing and metrics setup to standalone AI Gateway. Existing options are re-used when possible. |
||
|
|
ef0b5585d5 |
feat: record and expose terminal upstream interception errors (#26961)
Categorises the terminal error of a failed interception and persists it on the interception record, then surfaces it on the AI Gateway API. - Categorise into an enum (`bad_request`, `unauthorized`, `rate_limited`, `overloaded`, `server_error`, `unknown`), unwrapping the ResponseError envelope, the upstream Anthropic/OpenAI SDK errors, and key-pool exhaustion so blocking and streaming paths agree. - Thread the type and raw message through the recorder dRPC into the `aibridge_interceptions` row (optional proto fields; NULL on success). - Expose the error on the AI Gateway thread API from the root interception. *This PR was produced by opencode (agent) using the `anthropic/claude-opus-4-8` model, under human direction and review.* |
||
|
|
affb359d13 |
feat: synchronise provider changes with WatchAIProviders (#27091)
## Why PR #26797 was accidentally merged into the stale `graphite-base/26797` branch instead of `main` (Graphite picked the wrong base), so its changes never landed on `main`. This PR re-lands that work as a clean cherry-pick onto the current `main`. ## What Adds a `WatchAIProviders` streaming RPC to the `ProviderConfigurator` service so a running standalone AI Gateway refetches its provider set when the provider configuration changes. The server subscribes to `AIProvidersChangedChannel` (published by the provider CRUD endpoints) and forwards each event as a payload-free signal, plus one signal on subscribe; the gateway calls `GetAIProviders` on each signal to rebuild its pool. The aibridged API is bumped to v1.2. Env-seeded providers don't need a signal: seeding finishes before coderd serves the gateway connection, so the gateway's initial fetch already reflects the seeded set. ## For reviewers The change is split into two commits to make review easy: 1. **`feat: synchronise provider changes with WatchAIProviders`** is a faithful cherry-pick of #26797, identical to the originally reviewed PR. It is committed without pre-commit hooks because it does not build against current `main` on its own. 2. **`fix: resolve cherry-pick conflicts against main`** contains only the deltas needed to re-land on current `main`, and passes the full pre-commit suite: - `coderd/aibridged/proto/aibridged.pb.go` regenerated via the proto make target (the cherry-picked copy was generated against the older proto). - `enterprise/cli/aigatewaystart.go` import block unioned; `main` added `os` and `strings` while the PR added `sync`. - Three `aibridgedserver.NewServer` test call sites that landed on `main` after the original branch diverged now pass the new `pubsub` argument. Refs https://linear.app/codercom/issue/AIGOV-465 *This PR was produced by opencode (agent) using the `anthropic/claude-opus-4-8` model, under human direction and review.* |
||
|
|
48f07e6e13 |
feat: add user AI spend endpoint (#26978)
## Description
Adds the `GET /api/v2/users/{user}/ai/spend` endpoint returning the
user's current AI spend, effective budget, and period bounds.
## Changes
- Add `userAISpendStatus` handler under the same feature/experiment gate
as `/api/v2/users/{user}/ai/budget`.
- Add `codersdk.UserAIBudgetSummary` (embedded into `UserAISpendStatus`)
and a `UserAISpendStatus` client method.
- Move `LimitSource` from `coderd/aibridge/budget` to `codersdk` so the
type is shared across endpoints.
Closes https://linear.app/codercom/issue/AIGOV-472
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
|
||
|
|
60d0859bde |
feat: add /healtz and /readyz endpoints to standalone Gateway (#26988)
Adds `/healthz` and `/readyz` endpoints to standalone AI Gateway. * `/healthz`: returns 200 once the AI Gateways HTTP server is listening. * `/readyz`: returns 200 when the DRPC connection to `coderd` is established. Cleanup: Removed `initConnection*` fields from `aibridged.Server` as they where not used anywhere. |
||
|
|
ccba3969ab |
feat: add ai-gateway start command (#26605)
> AI Tools were used to produce this PR This PR adds `coder ai-gateway start` command that runs the AI Gateway as an independent process. - Standalone process doesn't have access to DB. Uses DRPC services under `/api/v2/ai-gateway/serve`for auth, recording and provider initialization. - It only handles LLM traffic, other endpoints (eg. `/sessions`) are only available though `coderd`. - The standalone gateway reuses applicable flags from AI Gateway deployment options. Provider-seeding and coderd-only options are excluded. - Only added to fat build, the slim build stub rejects the command. Some wiring used by this new command is added. **`NewWebsocketDialer`** - implements the standalone gateway's connection to coderd's `/api/v2/ai-gateway/serve` endpoint. It upgrades to a WebSocket, multiplexes with yamux, and wires all DRPC services. **`AIGatewayDataPlaneMiddleware`** - extracts the per-request middleware chain (concurrency limiting, rate limiting, BYOK gating) into a shared function used by both the embedded route and the standalone gateway. **`RootCmd.ResolveClientConnection`** - resolve the deployment URL and builds an HTTP transport without requiring a session token. Used in `ai-gateway start`command as it authenticates using different credential type. --------- Co-authored-by: Danny Kopping <danny@coder.com> |
||
|
|
90861ffa75 |
test(enterprise/dbcrypt): add Rotate/Decrypt/Delete regression tests (#26979)
Related to #25389. `enterprise/dbcrypt/cliutil.go`'s `Rotate`, `Decrypt`, and `Delete` are the only interface for migrating database encryption keys, but had zero test coverage. Adds `enterprise/dbcrypt/cliutil_test.go` with per-table regression tests for all three functions. ## Covered The 7 tables currently handled by `Rotate`/`Decrypt`/`Delete`: - `user_links`, `external_auth_links` - `user_secrets` - `gitsshkeys` - `ai_providers`, `ai_provider_keys`, `user_ai_provider_keys` Each table has a happy-path test verifying live rows (and, where the schema allows it, soft-deleted rows) round-trip correctly. `Rotate` and `Decrypt` additionally have a failure-path test per table confirming the operation aborts cleanly with a `DecryptFailedError`, leaving data untouched, when an active `dbcrypt_keys` entry isn't included in the operation. `Delete` takes no cipher argument, so instead each test confirms an encrypted row is wiped while a never-encrypted row is left untouched by the `WHERE ... IS NOT NULL` clauses in its SQL. ## Gaps (follow-up) `crypto_keys`, `mcp_server_configs`, and `mcp_server_user_tokens` are encrypted at rest but are not yet handled by `Rotate`/`Decrypt`/`Delete` at all. This is the bug reported in #25381: an incomplete `--old-keys`/`--keys` list against these tables fails with a foreign key violation on revoke. A follow-up PR will add both the `cliutil.go` fix and matching regression tests for these three tables. |
||
|
|
fc188fdaee |
fix: create agent firewall sessions without requiring agent read access (#26990)
## Overview Part of the **boundary correlation** feature. Fixes lazy creation of `boundary_sessions` rows so it works within the agent's RBAC constraints, and consumes the new `ConfinedProcessName` field reported by boundary. Pairs with coder/boundary#206, which adds `ConfinedProcessName` to `ReportBoundaryLogsRequest`. This branch bumps the `github.com/coder/boundary` module to pick up that work. ## Problem `ensureSession` did a pre-insert existence check via `GetBoundarySessionByID`. Agents are **not permitted to read boundary sessions**, so that read path is not viable when the session is created from an agent-reported log batch. ## Changes - **Remove the pre-insert read.** `ensureSession` now inserts directly and treats a primary-key unique violation as success, covering sessions already created by a prior batch, a reconnection, or another coderd replica — without requiring read access. - **Per-connection guard.** Add a mutex-protected `ensuredSessions` set so repeated log batches on the same connection skip the existence check and insert entirely, touching the database only for the logs. On a transient insert failure the session is left unmarked so the next batch retries. - **Consume `ConfinedProcessName`.** Pass `req.GetConfinedProcessName()` through to the session insert. - **Bump boundary module** from `v0.9.0` to `v0.9.1-0.20260706095856-35ba90f9e8b2`. - **Tests.** - Add `TestReportBoundaryLogsAgentRBAC` (`coderd/boundary_logs_test.go`), an integration test that connects as a real workspace agent, verifies the session and log are persisted under agent RBAC, and asserts the agent subject cannot read boundary sessions — guarding against reintroducing a pre-insert read. - Add `TestReportBoundaryLogsSessionGuard` (session inserted once across two batches, logs inserted per batch) and `TestReportBoundaryLogsSessionRetriedOnError` (insert retried after a transient error). - Regenerate `agent-firewall` CLI docs/golden files and adjust the clidocgen template to render the YAML path when a flag has no long name. > 🤖 This PR was opened by Coder Agents on behalf of @SasSwart. |
||
|
|
d51762440b | feat: add custom AI provider icons and instance-based model picker grouping (#27026) | ||
|
|
2ec7f5c69b |
test: fix TestUserAIBudgetOverride audit flake (#27013)
Closes https://linear.app/codercom/issue/AIGOV-502/flake-testuseraibudgetoverrideauditupsertspendlimit The `Audit/UpsertEverything` and `Audit/UpsertSpendLimit` subtests grabbed `rows[0]` from `GetAuditLogsOffset`, assuming strict time-desc order. Both upserts emit `AuditActionWrite` against the same override with no ordering tiebreaker, so when the create and update entries share a timestamp the create entry could sort first and fail the assertion. Select the update entry by the spend limit it results in via a new `auditLogByNewSpendLimit` helper instead of by row position. <hr/> We could make `auditLogByNewSpendLimit` more generic, but since we don't have any other use cases for it right now, I think we can postpone that until the need arises. Related PR: https://github.com/coder/coder/pull/26630/changes |
||
|
|
79a74fc817 |
Revert "chore: hide AI Gateway key management UI/CLI/API (#26879)" (#26913)
Reverting https://github.com/coder/coder/commit/377c1309b7a42ead9cfbd864f0af8e9b6e472851 since release 2.35 was already cut: https://github.com/coder/coder/tree/release/2.35 |
||
|
|
047c47495b |
refactor: drop chat_model_configs provider column (#26877)
The provider type already lives authoritatively in ai_providers.type, reachable on every active row through ai_provider_id, which the chat_model_configs_ai_provider_required_when_active CHECK makes mandatory. The stored provider string was a denormalized copy the system kept in sync with a startup backfill and no longer needs. Every surface now derives provider type from the linked ai_providers row. Telemetry is the one exception: it keeps emitting provider, now sourced from ai_providers.type via a JOIN, so the BigQuery column and the Nexus dashboards that read it are unaffected. The experimental HTTP/SDK response drops provider and makes ai_provider_id required, since those endpoints return only active configs; consumers resolve provider type from ai_provider_id and the AI providers listing. This ships in a single release with no compatibility window: production reads the table via SELECT *, so a pre-drop binary fails config reads the moment the column is gone. Operators must scale to zero before upgrading, and there is no rollback. Closes CODAGT-599 |
||
|
|
377c1309b7 |
chore: hide AI Gateway key management UI/CLI/API (#26879)
Hides UI, CLI and API related to AI Gateway key management + `/api/v2/ai-gateway/serve` endpoint. API endpoints and CLI commands are still working they are just not visible. |
||
|
|
e5b7e74847 |
test: migrate chatd tests to AI Gateway routing (#26658)
Refs CODAGT-681 Migrates all chatd tests from `AIGatewayRoutingEnabled = false` (direct routing) to AI Gateway routing using the test helpers extracted in #26639. - `coderd/x/chatd/chatd_test.go` — 6 full-server tests migrated to `NewWithAPI` + daemon, `directChatRoutingDeploymentValues` helper deleted, 3 bare-chatd tests renamed - `coderd/x/chatd/context_integration_test.go` — 2 tests migrated - `coderd/exp_chats_test.go` — `chatDeploymentValues` helper deleted, all 5 helper functions now use `NewWithAPI` + daemon internally (no call site changes) - `coderd/exp_chats_acl_test.go` — stale `chatDeploymentValues` reference replaced - `enterprise/coderd/exp_chats_test.go` — 9 sites across 5 `TestChatStreamRelay` subtests migrated - `cli/exp_scaletest_chat_test.go` — 1 test migrated - `coderd/x/chatd/model_routing_internal_test.go` — 1 direct-only test removed - `coderd/x/chatd/chatd_internal_test.go` — 1 direct-only test removed > 🤖 |
||
|
|
1dea00dd04 |
fix: deflake TestWorkspaceTagsTerraform with context-aware build waits (#26315)
`TestWorkspaceTagsTerraform` runs a real terraform provisioner but waited on builds with `coderdtest` helpers whose deadlines are sized for the echo provisioner used by most tests, which replays canned responses and completes in well under a second. On Windows runners, where terraform providers are not cached and every `terraform init` downloads from the registry, template imports exceeded the 25s budget in `AwaitTemplateVersionJobCompleted` and workspace builds exceeded the 10s context in `AwaitWorkspaceBuildJobCompleted`, even though the test intends a 120s budget. Add `AwaitTemplateVersionJobCompletedWithTimeout` and `AwaitWorkspaceBuildJobCompletedWithTimeout`, which take a caller-provided wait bound, and use them in the test with `2*testutil.WaitSuperLong` (120s). Also fix `AwaitWorkspaceBuildJobCompleted` creating a `WaitShort` (10s) context while polling for `WaitMedium` (15s), which guaranteed `context deadline exceeded` errors for the final five seconds of polling. `TestWorkspaceTemplateParamsChange` has the same shape (real terraform provisioner, 120s test context, plain await helpers) and the same latent bug, so it gets the same fix. Closes https://github.com/coder/internal/issues/1470 (Linear: PLAT-176) <details> <summary>Root cause analysis</summary> Two CI failures, same mechanism: - 2026-04-16 (run 24493089585, windows-2022): `overrides_with_dynamic_option_from_var/dynamic` failed at `coderdtest.AwaitTemplateVersionJobCompleted` with `Condition never satisfied ... make sure you set IncludeProvisionerDaemon!`. The template import job (real terraform init/plan, with network provider download) did not complete within `WaitLong` (25s). - 2026-05-27 (run 26492817796, windows-2022): `tag_param/dynamic` failed at `coderdtest.AwaitWorkspaceBuildJobCompleted` with `failed to get workspace build ...: context deadline exceeded`. The helper's internal context was `WaitShort` (10s) while its polling window was `WaitMedium` (15s), so after 10s every poll could only fail. The logged `terraform apply: exit status 1` and the `TempDir RemoveAll ... Access is denied` cleanup error are consequences of test teardown canceling the in-flight job while the provider exe was still file-locked. The test declares a 120s budget (`2*testutil.WaitSuperLong`, commented "This can take a while"), but the await helpers ignored it and applied their own 10-25s budgets. `testutil.CacheTFProviders` is a no-op on Windows, so real builds are much slower there. This change raises the ceiling for the tests rather than making terraform faster; both observed failure signatures are eliminated. The default helper budgets are unchanged for the ~880 existing call sites. One small behavior change: `AwaitTemplateVersionJobCompleted` previously marked the test failed on any transient poll error via `assert.NoError`; it now logs and keeps polling, matching the workspace build helper, and still fails on timeout. `TestWorkspaceTemplateParamsChange` is the sibling real-terraform test in the same file (also covered by the original provider-caching work in #20603). It runs three sequential real builds with the plain await helpers under a 120s context, so it is exposed to the same Windows slowness even though it has not produced its own issue yet. Its context is raised to `6*testutil.WaitSuperLong` to outlast three sequential await budgets. API note: a context-taking variant was considered first, but a `time.Duration` parameter avoids an implicit "context must have a deadline" contract and matches how the existing helpers manage their own wait budgets. </details> --- 🤖 This PR was generated by Coder Agents on behalf of @jscottmiller. |
||
|
|
56373a09fc |
chore: rename user-facing AI Bridge strings to AI Gateway (#26700)
Rename user-facing "AI Bridge" strings to "AI Gateway" in deployment config, RBAC display names, log messages, error strings, docs style guide, and Grafana dashboard README. Deprecated option names and descriptions (the `--aibridge-*` block) are intentionally kept as "AI Bridge". The `Name` field cannot be renamed because `serpent` uses it as a unique key during JSON serialization; duplicating names causes `UnmarshalJSON` failures (e.g. in the support bundle). Descriptions also stay as "AI Bridge" to avoid confusion between the deprecated and primary options. Refs https://linear.app/codercom/issue/AIGOV-226 > Generated with the assistance of Coder Agents (@ssncferreira) |
||
|
|
ce94d42e19 |
feat: fetch providers over DRPC (#26650)
Closes [AIGOV-455](https://linear.app/codercom/issue/AIGOV-455/extend-drpc-with-buildproviders). ## Why The AI Gateway (`aibridged`) is being split into a standalone process that must not touch the database. `coderd` stays the source of truth and seeds the `ai_providers` / `ai_provider_keys` tables from the environment. This PR adds a DRPC call so the gateway fetches provider config from `coderd` instead of reading the DB, for both the embedded and standalone daemons. ## What - **Proto:** new `ProviderConfigurator` service with a unary `GetAIProviders` RPC, plus `AIProvider` / `AIProviderBedrock` messages. `CurrentMinor` bumped to 1 (additive). - **Server (`coderd/aibridgedserver`):** `GetAIProviders` runs a read-only `InTx` under `LockIDAIProvidersEnvSeed` so it never returns a mid-seed snapshot, reads providers (incl. disabled) plus keys for enabled ones, and maps to proto under `dbauthz.AsAIBridged`. Unmappable rows are skipped and logged; plaintext keys and Bedrock secrets are never logged. - **Client:** `DRPCProviderConfiguratorClient` wired into the client union, `dialer.go`, and `CreateInMemoryAIBridgeServer`. - **cli:** `BuildProvidersFromProto` maps the response through the existing DB-neutral `buildProvider`. A shared `poolRPCReloader` does the fetch/build/replace for both daemons: the embedded daemon reloads on every `ai_providers` change and fails startup if it cannot subscribe; the standalone gateway drives the same reloader once at startup, retrying until success and staying interruptible. - **Dead code removed:** `BuildProvidersFromConfig`, `ProvidersFromConfig`, `AIProviderFromConfig`, and the DB-read `BuildProviders` path. |
||
|
|
9efbcc7601 |
chore: make dynamic parameters WorkspaceParametersPage the default (#25095)
|
||
|
|
c15d483863 |
chore: rename 'last_used_at' column (#26749)
Renames the `last_used_at` column to `last_heartbeat_at` in `ai_gateway_keys` table. `ai_gateway_keys` table has not been released yet. All references updated. |