Commit Graph
898 Commits
Author SHA1 Message Date
Yevhenii ShcherbinaandCian Johnston 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>
2026-07-27 12:09:21 -04:00
Susana Ferreira 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
2026-07-23 09:26:25 +01:00
Sas SwartandClaude Opus 4.8 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>
2026-07-21 14:34:23 +02:00
Spike Curtis 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.
2026-07-21 11:19:23 +02:00
Susana Ferreira 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
2026-07-20 13:04:37 +01:00
Susana Ferreira 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
2026-07-20 12:54:52 +01:00
Cian Johnston 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.
2026-07-20 10:30:02 +01:00
Susana Ferreira 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
2026-07-16 16:09:25 +01:00
Spike Curtis 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.
2026-07-16 09:42:23 +02:00
Callum Styan ad29777cb2 feat: NATS mTLS pubsub implementation (#26902) 2026-07-13 11:00:02 -07:00
Susana Ferreira 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
2026-07-13 17:52:45 +01:00
Danny Kopping 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.*
2026-07-08 15:32:17 +02:00
Susana Ferreira 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
2026-07-08 12:46:47 +01:00
Paweł BanaszewskiandDanny Kopping 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>
2026-07-08 11:12:53 +02:00
Yevhenii Shcherbina 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
2026-07-06 14:18:02 -04:00
Paweł Banaszewski 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
2026-07-02 13:45:05 +00:00
Mathias Fredriksson 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
2026-07-01 15:59:55 +03:00
Paweł Banaszewski 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.
2026-06-30 17:32:38 +00:00
Cian Johnston 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

> 🤖
2026-06-30 12:17:42 +01:00
J. Scott Miller 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.
2026-06-29 09:56:42 -05:00
Danny Kopping 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.
2026-06-29 13:34:58 +02:00
McKayla はな 9efbcc7601 chore: make dynamic parameters WorkspaceParametersPage the default (#25095) 2026-06-26 17:55:34 -06:00
Paweł Banaszewski 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.
2026-06-26 18:45:37 +02:00
Paweł Banaszewski 6189d6e386 feat: add /api/v2/aibridge/serve endpoint (#26506)
Adds a new enterprise-only `GET /api/v2/ai-gateway/serve` endpoint that standalone AI Gateway replicas use to connect to `coderd` over a DRPC-over-WebSocket transport, mirroring the existing in-memory path used by the embedded AI Bridge daemon.

- The endpoint upgrades the HTTP connection to a WebSocket, multiplexes it with yamux, and finally serves the three DRPC services (Recorder, MCPConfigurator, Authorizer).
- The `X-AI-Governance-Gateway-Key` header is used for authentication.
    - The key is looked up by its hashed secret
    - Missing or revoked keys return `401`.
- API version negotiation is enforced via a new `aibridged/proto` version (`v1.0`).
    - Incompatible versions return `400`.
- `FeatureAIBridge` entitlement is required.
- Key liveness (`last_used_at`) is recorded immediately on connection and refreshed every 60 seconds while the session remains open.
  - When key liveness detects the key was deleted (no rows where updated) session is closed.

#### Small refactors

* The three DRPC service registrations are extracted into `aibridgedserver.Register`, shared by both the in-memory and WebSocket paths.

* The literal `256 * 1024` used as the yamux-aligned WebSocket read limit is replaced with the named constant `drpcsdk.YamuxDefaultStreamWindowSize` in all call sites.
  * as noted in review comment https://github.com/coder/coder/pull/26506#discussion_r3461905223 order of `SetReadLimit` and `WebsocketNetConn` calls was fixed.
2026-06-26 18:27:37 +02:00
Cian Johnston 387011d725 test: extract AI Gateway test helpers for chatd (#26639)
Extracts test infrastructure for AI Gateway routing into shared helpers
under a new package `coderd/aibridgedtest` so both AGPL and enterprise
tests can use them.

- aibridgedtest.StartTestAIBridgeDaemon` spins up a real in-process 
  aibridged daemon wired to fake upstream providers.
- `chattest.MockAIBridgeTransport` is a mock `aibridge.TransportFactory`
   for the 3 bare-chatd tests that use `newActiveTestServer`.

> 🤖 Generated by Coder Agents under the eyes of a human.
2026-06-26 14:33:26 +01:00
Spike Curtis 98e1ce133c chore: modify replicasync to handle NATS explicitly (#26666)
relates to GRU-69

Modifies replicasync to handle discovering NATS enabled primary replicas explicitly, and passing that info to the NATS Pubsub.

This PR adds a new deployment value to explicitly represent the host or IP that the replica can be reached on. It isn't wired up to the CLI, but piggybacks on the DERP config for now.

We learn the NATS port directly from NATS at runtime, and propagate it thru replicasync to learn all peers for clustering.
2026-06-26 08:36:32 -04:00
Zach 953091c7bc refactor: use sync.WaitGroup.Go in tests (#26671)
Migrate `wg.Add(1); go func() { defer wg.Done(); ... }()` to
`wg.Go(func() { ... })` in tests.

Where the prior pattern passed the loop variable explicitly via a
closure parameter (`go func(id int) { ... }(i)`), drop the parameter and
reference the loop variable directly. Per-iteration loop variables since
Go 1.22 make this safe.
2026-06-25 15:41:09 -06:00
Susana Ferreira 5cae613af1 docs: rename AI Bridge to AI Gateway in swagger summaries (#26704)
Update `@Summary` and `@ID` annotations in
`enterprise/coderd/aibridge.go` from "AI Bridge" to "AI Gateway".
Regenerate swagger docs and API reference via `make gen`.

This was missed in the original API route aliases PR (#26475) which
renamed `@Tags` but not `@Summary` or `@ID` values. The `@ID` must also
change because a test (`assertConsistencyBetweenRouteIDAndSummary`)
enforces that the ID is the kebab-case form of the summary.

Refs https://linear.app/codercom/issue/AIGOV-230

> Generated with the assistance of Coder Agents (@ssncferreira)
2026-06-25 16:56:37 +01:00
Spike Curtis e8bd5004a2 chore: add replica_host and nats_port to replicas table (#26665)
relates to GRU-69

Adds cluster_host and nats_port to replicas table, to explicitly track NATS routes in the cluster.

I decided to make the NATS support explicit and transport the port number over the replicasync so that different Coder Servers can run on different ports. This is not something customers will typically care about, but is very useful for testing, so that they can all run on localhost within one machine.

I've also gone with a design where the NATS pubsub directly tells replicasync the port number _after_ it opens the socket. This is also very useful for testing because it allows us to have the OS assign the port number at runtime, avoiding races where we fail to bind to a free port.
2026-06-24 15:04:04 -04:00
Cian JohnstonandCopilot Autofix powered by AI e8c53f7968 chore: add test to document current behaviour on template ACL revocation (#26104)
Documents a question raised in
https://github.com/coder/coder/pull/26061#discussion_r3361458492 - I
couldn't find the exact answer, so adding a test and accompanying
documentation seemed like the prudent move here.

Obligatory disclosure: an agent wrote this code under my supervision.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-24 12:57:27 +01:00
Jon Ayers 4cfed1b3ed feat: plumb time_til_autostop_notify template field (#26439) 2026-06-23 17:32:47 -05:00
Yevhenii Shcherbina a06e5a3698 test: index AI budget audit logs by action (#26630)
Fixes
https://linear.app/codercom/issue/AIGOV-436/flake-testgroupaibudgetaudit
2026-06-23 13:57:21 -04:00
Callum StyanandMux d104d0dcb3 fix(enterprise/coderd): propagate license events over Postgres pubsub when NATS is in use (#26536)
Co-authored-by: Mux <mux@coder.com>
2026-06-23 09:58:45 -07:00
Jon Ayers c7ddcce62c fix: only return group member count for workspace acl (#26206) 2026-06-23 11:58:00 -05:00
Susana Ferreira 970bd73691 feat: add /api/v2/ai-gateway API route aliases (#26475)
## Description

Registers `/api/v2/ai-gateway/*` as the new API path for AI Gateway, replacing `/api/v2/aibridge/*`. Both prefixes share the same route builder (`aiBridgeRoutes`) backed by a single in-memory handler, so existing `/aibridge` endpoints continue to work. New endpoints must be registered on the enterprise API handler under `/api/v2/ai-gateway` only.

Swagger annotations now point to `/api/v2/ai-gateway` paths with a backward-compatibility note referencing `/aibridge`. The legacy `/aibridge` routes are skipped in the swagger documentation test.

## Changes

- Store one raw handler (`aiGatewayHandler`) instead of two prefix-stripped handlers
- Register `/ai-gateway` and `/ai-gateway/proxy` route aliases alongside legacy `/aibridge` routes
- Move `/aibridge/keys` to `/ai-gateway/keys`
- Update in-process transport to use `/api/v2/ai-gateway` prefix
- Update SDK client URLs and proxy forwarding URL
- Swap `@Router` and `@Tags` annotations from `aibridge`/`AI Bridge` to `ai-gateway`/`AI Gateway`
- Rename user-facing error messages from "AI Bridge" to "AI Gateway"
- Define consts for route prefixes (`AIGatewayRootPath`, `AIBridgeRootPath`)
- Update tests and comments to use new paths

Note: the following will be addressed in follow-up PRs:
- Frontend API URLs
- Frontend routes and redirects
- Dogfood main.tf updates
- Hand-written documentation URL updates
- aibridge internal comments and nits
- Scale tests path updates

Refs https://linear.app/coder/issue/AIGOV-230

> Generated with the assistance of Coder Agents (@ssncferreira)
2026-06-23 12:15:10 +01:00
Sas Swart adad5bdd49 feat: surface agent firewall correlation in AI Bridge sessions API (#26416)
Add `agent_firewall_session_id` and `agent_firewall_sequence_number`
fields to `AIBridgeThread` in the `GET
/api/v2/aibridge/sessions/{session_id}` response. These fields link each
thread to its agent firewall confinement session so the frontend can
discover the boundary session and compute sequence ranges for
interleaving firewall events within the thread timeline.

The database columns already exist on `aibridge_interceptions`
(migration 000520) and are already selected by
`ListAIBridgeSessionThreads`. This PR surfaces them through the SDK type
and the `db2sdk` conversion.

Depends on #24814

**Naming note:** The RFC uses `boundary_session_id` /
`boundary_sequence_number`, but the codebase standardized on
`agent_firewall_*` naming in the DB migration. The API fields follow the
existing convention.

</details>

> [!NOTE]
> This PR was authored by Coder Agents.
2026-06-22 15:17:37 +02:00
Sas Swart 335d6bda1b feat: add GET /api/v2/agent-firewall/sessions/{id}/logs endpoint (#24816)
Add a `GET /api/v2/agent-firewall/sessions/{id}/logs` endpoint that
returns agent firewall audit logs for a given session, sorted by
sequence number ascending.

The endpoint supports `seq_after` and `seq_before` (exclusive bounds)
and `limit` query parameters. This enables the frontend to fetch exactly
the firewall events that fall between two AI Bridge interceptions within
a thread, as described in FR 4 of the Boundary/Bridge correlation RFC.

Authorization reuses the `boundary_log` RBAC resource (owner and auditor
can read; members cannot). Returns 404 for unauthorized users to avoid
leaking existence information.

The endpoint is enterprise-only, gated behind `FeatureBoundary`
entitlement, matching the session endpoint from #24814.

Depends on #24814

> [!NOTE]
> This PR was authored by Coder Agents.
2026-06-22 13:56:29 +02:00
Ethan bc9cc8cb08 fix(enterprise/coderd): allow deleting external-agent workspaces (#26501)
I stumbled on this while manually testing another workspace change: if a
license expires after an external-agent workspace exists, deleting that
workspace is rejected because `CheckBuildUsage` enforces the
external-agent entitlement for every workspace transition. I assume this
is unintentional from a product POV.

This PR narrows the external-agent entitlement check to start builds.
Creating or rebuilding external-agent workspaces still requires the
feature entitlement, but stop and delete transitions stay available as
cleanup paths.

That matches the managed-agent entitlement precedent (i.e. the usage
billing check) in the same `CheckBuildUsage` path, where license
enforcement is scoped to start transitions instead of trapping users
with resources they can no longer remove.
2026-06-22 12:02:01 +10:00
Sas Swart 491a75294e feat: GET /api/v2/agent-firewall/sessions/{id} (#24814)
Add a GET endpoint at `/api/v2/agent-firewall/sessions/{id}` that
returns agent firewall session metadata (`id`, `workspace_id`,
`owner_id`, `confined_process`, `started_at`). The handler authorizes
against the `boundary_log` resource with `ActionRead` via dbauthz.

The endpoint is enterprise-only, gated behind the `FeatureBoundary`
entitlement.

The `GetBoundarySessionByID` SQL query JOINs through `workspace_agents`
→ `workspace_resources` → `workspace_builds` → `workspaces` to return
`workspace_id` and `workspace_owner_id` directly, avoiding a separate
query.

Also adds an `owner_id` column to the `boundary_logs` table (migration
000526) with a FK to `users(id)` and a backfill from
`boundary_sessions`. This enables user-scoped RBAC authorization for
`InsertBoundaryLogs` via `.WithOwner()`, ensuring workspace agents can
only insert logs for their own owner.

Depends on #24810

**RBAC behaviour:**

| Role    | Result |
|---------|--------|
| Owner   | read   |
| Auditor | read   |
| Member  | 404    |

> [!NOTE]
> This PR was authored by Coder Agents.
2026-06-18 20:50:17 +02:00
George K e702bfa535 test(enterprise/coderd): deflake TestInvalidateTemplatePrebuilds (#26499)
`TestInvalidateTemplatePrebuilds` assumed a stable ordering from preset
invalidation results, but the underlying `UPDATE ... RETURNING` query
does not guarantee row order. Local WIP schema changes were enough to
surface that latent flake when running tests.

Update the test to compare invalidated presets as a set instead of by
slice position. This keeps the behavior under test the same, while
removing dependence on unspecified database row ordering.
2026-06-17 16:33:46 -06:00
Ethan 9f2d88fe24 test(enterprise/coderd): stabilize TestUserSkillAuditDiffTracksContent (#26326)
`GetAuditLogsOffset` orders by `"time" DESC` with no tiebreaker, and
`dbtime.Now` rounds to microseconds, so two audit logs emitted in quick
succession (especially on platforms with coarser clock resolution like
Windows) can land in the same microsecond and Postgres is free to return
them in either order. The test then assumes positional ordering and
breaks.

Sort `rows` by `Action` descending before indexing so `rows[0]` is the
update log and `rows[1]` is the create log regardless of timestamp
collisions.

Closes CODAGT-585
Closes https://github.com/coder/internal/issues/1551
2026-06-18 00:22:19 +10:00
Hugo Dutka 4f74a7adee fix: enable goleak in chatd tests (#26335)
Enable goleak in chatd tests and fix some leaks. Addresses
https://github.com/coder/coder/pull/26109#discussion_r3380039964
2026-06-16 12:35:40 +00:00
Susana Ferreira 12d7ad6100 feat: add ai-gateway-cost-control experiment flag (#26399)
Adds the `ai-gateway-cost-control` experiment flag to gate new cost
control endpoints and upcoming frontend UI behind an explicit opt-in.

Currently AI Gateway cost control supports the following endpoints:
- `GET/PUT/DELETE /api/v2/organizations/{org}/groups/{group}/ai/budget`
- `GET/PUT/DELETE /api/v2/users/{user}/ai/budget`

Note: the group-level endpoints were already released in v2.34.0 and
remain ungated. Only the user-level endpoints are gated behind this
experiment. Future cost control endpoints and UI should use this
experiment for gating until the feature is stable.

> Generated by Coder Agents on behalf of @ssncferreira
2026-06-16 10:33:34 +01:00
Danny Kopping a1330e3a8c refactor: rename Ai* database identifiers to AI* (AIGOV-369) (#26327)
Adds `ai` to sqlc's `gen.go.initialisms` in `coderd/database/sqlc.yaml`
so the generated DB code follows Go's initialism convention. Adds the
matching `ai` -> `AI` case to the dbgen PascalCase helper
(`scripts/dbgen/main.go`) so the corresponding `dbmem` / mock
identifiers stay in sync. `make gen` regenerates the rest; hand-written
call sites that consume DB-generated identifiers
(`enterprise/audit/table.go`, `coderd/database/modelmethods.go`,
`enterprise/coderd/aigatewaykeys.go`, `coderd/database/dbauthz/*`, etc.)
are updated to match.

Scope is deliberately limited to the database layer:

- `coderd/rbac/*` (resource and scope generators) is untouched —
`ResourceAi*` / `ScopeAi*` constants stay on main's casing.
- `codersdk/*` (Go SDK) is untouched — `codersdk.ResourceAi*` /
`codersdk.APIKeyScopeAi*` constants stay on main's casing, so external
Go SDK consumers see no source-level break.
- `Aibridge*` identifiers (one SQL token `aibridge`, not `ai_bridge`)
are out of scope.

On-the-wire values are unchanged: enum strings, RBAC resource type
strings, API key scope strings, and JSON tags all stay the same. The
HTTP/JSON surface is unaffected.

Refs:
[AIGOV-369](https://linear.app/codercom/issue/AIGOV-369/change-ai-references-in-coderddatabasemodelsgo-to-ai)

🤖 Generated with [Coder Agents](https://coder.com)
2026-06-16 09:01:43 +00:00
Hugo Dutka 4debd23cbb fix: chatd refactor (#26270)
Implements the chatd stabilization RFC.

Combines:
- https://github.com/coder/coder/pull/25908
- https://github.com/coder/coder/pull/25923
- https://github.com/coder/coder/pull/26109
- https://github.com/coder/coder/pull/26110
- https://github.com/coder/coder/pull/26111
- https://github.com/coder/coder/pull/26112
2026-06-12 13:33:12 +02:00
Danny Kopping 4a07f61c50 refactor!: remove interceptions API, request logs view, and associated code (#26213)
## Summary

Removes the deprecated `/api/v2/aibridge/interceptions` endpoint and the
Request Logs frontend page, both replaced by the session-based view.

Closes https://linear.app/codercom/issue/AIGOV-266
Closes https://linear.app/codercom/issue/AIGOV-324

## Changes

### Backend
- Remove `GET /api/v2/aibridge/interceptions` HTTP handler and route
- Remove SDK types and client method (`AIBridgeInterception`,
`AIBridgeTokenUsage`, `AIBridgeUserPrompt`, `AIBridgeToolUsage`,
`AIBridgeListInterceptionsResponse`, `AIBridgeListInterceptionsFilter`)
- Remove SQL queries `CountAIBridgeInterceptions` and
`ListAIBridgeInterceptions`
- Remove `searchquery.AIBridgeInterceptions` parser
- Remove dbauthz wrappers, in-memory implementations, metrics, and mocks
for the interceptions list queries
- Remove the `coder aibridge interceptions list` CLI command and golden
files
- Regenerate API docs, swagger, mocks, and metrics

The `/models`, `/clients`, and `/sessions` endpoints stay; the sessions
list page still consumes all three.

### Frontend
- Delete the entire `RequestLogsPage/` directory (page, view, row,
filter, stories, tests)
- Remove the `/aibridge/request-logs` route and its lazy import
- Remove the `getAIBridgeInterceptions` API method,
`paginatedInterceptions` query, and mock interception entities
- `git mv` the shared filter and icon components used by the sessions
pages:
- `RequestLogsPage/RequestLogsFilter/{Client,Model,Provider}Filter.tsx`
→ `AIBridgePage/filters/`
- `RequestLogsPage/icons/AIBridge{Client,Model,Provider}Icon.tsx` →
`AIBridgePage/icons/`
- Drop the `getProviderIconName` hack and the duplicate `anthropic-neue`
icon case now that the FIXME no longer applies

## Commits

1. `refactor: remove interceptions API and request logs view` — the bulk
removal, with explicit renames for the shared filter/icon files.
2. `refactor(site/src/pages/AIBridgePage): drop getProviderIconName
hack` — cleanup of the FIXME that depended on RequestLogsPage existing.

> [!NOTE]
> Generated by Coder Agents on behalf of @dannykopping
2026-06-12 07:50:46 +02:00
Cian Johnston a4c867f11b fix: backfill legacy Bedrock AI provider rows and stale model config strings (#26155)
Fixes CODAGT-548

Adds two idempotent startup backfills run after `newAPI():

- `BackfillBedrockProviderType`: promotes `ai_providers` rows from
`type=anthropic` with Bedrock settings to `type=bedrock`.
- `BackfillChatModelConfigProviderStrings`: fixes stale
`chat_model_configs.provider = "anthropic"` strings on rows whose linked
provider was just promoted.
- `UpdateAIProvider` query now also writes the `type` column, so the
fix persists on any subsequent PATCH.


> 🤖 Generated by Claude with oversight from a human.
2026-06-11 15:31:31 +01:00
Callum StyanandMux 4e2c9f9cae fix: allow lifecycle code path to retry failed stop jobs (#26203)
Co-authored-by: Mux <mux@coder.com>
2026-06-10 16:29:00 -07:00
Nick Vigilante cfb03f52db fix: update stale docs URLs across non-TS files (#25750)
Closes [DOCS-256](https://linear.app/coder/issue/DOCS-256). Sibling to
[DOCS-253](https://linear.app/coder/issue/DOCS-253) (#25740).

Updates docs URL references across the non-TypeScript surface of
`coder/coder` to match the current docs site structure. Source-of-truth
for redirects is `coder/coder.com/redirects.json` (parent ticket
[DOCS-209](https://linear.app/coder/issue/DOCS-209)).

## What changed

| Area | Files | URL mapping |
|---|---|---|
| Top-level README | `README.md` | `/docs/workspaces` ->
`/docs/user-guides/workspace-management`, `/docs/templates` ->
`/docs/admin/templates`, `/docs/ides` ->
`/docs/user-guides/workspace-access` |
| Docs source | `docs/admin/security/0001_user_apikeys_invalidation.md`
| `/docs/admin/audit-logs` -> `/docs/admin/security/audit-logs` |
| Docs source | `docs/install/cloud/azure-vm.md` |
`/docs/coder-oss/latest/install` -> `/docs/install` |
| Dogfood | `dogfood/coder/guide.md` | `/docs/ides` ->
`/docs/user-guides/workspace-access` |
| Helm | `helm/coder/values.yaml` | `/docs/admin/workspace-proxies` ->
`/docs/admin/networking/workspace-proxies` |
| Enterprise coderd | `enterprise/coderd/coderd.go` |
`/docs/admin/encryption` -> `/docs/admin/security/database-encryption`
(error message) |
| Release tooling | `scripts/release/main_internal_test.go` |
`/docs/admin/upgrade` -> `/docs/install/upgrade` (test fixture, matches
`generate_release_notes.sh`) |
| AI bridge | `aibridge/client.go` | repinned to current `main` SHA on
renamed `docs/ai-coder/ai-gateway/monitoring.md`, line range `#L47-L57`
|
| Example templates | 12 `examples/templates/*/README.md`,
`examples/parameters/*`,
`examples/parameters-dynamic-options/README.md`,
`examples/workspace-tags/README.md`, `examples/parameters/main.tf`,
`examples/examples.gen.json` (regenerated) | `/docs/workspaces` ->
`/docs/user-guides/workspace-management`, `/docs/templates/parameters`
-> `/docs/admin/templates/extending-templates/parameters`,
`/docs/templates/dev-containers` ->
`/docs/admin/integrations/devcontainers`, `/docs/dotfiles` ->
`/docs/user-guides/workspace-dotfiles`,
`/docs/about/architecture#agents` ->
`/docs/admin/infrastructure/architecture#agents` |
| Live notification templates (DB) | New migration
`000510_fix_dormancy_notification_docs_urls.up.sql` and `.down.sql` plus
the four regenerated SMTP/webhook goldens under
`coderd/notifications/testdata/rendered-templates/` |
`/docs/templates/schedule#dormancy-threshold-enterprise` ->
`/docs/admin/templates/managing-templates/schedule#dormancy-threshold`,
`/docs/templates/schedule#dormancy-auto-deletion-enterprise` ->
`/docs/admin/templates/managing-templates/schedule#dormancy-auto-deletion`
|

The migration uses `REPLACE(body_template, ...)` scoped by template id
and `LIKE '%/docs/templates/schedule%'`, so it works regardless of which
intermediate state (`000232`, `000262`, `000305`, or `000311`) is
currently in the row.

## What did not change

Historical SQL migrations `000232`, `000262`, `000305`, and `000311` are
not modified because migrations are immutable history. The 18 remaining
stale URL references in those files are superseded at runtime by
migration `000510`. This decision matches the pattern used in the A1
sister PR (#25740).

## Verification

- `go test ./coderd/database/migrations/... -count=1` (UP+DOWN)
- `go test ./coderd/notifications/ -run TestNotificationTemplates_Golden
-update -count=1` to regenerate the four `.golden` files
- `go test ./scripts/release/ -run Test_removeMainlineBlurb -count=1`
- `make pre-commit` (gen + fmt + lint + slim build) ran clean as part of
the commit hook

I also fixed a pre-existing emdash on line 35 of
`examples/templates/azure-linux/README.md` that the lint flagged once
the file entered my diff. The line was already in `main`, but `make gen`
rewrites `examples/examples.gen.json` whenever a `README.md` changes, so
the line came back as a `+` in the diff against `origin/main` and the
`lint/emdash` step refused it.

<details>
<summary>Pre-mortem</summary>

| Risk | Mitigation |
|---|---|
| Migration overwrites future template edits | Used `REPLACE` instead of
full body overwrite. `WHERE id IN (...) AND body_template LIKE
'%/docs/templates/schedule%'` further scopes the write |
| Goldens drift from migrated body | Regenerated goldens via `-update`
after the migration was in place, so the goldens reflect the
post-migration state |
| Down migration leaves stale URLs | Down migration reverses the REPLACE
so a rollback restores the prior URLs |
| Fragment loss when redirect strips fragment | Verified the destination
`schedule.md` contains `## Dormancy threshold` and `## Dormancy
auto-deletion` anchors |
| Terraform parse breakage in `examples/parameters/main.tf` | Only
comments changed; Terraform parser is unaffected |
| Test fixtures in `scripts/release` diverging from
`generate_release_notes.sh` | Updated to match the script, which already
emits `/docs/install/upgrade` |

</details>

---

Generated by Coder Agent on behalf of @nickvigilante.
2026-06-10 13:40:50 -04:00
Yevhenii Shcherbina 360611ea15 feat: audit user AI budget override mutations (#25745)
Relates to
https://linear.app/codercom/issue/AIGOV-285/add-user-budget-overrides-table-and-crud-api

Adds audit-log support for `user_ai_budget_override` mutations. Without
it, an admin could quietly change a user's per-user spend cap (e.g. from
`$500` to `$50`), reassign it to a different group, or delete it
entirely with no record of who did it.

Both write (`create-or-update`) and delete actions now generate audit
log entries. Unlike group AI budgets, which only track `spend_limit`,
overrides also track `group_name`: an override can be reassigned to a
different attributed group, so that change needs to show up in the diff.
The raw `spend_limit_micros`, IDs, and timestamps are ignored in favor
of the human-readable `spend_limit` and `group_name`.

Depends on #25439.

## Screenshot

<img width="1343" height="514" alt="image"
src="https://github.com/user-attachments/assets/aee30f58-6e81-435e-9bca-5bc98f49d8d3"
/>
2026-06-10 00:29:06 +00:00