Commit Graph
15542 Commits
Author SHA1 Message Date
Jeremy Ruppel cf7f876880 feat(site): add generateUUID helper (#27661)
Adds a `generateUUID()` helper to `site/src/utils/uuid.ts`.

It uses `crypto.randomUUID()` when available, and otherwise falls back
to `crypto.getRandomValues()`, setting the version (4) and variant (RFC
4122) bits before formatting the 16 random bytes into the standard
`8-4-4-4-12` UUID string.

Seriously open to any implementation here, let me know if you have a
favorite!

---
_This PR was created by Coder Agents on behalf of @jeremyruppel._
2026-07-30 10:46:40 -04:00
Michael Suchacz b3852c707b fix(site/src/pages/AISettingsPage/SpendPage): announce cost controls move in v2.36 (#27688)
Corrects the version in the AI settings Spend banner: cost controls
features move to AI Governance in **v2.36**, not v2.37.

Updates the banner copy in `SpendPageView` and the matching Storybook
play assertion. No other changes.

The `release/2.36` backport is opened manually as #27690, so this PR
does not carry the `cherry-pick` label.

> Mux, an AI agent, prepared this PR on Mike's behalf.
2026-07-30 16:01:27 +02:00
Susana Ferreira b4eda32a2e fix: hide AI budget override controls without permission (#27654)
### Description

Setting a user's AI budget override updates both the user and the group
its spend is charged to, so it requires `user:update` and
`group:update`. Organization admins have group update but only site-wide
user read, so they could tick "Override group budget", enter an amount,
and then fail on save.

The dialog now shows the member's budget as read-only when the viewer
can't change it.

### Changes

- Gate the override controls on `user:update` (site-wide) in addition to
the group permission the page already checks
- Replace the form with a read-only view: the group's budget, followed
by "To update this limit, contact a Coder administrator."
- Swap the whole view rather than disabling the checkbox, since an
existing override seeds the form enabled and unchecking it would call
the delete endpoint and fail the same way
- Add stories for the read-only dialog and for the page-level wiring

> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by
@ssncferreira
2026-07-30 14:28:41 +01:00
Paweł Banaszewski e819dd4af6 fix(helm/ai-gateway): render service nodePort with an explicit if guard (#27682)
> Coder Agents generated this commit

Replace the with block around the Service nodePort field with an if
guard that references .Values.service.nodePort directly. The with form
rebinds the dot inside the block, so a later addition that needs .Values
or .Release there would break.

Extend the default_values fixture to enable ingress and httproute with
only the values each one requires, so the golden file covers every
template with the minimum viable configuration.

Add a mustNotContain list to the render test cases. Golden files are
rewritten wholesale by TestUpdateGoldenFiles, so these assertions pin
optional fields and resources that each fixture leaves unset, including
the Service nodePort.
2026-07-30 14:47:07 +02:00
Spike Curtis dc31791c88 test: don't use ptytest for client side of SSH session tests (#27681)
In our initial batches of test refactors, I left the SSH session tests using `ptytest` because I (erroneously) thought that we still needed a client side PTY when the SSH server creates a PTY. This is incorrect and plain in-process IO is fine on the client side.  
  
closes https://github.com/coder/internal/issues/1400   
(again)<!--

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.

-->
2026-07-30 14:30:15 +02:00
3f3fd1c4d7 feat: show network request summary on AI session detail card (#27418)
Frontend for the AI session network summary. Adds Network calls, Blocked
network requests, and Top domains rows to the Session summary card on
the individual AI session detail page, driven by the network fields on
the session threads response.

Renders "Disabled" when network monitoring was not active and "No
activity" when there were no calls. Covered by Storybook stories for
each state.

### PR map (merge strictly bottom-up)

This change is a 4-PR stack. Each PR depends on all the ones below it,
so merge in this exact order:

1. #27417 — backend network summary
2. #27418 — frontend summary rows
3. #27425 — backend per-call list `network_call_logs`
4. #27426 — frontend network-calls panel

Refs AIGOV-463

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cian Johnston <cian@coder.com>
2026-07-30 13:23:51 +02:00
841a1765f7 feat: add network calls summary to AI session threads API (#27417)
Backend for the AI session network summary. Exposes total/blocked
network calls and top destination domains on the session threads
endpoint (`GET /api/v2/ai-gateway/sessions/{id}`).

Total and blocked reuse the existing Agent Firewall aggregation from the
sessions list query, so the numbers match the sessions table. Top
domains are a new server-side aggregation
(`GetAIBridgeSessionTopDomains`) over boundary logs, using the same
interception-window correlation. There is no network-error state,
matching the current data model.

Frontend consuming these fields is in a separate stacked PR.

### PR map (merge strictly bottom-up)

This change is a 4-PR stack. Each PR depends on all the ones below it,
so merge in this exact order:

1. #27417 — backend network summary (base `main`)
2. #27418 — frontend summary rows (base #27417)
3. #27425 — backend per-call list `network_call_logs` (base #27418)
4. #27426 — frontend network-calls panel (base #27425)

Refs AIGOV-463

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cian Johnston <cian@coder.com>
2026-07-30 13:09:46 +02:00
Susana Ferreira 3660ffecdd fix: add warn log level to unpriced models message (#27678)
When a model is missing from the price table, token usage is still
recorded but with a NULL cost, so AI spend for that model goes
unattributed. This was logged at debug level, which means it is
invisible in a default deployment.
Log it at warn level instead so admins can see which provider/model
pairs need a price row and act on it.
2026-07-30 12:03:22 +01:00
Michael Suchacz 95a2c2ba02 feat: back the per-chat cost endpoint with AI Gateway data (#27328)
## Stack Context

This stack removes native chat cost tracking and native chat usage
limits, making the AI Gateway the single source of AI spend data and
budget enforcement.

1. **This PR:** re-back the per-chat cost endpoint with AI Gateway data.
2. Remove native chat usage limits end to end, rewiring the sidebar
indicator to gateway spend.
3. Remove native chat cost tracking end to end, deleting the
Analytics/Spend cost UI.

## What?

`GET /api/experimental/chats/{chat}/cost` summed
`chat_messages.total_cost_micros`, which native chat cost tracking
maintained. It now aggregates AI Gateway interception data instead, and
has no native fallback.

- New `GetAIBridgeChatCost` query, authorized through the root chat so
members can read their own chat's cost without gaining access to raw
interception rows.
- Response fields renamed: `priced_message_count` -> `request_count`,
`unpriced_messages_having_usage_count` -> `unpriced_request_count`.
- The chat summary sidebar keys its cost cache by root chat, and hides
the cost row where the AI Gateway is off or unlicensed. The root cost is
invalidated when a chat leaves an active status and when a generated
title lands, since title generation bills its own gateway request.

`GetChatModelUsageCostByChatID` and the rest of native cost tracking are
untouched here; PR 3 removes them.

## Why?

Native cost tracking duplicates what the AI Gateway already records, and
the two disagree. Repointing the endpoint first means the cost UI keeps
working while the native implementation is deleted later in the stack.

Two behaviour changes follow from gateway semantics and are intentional:

- **Requests, not messages.** The gateway records interceptions, so
counts are requests. Title-generation traffic now counts.
- **Whole-tree totals.** The gateway records the *spawning* chat's ID as
the interception session ID, so a subagent's requests are attributed to
its immediate parent, not always the root. Only a whole chat tree can be
summed, so the query resolves the root and aggregates the tree, and
every chat in a tree reports the same total. Native returned per-subtree
totals.

## Attribution and counting semantics

The aggregate groups token usage per interception before counting, so
the reported numbers are per request even though a request records one
usage row per provider response:

- `RequestCount` counts finished `Coder Agents` interceptions in the
tree, including unpriced ones.
- `UnpricedRequestCount` counts requests with at least one usage row the
gateway could not price. It is a subset of `RequestCount`.
- `TotalCostMicros` omits only unpriced usage, so a partially priced
request still contributes its priced portion. The sidebar therefore says
`Excludes unpriced usage from N request(s)` rather than claiming whole
requests were dropped.

A recorded cost of zero is a free request, not an unpriced one. Usage
without an effective group is excluded, matching what never reached
`ai_user_daily_spend`.

## Authorization

Reads go through `ExtractChatParam` plus `ResourceChat`, with no
cost-specific RBAC widening. `TestGetChatCost/MemberCanReadOwnChat`
covers a scoped `agents-access` member reading their own chat's cost,
and `MemberCannotReadOtherUsersChat` still asserts 404 for a non-owner.
Plain members without `agents-access` cannot create or read chats at
all, so they never reach this endpoint.

## Known limitation

AI Gateway data has its own retention period, 60 days by default and
configured independently of chat retention, so spend for requests older
than that is no longer reported. A chat whose gateway records have all
been purged reports zero cost, which is indistinguishable from genuinely
free usage under this contract. The endpoint documents the caveat;
#27330 documents it on the Spend Management page.

In-flight interceptions are excluded, since cost is only known once the
response is recorded. A chat's cost therefore lags the active turn by
one request.

## Rebase note

Rebased onto `main` after #27579 removed the `ai-gateway-cost-control`
experiment. The per-chat cost row is now gated on the `aibridge` feature
alone, matching how #27579 degated the other cost-control surfaces.

> Mux prepared this PR on Mike's behalf.
2026-07-30 13:01:48 +02:00
Jaayden Halko 54d5eb7ec2 feat: add hourly hb_agent_runtime_v1 usage events for Coder Agent runtime (#27312)
closes CODAGT-839
closes CODAGT-843
closes CODAGT-773

## Summary

Adds a new heartbeat usage event type, `hb_agent_runtime_v1`, measuring
the total agent-loop runtime of Coder Agents (chats) per UTC hour, plus
a reconciler that generates one event per hour with self-healing
backfill over a trailing 7-day window. Events flow to Tallyman through
the existing publisher unchanged. This measures the new Coder Agents
(the `chats` tables), not the deprecated Tasks counted by
`dc_managed_agents_v1`.

Independent of #27508, which fixes the dead ai-seats cron registration.
Both PRs carry the identical `usage_event` create permission hunk for
the usage-publisher subject (this feature's generator and the ai-seats
cron each need it for heartbeat inserts), so they can land in either
order and the overlap merges cleanly.

> [!WARNING]
> **Do not include this in a release until Tallyman accepts
`hb_agent_runtime_v1`.** The publisher marks permanently rejected events
as done-forever, and the generator then sees those buckets as complete
locally, so their usage would be silently and permanently lost.

## Details

Each event's payload is `{"runtime_ms": N}`: the sum of
`chat_messages.runtime_ms` for messages created in the hour bucket `[H,
H+1)`, across all chats (sub-agents, API-created, archived, and
soft-deleted messages included). Events use deterministic IDs
(`hb_agent_runtime_v1:<bucket start>`) with `created_at` set to the
bucket start, so concurrent replicas race safely via `ON CONFLICT (id)
DO NOTHING` without locking, and daily rollups attribute backfilled
hours to the correct day. Idle hours produce zero-valued events. A
bucket becomes eligible 5 minutes after it closes; hours missing for
longer than the 7-day window are forfeited, which can only undercount.

Note that this makes `usage_events.created_at` explicitly the *event
occurrence time* rather than the row insertion time; the two only
diverge for backfilled events. It already behaved as the occurrence
timestamp (it drives the daily rollup day and is shipped to
Tallyman/Metronome as the event timestamp), and the migration now
documents this with a `COMMENT ON COLUMN`, which also surfaces as a Go
doc comment on `UsageEvent.CreatedAt`.

The new `usage.Generator` runs unconditionally in enterprise builds; the
`publish_usage_data` license flag continues to gate egress only, so
air-gapped deployments still fill their local ledger. The
`aggregate_usage_event()` trigger sums `runtime_ms` per day into
`usage_events_daily` (unlike `hb_ai_seats_v1`, which takes the daily
max).

`InsertHeartbeatUsageEvent` now takes an explicit `createdAt` so
generators can backfill historical buckets; the cron passes
`clock.Now()` to preserve its existing behavior.

## Tallyman follow-up

<details>
<summary>Prompt for the Tallyman-repo agent</summary>

> **Task**: Add support for the new Coder usage event type
`hb_agent_runtime_v1` so Tallyman accepts, validates, and forwards it to
Metronome.
>
> **Background**: coder/coder PR (this PR) adds hourly heartbeat events
measuring Coder Agent runtime. Events arrive via the existing
`/api/v1/events/ingest` endpoint with: `event_type:
"hb_agent_runtime_v1"`, `event_data: {"runtime_ms": <int64 >= 0>}`,
deterministic `id` of the form `hb_agent_runtime_v1:2026-07-15_14:00:00`
(UTC hour bucket start), and `created_at` set to the bucket start (may
be up to ~8 days in the past due to backfill; within Metronome's 34-day
dedup window). Zero-value events are normal (idle hours).
>
> **Work**:
> 1. Update Tallyman's vendored/imported `coderd/usage/usagetypes` (or
equivalent) to the coder/coder commit that adds
`UsageEventTypeHBAgentRuntimeV1` and `HBAgentRuntime`.
> 2. Ensure ingestion validation accepts the type (`Valid()` switches)
and rejects negative `runtime_ms`.
> 3. Ensure Metronome forwarding maps the event with transaction ID
derived from the event `id` as for existing types, passing `runtime_ms`
through as the property for a SUM-aggregated billable metric ("Coder
Agent Hours" = `SUM(runtime_ms) / 3,600,000`).
> 4. Do NOT permanently reject unknown-but-well-formed future `hb_*`
types if avoidable; at minimum confirm current behavior for unknown
types (temporary vs permanent rejection) and report it.
> 5. Tests: ingest accept/validate, dedup by ID, Metronome payload
mapping.
>
> **Constraint**: this must be deployed to tallyman-prod **before** any
coder/coder release containing the event generator; coderd treats
permanent rejections as terminal per event.

</details>
2026-07-30 08:37:45 +01:00
McKayla はな 2b28515d9b refactor: migrate story snapshot params to pixel (#26844) 2026-07-29 18:13:55 -06:00
Bobby Ho d210b311dc ci(.github): retry build-tool downloads in Windows signing jobs (#27664)
## Problem

The Windows code-signing path downloads two build tools with bare `wget`
and no retry, in both `ci.yaml` (`build` job) and `release.yaml`
(`release` job):

- `rcodesign` from GitHub releases
- `jsign-6.0.jar` from GitHub releases

A single transient network failure on either fetch fails the whole job.
In `ci.yaml` that turns `main` red via the `required` aggregator; in
`release.yaml` it fails a release.

This has happened. `Install rcodesign` failed on **2026-02-25** (in the
since-deleted `build-dylib` job), **2026-03-04**, and **2026-04-30**.

## Root cause

Two parts, one structural and one local.

**Structural:** GitHub Actions has no per-step retry. This repo already
knows toolchain provisioning is network-flaky and has
`.github/scripts/retry.sh` (3 attempts, 2s/4s/8s backoff), applied in
roughly 20 places. But `retry.sh` is a shell wrapper, so it can only
wrap `run:` steps. These four downloads are `run:` steps that were
simply never wrapped.

**Local:** the failing step's body, under `set -euo pipefail`, is
exactly three commands:

```sh
wget -O /tmp/rcodesign.tar.gz https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/...
sudo tar -xzf /tmp/rcodesign.tar.gz -C /usr/bin --strip-components=1 ...
rm /tmp/rcodesign.tar.gz
```

`tar` and `rm` operate on a file that was just written, so they are
deterministic. The only nondeterministic command in the step is the
network fetch, and a truncated download surfaces as a `tar` failure
whose cause is still the network.

### How we know

Enumerated failed runs through the GitHub Actions API and extracted, per
run, every failed job together with the names of its failed steps.

| Scan | Scope | Runs |
|---|---|---|
| `ci.yaml`, `main` | 2025-08-01 to 2026-07-29 | 934 |
| `ci.yaml`, all branches | most recent failures | 150 |
| `release.yaml` | all recorded failures | 22 |

The 934 is effectively the complete set; the API reports 923 failed
`main` runs over that period and the scans overlap slightly.

`Install rcodesign` appears **3 times on 3 separate dates**. Being
spread across dates rather than clustered, these behave as
**independent** events. That distinction is what selects the remedy, and
it is why this change is retry rather than removal.

For contrast, the `Setup Java` failures in the same jobs are **4
failures inside a single 90-minute window** on 2026-05-28, all from an
`api.azul.com` edge failure. That is a correlated outage, where every
attempt shares the same degraded dependency and retry provably cannot
help. **That defect is not addressed here** and needs a different fix;
see "Not addressed" below.

### Limits of the evidence

Stating these plainly so a reviewer can weigh them:

- **Cause is not directly confirmed.** Logs for all three `rcodesign`
failures are past GitHub's 90-day retention. The inference from the step
body above is strong but circumstantial.
- **Step-level attribution only reaches back about five months.** GitHub
prunes per-step detail from the jobs API while keeping job-level
conclusions. Probed directly: runs from 2026-03-01 onward return
populated `steps` arrays; runs from 2026-02-05 and earlier return empty
ones. So the true count over the full period could be higher; it cannot
be lower.
- **Impact is small.** This whole class of failure is 10 of 800
attributed non-`required` job failures, about **1.25%** of measured
`main` CI failure volume. This is not a significant reliability
improvement and should not be reviewed as one. The Postgres-backed Go
tests alone are over 40%.

## Solution

Wrap all four downloads in the existing retry helper:

```yaml
- ./.github/scripts/retry.sh -- wget -O /tmp/rcodesign.tar.gz https://...
```

Four lines changed, one per site: `ci.yaml:1287`, `ci.yaml:1321`,
`release.yaml:199`, `release.yaml:225`.

**How it works.** `retry.sh` runs the command, and on non-zero exit
sleeps 2s, 4s, then 8s before re-attempting, up to 3 attempts, then
fails with the original command in the error message. On success the
first time, behavior is unchanged.

**Why it works for these failures.** They are independent events, so
each attempt is a fresh trial with an independent chance of success. A
GitHub releases CDN blip on one run says nothing about the next 2
seconds. This is exactly the regime retry is for.

**Why retry rather than deletion.** These artifacts genuinely are not
present on the runner, so the network call is unavoidable. It can only
be made survivable. (Where a dependency *is* avoidable, deletion is the
better answer, which is the shape the `setup-java` fix will take.)

**Why `wget -O` is safe to retry.** `-O` truncates its output file on
each attempt, so a partial download from a failed attempt is overwritten
rather than appended to. No corruption path.

## Risks

Low, and worth naming precisely.

| Risk | Assessment |
|---|---|
| Behavior change on the success path | None. `retry.sh` execs the
command directly; a first-attempt success is identical to today. |
| A persistently broken URL now takes longer to fail | Yes, by up to 14s
of backoff, then it fails exactly as it does today. Negligible against a
job that takes tens of minutes. |
| `retry.sh` mangling `wget`'s own flags | `retry.sh` parses its own
options with `getopt`, so this was the main correctness concern.
Verified explicitly, both argument orders used in these workflows. See
Verification. |
| Relative path `./.github/scripts/retry.sh` resolving wrongly | These
steps set no `working-directory`, so cwd is the repo root. Deliberately
**excluded** the third `wget` at `release.yaml:713`
(`publish-homebrew`), which runs after `cd "$temp_dir"` where a
repo-relative path would break. |
| Retry masking a real regression | Bounded to 3 attempts over 14s. This
is not job-level auto-retry, which would hide regressions and is
explicitly not proposed. |

### Verification gap a reviewer should know about

**The changed steps do not run on PR CI.** `ci.yaml`'s `build` job is
gated on `github.ref == 'refs/heads/main' || startsWith(github.ref,
'refs/heads/release/')`, and `release.yaml` runs only on release. So
these four steps will execute for the first time on merge to `main`.
Verification below is therefore local plus static analysis, not a live
run of the modified steps.

## Verification

`retry.sh` argument passing, using a stub that prints what it received,
for both argument orders present in these workflows:

```
--- form A: -O before URL (rcodesign style) ---
argc=3
  arg1=[-O]
  arg2=[/tmp/rcodesign.tar.gz]
  arg3=[https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-x86_64-unknown-linux-musl.tar.gz]
--- form B: URL before -O (jsign style) ---
argc=3
  arg1=[https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar]
  arg2=[-O]
  arg3=[/tmp/jsign-6.0.jar]
```

Order preserved and the `%2F` encoding in the rcodesign URL intact,
which was the specific failure mode to rule out.

`make lint/actions` (actionlint plus zizmor security audit):

```
✓ lint/actions/actionlint
No findings to report. Good job! (29 ignored, 102 suppressed)
```

`make pre-commit-light`:

```
✓ fmt/shfmt        ✓ lint/markdown    ✓ lint/actions/actionlint
✓ fmt/terraform    ✓ lint/shellcheck  ✓ lint/helm
✓ fmt/markdown     ✓ lint/bootstrap   ✓ lint/emdash
                   ✓ lint/migrations  ✓ lint/typos
                                      ✓ lint/mise-versions
✓ pre-commit-light passed (14s)
```

## Not addressed

Deliberately out of scope, listed so the remaining exposure is visible:

- **`actions/setup-java` with `distribution: "zulu"`** in both files.
This resolves a JDK from `api.azul.com` and downloads it from
`cdn.azul.com` on **every** run, confirmed from a successful `main`
build's log, because a `Java_Zulu_jdk` tool-cache lookup can never hit
the runner's cache. This is the correlated-outage defect from 2026-05-28
and retry cannot fix it.

The probe on this branch ([run
30492093096](https://github.com/coder/coder/actions/runs/30492093096))
has now answered what the fix should be. `depot-ubuntu-22.04-8` ships:

  ```
  RESULT: java found at /usr/bin/java
  OpenJDK Runtime Environment Temurin-11.0.31+11 (build 11.0.31+11)

  JAVA_HOME=/usr/lib/jvm/temurin-11-jdk-amd64
  JAVA_HOME_8_X64 / _11_X64 / _17_X64 / _21_X64 / _25_X64  (all present)
  tool cache: Java_Temurin-Hotspot_jdk
  ```

So the job downloads **Zulu 11.0.32+9 over two third-party hosts while
Temurin 11.0.31+11 is already on the runner's `PATH`**. The follow-up PR
will point `JAVA_HOME` at `$JAVA_HOME_11_X64` and drop the action, which
removes both Azul hosts while keeping the Java 11 pin rather than
inheriting whatever the image default becomes.
- **`storybook`'s `pnpm/action-setup`**, the last direct use in the repo
and the same unguarded `registry.npmjs.org` dependency originally
reported on the issue. Note `cache: true` does **not** mitigate it: per
the action's own `action.yml`, `cache` caches "the pnpm store
directory", not the pnpm binary.

## Scope

This PR is now a **single commit** (`673162b84e`) containing only the
four-line retry change.

A throwaway probe workflow briefly lived on this branch to answer the
JDK question above. It has served its purpose and the commit was
dropped, so nothing diagnostic remains here to review. Its result is
quoted in "Not addressed" and will be carried into the follow-up PR.

Refs coder/internal#929
2026-07-29 17:05:22 -07:00
Bobby HoandClaude Opus 5 4e512f786f fix: prefetch outdated Coder CLI in e2e setup instead of in the test (#27629)
## Summary

`e2e/tests/outdatedCLI.spec.ts` has a 30 second budget in which it must
create a template and workspace, start an agent, download an 84 MiB
release binary from GitHub, and then exercise the actual thing under
test: whether a `v2.8.0` client can still SSH into a workspace served by
HEAD. In the run that filed this ticket, `install.sh` spent **20.04
seconds** of that budget on an HTTP request the test does not need,
leaving 5.4 seconds for the download. The SSH flow never executed.

Worth being precise about the shape, because it changes the fix. The
stall is not in the code under test and it is not an SSH problem.
`install.sh` resolves the latest stable release version
**unconditionally**, even when `--version 2.8.0` is passed explicitly,
and on the pinned path that value feeds nothing but a cosmetic
post-install advisory string. Two thirds of the test's budget went to
producing one sentence of console output that the test discards.

Refs: https://github.com/coder/internal/issues/1571

## Problem

### What the test is for

This is a backward-compatibility test, and `v2.8.0` is the compatibility
floor it enforces rather than a "supported version" in the
release-channel sense. The pin traces to one code comment, `we no longer
support versions prior to Tailnet v2 API support`, citing 059e533544;
that commit first shipped in v2.8.0, so the pin sits exactly on the
boundary it names. Worth stating plainly: this is the oldest client
expected to still interoperate, not a version that receives patches.
Release support is mainline / stable / n-2 / ESR, all far newer.

The test's value is that it runs the *real* historical binary, compiled
in Feb 2024, against a current server: `codersdk` REST compatibility,
tailnet coordination v2, DERP negotiation, `coder ssh --stdio` as an SSH
transport, and the agent accepting a session. Nobody gets to assert what
that old client sends over the wire, which is exactly why the binary has
to be downloaded rather than faked.

The structural defect is that the download shares a timeout with the
assertion:

```text
┌─────────────────────────────────────────────────────────────────┐
│  ONE 30-second Playwright test budget                           │
├──────────────────────────────┬──────────────────────────────────┤
│  What we want to measure     │  Incidental setup                │
│  (deterministic, local)      │  (network, non-deterministic)    │
│                              │                                  │
│  • template + workspace      │  • HTTP HEAD to github.com       │
│  • agent connect             │  • 84 MiB download from          │
│  • coder ssh --stdio         │    GitHub release CDN            │
│  • SSH handshake + exec      │  • tar extraction                │
│  • workspace stop            │                                  │
└──────────────────────────────┴──────────────────────────────────┘
      ~7-14 s, stable                   0 s (cached) .. ∞ (unbounded)
```

A test that asserts protocol compatibility should not be able to fail
because `github.com` was slow.

### The evidence

CI runs Playwright with `DEBUG: pw:api`, and `downloadCoderVersion`
passes `TRACE=1` to `install.sh`, which makes it `set -x`. The job log
therefore stamps every phase. Reconstructed from [job
80049661296](https://github.com/coder/coder/actions/runs/27124540074/job/80049661296),
`t=` relative to test start:

```text
t=+0.000  08:17:44.671  browserContext.newPage                      <- test starts
t=+0.882  08:17:45.553  login complete
t=+4.228  08:17:48.899  workspace create submitted
t=+4.519  08:17:49.190  agent-status-ready visible                  <- startAgent returns
t=+4.526  08:17:49.197  install.sh: parse_arg --version 2.8.0 ...   <- downloadCoderVersion
t=+4.531  08:17:49.202  curl -sSLI https://github.com/coder/coder/releases/latest
                        :
                        :   20.042 SECONDS OF NOTHING
                        :   (agent logs keepalives; the page sits idle)
                        :
t=+24.573 08:18:09.244  response= 200 .../releases/tag/v2.33.6      <- probe returns
t=+24.575 08:18:09.246  STABLE_VERSION=2.33.6                       <- feeds a log line
t=+24.582 08:18:09.253  curl -#fL -o .../coder_2.8.0_linux_amd64.tar.gz.incomplete
                        :   5.4 s of an 84 MiB download
t=+30.000 08:18:14.671  Playwright kills the test
```

Three observations rule out the originally suspected cause (slow SSH
readiness or general runner slowness):

- **The SSH flow never started.** `sshIntoWorkspace` is called after
`downloadCoderVersion` returns, and it never returned. There is no
`coder ssh --stdio` process in the log.
- **The agent was healthy.** `agent-status-ready` resolved in 88 ms, and
through the entire 20 second stall the agent logs a live DERP
connection, successful STUN, and a completed wireguard handshake.
- **The runner was fast, not slow.** Login plus template plus workspace
plus agent took 4.5 seconds.

### Where the 20 seconds goes

```text
install.sh main()
  ...
  L431   STABLE_VERSION=$(echo_latest_stable_version)   <- ALWAYS runs
                |
                +-- echo_latest_stable_version()  (install.sh:94)
                      curl -sSLI https://github.com/coder/coder/releases/latest
                      #  no --connect-timeout
                      #  no --max-time
                      #  non-200 => exit 1  (hard failure)

  L454-461  the only consumers when --version is pinned:
              if VERSION == STABLE_VERSION: STABLE=1

  L148      advisory="To install our stable release (v${STABLE_VERSION}), ..."
  L159      "Coder ${channel}release v${VERSION} installed. ${advisory}"
```

That is the whole dependency chain. `-sSLI` also follows redirects and
`/releases/latest` *is* a redirect, so this is at minimum two
round-trips to `github.com` with no timeout ceiling on either.

### Why 30 seconds and not 60

`test.setTimeout(60_000)` used to be on this test. #16236 removed it,
and that removal was deliberate: it was itself a flake fix
(coder/internal#204, #279) whose thesis was that `go run` compiling
inside a resource-constrained test run was the problem. Having pre-built
the binary, it consistently stripped the allowances that existed to
absorb compile time:

| File | Change in #16236 | Was that allowance really compile time? |
|---|---|---|
| `app.spec.ts` | `setTimeout(75_000)` removed, click timeout `60_000`
-> `10_000` | Yes |
| `webTerminal.spec.ts` | `setTimeout(75_000)` removed | Yes |
| `helpers.ts` | agent-ready wait `45_000` -> `15_000` | Yes |
| `outdatedCLI.spec.ts` | `setTimeout(60_000)` removed | **No: also an
84 MiB download** |
| `outdatedAgent.spec.ts` | timeout untouched, 60 s survives | n/a |

The reasoning was sound and the sweep internally consistent. It had one
blind spot: for `app.spec.ts` and `webTerminal.spec.ts` that budget
genuinely was the compiler's, but here it covered compile time **plus**
a release download, and only the compile half went away. With 60
seconds, the failing run above would have finished in roughly 31 to 43
seconds and passed.

### Budget arithmetic

At `t=+24.58` the test still had to do:

| Remaining work | Realistic cost |
|---|---:|
| Download 84 MiB tarball | 2 - 8 s |
| `tar` extract | 0.3 - 1 s |
| `coder ssh --stdio` cold start | 0.5 - 2 s |
| Tailnet dial + SSH handshake | 1 - 3 s |
| `stopWorkspace` | 2 - 4 s |
| **Needed** | **~6 - 18 s** |
| **Available** | **5.42 s** |

## Fix

Move the download into the existing `testsSetup` Playwright project,
where it gets a 300 second budget and where a failure is attributed to
the download rather than to SSH.

```mermaid
flowchart TB
    subgraph BEFORE["BEFORE: one budget, two concerns"]
        direction TB
        T1["tests project, timeout 30s"]
        T1A["outdatedCLI.spec.ts<br/>login / template / workspace / agent<br/><b>downloadCoderVersion &lt;- NETWORK</b><br/>sshIntoWorkspace / exec / stopWorkspace"]
        T1 --> T1A
    end

    subgraph AFTER["AFTER: network work has its own clock"]
        direction TB
        S2["testsSetup project, timeout 300s"]
        S2A["downloadCoderVersions.spec.ts<br/>stable-version probe + 84 MiB + retries<br/>all live HERE"]
        T2["tests project, timeout 60s"]
        T2A["outdatedCLI.spec.ts<br/>downloadCoderVersion = cache hit, ~300ms<br/>SSH path gets the whole budget"]
        S2 --> S2A
        S2A -- "dependencies" --> T2
        T2 --> T2A
    end

    BEFORE ~~~ AFTER

    style T1A fill:#ffe5e5,stroke:#cc0000,stroke-width:2px
    style S2A fill:#e5ffe5,stroke:#007700,stroke-width:2px
    style T2A fill:#e5ffe5,stroke:#007700,stroke-width:2px
```

### Why it works

`downloadCoderVersion` was already idempotent and cache-checking: it
spawns `<binaryPath> version` first and returns early on exit 0. So the
test keeps its existing call and that call simply becomes a no-op
costing a few hundred milliseconds. **No test logic changes.**

```mermaid
sequenceDiagram
    autonumber
    participant S as testsSetup:<br/>downloadCoderVersions
    participant IS as install.sh
    participant GH as github.com
    participant T as tests:<br/>outdatedCLI
    participant CD as coderd + agent

    Note over S: budget 300s
    S->>IS: downloadCoderVersion(v2.8.0)
    IS->>GH: stable-version probe (unbounded)
    IS->>GH: fetch 84 MiB asset
    GH-->>IS: /tmp/coder-e2e-cache/bin/coder-e2e-2.8.0
    IS-->>S: binaryPath

    Note over T: budget 60s, local only
    T->>T: downloadCoderVersion(v2.8.0)
    Note right of T: spawn "<bin> version" -> exit 0<br/>returns early, ~300ms, no network
    T->>CD: coder ssh --stdio, handshake, exec "exit 0"
    CD-->>T: exit code 0
```

### Why the prefetch is non-fatal

The obvious implementation raises on failure. That would be wrong here,
and I verified why rather than assuming: `tests` declares `dependencies:
["testsSetup"]`, and a failing setup project stops dependent tests from
**running at all**. Adding a deliberately-throwing setup spec produced:

```text
✓  1 [testsSetup] › addUsersAndLicense.spec.ts › setup deployment (11.7s)
✓  2 [testsSetup] › downloadCoderVersions.spec.ts › download outdated CLI (353ms)
✘  3 [testsSetup] › zzTempFail.spec.ts › temporary blast radius probe (0ms)
  1 failed
  1 did not run      <- outdatedCLI never ran
  2 passed
```

So raising would convert a one-test flake into a whole-suite outage on
any GitHub hiccup. Instead the prefetch logs a warning and returns, and
the test's own `downloadCoderVersion` call fetches inline as it does
today. The failure path is therefore no worse than the status quo, and
the success path removes the network from the test entirely.

Of the three policies available (fail hard, fall back inline, or skip
the test), this is the only one that cannot regress anything: it never
blocks the suite, and it never silently drops coverage the way an
auto-skip would.

### Restoring the 60 second budget

This is the second half of the change, and it exists for the fallback
path above. It cannot reintroduce what #16236 fixed: the timeout value
has no causal relationship to how the binary is produced, `coderBinary`
stays pre-built, `go run` stays gone, and only `outdatedCLI.spec.ts` is
touched.

It does give back a bounded sliver of the CI-latency goal, and the bound
is small. A passing run is unaffected. The cost lands only when this one
test hangs, and then it is +30 s once: `--workers 1` so there is no
fan-out, `CODER_E2E_TEST_RETRIES` is unset in CI so `retries` is 0 and
nothing multiplies it, and the job budget is `timeout-minutes: 20`.

## Measurements

Four scenarios, locally on darwin/arm64 against a freshly built
`site/e2e/bin/coder`:

| Scenario | setup spec | `outdatedCLI` | `install.sh` inside the test?
| Result |
|---|---:|---:|---|---|
| Cold, empty cache | 5.7 s | 10.0 s | **no**, ran in setup | ✓ passed |
| Warm cache | 340 ms | 11.9 s | **no**, 0 invocations | ✓ passed |
| Prefetch fails, cache empty | 1 ms | 15.4 s | yes, inline fallback | ✓
passed |
| Setup spec throws | n/a | did not run | n/a | blast radius above |

The cold run is the load-bearing one: `install.sh` is invoked from the
setup spec and the test runs local-only in 10.0 s, so the 84 MiB
download and the 20 s probe are no longer on the assertion's clock.

For context on what "local only" costs, eight consecutive `main` runs
where the CI cache already made `install.sh` a no-op:

| Job | duration |
|---|---:|
| 90382498172 | 11.7 s |
| 90352398170 | 12.1 s |
| 90335547858 | 8.1 s |
| 90317232684 | 13.7 s |
| 90297156949 | 8.4 s |
| 90280065546 | 6.9 s |
| 90265047082 | 6.9 s |
| 90250803989 | 6.6 s |

6.6 to 13.7 seconds. This change makes that the only path rather than
the lucky one.

Also checked: the test name is byte identical (`ssh with client v2.8.0`)
so flake tracking keeps matching it, `outdatedAgent` remains skipped,
and `webTerminal`, `auditLogs`, and `updateTemplate` still pass, so the
added setup dependency disturbs nothing. The full 60-test suite was not
run locally because the premium tests need `CODER_E2E_LICENSE`.

## Also in this change

The pinned versions move to `site/e2e/constants.ts` as
`oldestSupportedCLIVersion` and `oldestSupportedAgentVersion`, so the
setup spec and the tests share one source of truth, and the comments
explaining *why* those particular versions travel with them. The CI
cache key follows them there: it previously hashed the two spec files,
and now hashes `constants.ts`, so it still invalidates exactly when a
pinned version changes.

## Not addressed here

The 20 second probe is relocated, not removed. `install.sh` still
resolves the latest stable version on every pinned install, with no
`--connect-timeout` or `--max-time`, and still treats a non-200 as
fatal, so a GitHub hiccup can fail an install whose target tarball is
already cached locally. That is a user-facing bug in its own right and
wants its own PR, since fixing it means deciding what a pinned install
should print when we no longer look up what "stable" currently is.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 16:26:13 -07:00
Cian Johnston dc1d6c3f3a ci: explicitly specify bash in mise tools installation (#27666)
Should hopefully fix [this
issue](https://github.com/coder/coder/actions/runs/30412236628/job/90710968864?pr=27628)
2026-07-29 21:48:41 +00:00
Susana Ferreira 740f5f7e1e chore: drop stale cost control experiment params (#27650)
Removes two stale `experiments: ["ai-gateway-cost-control"]` story
parameters from `GroupPage.stories.tsx`.

Refs #27579 #27553
2026-07-29 19:15:46 +01:00
Paweł BanaszewskiandCian Johnston 18128b7b52 docs: add standalone AI Gateway docs (#27592)
Documents standalone AI Gateway deployment, Gateway key authentication,
monitoring, and the updated embedded vs standalone topology in the AI
Gateway docs.

---------

Co-authored-by: Cian Johnston <cian@coder.com>
2026-07-29 19:38:22 +02:00
Jake Howell 6c42309ccb feat(site): modernize OAuth2 applications settings UI (#27562)
Follow up to #27561 

Modernizes the Deployment Settings OAuth2 Applications create/edit/list
UI to match the AI Providers pattern: fat page views, card layout, and a
Formik + Yup form using shared field primitives.

- Rework CreateOAuth2AppPageView / EditOAuth2AppPageView into fat views
with provider-style layout (back link, avatar + title, bordered cards,
cancel/submit footer)
- Rewrite OAuth2AppForm with Formik/Yup, FormField descriptions, and
IconPickerField (live header avatar on create/edit)
- Order edit page as settings → endpoints (Client ID / Auth / Token via
CodeExample) → secrets
- Align list page row styling and add a Callback URL column
- Update Storybook stories for the new fat-view + form validation
behavior

| Old | New |
| --- | --- |
| <img width="2936" height="1802" alt="old-oauth2-application-create"
src="https://github.com/user-attachments/assets/98c1dec1-c273-43a7-a517-a31ae48bc17c"
/> | <img width="2936" height="1802" alt="new-oauth2-application-create"
src="https://github.com/user-attachments/assets/9d2e1d6b-6905-469f-b9e0-cf8ae3b14f3d"
/> |
| <img width="2936" height="1802" alt="old-oauth2-application-list"
src="https://github.com/user-attachments/assets/3d38daab-1d93-4acb-bff0-d7299da884fd"
/> | <img width="2936" height="1802" alt="new-oauth2-application-list"
src="https://github.com/user-attachments/assets/b3cc56ac-a810-4742-9ea7-27e20caa9bb7"
/> |
| <img width="2936" height="2030" alt="old-oauth2-application-update"
src="https://github.com/user-attachments/assets/544ad92b-ae2b-4b10-907d-c94bcc3d12c4"
/> | <img width="2936" height="3470" alt="new-oauth2-application-update"
src="https://github.com/user-attachments/assets/6e4d228d-8deb-4934-989a-b9c98b633509"
/> |
2026-07-30 02:46:09 +10:00
Jake Howell 659fb48a1d fix: demui <OAuth2AppForm /> (#27561)
This pull-request removes the MUI styles from the `<OAuth2AppForm />`
and adjacent components.
2026-07-30 00:47:06 +10:00
Jake Howell 0b93731ebf fix(site): reflect submitting state during batch update (#27630)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.

## Problem

When bulk updating workspaces, the confirmation modal's **Update**
button never entered a submitting/loading state, so there was no
feedback that the update was actually in progress.

## Root cause

The `BatchUpdateModalForm` shows a spinner when its `isProcessing` prop
is `true`. That prop is fed by `batchActions.isProcessing` from
`useBatchActions`. However, the `isProcessing` value was OR-ing together
every mutation's `isPending` flag **except** `updateAllMutation` — the
one that actually performs the batch update:

```ts
isProcessing:
  favoriteAllMutation.isPending ||
  unfavoriteAllMutation.isPending ||
  startAllMutation.isPending ||
  stopAllMutation.isPending ||
  deleteAllMutation.isPending,
  // updateAllMutation.isPending was missing
```

As a result, the button stayed idle for the entire duration of a bulk
update.

## Fix

Include `updateAllMutation.isPending` in the `isProcessing` derivation
so the button spinner and disabled state correctly reflect an in-flight
batch update.

## Testing

- [ ] Manually verify the Update button shows the spinner and is
disabled while a bulk update runs.
2026-07-30 00:46:33 +10:00
Michael Suchacz 4bf9b9d1e6 feat(site): surface chat lifecycle hook outcomes in the chats UI (#27430)
Surfaces chat lifecycle hook outcomes in the chats UI. Final PR of the
lifecycle hooks stack (#27401, #27428, #27429), all now merged.

- Show hook notices attached to their user message as timeline notes
(`role="note"` so historical notices stay out of the screen reader's
assertive live region), and show an info tooltip for notices on queued
messages.
- Cache the full inserted message batch from send and edit responses so
hook-inserted messages survive stream reconnects and queue promotion.
- Reconcile the promoted queue head after sending to an errored chat so
a missed or delayed queue update neither duplicates nor hides messages,
and clear the stale error status so the Thinking indicator appears
before the websocket status event.
- Ignore an authoritative queue snapshot that still contains a
just-promoted message: queued messages are delete-only, so such a
snapshot predates the promotion and would both re-show the promoted
message and drop messages queued since. Fresh snapshots apply in full
and clear the suppression.
- Cache the store's reconciled queue on `queue_update` instead of the
raw event, so a stale update cannot re-show a promoted message after
REST re-hydration.
- Refresh chat details when a send or edit fails, because a failed hook
dispatch can move the chat to the error state.
- Surface tool result error text in the tool rows: the execute failure
tooltip shows the actual error instead of a hardcoded "Command failed",
and a failed `write_file` renders an error label with the result error
text instead of "Wrote <file>" with an args-derived diff of content that
was never written. This makes hook tool denials legible in the timeline,
and benefits every failed execute or write.

- Label a tool call blocked by `pre_tool_use` as failed instead of `Ran
<command>`, matching what the write and edit tools already do. The
wording derives from the tool-result error flag, so a command that ran
and exited non-zero is unaffected.
- Render a hook notice below the message it annotates rather than above
it, which reads correctly for a "your prompt was rewritten" card.
- Give both hook outcomes their own treatment on the create path, where
they previously fell through to the generic error alert and an expected
policy decision appeared with a stack trace, response data, and a
workspaces action. Classification keys on the structured response body
rather than the status code, so ordinary permission errors keep their
existing rendering.

- Unrelated to the hooks work, de-flake `SchedulePage.test.tsx`. Its
`fillForm` helper wrapped an already-retrying `findByLabelText` in
`waitFor`, so the two 1s budgets raced and a slow first render failed
`test-js` with "Timed out in waitFor". This is separable from the rest
of the PR if you would rather it land on its own.

> This PR was written by Mux, an AI coding agent, on Mike's behalf.
2026-07-29 16:45:11 +02:00
Ehab Younes e3a5a697ab fix(site): refresh group member budgets after overrides (#27553)
Group member budget rows remained stale after saving or deleting a user override because the cached member-spend query was not refreshed.

Invalidate the affected user's override query and only cached group-member spend queries whose user ID list contains that user. This refreshes relevant rows without invalidating unrelated groups.
2026-07-29 17:09:15 +03:00
Susana Ferreira 3deecb481e chore: remove ai-gateway-cost-control experiment flag (#27579)
## Description

Closes
[AIGOV-443](https://linear.app/codercom/issue/AIGOV-443/remove-ai-gateway-cost-control-experiment-flag-once-feature-is-stable).

The AI Gateway cost control feature is planned for GA on the upcoming
release, so this removes the `ExperimentAIGatewayCostControl` experiment
and all of its gating. The cost control API endpoints remain gated by
the `FeatureAIBridge` license feature (the AI Governance add-on), so
this only drops the experiment layer.

## Changes

- **`codersdk/deployment.go`**: remove the
`ExperimentAIGatewayCostControl` const, its `DisplayName()` case, and
its `ExperimentsKnown` entry.
- **`enterprise/coderd/coderd.go`**: remove the
`httpmw.RequireExperiment(...)` gating from the AI cost control routes.
They keep `RequireFeatureMW(codersdk.FeatureAIBridge)`. Affected
endpoints:
  - `GET /organizations/{organization}/groups/ai/spend`
- `GET
/organizations/{organization}/groups/{groupName}/members/ai/spend`
  - `GET /organizations/{organization}/ai/spend/export`
  - `GET /groups/{group}/members/ai/spend`
  - `GET /groups/{group}/ai/spend`
- `GET/PUT/DELETE /users/{user}/ai/budget/override` and `GET
/users/{user}/ai/spend`
- **`enterprise/coderd/aibridge_test.go`**: drop the experiment from
test setup and remove the now-obsolete `RequiresExperiment`
negative-path tests.
- **Frontend (`site/src/...`)**: remove the `ai-gateway-cost-control`
experiment checks from the cost control UI (Groups pages, user dropdown)
and their stories/mocks. The feature is now driven solely by the
`aibridge` feature visibility.
- **Generated**: regenerated `coderd/apidoc/*`,
`docs/reference/api/schemas.md`, and `site/src/api/typesGenerated.ts`.

## Out of scope

The dogfood `CODER_EXPERIMENTS` config lives in a separate infra repo,
not `coder/coder`. Leaving `ai-gateway-cost-control` there is harmless:
unknown experiment values are logged as `"ignoring unknown experiment"`
at startup and otherwise ignored, so no ordering dependency or breakage.
That cleanup can be a follow-up.

<details>
<summary>Implementation notes</summary>

- Verified how unknown experiments are handled in `coderd/coderd.go`
`ReadExperiments`: unknown values produce a warning log and are inert,
so removing the definition before the dogfood config is updated is safe.
- Noticed the group `ai/budget` routes (`/groups/{group}/ai/budget`)
were already gated only by `FeatureAIBridge`, never by the experiment.
After this change all cost control routes are uniformly feature-gated,
resolving that inconsistency.
- Removed an obsolete `RequiresExperiment` subtest in
`TestUserAISpendStatus` that only asserted a 403 from the experiment
gate; with the gate gone it would no longer be blocked pre-RBAC.

</details>

---

_This PR was created by Coder Agents on behalf of @ssncferreira._
2026-07-29 14:59:58 +01:00
Susana Ferreira d6a5c8e9f8 refactor: make user AI budget and spend endpoints consistent (#27611)
## Description

Makes the user AI cost control endpoints consistent.

## Changes

- Replaces the flat `spend_limit_micros` and `limit_source` fields on
`GET /users/{user}/ai/spend` with a nested `effective_budget`, reusing
the type behind `group_budget`. The flat pair made it possible to encode
a limit without a source.
- Renames `AIGroupBudget` to `AIBudgetLimit`, since it also carries
`user_override` limits and is no longer group-specific. The type name is
not part of the wire format.
- Moves `/users/{user}/ai/budget` to `/users/{user}/ai/budget/override`.
The endpoint only ever managed the per-user override, which the type,
the handlers, and the operation IDs all already said; the path was the
only place that didn't.

> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
2026-07-29 14:01:32 +01:00
Susana Ferreira e71249a821 fix: ai cost control cap configurable AI spend limit (#27640)
## Problem

A configured AI spend limit was only validated as `gte=0`, with no upper
bound. The group spend query multiplies the per-member limit by the
number of attributed members, so a large enough limit overflows `bigint`
and fails the whole query, returning an error for every group in the
request rather than just the misconfigured one.

## Changes

- Add `MaxAISpendLimitMicros`, $1,000,000 per member per budget period.
- Reject group budgets and per-user overrides above the maximum with a
400 naming the limit.
- Bound both budget forms in the UI so they show the valid range before
submitting.

Follow-up
https://github.com/coder/coder/pull/27589#discussion_r3668956350
Depends on https://github.com/coder/coder/pull/27589

> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by
@ssncferreira
2026-07-29 14:00:13 +01:00
Matt Vollmer 4987afada7 docs: present AI Governance as included with Premium (#27545)
## Summary

AI Governance is now included with Premium licenses instead of being
sold as a separate per-user add-on. This updates `docs/` to describe the
new packaging, removes "Add-On" from AI Governance references, and
refreshes the editions architecture diagram.

## Changes

- **`docs/ai-coder/ai-governance.md`**: title is now "AI Governance";
rewrote the licensing statements (previously "a separate, per-user
license... not included with a Premium subscription and must be
purchased separately") to state it is included with Premium. The
usage-pool section now attributes the shared Agent Workspace Build pool
to Premium deployments.
- **Repeated admonition (28 files under `ai-coder/agent-firewall/` and
`ai-coder/ai-gateway/`)**: replaced "requires the AI Governance Add-On /
as of Coder v2.32, deployments without the add-on..." with "is part of
AI Governance, which is included with a Premium license." The v2.32
add-on gate no longer applies; the gate is now Premium vs. Community.
- **`docs/ai-coder/index.md`, `security.md`, `tasks.md`,
`usage-data-reporting.md`, `admin/licensing/index.md`,
`install/releases/esr-2.29-2.34-upgrade.md`,
`ai-gateway/ai-gateway-proxy/setup.md`,
`ai-gateway/clients/claude-code.md`**: reworded add-on references to
Premium inclusion.
- **`docs/manifest.json`**: nav title "AI Governance Add-On" → "AI
Governance", updated two descriptions, and swapped the 25 `"state": ["ai
governance add-on"]` badges to `["premium"]` so the sidebar badge reads
"Premium" instead of "AI Governance Add-On".
- **`docs/images/single-region-architecture.png`**: refreshed the
diagram in the **Community and Premium editions** tab on
[Architecture](https://coder.com/docs/admin/infrastructure/architecture).
Also deleted the unreferenced `single-region-architecture.svg` copy.

## Follow-ups outside this PR

- The `"ai governance add-on"` doc-state badge is defined in
`coder/coder.com` (`src/utils/docs/state.ts`). After this merges, no
manifest entry uses that key, so it becomes dead config and can be
removed there.
- `enterprise/coderd/license/license.go:564-572` still warns admins that
"The AI Governance add-on is required to use AI Gateway." That backend
string will contradict these docs once shipped.

## Verification

- `pnpm run lint-docs`: 0 errors across 504 files
- `make lint/emdash`: clean
- Vale on the changed Markdown files: 0 errors; remaining warnings are
pre-existing gerund headings on untouched lines
- `docs/manifest.json` validated as JSON
- Confirmed the deleted SVG had no references anywhere in the repo

---

PR generated with Coder Agents on behalf of @mattvollmer.
2026-07-29 08:45:20 -04:00
Michael Suchacz fb30674806 fix(site/src/pages/AgentsPage): order chat transcript by message id (#27620)
## Context

Follow-up to #27495 (append-order guarantee for `chat_messages.id`) and
#27619 (prompt query ordering), both merged. This PR applies the same id
ordering to the transcript the user actually sees.

## Why?

`buildOrderedMessageIDs` in `chatStore.ts` sorted by `created_at`, which
is `now()` and therefore shared by every row in an insert batch. It
builds `orderedMessageIDs`, which is what the transcript renders, so it
re-imposed the ordering the backend PRs remove.

The failure needs the merge path, not a plain fetch. Initial REST
hydration already sorts by numeric id before reaching the store, and
`Array.prototype.sort` is stable, so a single correctly ordered response
rendered correctly. But `upsertDurableMessages` copies the existing
message `Map`, appends new ids, and re-sorts. `Map` iteration is
insertion ordered, so when a refetch or reconnect merges earlier ids
into a map that already holds later ones, the stable timestamp sort
faithfully preserves the wrong order.

This also makes the store consistent with `useChatStore.ts` and
`api/queries/chatMessageEdits.ts`, which already sort by `id`.

## Changes

`buildOrderedMessageIDs` now calls `toSorted` with an `id` comparator
inlined at its single call site, and the `byMessageCreatedAt` helper is
gone. `ChatMessage.id` is a `number` in `typesGenerated.ts`, backed by a
Go `int64`, so numeric subtraction is correct.

## Testing

Two vitest cases, both verified red by restoring the timestamp
comparator:

- `sorts messages by id when created_at disagrees with append order`
returned `[2,1]`.
- `orders merged messages by id rather than by arrival` returned
`[3,4,1,2]`, the exact inversion the merge path produces.

`MergedMessagesRenderInIDOrder` in `ChatPageContent.stories.tsx` covers
the same merge path through the rendered timeline.

All 334 `ChatConversation` unit tests and the 4 `ChatPageContent`
storybook interaction tests pass, and `tsc -p .` plus biome are clean.

> Opened by Mux on behalf of Mike.
2026-07-29 12:43:11 +00:00
Michael Suchacz 1c722ff969 fix(coderd/database): order the chat prompt query and its boundary by id (#27619)
## Stack context

Follows #27495 (merged), which gives `chat_messages.id` an append-order
guarantee and moves the history reads onto it. This PR applies the same
fix to the query that builds the model prompt.

## Why?

`GetChatMessagesForPromptByChatID` mixed two orderings. It selected the
compaction boundary with `created_at DESC, id DESC`, then applied that
boundary with an `id >` comparison, and returned rows with `created_at
ASC, id ASC`.

`created_at` is `now()`, so it is the transaction start time. Every row
in one insert batch shares it, and concurrent transactions can commit in
the opposite order to the one they started in. Two consequences, both
reaching the provider:

- **Malformed prompts.** A tool result could be ordered ahead of the
assistant message that requested it.
`chatprompt.injectMissingToolResults` does not repair this: it only
handles tool rows already contiguous after an assistant row, and adds
missing results. It never moves a tool row that precedes its assistant,
and nothing re-sorts the rows in Go.
- **Wrong compaction boundary.** The boundary is picked by timestamp but
compared by id, so a stale compressed summary could be retained while
the actual latest one was dropped.

## Changes

Both the boundary CTE and the outer query order by `id`. The `id >`
predicate is unchanged, which is the point: the ordering now matches the
comparison that was always being made.

**The boundary index was dead, so it is rebuilt to match.**
`idx_chat_messages_compressed_summary_boundary` was created for exactly
this lookup, but its predicate requires `role = 'system'` while
compaction writes its summary with the user role
(`message_conversion.go:334`, the only writer of `compressed = true`).
It matched zero rows, and no other query can use it. Migration `000560`
rebuilds it as `(chat_id, id DESC) WHERE compressed AND NOT deleted AND
visibility = 'model'`, which also matches the new order key.

Measured on PostgreSQL 13 with a 20k-message chat, 11 summaries, and 14
sibling chats so `chat_id` is selective:

| boundary lookup | plan | buffers |
|---|---|---|
| old predicate | Index Scan `idx_chat_messages_chat`, 19,989 rows
filtered | 267 |
| rebuilt index | Index Only Scan | 2 |

Not in scope: the outer `SELECT` still inspects every row of the chat,
because its `role = 'system' AND compressed = FALSE` disjunct has no
lower `id` bound. That predates this PR and needs a query rewrite rather
than an index.

## Testing

Two subtests, both verified red by reverting the `ORDER BY` and
regenerating:

- `OrdersByIDWhenTimestampsDisagree` returned `[4,3,2,1]` instead of
`[1,2,3,4]`, placing the tool result before the assistant call.
- `CompactionBoundaryUsesID` selected the stale summary and leaked the
messages between the two summaries into the prompt.

Existing subtests pass unchanged. Migration up/down tests pass, and the
rebuilt index was verified red-green: restoring the old predicate
returns the plan to a 267-buffer scan, and the old predicate matches 0
rows in the fixture.

> Opened by Mux on behalf of Mike.
2026-07-29 14:04:59 +02:00
Cian Johnston b371262e5c fix(aibridge): handle sonnet 5 adaptive thinking in bedrock (#27339)
Adds sonnet 5 to the list of models that require adaptive thinking for
Bedrock InvokeModel.

Smoke-tested locally.

> Obligatory disclosure: a Coder agent helped with this.
2026-07-29 12:40:24 +01:00
Michael Suchacz c17bed25e0 feat: wire chat lifecycle hooks into chatd (#27429)
Wires chat lifecycle hooks into chatd, gated by the
`agent-lifecycle-hooks` experiment. Part of the lifecycle hooks stack
(#27401, #27428, #27430). See `docs/admin/setup/chat-lifecycle-hooks.md`
for the consumer-facing contract.

## Summary

When a hook URL is configured, chatd dispatches `session_start`,
`user_prompt_submit`, `pre_tool_use`, `post_tool_use`, `pre_compact`,
`post_compact`, and `stop` events to the consumer and applies its
responses.

## Design

- **Stateless**: Coder stores no hook dispatch or decision state.
Delivery is at least once; consumers deduplicate on stable payload
identifiers (chat ID, event type, tool-use ID) and answer duplicates
with the same decision.
- **Admission-time prompt effects**: `user_prompt_submit` dispatches
exactly once per submission (create, send, queue, edit, subagent spawn)
and folds its effects into the stored prompt as typed message parts:
original-or-overridden user parts, then model-only `hook-context`, then
a user-visible `hook-notice`. Hook context is stripped from every
client-facing conversion; hook notices are excluded from model prompts.
The server rejects hook parts in client-submitted content.
- **Tool gating**: `pre_tool_use` allow can override tool input; deny
becomes a synthetic denied tool result, with any returned model context
persisted as a model-only transcript row so it never reaches clients.
The denial text identifies an external policy (the deployment's
lifecycle hook) as the source and marks the decision as persistent, so
the model explains the denial instead of retrying it or misreporting it
as an infrastructure failure.
- **Fail closed**: a dispatch failure rejects the triggering request or
moves the chat to the error state in the same transaction as the
affected step, so a runnable state is never published with unapproved
content.
- **Admission before persistence**: `pre_tool_use` is dispatched for the
calls the model produced, before the assistant message is stored. See
"Staged tool admission" below.
- **Fresh dispatch per tool call**: every non-provider-executed tool
call is decided by its own `pre_tool_use` dispatch; Coder never reuses
an earlier decision on the consumer's behalf. Retries re-dispatch the
same logical event.

## Structure

All hook dispatch flows through one seam: entry points build a
`chathooks.Chat` (chat identity) and a `chathooks.Message` (event
details) and call `Trigger.Trigger`, the only component that talks to
the dispatcher. The integration lives in the `coderd/x/chatd/chathooks`
subpackage, split by responsibility:

- `trigger.go`: the trigger seam; builds the wire envelope per event,
normalizes deny into a typed error, and holds the package's single
enabled-check.
- `effects.go`: pure conversion of hook results into transcript rows and
prompt parts.
- `errors.go`: failure classification (dispatch error messages, denial
mapping, tool-result dispatch-failure scanning).
- `tooluse.go`: the tool-call gate (`pre_tool_use` preflight,
`post_tool_use` payloads, applying admitted input to the step).

Server-bound glue stays in `coderd/x/chatd/hook_server.go`: the
chat-parking dispatch error handlers, the step-commit row insertion
wrappers, and the dynamic post-tool-use state loader, which depends on
chatd validation types.

This PR adopts the `codersdk/x/agenthooks` and
`coderd/x/agenthooks/dispatch` import paths introduced at the tip of
#27401; intermediate commits still reference the pre-move paths and are
not individually buildable.

## Staged tool admission

`pre_tool_use` originally ran at tool execution time, which is after the
assistant message carrying the tool call was already committed. An
`input_override` therefore had to rewrite stored message content in
place. @hugodutka pointed out that chatd treats message content as
immutable, and that the rewrite was a shortcut rather than a
requirement.

It was also a correctness problem in its own right: the rewrite only
updated the database, so the transcript could show one input while a
different one had executed.

The hook now runs before the step is persisted:

```text
provider stream ends (tool calls complete, in memory)
  -> pre_tool_use dispatch per call
  -> ONE transaction: assistant row with admitted inputs, synthetic denials, hook rows
  -> execute
```

The step is inserted once, carrying the input the tool runs with.
`UpdateChatMessageContentByID` and `Tx.UpdateMessageContent` are deleted
from #27428, so message content stays immutable.

Two consequences, both intentional:

- **Clients converge rather than wait.** Tool-call parts still stream
live, so a rewritten call briefly shows the model's proposed input
before the committed message replaces it. The chat store already clears
stream state when an assistant message arrives, so the stored input wins
with no frontend change and no added latency before tool cards appear.
- **A call already in history was already admitted.** Execution consumes
the stored input instead of dispatching a second decision, which keeps
one dispatch and one set of hook effects per call. A consumer policy
change between admission and execution applies to later calls, not to
calls already admitted.

The per-chat debug endpoint still records the provider's original tool
input. Its purpose is to report provider behavior, and it requires an
explicit per-chat debug flag; the invariant here covers the transcript.

## Configuration

Adds `chat-hook-url`, `chat-hook-secret`, `chat-hook-timeout`, and
`chat-hook-enabled` deployment options with startup validation. The
flags are hidden from `coder server --help` while the feature is
experimental; the setup guide documents them.

## Tool input validation

Built-in tool arguments reach a consumer as raw JSON with key spelling
preserved, but the tools decode those bytes with Go, which matches
struct fields case-insensitively and keeps the last match. A policy
reading `path` could therefore authorize one value while the tool
executed another, and a lone case variant such as `{"PATH":"/secret"}`
was invisible to a policy checking for `path`.

Coder now rejects a built-in tool call whose input repeats a key or
spells a schema property with different capitalization, before the
`pre_tool_use` dispatch, so a consumer is never asked to authorize bytes
whose meaning depends on the reader. Rejected calls produce an error
result the model can retry; unambiguous calls in the same batch still
run. A consumer-authored `input_override` is rechecked after the
dispatch and fails the turn closed, because the model cannot correct it.
Dynamic and MCP inputs are excluded because the client and the workspace
agent execute those calls rather than coderd.

Two paths needed more than a schema check. Execution resolves a
deprecated tool name to its canonical tool, so validation resolves
aliases first. The `edit_files` decoder also reads `search` and
`replace`, which its schema does not advertise, so those aliases are now
matched exactly and their case variants ignored.

A hook denial now returns a structured 403 carrying `kind:
"hook_denied"`, mirroring the dispatch-failure response that already
carries its own kind. Without it a client cannot tell a policy decision
apart from a generic failure, and the chat UI titled a denial "Request
failed". Adding a kind needs no migration: `ChatErrorKind` is persisted
only inside the JSONB `chats.last_error` column, whose decoder accepts
unknown kinds.

The hook docs also correct the tool-input convergence window. A batch
dispatches sequentially before the assistant row commits, so the
original input stays visible for a span that scales with the number of
tool calls in the step rather than a single hook timeout.

> This PR was written by Mux, an AI coding agent, on Mike's behalf.
2026-07-29 11:39:12 +00:00
Michael Suchacz 91c7232d97 feat: add chat suffix messages, idle failure, and content update support (#27428)
Adds generic chat state and query capabilities that the lifecycle hooks
integration (#27429) builds on. Part of the lifecycle hooks stack
(#27401, #27429, #27430).

- `chatstate`: `EditMessage` accepts caller-provided suffix messages
inserted after the replacement in the same transaction, transitions can
carry a typed error kind, and `FinishError` is also allowed from waiting
chats so admission-time failures can park an idle chat in error.
- `chatstate`: `ValidateToolResults` holds the submitted-tool-result
rules (duplicate, invalid JSON, missing, unexpected) in one place, so
`CompleteRequiresAction` and API-level prechecks reject the same
payloads with the same typed causes.
- `database`: `InsertChat` accepts an optional caller-provided ID.

No hook-specific state or behavior is introduced here; these primitives
are usable by any caller.

An earlier revision added a message-content rewrite primitive so a
`pre_tool_use` override could update an already-committed tool call.
Message content is immutable by design, and @hugodutka pushed back on
changing that. The rewrite is gone: #27429 now dispatches the hook
before the assistant message is stored, so the stored input is the one
that runs and nothing needs updating.

> This PR was written by Mux, an AI coding agent, on Mike's behalf.
2026-07-29 13:16:29 +02:00
Cian Johnston 5d2a69d85a fix(coderd/x/chatd/chaterror): extract plain-text provider error bodies (#27597)
Follows up #27538.

Fixes an issue where `chaterror` would not classify a plain-text
aibridge budget error 403 as ChatErrorKindUsageLimit.

Root cause: Anthropic adapter drops the body from
`ProviderError.Message`, and `providerErrorResponseMessage` extracted
only JSON.

- Parses the dumped response with `http.ReadResponse` (strips headers,
de-chunks, leaves non-dump payloads like Google's raw messages whole).
- When JSON extraction yields nothing, falls back to the trimmed first
line of the body.
- Falls back on `Content-Type: text/plain` only, and never on valid JSON
as `Detail` is user-facing.
- Adds an end-to-end regression test that Anthropic-shaped 403 budget
error → `usage_limit`, not retryable (was `auth`).
- Adds table-driven test cases: HTML skipped, whitespace skipped,
first-line-only, JSON-without-message, chunked, Google raw message.

---

> Generated by Coder Agents on behalf of @johnstcn.
2026-07-29 10:38:15 +01:00
Susana Ferreira 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
2026-07-29 09:16:37 +01:00
Michael Suchacz e96d8646e2 fix(coderd): give chat message ids an append-order guarantee (#27495)
Chat message ordering was derived from `created_at`, which is `now()`
and therefore the transaction start time. That makes it unusable as an
append-order column for two independent reasons: every row in one
`InsertChatMessages` batch shares a single timestamp, and two concurrent
transactions can commit in the opposite order to the one they started
in.

This PR gives `chat_messages.id` a real append-order guarantee and moves
the history reads onto it.

## Changes

**`InsertChatMessages` had no input-order guarantee.** Callers index the
returned slice by input position. That only worked because PostgreSQL
happens to evaluate the `BIGSERIAL` default in row order. Ids are now
allocated up front and the k-th smallest is assigned to input index k,
so the pairing does not depend on where the column default is evaluated.
Returned rows are explicitly `ORDER BY id`.

**Three history reads now order by `id`.**

| Query | Was | Now |
|---|---|---|
| `GetChatMessagesByChatID` | `created_at ASC` | `id ASC` |
| `GetChatMessagesByRevisionForStream` | `created_at ASC, id ASC` | `id
ASC` |
| `GetLastChatMessageByRole` | `created_at DESC, id DESC` | `id DESC` |

`GetChatMessagesByChatID` paginated by `id` while ordering by
`created_at`, which is incoherent on its own terms.

The other two matter because of who consumes them. The stream query
supplies incremental updates on the same socket that emits a full
`GetChatMessagesByChatID` snapshot on history reset, so once that
snapshot moved to `id` the two disagreed under timestamp skew.
`GetLastChatMessageByRole` returns an id that is then used as an id
cursor, both as `AfterID` when synthesizing tool cancellations and as
`chats.last_read_message_id`, where a stale anchor leaves later
assistant messages permanently unread.

A tie-breaker would not have fixed either one. It only resolves equal
timestamps; leading with `created_at` is the actual defect.

**`GetLastChatMessageByRole` loses its index, so this adds one.** `ORDER
BY created_at DESC, id DESC` could take an ordered scan of
`idx_chat_messages_chat_created`. Nothing in the schema can supply
`ORDER BY id DESC LIMIT 1` for a given `chat_id` and `role`, so the
planner switches to a backward scan of the primary key and filters every
newer row in the table, scanning all of it when the chat has no message
in that role, which is the routine case for a fresh chat. Migration
`000559` adds `(chat_id, role, id DESC) WHERE deleted = false`, the same
shape as the existing `idx_chat_messages_user_prompts`. This matters
because the query is hot: it runs on every stream connect and
disconnect, and once per turn when synthesizing tool cancellations.

`GetChatMessagesForPromptByChatID` has the same defect and is fixed in
the stacked PR, because its compaction boundary change is semantic and
deserves a separate review. Auto-archive stays timestamp-based
deliberately: it measures activity, not order.

Wrapping the insert in a CTE (needed because `INSERT` cannot take `ORDER
BY`) makes sqlc synthesize `InsertChatMessagesRow`. It is structurally
identical to `ChatMessage`, so the call sites use a direct struct
conversion that stops compiling if the two ever diverge.

## Testing

Behavior tests write `created_at` values inverted against id order, so a
reader that leads with `created_at` returns the batch backwards. All
three queries were verified red by reverting the `ORDER BY` and
regenerating: the stream query returned `[3,2,1]` for `[1,2,3]`, and
`GetLastChatMessageByRole` picked id 1 instead of id 3.

`TestInsertChatMessagesOrderContract` asserts against the generated SQL,
covering what a behavior test cannot: PostgreSQL evaluates the id
default in row order anyway, so a batch still looks ordered once the
guarantee is removed.

`TestChatMessagesSequenceCacheIsOne` guards the cross-batch half of the
invariant. Ids follow chat row lock order only while the sequence hands
out one value at a time; sequence cache blocks are per session, so with
a cache above one a backend holding stale cached values can lock second
and still commit lower ids. Bumping a sequence cache is an ordinary
throughput tweak, and it would silently corrupt history order.

The index was checked on a 200k row fixture. Without it, the zero-match
lookup filters all 200,000 rows over 2763 buffers; with it, the plan is
an index scan with both `chat_id` and `role` in the index condition, no
sort node, and 3 buffers.

Note that the within-batch mapping does not depend on the cache size. It
is established by `ROW_NUMBER() OVER (ORDER BY id)` over the allocated
ids, so it holds regardless of `nextval` evaluation order.

## Note on the deleted subagent hand-sort

The subagent history reader's hand-sort stays deleted, but calling it
redundant was imprecise. It sorted by `created_at` then `id`, so it is
only equivalent to `id` ordering when the two agree. When they disagree
the old code selected a different "latest assistant". This is a
deliberate behavior change to match the new invariant, not dead-code
removal.

> Opened by Mux on behalf of Mike.
2026-07-29 07:17:42 +00:00
Jaayden Halko 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 051ed34580 ("feat:
convert soft_limit to limit", #22048) collapsed managed-agent soft/hard
into a single `limit`. This reintroduces soft/hard as optional fields
without changing managed-agent behavior.
- Existing usage-period machinery populates `UsagePeriod` from
`nbf`/`exp` (`usagePeriodStart`/`usagePeriodEnd` in
`LicensesEntitlements`); reused unchanged, consistent with managed
agents.
- `Entitlements.AddFeature` replaces whole `Feature` structs (no
merging), so soft/hard automatically ride along with the winning
license. No `Feature.Compare` logic change; doc updates plus tests pin
that soft/hard are not comparison inputs.
- The feature name itself is not accepted as a claim; the allocation
must come from the dedicated claim so it is validated against soft/hard
(prevents a validation bypass where a direct feature-name claim could
win precedence with unvalidated thresholds).
- The generic "enabled but not entitled/expired" warning loop skips the
feature, mirroring `FeatureManagedAgentLimit`; usage-based warnings
arrive with B4.
- No premium default for this feature (unlike managed agents).

## Changes

1. `codersdk/deployment.go`: new `FeatureAgentRuntimeHours` (in
`FeatureNames`, `UsesLimit()`, `UsesUsagePeriod()`, keeping it out of
`FeatureSet` expansion); `Feature.SoftLimit`/`Feature.HardLimit`
(`soft_limit`/`hard_limit`, omitempty); doc updates for `UsagePeriod`
and `Compare`.
2. `enterprise/coderd/license/license.go`: canonical claim constants;
validation helper called from `validateClaims`; al-la-carte loop maps
the allocation claim to the feature and attaches soft/hard from the
companion claims; skips for the companion claims and the raw feature
name; generic warning loop skip.
3. `enterprise/coderd/coderdenttest`: `AgentRuntimeHours(allocation)`
builder.
4. Tests:
- `TestAgentRuntimeHoursLicenses`: entitled/grace round-trips (including
JSON field assertions), allocation-only, explicit zero,
`IssuedAtRanking` mirror, soft/hard ride-along with a newer
allocation-only license, direct feature-name claim ignored,
unknown-claims compatibility (old-server simulation).
- `TestAgentRuntimeHoursClaimValidation`: table of valid/invalid claim
combinations against `ParseClaims`, plus stored-license entitlements
error.
- `TestPostLicense`: API-level 400 rejection and a happy-path POST +
`GET /api/v2/entitlements` round-trip.
- `TestFeatureComparison`: soft/hard ignored in comparison; newest `iat`
wins over larger soft/hard.
5. `make gen`: regenerated `site/src/api/typesGenerated.ts`,
`coderd/apidoc/*`, `docs/reference/api/*`.

## Verification

- `go test ./enterprise/coderd/license/ ./codersdk/` and `go test
./enterprise/coderd/ -run 'TestPostLicense|TestEntitlements'` pass.
- `golangci-lint` clean on changed packages; `make lint/emdash` clean;
FE `tsc --noEmit` clean.
- Independent agent review of the diff found no blockers; its minor
findings (direct feature-name claim validation bypass, precedence test
gap, missing API happy-path test) were addressed.

</details>

> [!NOTE]
> Generated by Coder Agents on behalf of @jaaydenh (Linear CODAGT-837
agent session).
2026-07-29 07:58:30 +01:00
Jake Howell d072aa7bd0 feat(site): confirm before batch stopping workspaces (#27631)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.

## What

Bulk stopping workspaces from the workspaces table currently fires
immediately with no confirmation, whereas the single-row **Stop** action
and the bulk **Delete** / **Update** actions all show a dialog first.
This adds a confirmation dialog to the bulk **Stop** action so it
matches the rest.

## How

- Added `BatchStopConfirmation`, a small `ConfirmDialog` wrapper
mirroring the wording of the single-workspace stop confirmation but
pluralized for the selected count.
- Wired it into `WorkspacesPage`: `onBatchStopTransition` now opens the
dialog (`setActiveBatchAction("stop")`) instead of calling
`batchActions.stop(...)` directly, and the actual stop runs on confirm.
Added `"stop"` to the `BatchAction` union.

No change to the underlying `batchActions.stop` behavior (still only
stops `running` workspaces).

<details>
<summary>Reviewer notes</summary>

Before: `onBatchStopTransition={() =>
batchActions.stop(checkedWorkspaces)}` — no confirmation.

After: opens `BatchStopConfirmation`; confirm calls
`batchActions.stop(checkedWorkspaces)` then clears the active action,
consistent with how `BatchDeleteConfirmation` and `BatchUpdateModalForm`
are handled.

</details>

---
_Opened as a draft. Disclosure: this PR was generated by Coder Agents on
behalf of @jakehwll._
2026-07-29 04:26:09 +00:00
Bobby Ho 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).
2026-07-28 16:59:33 -07:00
J. Scott Miller ce769680ba feat(site): hide workspace resources when lacking workspace-create permission (#27278)
Context: experiment `minimum-implicit-member ` added the ability to set
the default member-roles at a per-organization level. This, along with
the related PR stacked listed below, will be used to enable "gateway
accounts", which are accounts that are entitled to use the AI Gateway
but not create or use workspaces. The Workspaces tab is intentionally
left visible for now.

Hides the "New workspace" button and the empty-state creation CTA on the
Workspaces page for users who cannot create a workspace in any
organization, and guards the creation page itself.

Adds a shared `createWorkspace` authorization check (`workspace`
resource, `create` action, `owner_id: me`, `any_org: true`) to
`site/permissions.json` and threads the result through
`WorkspacesPageView`, `WorkspacesTable`, and `WorkspacesEmpty`. Users
without the permission see an empty state explaining they don't have
permission to create workspaces instead of a dead-end CTA. The create
CTAs on the Templates pages were already gated by per-organization
checks; this brings the Workspaces page in line.

`CreateWorkspacePage` is also gated: it adds an org-scoped
`createWorkspaceForUserID` check to its existing authorization batch and
wraps the view in `RequirePermission`, so a direct URL shows the
standard denial dialog instead of a form that 403s on submit. Users who
can create workspaces for others (`createWorkspaceForAny`) still see the
form.

To see this behavior, enable the experiment. As an admin, visit
Organization -> Roles, and remove "Organization Workspace Access" from
the default roles. Login as a user that is not granted workspace access
via a member role.

Storybook coverage: `CannotCreateWorkspace` (empty state + hidden
button), `CannotCreateWorkspaceWithWorkspaces` (button hidden while the
table renders), `CannotCreateWorkspaceWithFilter` (pins the filter empty
state's priority over the no-permission one), and `PermissionDenied` for
the CreateWorkspacePage gate. The Go SSR permissions test also asserts
the new `createWorkspace` entry.

## Stack

This PR is independent but related to the gateway-accounts stack:

1. **#27279**: permission-based license seat counting. Behind the
`permission-based-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.

This PR (#27278) stands alone: it hides the Workspaces page create CTAs
for users without workspace-create permission and can merge in any
order.
2026-07-28 18:09:54 -05:00
dylanhuff-at-coder efbf802319 feat: add bulk secret import upload to Add secret dialog (PLAT-240) (#26725)
Adds a file dropzone to the create branch of the Add secret dialog
(final PR in the PLAT-240 stack, after #26723 and #26724). The browser
reads the file, derives the format from the extension
(`.env`/`.json`/`.yaml`/`.yml`), and imports via `POST /secrets/batch`;
per-entry backend errors surface in an alert and the success toast flags
secrets imported without an env name. Storybook play stories and vitests
cover the flow.

Also documents the upload flow in `docs/user-guides/user-secrets.md`.

Closes https://linear.app/codercom/issue/PLAT-240

> Reviewed and updated by Coder Agents on behalf of @dylanhuff-at-coder.
2026-07-28 15:30:15 -07:00
dylanhuff-at-coder 0b2a6cac78 feat: add coder secret import for bulk secret files (#27534)
Adds `coder secret import <file>` to bulk-import dotenv, JSON, or YAML
secrets through the existing batch API. The command infers the format
from the extension or accepts `--input-format`, supports non-interactive
stdin, validates files locally before upload, and warns when imported
keys cannot be injected as environment variables.

Reviewed and updated by Coder Agents on behalf of @dylanhuff-at-coder.
2026-07-28 14:37:29 -07:00
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>
2026-07-28 15:30:12 -05:00
George KandBobby Ho 8cc7f2bb0e fix(coderd): reject workspace proxy hostname prefixes (#27544)
A workspace proxy hostname prefix could be accepted as a valid proxy
access URL. An authenticated user could then be redirected to an
attacker-controlled domain with an application-connect API key in the
URL.

Require proxy access URL matches to have a hostname boundary after the
candidate hostname, allowing only the end of the URL, a port, or a
path.

Add regression coverage for proxy access URL and wildcard hostname
prefixes.

Refs: https://linear.app/codercom/issue/PLAT-384

---------

Co-authored-by: Bobby Ho <bobbidinho@gmail.com>
2026-07-28 13:20:59 -07:00
Andrew Aquino 09a69e624a feat: search users by display name (#27398)
Free-text member search previously matched only username and email, so
typing a person's display name returned no results even though the UI
shows the display name as the primary label. This broadens the free-text
`@search` filter to also match `users.name`.

The change is in three queries: `GetUsers`,
`PaginatedOrganizationMembers`, and `GetGroupMembersByGroupIDPaginated`.
This covers every server-filtered surface: the Users page, the
Organization Members page, the Group Members page, and the
`UserAutocomplete` / `WorkspaceUserAutocomplete` pickers (which query
`GetUsers` with `q`). The org member picker (`MemberAutocomplete`)
filters client-side via cmdk, so display name is added to its
`keywords`.

Explicit filters (`name:`, `username`/`email`) and pagination counts are
unchanged; the group members count still comes from the filtered
`COUNT(*) OVER()` in the same query.

Refs DEVEX-484
Refs DEVEX-565

<details>
<summary>Implementation plan</summary>

## Problem

Member search (both the global Users page and the Organization Members
page) matches only on `username` and `email`. It does not match on the
user's display name (`users.name`), even though the Organization Members
table shows `name` as the primary title. So typing a person's full name
in the search box returns nothing.

Today a bare search term (`alice`) is routed to the SQL `@search`
filter, which only checks `email`/`username`. Display name is only
matched if the user explicitly types `name:alice`, which is
undiscoverable.

## Design decision

Include `name` in the free-text `@search` condition in the affected SQL
queries. A bare term then matches `email OR username OR name`, using the
same case-insensitive substring `ILIKE` already in place. This keeps the
existing explicit `name:` filter working.

Tradeoff: this broadens the meaning of free-text `search` globally
(anything using these queries now also matches display name). This is
the intended behavior, confirmed against DEVEX-565 (display name search
in the user picker).

## Affected files

Backend:
- `coderd/database/queries/users.sql` (`GetUsers`)
- `coderd/database/queries/organizationmembers.sql`
(`PaginatedOrganizationMembers`)
- `coderd/database/queries/groupmembers.sql`
(`GetGroupMembersByGroupIDPaginated`)
- `coderd/database/queries.sql.go` regenerated via `make gen`

Frontend:
- `site/src/components/UserAutocomplete/UserAutocomplete.tsx` (add
`name` to client-side cmdk keywords)

Tests:
- `coderd/coderdtest/users.go` (shared `UsersFilter` helper): added a
`DisplayNameSearch` case and extended search-based expectations to
include `name`. Exercised by `TestGetUsersFilter`,
`TestGetOrgMembersFilter`, and `TestGetGroupMembersFilter`.

Docs:
- `docs/admin/users/index.md`: documented that free-text search matches
username, email, and display name.

## Frontend surface coverage

| Surface | Sends | Backend | Query |
|---|---|---|---|
| Users page | `q` | `GET /users` | `GetUsers` |
| Organization Members page | `q` | paginated members |
`PaginatedOrganizationMembers` |
| Group Members page | `q` | `groupMembers` |
`GetGroupMembersByGroupIDPaginated` |
| User pickers (server-filtered) | `q` | `GET /users` | `GetUsers` |
| Org member picker (client-filtered) | local cmdk | n/a | keyword
change |

## Out of scope

- Trigram/similarity (fuzzy) matching; keeps `ILIKE` substring
semantics.
- Sort/pagination ordering (still `LOWER(username)`).

</details>

---
_Created by Coder Agents on behalf of @aqandrew._
2026-07-28 12:13:58 -07:00
Callum Styan e657d2ab9d chore(scaletest/prebuilds): reduce workspace poll interval to 5s (#27548) 2026-07-28 10:10:19 -07:00
Steven Masley 206938154a chore: remove emyrk from coderowners of commonly touched rbac (#27596) 2026-07-28 16:16:59 +00:00
Cian Johnston 75fd7bc09a fix: remove chatd usage limit enforcement (#27535)
This PR surgically removes enforcement of Agents spend limits:
- Adjusts the relevant function that checks usage to always return nil
- Deletes tests that expect a usage limit error.
2026-07-28 17:14:12 +01:00
Mathias Fredriksson eb905702c8 fix(coderd/util/syncmap): match sync.Map semantics in the typed wrapper (#27582)
Fixes CODAGT-869
2026-07-28 19:05:59 +03:00
Zach 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.
2026-07-28 09:58:33 -06:00
Marcin Tojek 3c61a9a939 chore(docs): update release docs for v2.34.7 (#27591)
Automated docs update for v2.34.7 release.

Created by `releasetui`.
2026-07-28 16:51:31 +01:00
Danielle Maywood be226409b8 fix: delete the unused ChatMessagePart.Signature field (#27588) 2026-07-28 16:26:19 +01:00
Jake Howell 5f72c1525e chore(site): demui <ScheduleForm /> component (#27563)
This pull-request deMUIs the `/settings/schedule` page for users. 

| Old | New |
| --- | --- |
| <img width="1041" height="371" alt="PREVIEW_QUIET_HOURS_OLD"
src="https://github.com/user-attachments/assets/15e1e285-a33e-475e-beab-924a53152e00"
/> | <img width="1041" height="406" alt="PREVIEW_QUIET_HOURS_NEW"
src="https://github.com/user-attachments/assets/27daa080-b3e9-4615-9ad3-eefa0d55295b"
/> |
2026-07-29 01:06:38 +10:00