> Mux updated this PR on behalf of Mike.
## Stack Context
This stack splits experimental personal skills into smaller reviewable
PRs. Personal skills are user-owned `SKILL.md` files stored by Coder and
injected into chatd alongside workspace skills.
Stack order:
1. #25362 personal skill resolver
2. #25363 storage, permissions, API, and SDK
3. #25365 API test coverage
4. #25366 chattool and chatd integration
5. #25066 settings UI and docs
6. #25386 personal skills slash menu
## What?
Adds the shared personal skill parser and resolver package, plus
reusable skill-name validation exported from `workspacesdk`.
The parser enforces the full personal skill contract: max raw size,
kebab-case name, max name length, and non-empty body.
## Why?
The rest of the stack needs one source-aware resolver for personal and
workspace skills, including collision handling and qualified aliases.
Keeping personal skill constraints in the parser prevents callers from
accidentally parsing invalid personal skills.
## Validation
- `go test ./coderd/x/skills ./codersdk/workspacesdk`
- pre-commit hooks on this branch
This PR builds on top of https://github.com/coder/coder/pull/25070 to
add a way of running the larger "fake agent" manager via the existing
CLI, pulling in the URL/credentials already set.
With this, we can run a pod per scaletest region to act as all the
workspaces in that region.
This is in a new subcommand `scaletest agentfake` currently.
---------
Signed-off-by: Callum Styan <callumstyan@gmail.com>
The dogfood Dockerfiles consume the repo-root `mise.toml` and
`mise.lock` at build time (see `.dockerignore` allowlist), but the
template's `pull_triggers` list ignored them, so mise-only changes (tool
bumps, new tools) didn't roll out to existing workspaces.
Mirror the `nix.hash` pattern: a Makefile rule writes the sha256 of both
files into `dogfood/coder/mise.hash`, and `main.tf` hashes that
in-module file via `filesha1`. Run `make dogfood/coder/mise.hash` after
editing `mise.toml`/`mise.lock`.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
The `noEmptyInterface` rule is enabled by default as a recommended Biome
rule. Its safe fix converts empty interfaces to type aliases (e.g.
`export interface X {}` becomes `export type X = {}`), which then
conflicts with `noBannedTypes` on the empty `{}` type. Since empty
interfaces are legitimately produced by Go-to-TypeScript code
generation, this disables the rule entirely.
<details><summary>Context</summary>
Discussion in Slack concluded that disabling the rule is preferable to
adding per-type exceptions in `scripts/apitypings/main.go`, since the
safe fix itself creates a worse lint violation.
</details>
Follow-up to #25387. The top-level `[env] CGO_ENABLED = "1"` was
re-exported through every mise shim at runtime, forcing cgo on for `go
build` calls and breaking cross-compilation of coderd (slim builds,
`./scripts/develop.sh`) on the dogfood image.
`scripts/build_go.sh` sets `CGO_ENABLED=0` explicitly for
non-boringcrypto builds, but the mise shim overwrites it back to `1`
before `go` runs. Scope the variable to sqlc's `install_env` so it only
applies during the one `go install` that needs it.
Ref: https://mise.en.dev/configuration.html#tools-dev-tools
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
- Wire the Appearance settings page to the new theme mode dropdown and
sync or single theme selectors.
- Update Appearance page tests and stories for theme mode behavior.
- Update the user settings e2e test to exercise single theme selection.
## Dependencies
- Depends on #25076, #25180, #25181, and #25182.
- This PR targets helper branch `pr25077/05-theme-mode-dropdown-base`,
which contains dependency commits only, so this PR diff stays focused on
final dropdown wiring. Rebase and retarget after the dependency PRs
merge.
## Validation
- `pnpm -C site exec vitest run --project=unit
src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx
src/theme/themeMode.test.ts src/api/queries/users.test.ts`
- `pnpm -C site lint:types`
- `pnpm -C site storybook:ci`
- `pnpm -C site build`
- `pnpm -C site playwright:test -- e2e/tests/users/userSettings.spec.ts`
- Pre-commit hook passed on the branch commit.
Three changes to make mise-managed tooling reach every dogfood workspace
cleanly, with the upstream `devcontainers-cli` module fix as the
original trigger.
## Why the module breaks
The upstream [`devcontainers-cli` coder
module](https://github.com/coder/registry/blob/main/registry/coder/modules/devcontainers-cli/run.sh)
does `npm install -g @devcontainers/cli` and then verifies the binary is
on `PATH`. With mise-managed Node (introduced in #25282), `npm install
-g` lands the binary at `$MISE_DATA_DIR/installs/node/<ver>/bin/`, which
is *not* on `PATH` and which `mise reshim` does not surface as a shim.
The post-install check fails:
```
Installing @devcontainers/cli using npm...
changed 1 package in 661ms
Reshimming mise 26...
Installation completed but 'devcontainer' command not found in PATH
```
Even though nothing the user does is actually broken.
## What this PR does
1. **`mise.toml`** — pre-install `@devcontainers/cli` via mise's `npm:`
backend (`npm:@devcontainers/cli = "0.87.0"`). The mise shim lands at
`$MISE_DATA_DIR/shims/devcontainer`, on `PATH`. The upstream module's
`run.sh` short-circuits on its `command -v devcontainer` check and exits
0 without ever running the broken npm-install path. Strictly redundant
after fix the second point makes `npm i -g` work natively, but kept for
build-time pre-install and pinned-version reasons matching the other
mise-pinned CLIs.
2. **`dogfood/coder/ubuntu-*.04/Dockerfile`** — set
`NPM_CONFIG_PREFIX=/home/coder/.npm-global` and prepend
`/home/coder/.npm-global/bin` to `PATH`. With this, generic `npm install
-g <pkg>` (prettier, biome, anything frontend folks reach for) lands in
a stable home-volume dir that is already on `PATH`, survives node
version bumps, and needs no `mise reshim`. The mise `npm:` backend keeps
using its own `--prefix` internally so the `npm:@devcontainers/cli` pin
still installs under `$MISE_DATA_DIR` as before.
3. **`dogfood/coder/ubuntu-*.04/Dockerfile`** — install image tools into
`/opt/mise/data` at build time (owned by `coder`) and expose them at
runtime via `MISE_SHARED_INSTALL_DIRS=/opt/mise/data/installs`, keeping
`MISE_DATA_DIR=/home/coder/.local/share/mise` for the user's own
installs. This decouples baked tool versions from the home volume's
copy-on-first-mount: fresh and existing workspaces both immediately see
the image's tool set without a `mise install` step, and the user's own
`mise install <tool>` / `mise use --global` still lands on the home
volume. The `/opt/mise/data/shims` dir trails the user shim dir on
`PATH` so a user-installed version wins when both exist.
Pinned to `0.87.0` (current latest) so Renovate/Dependabot can bump
deliberately, matching the policy applied to the other floating tools
during the mise migration (`lazygit`, `doctl`, `jj`, `typos`,
`watchexec`).
---------
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Problem
`Docs CI` fails on PRs that only touch binary assets under `docs/`.
Example: [#25314](https://github.com/coder/coder/pull/25314), which
swaps a single PNG and produces thousands of `MD010/no-hard-tabs`,
`MD049/emphasis-style`, and `MD018/no-missing-space-atx` errors at
columns like 16,285 of the image.
## Root cause
The single `tj-actions/changed-files` step was doing two jobs at once:
detecting which Markdown files changed (for `lint` and `fmt`), and
gating whether the workflow had anything to do at all. Its `files`
filter matched `docs/**` in addition to `**.md`, so any non-Markdown
file under `docs/` (PNG, GIF, JPG, MP4, SVG) ended up in
`all_changed_files` and was passed straight to `markdownlint-cli2`,
which opened the file and parsed the binary bytes as Markdown.
`markdownlint-cli2`'s own `ignores` setting is a discovery-time filter
and does not gate files passed explicitly on the command line, so the
filtering has to happen in the caller.
## Fix
Adopt a per-tool convention: each downstream tool gets its own
`changed-files` step scoped to the files that tool can process. For now
that is a single `changed-md` step matching `**.md`, consumed by `lint`
and `fmt`. A future tool (e.g. an image linter, video size check, or
link checker) can be added purely additively, by appending another
`changed-*` step and a step that consumes its output, without changing
the existing filters.
The workflow-level `on.push.paths` / `on.pull_request.paths` triggers
stay broad (`docs/**`, `**.md`) so the workflow still runs on
screenshot-only PRs; the per-tool filters decide which individual steps
execute. On a screenshot-only PR the existing `if:
steps.changed-md.outputs.any_changed == 'true'` guard skips `lint` and
`fmt` cleanly.
## Verification
- `actionlint .github/workflows/docs-ci.yaml` passes.
- Reproduced the original failure locally: `pnpm exec markdownlint-cli2
docs/images/install/install_from_deployment.png` produces the same flood
of violations seen in the failing CI run on #25314.
- First revision of this PR (workflow with `**.md`-only filter, single
`changed-files` step) was green on `Docs CI`; the current revision is
structurally equivalent for the existing tools and just renames the step
id and adds the per-tool comment.
<details>
<summary>Decision log</summary>
- Considered adding `ignores` to `.markdownlint-cli2.jsonc` to skip
non-Markdown files. Rejected: `markdownlint-cli2` treats `ignores` as a
discovery-time glob filter and still lints files passed explicitly on
the command line, so it would not have fixed the failure.
- Considered narrowing the existing single `changed-files` step's
`files` filter to `**.md` only. Rejected as the final shape: it solves
the immediate bug but conflates "which Markdown files changed" with
"should the workflow run at all", so adding a second tool with a
different file set later (e.g. an image linter) would require contorting
or duplicating that step.
- Chose the per-tool-filter shape so adding a future tool is additive:
one new `changed-*` step plus one new step that consumes its output,
with no edits to existing steps.
</details>
## Disclosure
Opened on behalf of @nickvigilante by Coder Agents.
Fixescoder/internal#1535
## Problem
`TestPatchChatMessage/ChangesModel` is flaky because it races with the
chat daemon's background processing.
`CreateChat` sets the chat to `pending` and the daemon picks it up
asynchronously. The test immediately calls `EditChatMessage` (which
changes the model to an override) while the first processing round is
still running. The `InsertChatMessages` SQL CTE unconditionally updates
`chats.last_model_config_id` to the model of the last inserted message.
When the daemon's in-flight message insertions commit after the edit
transaction, they overwrite `last_model_config_id` back to the default
model.
Similarly, after the edit sets the chat back to `pending`, the daemon
re-processes it. The test's `GetChat` call could race with this second
round.
## Fix
Poll for the chat to reach `waiting` (or `error`) status:
1. **Before editing**: wait for the initial processing round to complete
2. **After editing**: wait for the second processing round (triggered by
the edit) to complete
Then assert `last_model_config_id`, which is now stable.
> Generated with [Coder Agents](https://coder.com/agents) by @kylecarbs
Three workflows besides `deploy-docs.yaml`
([DOCS-124](https://linear.app/codercom/issue/DOCS-124),
[#25285](https://github.com/coder/coder/pull/25285)) self-reference in
their `paths:` triggers: `docker-base.yaml`, `docs-ci.yaml`,
`dogfood.yaml`. This was flagged during review of #25285
([DEREM-1](https://github.com/coder/coder/pull/25285#discussion_r3234975475))
as a bug class worth treating uniformly. This PR is the audit.
Each self-reference is either justified inline or removed:
* **`docker-base.yaml`** keeps the self-reference. It's PR-only and
gated by `push: ${{ github.event_name != 'pull_request' }}` on the
`depot/build-push-action`, so PRs build the base image without
publishing.
* **`docs-ci.yaml`** drops the self-reference. The `lint` and `fmt`
steps gate on `tj-actions/changed-files` matching `docs/**` or `**.md`,
so a workflow-only run no-ops. `actionlint` and `make lint/actions`
catch YAML problems before merge regardless.
* **`dogfood.yaml`** keeps the self-reference. PR runs build images
without pushing and run `terraform init` + `validate` only; pushes to
main retag rolling tags on `codercom/oss-dogfood`,
`oss-dogfood-vscode-coder`, and `oss-dogfood-nix`, plus `terraform
apply` against dev.coder.com which produces new `coderd_template`
versions with unchanged content. Idempotent and bounded.
Refs DOCS-121, DOCS-129.
<details>
<summary>Decision table</summary>
| Workflow | Self-ref location | Effect on workflow-only edit | Decision
|
|---|---|---|---|
| `deploy-docs.yaml` | push + workflow_dispatch | Destructive (DOCS-121)
| Removed in [#25285](https://github.com/coder/coder/pull/25285) |
| `docker-base.yaml` | PR-only | Build base image, never push | Keep
with inline comment |
| `docs-ci.yaml` | push + PR | Empty run; lint/fmt skipped by `if:` |
Remove (wasted runner minutes) |
| `dogfood.yaml` | push + PR | PR: build without push, terraform
validate. Main: retag rolling tags, terraform apply, new cosmetic
template versions | Keep with inline comment |
</details>
---
_Coder Agents on behalf of @nickvigilante._
Edits to `.github/workflows/deploy-docs.yaml` previously self-triggered
the workflow on push to `main` and `release/*` because the file was
listed in its own `paths:`. On 2026-05-12, this caused merge of #25049
to fire a production reindex with no `docs/**` changes, which entered
the empty-`paths_json` whole-branch path in the Algolia handler and
wiped the `docs` index (see DOCS-121).
This change removes `.github/workflows/deploy-docs.yaml` from `paths:`
so the workflow only runs against real docs content. Reindexes from a
workflow edit alone now require `workflow_dispatch`, which already
accepts a `ref` input and an `action` choice of `index` or `delete`. The
other safety net (a workflow-level `paths_json=[]` guard in
`algolia-and-isr`) is tracked separately in DOCS-122.
Refs DOCS-121, DOCS-122, DOCS-124.
---
_Coder Agents on behalf of @nickvigilante._
## Summary
- Add appearance mutation context so optimistic cache updates can be
rolled back on failure.
- Restore previous appearance settings, or remove the optimistic cache
entry when no previous data exists.
- Merge successful server responses back into the appearance cache while
preserving request fields when responses are partial.
## Dependencies
- Stacked on #25076 because the tests and generated types use the new
appearance theme mode fields.
## Validation
- `pnpm -C site exec vitest run --project=unit
src/api/queries/users.test.ts`
- `pnpm -C site lint:types`
- Pre-commit hook passed on the branch commit.
This PR replaces the hand-rolled `curl | tar | go install | cargo
install` chains in the dogfood Ubuntu 22.04 and 26.04 Dockerfiles with a
single `mise install` driven by a new repo-root `mise.toml`.
The previous Dockerfiles installed ~25 CLIs across three multi-stage
builds with versions hardcoded inline. Version bumps were scattered
across the Dockerfiles, the root `mise.toml` (added in #24618 but
otherwise unused at runtime), and CI's setup actions; build-time network
failures came from a dozen distinct endpoints; and `mise` itself sat in
the image with no manifest to install from.
The new flow:
- The repo's `mise.toml` is the single source of truth for image tool
versions. The Dockerfiles `COPY` it to `/etc/mise/config.toml` and run a
single `mise install` as the `coder` user.
- Tools are installed into `/opt/mise/data` rather than the default
`/home/coder/.local/share/mise`, so they live in the image (not on the
persistent home volume) and reach every workspace on recreate.
- Build context moves to the repo root so the Dockerfile can `COPY
mise.toml`; an allowlist `.dockerignore` keeps the transferred context
to ~24 kB.
- Optional `--secret id=github_token` plumbing through the Makefile and
`.github/workflows/dogfood.yaml` lifts aqua's GitHub API quota from
60/hr unauthenticated to 1000/hr with `secrets.GITHUB_TOKEN`.
- `MISE_TRUSTED_CONFIG_PATHS=/home/coder:/etc/mise` is set as an ENV so
users who clone the coder repo into their workspace home aren't prompted
to `mise trust`.
Net diff for the two Ubuntu Dockerfiles: -399 / +244 lines (~200 lines
shorter each). The `FROM rust-utils`, `FROM go`, and `FROM proto`
multi-stage builds are gone; so are the NVM/Node block, the bulk
binary-install block (golangci-lint, helm, kubectx, syft, cosign, bun),
the gh `.deb`/lazygit/doctl tarball installs, the gofmt
`update-alternatives` line, and the `yq`→`yq4` rename
(`scripts/lib.sh:267-275` already auto-detects either name).
Both images were built and smoke-tested with Apple's `container` CLI on
macOS — every migrated tool resolves to the expected pinned version
including outside the cloned coder repo (e.g. `gh` from `/home/coder`,
matching the workspace startup script in `dogfood/coder/main.tf`),
`sqlc` runs (proving `CGO_ENABLED=1` was honoured at install), `yq
--version` reports v4 for `scripts/lib.sh`'s detection, and `gofmt`
resolves via the mise shim.
Follow-ups (out of scope here):
- Commit a multi-platform `mise.lock` so `gh = "latest"` and the other
floating versions resolve deterministically across rebuilds and dev
machines.
- Migrate CI's `setup-go` / `setup-node` actions to consume `mise.toml`
so image and CI versions stop being able to drift.
---------
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `restart = "unless-stopped"` to the dogfood workspace container so
it automatically recovers from crashes, daemon restarts or server
reboots without requiring manual intervention.
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6/4.7*
Fixes
[coder/aibridge#280](https://github.com/coder/aibridge/issues/280).
Claude Opus 4.7 (and future adaptive-only Bedrock models) reject the
legacy `thinking.type: "enabled"` + `budget_tokens` shape with a 400.
Claude Code falls back to that shape when it cannot read the upstream
model's capability metadata, which is exactly the case when AI Bridge
sits between the client and Bedrock. Pinning back to Opus 4.6 is the
only operator workaround today.
This is the counterpart to the `adaptive -> enabled` conversion added in
[coder/aibridge#225](https://github.com/coder/aibridge/pull/225) for
older Bedrock models.
## Behavior
- New `bedrockModelRequiresAdaptiveThinking()` helper matches Opus 4.7
(covers `us.anthropic.claude-opus-4-7`, ARN-style application inference
profile names that include the model ID, etc.).
- New `RequestPayload.convertEnabledThinkingForBedrock()` rewrites
`thinking: {type: enabled, budget_tokens: N}` to `thinking: {type:
adaptive}`. The budget hint is dropped; an explicit
`output_config.effort` from the caller is preserved naturally because we
never touch that field. We deliberately do **not** derive an effort
label from the budget (see decision log).
- `removeUnsupportedBedrockFields` learns a variadic `exemptFields`
parameter. Adaptive-only models support `output_config` natively (no
beta flag required), so `augmentRequestForBedrock` exempts that field
for those models.
- Bedrock Opus 4.7 accepts `output_config.effort` but rejects
`output_config.format` (structured outputs) with the same "Extra inputs
are not permitted" 400. The generic strip pass operates at top-level
granularity only, so a small targeted pass drops `output_config.format`
after the top-level strip for adaptive-only models.
The whole Bedrock thinking-type shim block carries a header comment
flagging it as temporary; a planned native Bedrock provider removes the
impedance mismatch and lets us delete it.
## Out of scope
The issue calls out a possible follow-up around `Anthropic-Beta:
interleaved-thinking-2025-05-14` for adaptive-only models; best evidence
is that Opus 4.7 still accepts those flags, so this PR is a no-op there.
<details>
<summary>Decision log</summary>
- `bedrockModelSupportsAdaptiveThinking` now also returns `true` for
adaptive-only models. That keeps the existing
`convertAdaptiveThinkingForBedrock` branch from running on Opus 4.7
(which would otherwise be incorrect; `adaptive` is the supported native
type there), and the new `convertEnabledThinkingForBedrock` runs only
for adaptive-only models via the explicit
`bedrockModelRequiresAdaptiveThinking` switch case. The two model sets
are disjoint by construction.
- The reverse conversion does **not** derive `output_config.effort` from
`budget_tokens / max_tokens`. The two thinking shapes encode different
intents (`enabled+budget` is "give me exactly N tokens,"
`adaptive[+effort]` is "model, pick a budget, optionally biased") and
there is no canonical mapping between them. An earlier draft of this PR
derived effort via midpoints of an invented anchor table; it was
symmetric-looking but lossy and required a lot of scaffolding (sorted
anchors, init-time invariant guard, round-trip tests) to keep two halves
consistent. The reverse direction now just rewrites the shape, which is
honest about the information loss and matches platform-defined adaptive
behavior when no effort hint is present.
- `output_config.format` is stripped only for adaptive-only models.
Other Bedrock models either don't get `output_config` through at all
(top-level strip handles them) or accept it via a beta flag that may
imply broader feature support. Easy to widen if the same 400 shows up
elsewhere.
- I chose `variadic exemptFields ...string` over passing the model down
to `removeUnsupportedBedrockFields`, to keep that function focused on
stripping and to localise the model-aware policy in
`augmentRequestForBedrock`.
</details>
Generating `coderd/database/dump.sql` previously required a
Docker-compatible socket via `ory/dockertest`. Contributors using
runtimes that don't expose one (e.g. Apple's `container` CLI) hit a
panic during `make gen`:
```
build: panic: open containerized database failed: open container: could not start resource: dial unix /var/run/docker.sock: connect: no such file or directory
```
Fall back to `fergusstrange/embedded-postgres` (already a direct module
dep, used by `scripts/develop/dbrecovery.go`) when
`dbtestutil.OpenContainerized` fails. The server's timezone is forced to
UTC so `timestamptz` DEFAULT expressions canonicalize identically to the
Docker-based path; otherwise the host's local TZ leaks into the dump as
values like `'0001-12-31 23:06:32+00 BC'`.
`PGDumpSchemaOnly` still needs `pg_dump` v13.x on PATH (the
embedded-postgres archive ships only `initdb`/`postgres`/`pg_ctl`). When
neither `pg_dump` nor `docker` is available, the existing error is
supplemented with install hints for `mise`, `brew`, and `apt`.
CI keeps using the Docker path unchanged; the fallback is local-dev-only
and produces a byte-identical `dump.sql`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`TestWatcher_SharedParentRefcount` was deterministically broken on
macOS: `t.TempDir()` lives under `/var` which is a symlink to
`/private/var`, but the watcher canonicalizes paths via
`filepath.EvalSymlinks` before storing them, so the test's `w.dirs[dir]`
lookup missed and returned `0` instead of `2`.
Adds `testutil.TempDirResolved`, a shared helper that returns
`t.TempDir()` with symlinks resolved and falls back to the raw temp dir
on error (Windows-friendly). Migrates the matching inline
`EvalSymlinks(t.TempDir())` callsites in
`agent/agentgit/agentgit_test.go` to use it.
Closes https://github.com/coder/internal/issues/1531
These chatd tests are flaking for the same stale control-notification
race tracked by CODAGT-353, so this change skips the newly reflaking
advisor-chain and `TestPatchChatMessage/ChangesModel` tests and rewrites
the older `TODO(hugodutka)` skips to point at the same root cause. This
keeps the known flakes documented consistently until the chatd
notification-flow refactor lands.
Closes CODAGT-427
Closes https://github.com/coder/internal/issues/1510
This PR introduces a "fake agent" + manager, which can be used during
scaletests to run a single executable that acts as many workspace
agents. The goals of these are to provide a much lighter weight
implementation of a workspace in terms of resource cost and startup time when executing scaletests.
---------
Signed-off-by: Callum Styan <callumstyan@gmail.com>
Co-authored-by: Mux <noreply@coder.com>
Closes
https://linear.app/codercom/issue/AIGOV-284/add-group-budgets-table-and-crud-api
## Summary
Adds the `group_ai_budgets` table and the following endpoints:
- `GET /api/v2/groups/{group}/ai/budget`
- `PUT /api/v2/groups/{group}/ai/budget`
- `DELETE /api/v2/groups/{group}/ai/budget`
Each group may have at most one budget row. If no row exists, no budget
is enforced.
### Feature gate
Added `RequireFeatureMW(FeatureAIBridge)` on the `/ai/budget` sub-route.
## RBAC
Authorization reuses `rbac.ResourceGroup` with the existing
`.InOrganization(...).WithID(...)` scoping model.
The `dbauthz` wrappers load the parent `groups` row and authorize
against it.
No new resource type is introduced. As a result, anyone with
`group:update` permissions (Owner, OrgAdmin, or UserAdmin within the
organization) can manage AI budgets for that group.
## Read access for group members
`database.Group.RBACObject()` grants `policy.ActionRead` to all members
of the group through the group ACL:
```go
func (g Group) RBACObject() rbac.Object {
return rbac.ResourceGroup.WithID(g.ID).
InOrg(g.OrganizationID).
// Group members can read the group.
WithGroupACL(map[string][]policy.Action{
g.ID.String(): {
policy.ActionRead,
},
})
}
```
Because the `GET` endpoint authorizes against the same loaded `Group`
object, any group member can call:
```text
GET /api/v2/groups/{group}/ai/budget
```
`PUT` and `DELETE` remain admin-only. The group ACL grants only
`ActionRead`, so write operations continue to require role-based
`group:update` permissions.
## Alternative considered
A dedicated `rbac.ResourceGroupAiBudget` resource would allow budget
management to be separated from general group administration.
We decided not to add that complexity for now.
The `update-release-calendar.sh` script did not account for Extended
Support Release (ESR) versions. Running it would drop ESR entries (e.g.
2.24) from the calendar entirely or mark them as "Not Supported" instead
of "Extended Support Release".
## Changes
- Add `ESR_VERSIONS` array for tracking active ESR minor versions
- Add `is_esr_version()` helper to check ESR membership
- Extract `generate_release_row()` to reduce duplication
- Prepend ESR versions older than the standard window
- Override "Not Supported" status for ESR versions within the window
> [!NOTE]
> When new ESR versions are designated or old ones reach end of life,
update the `ESR_VERSIONS` array at the top of the script.
<!-- This PR was authored by Coder Agents -->
Move the "Read the docs" button from the Audit Logging subsection up to
the primary Observability header's `actions` prop, matching the layout
pattern used by General, Network, and other deployment settings pages.
Also updates the docs URL from `/admin/security/audit-logs` to
`/admin/monitoring` to reflect the page-level scope.
> Generated by Coder Agents on behalf of @designertyler
---------
Co-authored-by: TJ <tracy@coder.com>
Previously, `hasAITaskResources()` scanned the Terraform graph for
`coder_ai_task` node labels. The graph includes resource definitions
regardless of `count`, so templates with `count = 0` were incorrectly
marked as `HasAITasks = true`, causing them to appear on the `/tasks`
page when no AI task resources would be created.
Replace the graph-based check with `len(aiTasks) > 0`. The `aiTasks`
slice is populated from state modules where Terraform has already
evaluated `count`, so it correctly reflects actual resource instances.
ref:
https://linear.app/codercom/issue/ECO-39/make-coder-tasks-respect-count
> Generated with [Coder Agents](https://coder.com/agents)
---------
Signed-off-by: 35C4n0r <work.jaykumar@gmail.com>
Signed-off-by: Jay Kumar <jay.kumar@coder.com>
`FakeSink` was silently capturing log entries without forwarding them to
`testing.TB.Log`. This made debugging test failures harder because logs
were invisible in `go test -v` output.
Store `testing.TB` in `FakeSink` and call `t.Log` on each entry, guarded
by a check to avoid logging after the test has finished.
Split out from #25012.
> 🤖 Generated with [Coder Agents](https://coder.com)
Partially reverts #25136 for non-darwin platforms.
In general we want to avoid pinning trust roots to embedded Certs, since that limits operational flexibility. If Azure changes CAs, operators should, at most, be able to update the OS trust store to keep Coder working correctly. Embedding roots means we need to upgrade the Coder binary.
Since Coder Server on macOS is not really supported for production use, embedding only in that case to ease development and testing is OK.
## Summary
- bump `github.com/coder/paralleltestctx` from v0.0.1 to v0.0.2
- pick up the latest paralleltestctx timeout-context detection
improvements in `go tool ... paralleltestctx` runs
## Summary
- Add `legacyAutoToSync` to map legacy auto-family theme preferences to
light and dark sync pairs.
- Expand colorblind theme tests for legacy auto-family handling.
- Move tritan success and git-added surfaces onto the sky-blue palette
in theme roles and CSS variables.
## Dependencies
- None. This can merge independently before the dropdown work.
## Validation
- `pnpm -C site exec vitest run --project=unit
src/theme/colorblind.test.ts`
- `pnpm -C site lint:types`
- Pre-commit hook passed on the branch commit.
## Summary
- Update AgentsPage diff viewer CSS so changed lines use theme-aware git
added and deleted surfaces.
- Add unit coverage for the changed-line CSS variables and line-type
selectors.
## Dependencies
- None. This is independent of the theme mode API and dropdown stack.
## Validation
- `pnpm -C site exec vitest run --project=unit
src/pages/AgentsPage/components/ChatElements/tools/utils.test.ts`
- Pre-commit hook passed on the branch commit.
The `websocketPair` test helper was not calling `CloseRead` on either
side of the connection. Without `CloseRead`, the websocket library does
not process control frames (ping/pong), so the heartbeat tests were
passing only because no pings had yet failed, not because pings were
actually succeeding.
Add `CloseRead` on both the client and server connections so that pong
frames are delivered in response to pings.
Split out from #25012.
> 🤖 Generated with [Coder Agents](https://coder.com)
## Problem
`coder exp sync want` and `coder exp sync start` both printed generic
success messages, which hid the dependency units involved in startup
coordination.
Before, declaring dependencies with `sync want` printed:
```text
Success
```
Before, `sync start` printed while waiting, then finished with another
generic success message:
```text
Waiting for dependencies of unit 'test-unit' to be satisfied...
Success
```
## Solution
Print the dependency units in both cases, using wording that matches
where the command is in the lifecycle.
After, `sync want` prints the dependencies it declared for the unit:
```text
Unit "test-unit" declared dependencies: [dep-unit]
```
After, `sync start` enumerates the dependencies while it is waiting,
then prints the same dependencies after the unit starts executing:
```text
Unit "test-unit" is waiting for dependencies to be satisfied: [dep-unit, dep-unit-2]
Unit "test-unit" finished waiting for dependencies: [dep-unit, dep-unit-2]
```
The sync golden tests now cover the updated output, including multiple
dependencies for `sync start`.
Follow-up to #25004. The merged change cycles only through messages
already loaded in the in-memory chat store (page size 50). Long chats
and chats whose oldest turns have rolled out of the page lose access to
their earlier prompts in the composer's up/down arrow cycle. This PR
adds a dedicated server endpoint that returns the full prompt history,
newest first, and rewires the composer to use it.
## What changed
### Endpoint
`GET /api/experimental/chats/{chat}/prompts?limit=N`
```go
type ChatPrompt struct { ID int64; Text string }
type ChatPromptsResponse struct { Prompts []ChatPrompt }
```
- `limit`: `0..2000`. `0` (the default) is treated as the server-side
default of 500; out-of-range values return `400`. Negative values are
rejected by the SDK's `PositiveInt32` parser before reaching the
handler.
- Auth: parent-chat read in `dbauthz`, mirroring
`GetChatMessagesByChatID`.
- The SQL filters `role='user'`, `deleted=false`, `visibility IN
('user','both')`, guards the lateral with `jsonb_typeof(content) =
'array'` so legacy V0 scalar-string rows are silently skipped, then
unrolls `content` JSONB with `WITH ORDINALITY` and concatenates only
`type='text'` parts in original order via `string_agg(... ORDER BY
ordinality)`. Messages whose joined text is whitespace-only are dropped
via `HAVING ... ~ '\S'` so cycling never lands on a blank entry.
### Partial index (migration `000494`)
```sql
CREATE INDEX idx_chat_messages_user_prompts
ON chat_messages (chat_id, id DESC)
WHERE deleted = false
AND role = 'user'
AND visibility IN ('user', 'both');
```
The partial WHERE matches the query's filter exactly and the key order
matches `ORDER BY id DESC`, so the planner gets both the filter and the
ordering from the index without a sort step.
`EXPLAIN ANALYZE` on a synthetic 51-chat × 5,000-message dataset (≈260k
rows, 10k user prompts in the target chat, `random_page_cost=1.1`):
| | Plan | Buffers hit | Time |
|---|---|---|---|
| Without index | `Index Scan Backward using chat_messages_pkey`,
**250,848 rows removed by filter** | 6,683 | 32.4 ms |
| With index | `Index Scan using idx_chat_messages_user_prompts`, no
filter | 38 | 1.3 ms |
≈25× faster, 175× fewer buffer hits.
### Frontend
- `chatPromptsKey` / `chatPromptsQuery` factories in
`site/src/api/queries/chats.ts` (`staleTime: 30s`, `enabled: chatId !==
""`, asks the server for 500 prompts).
- `ChatPageContent.tsx` replaces the in-memory derivation with
`useQuery(chatPromptsQuery(chatId ?? ""))`. The composer's existing
`cycleHistorySnapshotRef` anchors the in-flight cycle so a refetch
arriving mid-cycle cannot shift the indexed prompt out from under the
user.
- `getEditableUserMessagePayload` now concatenates user-message text
parts verbatim, mirroring the server's `string_agg(part->>'text', ''
ORDER BY ordinality)`, instead of routing through the streaming-oriented
`parseMessageContent` / `appendText` pipeline (which drops
whitespace-only chunks — correct for assistant streams, wrong for a
user's persisted message). This keeps the cycle and the edit path in
agreement on the same message. File blocks are still pulled separately
via
`parseMessageContent(...).blocks.filter(isEditableUserMessageFileBlock)`.
- Cache invalidation in `createChatMessage.onSuccess`,
`editChatMessage.onSettled`, and `useChatStore.upsertCacheMessages`
(only when an upserted message has `role === "user"`).
- Page-level stories pre-seed `chatPromptsKey(CHAT_ID)` from the same
`messagesData` to keep them offline.
## Tests
- New `TestGetChatUserPrompts` in `coderd/exp_chats_test.go` with five
subtests:
- `NewestFirstFiltering` — multi-part concatenation, non-text parts
skipped, whitespace-only filtered, soft-deleted excluded, `model`-only
visibility excluded, assistant-role excluded by `cm.role = 'user'`,
legacy V0 scalar row silently excluded by the `jsonb_typeof` guard,
ordering newest first.
- `LimitClampsResults` — explicit `limit=2` returns the two newest
prompts.
- `InvalidLimitRejected` — `limit=5000` is `400 Bad Request`.
- `NotFoundForOtherUsers` — a separate user in the same org gets `404`,
not the prompts.
- `EmptyResultIsJSONArray` — zero-message chat and assistant-only chat
both return `Prompts: []` (non-nil, empty).
- New unit test in `messageParsing.test.ts` asserting that
`getEditableUserMessagePayload(["hello", " ", "world"])` returns `"hello
world"`, locking in the agreement with the SQL `string_agg`.
- `dbauthz_test.go` adds the
`MethodTestSuite.TestChats/GetChatUserPromptsByChatID` entry, asserting
parent-chat `policy.ActionRead`.
- `pnpm test src/pages/AgentsPage` — 1159 passed, 2 skipped.
- `make gen` produces no diff.
## Manual verification
Seeded a dev chat with Claude Sonnet 4.6 via the aibridge Anthropic
provider and posted 20 user prompts end-to-end. Verified that the
`/prompts` endpoint returns 20 rows newest-first, that `limit=10` clamps
correctly, that `limit=0` uses the server default of 500, and that the
up/down keyboard cycle in the composer walks the same sequence (and
reverses correctly back to the empty draft).
## Out of scope
- Cross-chat history.
- Per-user opt-out for the cycle.
- File-reference / attachment cycling — the cycle continues to reproduce
plain text only, by design.
<details>
<summary>Implementation plan</summary>
# CODAGT-319 Follow-up — Dedicated `/prompts` endpoint
## Context
The merged feature ([#25004](https://github.com/coder/coder/pull/25004)
/ [d32842f](https://github.com/coder/coder/commit/d32842f)) cycles only
through messages already loaded in the in-memory chat store, which is
capped at the first 50 messages of the current page. Long chats and
chats whose oldest turns have rolled out of the page can no longer
recall their full prompt history. This follow-up exposes a dedicated
server endpoint that returns the user-authored prompts in a chat, newest
first, and rewires the composer to use it.
## Design
### Endpoint
`GET /api/experimental/chats/{chat}/prompts?limit=N`
Returns:
```go
type ChatPrompt struct {
ID int64
Text string
}
type ChatPromptsResponse struct {
Prompts []ChatPrompt
}
```
- `limit`: `0..2000`. `0` (the default) → server-side default of 500.
The wire-level default is encoded in SQL as `COALESCE(NULLIF($limit, 0),
500)`. Negatives are rejected upstream by `PositiveInt32`; the handler
only caps the upper bound.
- Auth: parent-chat read in `dbauthz`, mirroring
`GetChatMessagesByChatID`.
- Listed under the experimental router so we can iterate without API
guarantees.
### SQL
The query lives in `coderd/database/queries/chats.sql` as
`GetChatUserPromptsByChatID`:
- Filters `role='user'`, `deleted=false`, `visibility IN
('user','both')` to mirror the composer's "what the user actually typed
and can re-send" contract.
- Guards the lateral with `jsonb_typeof(content) = 'array'` so legacy V0
rows whose content is a scalar JSON string (predates migration `000434`)
are silently excluded instead of raising `"cannot extract elements from
a scalar"`.
- Unrolls `content` JSONB with `jsonb_array_elements WITH ORDINALITY`
and concatenates only `type='text'` parts, preserving original order via
`string_agg(... ORDER BY ordinality)`.
- Casts the result to `text` so sqlc emits a `string` field instead of
`[]byte`.
- Drops whitespace-only prompts via `HAVING string_agg(...) ~ '\S'` so
cycling never lands on a blank entry.
- Orders by `cm.id DESC` (`id` is a sequence, so this is "newest first"
without relying on `created_at`).
### Index
New partial index added in migration `000494`:
```sql
CREATE INDEX idx_chat_messages_user_prompts
ON chat_messages (chat_id, id DESC)
WHERE deleted = false
AND role = 'user'
AND visibility IN ('user', 'both');
```
The partial WHERE clause matches the query's filter exactly, so the
planner can use the index for both filtering and ordering without a sort
step.
### Frontend
- `chatPromptsKey(chatId)` and `chatPromptsQuery(chatId)` factories in
`site/src/api/queries/chats.ts`. `staleTime: 30s`, `enabled: chatId !==
""`. Asks the server for 500 prompts (well below the 2000 max, plenty
for the cycle).
- `ChatPageContent.tsx` replaces the in-memory derivation with
`useQuery(chatPromptsQuery(chatId ?? ""))`. The composer's
`cycleHistorySnapshotRef` already takes a stable snapshot at cycle
entry, so a refetch arriving mid-cycle cannot shift the indexed prompt
out from under the user.
- `getEditableUserMessagePayload` extracts the edit-path text from raw
user-message parts (filter `type === "text"`, join verbatim) instead of
going through `parseMessageContent` / `appendText`, which is built for
assistant streams and intentionally drops whitespace-only chunks.
Without this, cycling and clicking Edit on the same message could
produce different draft text for messages with whitespace-only
interleaved text parts.
- Cache invalidation: `createChatMessage.onSuccess`,
`editChatMessage.onSettled`, and `useChatStore.upsertCacheMessages`
(when at least one upserted message has `role === "user"`) all
invalidate `chatPromptsKey(chatId)`.
### Tests
- `TestGetChatUserPrompts` (`coderd/exp_chats_test.go`) covers:
- `NewestFirstFiltering` — multi-part concatenation, non-text parts
skipped, whitespace-only filtered, soft-deleted excluded, `model`-only
visibility excluded, assistant-role excluded by `cm.role = 'user'`,
legacy V0 scalar row silently excluded by the `jsonb_typeof` guard,
ordering newest first.
- `LimitClampsResults` — explicit `limit=2` returns the two newest
prompts.
- `InvalidLimitRejected` — `limit=5000` is `400 Bad Request`.
- `NotFoundForOtherUsers` — a separate user in the same org gets `404`,
not the prompts.
- `EmptyResultIsJSONArray` — zero-message chat and assistant-only chat
both return `Prompts: []` (non-nil, empty).
- `messageParsing.test.ts` adds a unit test asserting that
`getEditableUserMessagePayload(["hello", " ", "world"])` returns `"hello
world"`, locking in the agreement with the SQL `string_agg`.
- `dbauthz_test.go` adds the
`MethodTestSuite.TestChats/GetChatUserPromptsByChatID` entry, asserting
the parent-chat `policy.ActionRead`.
## Out of scope
- Cross-chat history.
- Per-user opt-out for the cycle.
- File-reference / attachment cycling — the cycle still reproduces plain
text only, by design.
</details>
<details>
<summary>coder-agents-review history</summary>
Four review rounds, eight unique findings, all addressed in this PR
(approved twice). Rebased onto `main` twice after R4: first to pick up
new migrations `000491` / `000492`, then again for
`000493_idx_chat_diff_statuses_url_lower`. The prompts-index migration
was renumbered `000491 → 000493 → 000494` via
`coderd/database/migrations/fix_migration_numbers.sh`; no other diff
changes.
| Round | Head | Outcome |
|---|---|---|
| R1 | `725422ab` | `COMMENTED` — 7 findings (DEREM-1..7) |
| R2 | `ab2a8936` | `COMMENTED` — 1 new (DEREM-10) + 1 reraised
(DEREM-5) |
| R3 | `648c5d1f` | **`APPROVED`** — 7 fixed, DEREM-5 deferred via
#25125 |
| R4 | `93b6f450` | **`APPROVED`** — DEREM-5 also fixed in-PR, #25125
closed |
| ID | Where | Resolution |
|---|---|---|
| DEREM-1 | `chats.sql` | Added `jsonb_typeof(content) = 'array'` guard
against V0 scalar rows |
| DEREM-2 | `exp_chats.go` | Removed dead `limit < 0` branch (SDK
rejects upstream) |
| DEREM-3 | `useChatStore.ts` | Rewrote misleading invalidation comment
|
| DEREM-4 | `exp_chats_test.go` | `NewestFirstFiltering` now inserts an
assistant-role message so the `role='user'` filter is exercised
end-to-end |
| DEREM-5 | `messageParsing.ts` | Rewrote
`getEditableUserMessagePayload` to concatenate text parts verbatim,
mirroring the SQL `string_agg` |
| DEREM-6 | `exp_chats.go` | Tightened swagger doc + error message to
spell out the 0–2000 range |
| DEREM-7 | `exp_chats_test.go` | Added `EmptyResultIsJSONArray` subtest
|
| DEREM-10 | `exp_chats_test.go` | `NewestFirstFiltering` now inserts a
raw V0 scalar-content row; verified locally that removing the guard
makes the test fail |
</details>
---
This PR was created on behalf of @ibetitsmike by Coder Agents.
Updates the shared setup-node composite action to current Node 24 based
releases of `pnpm/action-setup` and `actions/setup-node`. This avoids
the deprecated Node 20 action runtime seen in CODAGT-178 while keeping
the third-party actions pinned by SHA.
Adds an explicit post-setup check that fails inside Setup Node when
`node --version` is not `v22.19.0`, so self-hosted runner/toolcache
mismatches are surfaced before `pnpm install` reports a dependency
engine error.
Closes https://github.com/coder/internal/issues/1457
Generated by Coder Agents.
The `mockEventSenderWrite` function in `newOneWayWriter()` wrote
WebSocket frame data to both the `net.Pipe` and the
`httptest.ResponseRecorder`. After `websocket.Accept()` calls
`WriteHeader(101)`, the recorder rejects body writes with `"response
status code does not allow body"`. When `HeartbeatClose` sends a ping,
the control frame flush routes through the recorder, producing an
ERROR-level log that `slogtest` catches as a test failure.
Removed the `recorder.Write(b)` call from the write function. The
recorder is only needed for header/status inspection; WebSocket frame
data should only go through the `net.Pipe`.
Closes https://github.com/coder/internal/issues/1521
> 🤖 Generated by Coder Agents
Part 1: Backend portion of a change broken into 2 PRs.
Part 2: #25077
Adds three new UserAppearanceSettings fields (theme_mode, theme_light,
theme_dark) on top of the existing theme_preference and terminal_font.
Replaces GetUserThemePreference and GetUserTerminalFont with a single
GetUserAppearanceSettings aggregate query. The PUT handler is wrapped in
db.InTx so sync-mode's mode + slot writes can never half-apply.
Mid-stream HTTP/2 peer resets from LLM providers can arrive after a 200
streaming response has already emitted provisional parts. Previously
those resets fell through as generic non-retryable errors because
`stream ID` messages did not match retryable transport signals, and
stream IDs could be misread as HTTP statuses.
Classify retryable HTTP/2 RST_STREAM codes as transient timeout
failures, ignore stream IDs during status extraction, and keep the
existing `retry` event as the rollback boundary for provisional message
parts so replacement attempts do not replay failed-attempt output.
Closes CODAGT-382