Commit Graph
4802 Commits
Author SHA1 Message Date
Michael Suchacz 65c8dc52cc test(coderd/x/chatd/chatstate): pin the dynamic-only cancellation filter
CancelRequiresAction from A1 now runs on a seed with one dynamic and
one non-dynamic outstanding call, so dropping the dynamic filter fails
the transition matrix. Also tighten the outstandingToolCalls doc and
build the unparsable test row directly.
2026-10-15 21:43:52 +00:00
Michael Suchacz 062f56941d refactor(coderd/x/chatd/chatstate): separate last-assistant loading from call policy
lastAssistantTurn loads the last assistant message and the tool calls
already answered; outstandingToolCalls keeps the eligibility rules.
Test fixtures share one chatMessage builder, and the waiting-state
cancellation test now covers provider-executed, answered, and
unparsable cases and part order.
2026-10-15 21:30:08 +00:00
Michael Suchacz 7b3405fdd1 refactor(coderd/x/chatd/chatstate): share one outstanding tool-call scan
Cancellation synthesis and the pending-call checks each repeated the
last-assistant lookup, tool-result tracking, and eligibility rules.
outstandingToolCalls now owns that scan; its callers only build
cancellation messages or the pending-call map.
2026-10-15 21:16:37 +00:00
Atif Ali c85ebc1346 fix: make workspace create denials on behalf of another user actionable (#30537)
Creating a workspace for another user as a caller without permission
returned the same 403 as creating your own: "You are unable to create a
workspace in this organization. It is possible to have access to the
template, but not be able to create a workspace." For service accounts
and integrations that always create for someone else, that doesn't say
who the workspace was for, which organization, or what to do about it.

When the caller is not the owner, the two 403s now say what is missing
and for whom. Messages and details only; status codes and authorization
are unchanged, and self-creates keep their existing text.

Missing create permission:

> You can't create workspaces for other users in this organization.
> Creating a workspace for "alice" needs permission to create workspaces
owned by other users in organization "coder", which your account doesn't
have. Use an account with that permission (for example an organization
admin), or create the workspace as "alice".

Missing `use` on the template:

> You can't use the template "demo" to create workspaces for other
users.
> Creating a workspace for "alice" from this template needs "use" access
to template "demo", which your account doesn't have. Ask a template
admin to grant it, or use an account that has it.

`preflightWorkspaceCreate` now receives the caller's ID and the owner
(name included) instead of just the owner ID. `coder create` prints the
detail already, and the web UI shows it through `ErrorAlert`.

**How it renders**

Web UI (create page, `ErrorAlert`):

<img width="2592" height="1490" alt="Web UI error for creating a
workspace on behalf of another user"
src="https://github.com/user-attachments/assets/9b1f1378-e4df-40df-8b6f-ca6cccdc4238"
/>

CLI (`coder create alice/ws1 --template demo` as a plain member):

```
error: Trace=[create workspace: ]
You can't create workspaces for other users in this organization.
Creating a workspace for "alice" needs permission to create workspaces owned by other users in organization "coder", which your account doesn't have. Use an account with that permission (for example an organization admin), or create the workspace as "alice".
```

Refs PLAT-726. Related owner-state inconsistencies are tracked in
PLAT-749.
2026-10-15 17:27:13 +05:00
cdrciandgithub-actions[bot] 7e69cd0af7 chore: refresh AI model price book (#30828)
## Price book changes

0 models added, 0 models removed, 24 models changed.

<details>
<summary>Changed</summary>

- openrouter/deepseek/deepseek-v4-pro
- openrouter/deepseek/deepseek-v4-pro-0813
- openrouter/deepseek/deepseek-v4.1-flash
- openrouter/google/gemma-4-31b-it
- openrouter/minimax/minimax-m2.5
- openrouter/mistralai/mistral-small-3.2-24b-instruct
- openrouter/moonshotai/kimi-k3
- openrouter/openai/gpt-5.6-sol
- openrouter/openai/gpt-5.6-sol-pro
- openrouter/openai/gpt-oss-120b
- openrouter/qwen/qwen3.6-35b-a3b
- openrouter/qwen/qwen3.8-27b
- openrouter/tencent/hy3
- openrouter/tencent/hy4-preview
- openrouter/z-ai/glm-5
- openrouter/z-ai/glm-5.1
- openrouter/z-ai/glm-5.2
- openrouter/z-ai/glm-5.3
- openrouter/~deepseek/deepseek-flash-latest
- openrouter/~deepseek/deepseek-pro-latest
- openrouter/~google/gemini-pro-latest
- openrouter/~moonshotai/kimi-latest
- openrouter/~z-ai/glm-flash-latest
- openrouter/~z-ai/glm-latest
</details>

## Review notes

Regenerated by `make gen/aibridge-prices` from the live
[models.dev](https://models.dev) catalog. Both artifacts come from one
snapshot, so they ship together:

- `coderd/aibridge/prices/data/prices.json`
- `site/src/modules/aiModels/knownModels/knownModelsGenerated.json`

These are customer-visible cost numbers taken from upstream data, so
this PR is never merged automatically. The summary above lists what
moved; check the diff for exact figures before approving.

Opened automatically by the [aigateway-prices-refresh
workflow](https://github.com/coder/coder/actions/runs/38710433812).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-15 12:01:26 +01:00
Ethan aef7f15510 fix(coderd/x/chatd): only retry without drop_block when it's rejected (#30821)
In #29792, once a provider and model had accepted `drop_block`, any 400
on a request with it retried once without it, since I didn't know what
error a provider returns for an unsupported beta. That turned out to
hide real errors. On dogfood, a chat replaying some very tall
screenshots hit Anthropic's many-image limit:

```
messages.9.content.43.image.source.base64.data: At least one of the image dimensions exceed max allowed size for many-image requests: 3000 pixels
```

The retry without `drop_block` then hit the stale thinking block, so the
chat failed with the binding error instead, and the image error only
showed up in a warn log.

To fix, we'll only retry without `drop_block` when the 400 is a known
rejection of the field or its beta, and otherwise return the error as is
and keep the hint. The catch is that if a provider rejects the beta with
an error we don't recognise, chats on that model will see it until a
restart clears the hint, rather than us recovering.

Claude Code (as of 2.1.300) does the same on Anthropic's own API, Claude
Platform on AWS included: it only retries without the beta when the 400
names it or the `block_binding` field. On Bedrock, Vertex and Mantle it
also retries on any 400 it doesn't otherwise recognise, which we could
copy if that catch turns out to matter.

The oversized screenshots are a separate problem, since chatd doesn't
resize images at all.
2026-10-15 14:39:21 +11:00
Bobby Ho 603dd420d1 feat: add ServerTailnet logs and metrics for unreachable agents (#30133)
Add default-level metrics and warn-level diagnostics to investigate
ServerTailnet reachability failures causing agent-specific 502s, without
restarting coderd or enabling debug logging.

- Enrich request logs and PTY/chatd warnings with agent ID, failure
  reason, wait duration, coordinator state, WireGuard handshake and
  byte counts, and DERP regions.
- Add metrics for unreachable attempts by reason, reachability latency,
  embedded DERP connections, and offline app/terminal requests.
- Distinguish reachability failures from subsequent TCP dial failures,
  cap reverse proxy dials at one minute, and exclude expected chatd
  readiness polls and superseded-agent dials from unreachable counts.
- Report missing handshakes as zero time instead of 1970, also fixing
  `coder ping`.

Part of PLAT-680, a sub-issue of PLAT-360.
2026-10-14 14:55:29 -07:00
Michael Suchacz e7c2fd5d2e feat: add Gemini 4 Argon to Agents and AI Gateway (#30780)
Add Gemini 4 Argon to the Agents known models catalog, so it appears as
a suggestion when an admin adds a chat model, and regenerate the AI
Gateway price book from models.dev.

| Provider | Model ID | Placed before |
|---|---|---|
| google | `gemini-4-argon` | `gemini-3.1-pro-preview` |
| openrouter, vercel | `google/gemini-4-argon` |
`google/gemini-3.8-flash` |

**Pricing.** The price book carries Google's launch rates on google,
openrouter, vercel, and copilot: $2 per 1M input tokens, $10 per 1M
output tokens, and $0.10 per 1M cached input tokens. Google doubles them
on 2026-12-01 ($4, $20, $0.20); the weekly refresh picks that up once
models.dev changes. The google entry keeps models.dev's 32,768 max
output tokens, the cap Google enforces on non-streaming
`generateContent` calls (larger values return HTTP 400 there).

**Thinking levels.** `googleSupportedThinkingLevels` did not recognize
the `argon` name token and failed closed, so chatd sent Argon no
`thinking_level` and no `include_thoughts`: the native Google path
dropped the reasoning effort, and neither path showed thought summaries.
Argon accepts LOW, MEDIUM, and HIGH like Gemini 3.1 Pro and rejects
MINIMAL, so the map now treats it like the Pro family.

**Upstream drift.** `prices.json` covers every model of its 8 providers,
so the regeneration also brings the changes since the last weekly
refresh, all on OpenRouter and Vercel: 14 other new entries, 26 entries
models.dev delisted, and 51 price changes. The catalog picks up the same
changes for 8 curated OpenRouter entries (DeepSeek, Kimi, Qwen, and
GLM).

## Testing

- Live requests to `gemini-4-argon` through the AI Gateway's Google
provider (Google's OpenAI-compatible endpoint): `reasoning_effort` and
`thinking_level` values `low`, `medium`, and `high` return 200 and
`minimal` returns 400; `include_thoughts` returns thought text in
streaming and non-streaming responses.
- Red-green: removing the `argon` token fails
`TestGoogleSupportedThinkingLevels/gemini-4-argon` (expected LOW,
MEDIUM, HIGH; got nil).
- Regenerating from the saved models.dev snapshot reproduces both
generated files byte for byte.
- `go test ./scripts/aibridgepricesgen/... ./coderd/aibridge/prices/...
./coderd/x/chatd/chatprovider/...` and `pnpm exec vitest run
--project=unit src/modules/aiModels/` pass. The pre-commit hook passed;
local pre-push checks were skipped, so CI is the first full run.

## Review record

- Frontend review: the only `site/` change is the generated
`knownModelsGenerated.json`, so the FE1 to FE10 checklist does not
apply; no hand-written frontend code changed.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-14 22:39:00 +02:00
Bobby Ho ee5e3a4029 feat: send script resource addresses and dependencies in the agent manifest (#30627)
feat: include script dependencies in agent manifests

Send persisted script Terraform addresses and dependency lists on every
manifest request, preserving them across agent restarts, reconnects, and
shutdown without rebuilding the workspace.

- Add `resource_address` and `dependencies` to the agent proto, with
  `SUCCESS` and `COMPLETION` requirements.
- Reuse shared database-to-SDK validation and add bidirectional SDK/proto
  conversion. Reject empty prerequisite addresses and invalid requirements
  rather than accepting partial lists.
- Bump the agent API to v2.13 and update the agent's RPC connection.
  Older agents ignore the new fields; older servers return empty fields.
- Test manifest delivery, subagent isolation, round trips, invalid
  dependencies, and unchanged encoding when the new fields are unset.

Script execution is unchanged; ordering enforcement follows in PLAT-549.
Production dependency lists remain empty until #30556 merges.

Stacked on #30625. Refs PLAT-548.
2026-10-14 17:26:31 +00:00
Nick Vigilante 6afd1515e0 fix(coderd/templatebuilder): guard quickstart C/C++ install on every tool it installs (#30374)
The Quickstart base's C/C++ branch guarded on `command -v gcc`, but
installs `gcc g++ make cmake`. `codercom/enterprise-base:ubuntu` ships
gcc but not cmake, so cmake was never installed while the script
reported success. The guard now checks every tool the install provides
(`gcc`, `g++`, `make`, `cmake`).

Adds `TestQuickstartCppInstallGuard`, which runs the rendered C/C++
branch against stubbed tools and asserts the install runs when any of
them is missing. It fails without the fix.

Fixes DOCS-736. The matching example template change is in the DOCS-613
PR.

Generated by Coder Agents on behalf of @nickvigilante.
2026-10-14 12:35:48 -04:00
Garrett Delfosse 2b9fad8bba feat: add chat project sharing database layer (#30479)
Database and authorization layer for chat project sharing (experiment
`chat-projects`), part 1 of 2. #30439 adds the API on top of it. Nothing
here changes behavior: projects start with empty ACLs, no endpoint
writes them yet, and deleting a project still detaches its chats.
Project deletion is in #30683.

- **Schema:** migration `000621` adds `user_acl` and `group_acl` jsonb
columns to `chat_projects`, typed as `ChatACL` like chats, with GIN
indexes. It also adds the `chat_project:share` API key scope.
- **Queries:**
- `GetChatProjectsOwnedOrSharedWithUserID`: owned projects plus projects
with an ACL entry for the user directly, through a group, or through the
Everyone group (whose ID is the organization ID). Entries count only for
members of the project's organization. Keys match regardless of actions,
so dbauthz filters the rows.
- `IsChatProjectAccessibleByUserID`: whether a user owns a project or
holds a `read` or `*` grant on it, for code running under a system
subject. With sharing disabled, dbauthz answers owner-only.
- `GetChatProjectByIDForUpdate`: lets the ACL update read, modify, and
write in one transaction.
  - `UpdateChatProjectACLByID`.
- **RBAC:**
- New `share` action on `chat_project`. dbauthz requires it to update
the ACL.
- `ChatProject.RBACObject` attaches the project ACL. Memory objects get
a derived ACL: project `read` grants memory read, and `read` with
`update` grants memory create and delete. Wildcard grants count. Read
sharees change memories through their chats' agents, which #30439 gates.
Project instructions authorize through the same object, so a `read`
grant reads them and an `update` grant edits them.
- `--disable-chat-sharing` also covers projects: ACLs are ignored, the
ACL update is rejected, and roles deny `chat_project:share`. The help
text and chat sharing docs say so.
- **Audit:** `user_acl` and `group_acl` changes are tracked.
- **Tests:**
- Querier tests run the access queries both raw and through dbauthz
(owner, direct user, group, Everyone, wildcard grants, a member of
another org, grants without read).
- RBAC object and memory ACL tests, with an authorizer check, in both
sharing modes.
- dbauthz cases including the disabled guards, and the
`chat_project:share` negation in the roles test.

The generated TypeScript changes only add the new scope value.

> Generated by Coder Agents.
2026-10-14 10:42:00 -04:00
coder[bot] 69fab7de06 chore!: remove the coder_report_task MCP tool (#30742)
Coder Tasks have been removed from coder/coder, so the
`coder_report_task` MCP tool and the status reporting pipeline behind it
no longer do anything useful. This PR removes the tool and the code that
only existed to support it.

### Removed

- `codersdk/toolsdk`: `ToolNameReportTask`, `ReportTaskArgs`,
`ReportTask`, `Deps.report`, `WithTaskReporter`, and the `All` entry,
plus their tests.
- `coderd/mcp`: the remote MCP exclusion for `coder_report_task`.
- `coder exp mcp server`: the task reporter, the AgentAPI screen
watcher, the agent socket connection, and the `coder_report_task` skip
logic. The server now requires an authenticated user client, because
every remaining tool needs one.
- `coder exp mcp configure claude-code`: the
`mcp__coder__coder_report_task` allowed tool, the injected
`coder_report_task` CLAUDE.md prompt, the `report tasks properly`
history line, the default system prompt asking the agent to send task
status updates, and the `CODER_MCP_APP_STATUS_SLUG` /
`CODER_MCP_AI_AGENTAPI_URL` env written to the Claude MCP config.
- `cli/cliutil/queue.go`: only used by the reporter.
- `github.com/coder/agentapi-sdk-go` (and transitive `tmaxmax/go-sse`)
via `go mod tidy`.
- The "task reporting" mention in `docs/ai-coder/mcp-server.md`.

### Breaking: removed `coder exp` flags and env

`coder exp` is experimental. Any scripts that still pass these flags
will now fail with an unknown flag error. Env vars that are no longer
read are ignored.

| Command | Flag | Env |
|---|---|---|
| `coder exp mcp server` | `--app-status-slug` |
`CODER_MCP_APP_STATUS_SLUG` |
| `coder exp mcp server` | `--ai-agentapi-url` |
`CODER_MCP_AI_AGENTAPI_URL` |
| `coder exp mcp server` | `--socket-path` | `CODER_AGENT_SOCKET_PATH`
(still used by `coder agent` and `coder exp sync`) |
| `coder exp mcp configure claude-code` | `--claude-app-status-slug` |
`CODER_MCP_APP_STATUS_SLUG` |
| `coder exp mcp configure claude-code` | `--ai-agentapi-url` |
`CODER_MCP_AI_AGENTAPI_URL` |

`--claude-system-prompt` no longer has a default.

<details>
<summary>Intentionally kept</summary>

- **Workspace app status reporting** (`UpdateAppStatus` on the agent API
and agent socket, `agentsdk.PatchAppStatus` / `ProtoFromPatchAppStatus`,
the deprecated `PATCH /workspaceagents/me/app-status` endpoint): still
served by coderd and the agent and part of versioned protocols. With
this change, nothing in the CLI calls the agent socket `UpdateAppStatus`
client anymore. Removing it would need a separate protocol change.
- **`report_tasks` in
`coderd/templatebuilder/modules/{aider,amazon-q}`**: these catalog
entries mirror the input variables of pinned coder/registry module
versions, and the behavior lives in those modules. Dropping the variable
here would not disable it, because `amazon-q` defaults it to `true`. The
registry modules need a follow-up instead.
- **`UserClientOptional` on toolsdk tools**: other tools still set it,
and remote MCP still uses it.
- No CLI golden files or generated CLI docs changed, because `coder exp`
commands are hidden.

</details>

### Verification

- `go build ./...`
- `go test ./codersdk/toolsdk/... ./coderd/mcp/... ./cli/cliutil/...`
- `go test ./cli -run 'TestExpMcp|TestCommandHelp'`
- `golangci-lint run ./cli/... ./codersdk/toolsdk/... ./coderd/mcp/...`
(0 issues)

---

_Generated by Coder Agents._

Co-authored-by: coder[bot] <137810501+coder[bot]@users.noreply.github.com>
2026-10-14 16:34:56 +05:00
Ehab Younes f573ec3533 feat: report agent connection methods and app identities (#30467)
Agents now report how a connection was made and which app made it, so
the connection log records the method and the client app separately.

- Bump the Agent API to v2.13. `Connection` gains `connection_method`
  and `app_name`, and `type` is deprecated.
- SSH and reconnecting PTY handlers set the method themselves, so a
  client-supplied app name can't change it.
- SSH sessions report the app from `CODER_SSH_SESSION_TYPE`. Sessions
  without one count as SSH but report no app.
- Disconnect reports repeat the connect report's method, app, and
  client session ID.
- Coderd logs the reported method and normalized app name. For agents
  older than v2.13, it maps `type` as before.
- Coderd rejects unknown methods instead of logging them as SSH.

Closes CLIENT-1037
2026-10-14 13:54:17 +03:00
Ethan 60c6c0959e fix: run chat interrupt messages before earlier queued messages (#30729)
If an agent is working with a message already queued, and you send
another message with `busy_behavior=interrupt`, the agent stops, but
then answers the queued message first and yours second. The interrupt
send appends to the end of the queue, and finishing the interruption
promotes the head of the queue, which is the older message.

To fix, we'll put the message that starts the interruption at the front
of the queue, so it's the one promoted once the partial response is
saved. Messages sent whilst an interruption is already in progress still
go to the back, as they do today, so a burst of interrupts keeps the
order it was sent in. On a chat waiting on dynamic tool results, or one
that errored with messages still queued, an interrupt send now goes
straight into history ahead of the queue (cancelling any pending tool
calls), instead of waiting behind it.

This is just a narrow fix to the current queue behaviour, and it doesn't
change the API shape. It's compatible with the queued message delivery
rework I've got in progress, which replaces these code paths. The one
case this doesn't fix is a second interrupt sent during the first, which
still waits behind earlier queued messages; that rework handles it
properly.
2026-10-14 14:00:19 +11:00
Bobby Ho 0ea3f16dd3 fix(coderd/database/migrations): renumber MCP OAuth2 resource migration to 000622 (#30722)
Main is red: #30625 and #30651 both added a migration numbered 000618,
and the
loader refuses to start with a duplicate number. See the failing run on
fa7cd62c8b.

- Rename 000618_mcp_server_oauth2_resource.{up,down}.sql to 000622, the
next
  free number after 000621_skills.
- No schema change; dump.sql regenerates identically.
2026-10-13 23:08:37 +00:00
Michael Suchacz fa7cd62c8b feat: send RFC 8707 resource parameter on MCP server OAuth2 flows (#30651)
Coder's MCP OAuth2 client never sent the RFC 8707 `resource` parameter,
which the MCP authorization spec requires on authorization and token
requests. Authorization servers that enforce it cannot be connected:
with Dagster+, discovery and dynamic client registration succeed, then
the authorize request fails with "MCP OAuth requests must include
resource".

MCP server configs get an `oauth2_resource` field (migration 000618,
admin-only in API responses, audited). Coder sends it on the authorize
redirect, the authorization-code exchange, and token refresh. Refresh
now goes through `clientcredentials` with `grant_type=refresh_token`,
because `oauth2.Config` cannot add parameters to refresh requests; it
keeps the same auth-style detection and error types, and adds no
dependency.

Auto-discovery fills the field with the most specific Protected Resource
Metadata `resource` that covers the server URL, stored as advertised. As
in the MCP SDKs, a resource covers URLs on its origin at or under its
path, so `/mcp` covers `/mcp/` but `/mcp/` does not cover `/mcp`. URLs
compare in RFC 3986 normal form (default ports, percent-encodings, dot
segments, and an empty path as `/`), ports ignore leading zeros, IPv6
literals compare by address, and `%2F` stays inside its segment. Unlike
the SDKs, a resource with a query, even an empty one, only covers a
server URL with the same query. When none covers it, Coder uses the
server URL with the scheme and host lowercased and user information and
fragment removed, or sends no resource when that is not a valid resource
indicator. An explicit value in the create request wins.

Manual configurations send no `resource` unless an admin sets one,
because some providers reject it (Microsoft Entra ID v2 fails with
AADSTS901002). Values must be absolute RFC 3986 URIs without a fragment,
user information, or an IPv6 zone identifier.

The resource binds grants like the other OAuth settings: changing it
deletes stored user tokens, and a callback whose config resource changed
after the user started connecting returns 409 before the code is
redeemed. The connect step records the resource in a callback-scoped
cookie for that check.

Discovery only runs when a configuration is created, so existing
auto-discovered configurations, including the Dagster+ one that prompted
this, keep an empty resource until an admin sets the new "Resource URL"
field or re-creates the server.

![MCP server edit form showing the Resource URL field filled by
auto-discovery](https://github.com/user-attachments/assets/319bc8a0-f6e1-4b7d-8878-8e341c94e590)

## Testing

- Go: discovery (most specific match, path and trailing-slash
boundaries, query matching including empty queries, equivalent ports,
IPv6 literals, escapes, and dot segments including escaped ones, the
normalized fallback and an invalid one, explicit override), validation
(URI characters, brackets, IP literals, user information, trimming),
grant deletion on change, clearing on an auth type change, redaction for
members, refresh with and without a resource, the 409 when the resource
changes mid-flow (the authorization server never receives the code), and
a connect and callback flow against an authorization server that rejects
requests without the resource. Each new test was checked to fail when
the behavior it covers is broken, except two malformed IP literal hosts
that Go's URL parser already rejects.
- Vitest: the form round-trips the stored value, and clearing the field
sends `""` on update.
- Remote UAT through Coder Agents
([chat](https://dogfood.cdr.dev/agents/58695d5b-2112-400b-88f7-ab64c678f756))
on a test deployment with loopback MCP and authorization server mocks:
- Round 1 on 2b875068e0: literal verdict FAIL. When the resource
changed between connect and callback, the callback redeemed the code for
the new resource and stored a grant. The 409 binding fixes it.
- Round 2 on 4b54ffb5f9: PASS. The mid-flow change returned 409 with no
token request in every variant (strict and lenient servers, set,
cleared, abandoned flows, parallel configs); auto-discovery, connect,
refresh, validation, and the docs matched. Minor observations: the
binding cookie is an unkeyed digest, so a member who edits their own
HttpOnly cookie can redeem a code with the current resource, which a
fresh connect also gives them; other settings changed mid-flow still
reach the token endpoint, as on main; the 409 popup shows raw JSON; the
UI does not warn that a resource change disconnects users; validation
toasts omit the server's detail.
- Round 3 on the final head 2d6abfac49: PASS. The head moved during the
round, so it ran in four parts, the last on 2d6abfac49. That part
covered the matching rules (queries including empty ones, default and
zero-padded ports, `%2F`, escaped dot segments, trailing slashes, IPv6
literals), the invalid fallback (stored empty, no `resource` sent),
every validation value through the API and the UI, and the discovery,
connect, refresh, and mid-flow 409 flows. The equivalent-encoding cases
last ran live on 5c23fe00dca; unit tests cover them on the final head.
Observation: the path-aware metadata URL is built from the decoded path,
so `/mcp%2Ftenant` fetches metadata for `/mcp/tenant`, as on main.
- `frontend-review` and the eight-role `coder-frontend-review` ran on
the `site/` diff; every role passes after the label, help text, row
layout, API doc wording, and form test fixes. Declined: linking field
descriptions with `aria-describedby`, because the form's shared `Field`
primitive renders every description without one, so it belongs in a
change to all fields.

## Review record

| Review | Commit | Findings | Disposition |
|---|---|---|---|
| [Codex round
1](https://github.com/coder/coder/pull/30651#pullrequestreview-5492085359)
| 4b54ffb5f9 | P2: a resource with another query was treated as
covering the server URL | Reproduced with a failing test, fixed in
bf0879560c, resolved |
| [Codex round
2](https://github.com/coder/coder/pull/30651#pullrequestreview-5492305363)
| bf0879560c | P2: explicit default ports; P2: `%2F` treated as a
segment boundary; P2: `url.Parse` accepts spaces and invalid escapes |
Each reproduced with a failing test, fixed in 16a1d783fa, resolved |
| [Codex round
3](https://github.com/coder/coder/pull/30651#pullrequestreview-5492677517)
| 16a1d783fa | P1: docs wrapped mid-sentence (DOC1); P2: equivalent
percent-encodings compared unequal; P2: brackets accepted outside an IP
literal host | Fixed in 8d617f0528 (the P2s reproduced with failing
tests; the same pass also covers the query, dot segment, and user
information cases), resolved |
| [Codex round
4](https://github.com/coder/coder/pull/30651#pullrequestreview-5493033885)
| 8d617f0528 | P2: the server URL fallback was not validated; P2:
escaped dot segments were compared before decoding | Each reproduced
with a failing test, fixed in 5c23fe00dc, resolved |
| [Codex round
5](https://github.com/coder/coder/pull/30651#pullrequestreview-5493342889)
| 5c23fe00dc | P2: an empty query (`?`) was treated as no query |
Reproduced with a failing test, fixed in 72c019ff5e, resolved |
| [Codex round
6](https://github.com/coder/coder/pull/30651#pullrequestreview-5493524846)
| 72c019ff5e | P2: `/mcp/` covered `/mcp`; P2: equivalent IPv6 literals
compared unequal; P2: malformed IP literals accepted | The first two
reproduced with failing tests. Go 1.26.5 `url.Parse` already rejects the
hosts named in the third, but it accepted an IPv6 zone identifier. All
fixed in ef2df2c4e7, resolved |
| [Codex round
7](https://github.com/coder/coder/pull/30651#pullrequestreview-5493668131)
| ef2df2c4e7 | P2: leading zeros in a port made equal origins differ |
Reproduced with a failing test, fixed in 2d6abfac49, resolved |
| [Codex round
8](https://github.com/coder/coder/pull/30651#issuecomment-6128743972) |
2d6abfac49 | None: "Didn't find any major issues" | Clean verdict on
the current head |

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-13 23:03:08 +02:00
Michael Suchacz b7d63121b0 refactor(coderd/x/chatd): move chat mutations into chat_mutations.go (#28756)
Moves the chat mutation verbs out of `chatd.go` into
`chat_mutations.go`. They share one helper, `Server.updateChat`, instead
of each repeating the transition, reload and publish steps.

## Changes

- `chat_mutations.go`: `SendMessage`, `EditMessage`, `PromoteQueued`,
`DeleteQueued`, `ArchiveChat`, `UnarchiveChat`, `SubmitToolResults`,
`InterruptChat`, `CompactChat`, `ClearChat` and
`ReconcileInvalidStateChat`, plus their private helpers. They stay
`*Server` methods; there is no wrapper type.
- `updateChat` runs the transition in one chatstate transaction and
reloads the chat inside it, so the result and the sidebar watch event
carry the snapshot bump. Then it publishes only the `status_change`
watch event. Compact and clear already hold the post-transition row, so
they skip the reload.
- Edit-target checks reuse `validateEditTarget`. The root-chat archive
guard lives only in `setChatFamilyArchived`. `publishChatPubsubEvent(s)`
stay in `chatd.go`.
- Net diff: +1,125/-1,128 (production -83, tests +80).

## Behavior

No intended behavior change. Transaction boundaries, locking, hook
order, error strings, publish order and the debug cleanup cutoff match
main. The moved code includes main's later changes: inline MCP server
replacement on send (#29546), the first-turn fallback title (#27111),
configurable limits (#28927), queue wait metrics (#29771) and inline
interruption finish (#30259). `auto_archive.go` keeps its own archive
path. Its precondition is checked under the lock, it uses the worker's
store, and it returns rows for auditing.

## Testing

Rebuilt on main `b2c8798e0e0`. A 42-mutant sweep ran on main and on this
branch, and nothing main's tests caught goes uncaught here. New tests,
each red-green checked by removing the code it guards:

- `TestUpdateChat` covers the reload, skipping the reload, and rollback
with nothing published.
- `TestArchiveStateRejectsChildChat` uses the public methods on a real
database, uses `ErrorIs`, and checks both archive flags stay unchanged.
It replaces `TestUnarchiveChildChat` and
`TestArchiveChat_RejectsChildChat`.
- `TestArchivedChatRejectsMutations` covers the in-transaction archived
check for six verbs; no test covered it before.
- A `title_change` assertion is added to
`TestSendMessageFirstUserTurnFallbackTitle`, and
`TestArchiveChatDeletesDebugRuns` covers archive debug cleanup.

Validation:

- `go build ./coderd/... ./enterprise/...`, `go vet
./coderd/x/chatd/...`, `golangci-lint` and `make lint/go/new-code`:
clean.
- `go test ./coderd/x/chatd/... -count=1` passes on all packages, and a
race run on the touched tests passes.
- `./coderd` chat tests pass, including `TestInlineMCPServers`,
`TestWatchChats` and `TestSubmitToolResults` (158 runs), as do the chat
state and hook tests and `./enterprise/coderd` chat tests.
- Local pre-push checks were skipped at the author's request; CI is the
gate.

## Review record

- [Codex
OK](https://github.com/coder/coder/pull/28756#issuecomment-6151488087)
on head `fc327b8b873`, no unresolved threads. [CI
passed](https://github.com/coder/coder/actions/runs/38439818111) on the
same head. The first CI run on `3d9b0e83369` caught a test bug: parallel
subtests shared one parent context and timed out while waiting for a
slot. `fc327b8b873` gives each subtest its own context.
- Earlier Codex OKs and @ThomasK33's approval are on superseded heads
(`fe038a5f13a` and earlier). This description replaces the old "single
mutator" framing.
- Independent agent review of the rebuilt branch: GO, with every verb's
behavior equal to main. P3 fixes applied: moved code kept in main's
formatting, and a retry rationale comment restored. Not added: a unit
test of the tool-result validation detail strings, because it would make
the PR net-positive. Main also only checks those details through coderd
`TestSubmitToolResults`.
- This PR and #28757 conflict in `chatd_test.go`. Whichever merges
second needs main merged in.

> Xum (an AI coding agent) rebuilt this PR on behalf of @ibetitsmike.
2026-10-13 22:57:46 +02:00
Michael Suchacz adcce66826 feat: load organization skills in chats (#30613)
## Stack

Closes
[CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents).
This stack adds organization skills to Coder Agents: admins publish a
skill once per organization, share it with groups or members, and chats
in that organization load it next to personal and workspace skills. It
replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to
top:

1. #30609 data model: one migration, queries, RBAC and dbauthz
2. #30610 organization skill API routes and the scope-neutral SDK
3. #30611 sharing ACL routes
4. #30612 settings UI: shared skills table, admin page, member section
5. **#30613** chats: chatd loading, `/` menu, docs (this PR)

## What changes

This PR makes chats use organization skills: chatd adds the skills
shared with the chat owner in the chat's organization to the skill
index, `read_skill` loads them, the `/` menu offers them, and the docs
describe the feature.

**chatd** (`coderd/x/skills`, `coderd/x/chatd`):

- New source `skills.SourceOrganization` (`"org"`).
`MergeSkills(personal, organization, workspace)` sorts by name, then
source. A name in one source keeps its bare alias; a name in more than
one source gets a qualified alias in every source that has it
(`personal/<name>`, `org/<name>`, `workspace/<name>`), so no source
overrides another. `Lookup` returns `ErrSkillAmbiguous` for a bare name
that matches several sources.
- **Authorization (please review closely):** each turn, chatd reads
organization skill metadata and bodies as the chat owner's real RBAC
subject with their roles and groups (`chatOwnerSubject`), never as the
chatd system actor, so per-skill ACLs apply. Disabled skills are
skipped. A skill that is deleted, disabled, or no longer shared by the
time `read_skill` runs is reported as not found. If the owner subject or
the metadata can't be loaded, turn preparation fails with the error, as
main now does for personal skills (#30492), instead of running the turn
without organization skills.
- `read_skill` loads personal and organization bodies through one
`LoadStoredSkillBody` callback. An index description longer than 1,024
runes, the Agent Skills specification limit
(`maxSkillIndexDescriptionRunes`), is cut to 1,023 runes plus an
ellipsis so it stays within the limit; `read_skill` still returns the
full skill.
- Parse and lookup errors keep `%w` last on purpose: with `xerrors`, a
`%w` that isn't last repeats the cause, so main's detail read "skill is
too large: got N bytes, maximum is 65536 bytes: skill is too large".
This stack returns the cause once (ThomasK33 asked about the order in
review).

**`/` menu** (`site/src/pages/AgentsPage`): the chat page fetches the
chat organization's enabled skills (`organizationSkills(...)` from
#30612's queries) next to personal ones. `utils/skillAliases.ts`
(`resolveSkillTriggers`) follows `MergeSkills`: a personal or
organization trigger is bare unless its name is in more than one source,
then it becomes `/personal/<name>` or `/org/<name>`. While any list is
unknown every trigger is qualified, because a qualified alias always
resolves; workspace triggers are always qualified. Built-in `/clear` and
`/compact` stay pending until every skill list is known, and a list with
no data counts as unknown even after an error, so a failed fetch can't
make a built-in run in place of a same-named skill. Disabled skills
never shadow a built-in.

**Docs:** `docs/ai-coder/agents/extending-agents.md` gains two sections,
Organization skills (manage them, control who can use one, find the ones
you can use) and How chats load stored skills (turning a skill on or
off, skills with the same name). The admin page's organization picker
shows only for a user who can view more than one organization's skills.
The member "From your organizations" section shows one organization at a
time with its own picker, starts on the default organization, and has
Export all for the selected organization. The Enabled column is a switch
for editors and a check or X otherwise. The settings table in
`platform-controls/organizations.md` gains rows for organization and
personal skills.

## Commits

The first commit reproduces the reviewed chatd code from the old stack;
the site and docs commits reapply the old layers on #30612's queries,
and the rest are labeled trims or fixes.

- `aa634eec94a` feat(coderd/x): load organization skills in chats.
Reproduction of the old skill sources and chatd layers (old head
`a1047f4986`).
- `97945db4e86` test(coderd/x/skills): make `TestMergeSkills` (6 cases)
and `TestLookup` (8 cases) table-driven; coverage is 100% before and
after, with the same blocks.
- `dde61cc5fe2` test(coderd/x/chatd): drop skill tests duplicated by the
organization skills integration test
`TestOrganizationSkillsFollowChatOwnerAccess`; the covered block set of
the touched code is identical (348 of 506 blocks).
- `aad14b2dd05` feat(site/src/pages/AgentsPage): show organization
skills in the slash menu. The old alias helper and slash menu layers,
applied cleanly on #30612's queries.
- `712e272e140` test(site/src/pages/AgentsPage): trim duplicated slash
menu tests and stories. Deletes `AgentChatPage.test.tsx` (177 lines; its
assertions live in `submitChatTurn.test.ts` and
`ChatMessageInput.test.tsx`), two duplicate `ChatMessageInput` cases,
six of 17 `SkillsTriggerMenu` stories, and one disabled-skill fixture
row from a `ChatMessageInput` story.
- `232535e1011` docs(docs/ai-coder/agents): document organization
skills. From the old docs layer, with the View line updated for #30612's
read-only dialog and Download.
- `62dbbd2dfd3` refactor(site/src/pages/AgentsPage): address frontend
review on the slash menu.
- `7349eac4389` Merge the updated settings-ui layer into chats. Brings
in the #30610 and #30612 fixes; the merge has no changes of its own.
- `0f84a30ec69` Merge the updated settings-ui layer into chats again,
for #30612's read-only View fix; no changes of its own.
- `fb37822f985` fix(coderd/x/chatd/chattool): keep truncated skill index
descriptions within 1,024 runes. The cut now happens at 1,023 runes so
the ellipsis fits; the existing `skill_test.go` assertion now expects
1,023 runes plus the ellipsis.
- `1543666b584` docs(docs/ai-coder/agents): state the 1,023-character
cut before the skill index ellipsis.
- `96f1da47c9a` and `13f03d095ff` merge the updated settings-ui layer
for #30612's cache and Skills link fixes; no changes of their own.
- `b03c14c86de` and `41cc078b85a` merge the updated settings-ui layer;
`b03c14c86de` moves the `AgentChatPage.tsx` and `ChatMessageInput.tsx`
call sites, and their tests and stories, to #30612's split
`organizationSkills(...)` query.
- `1a32d05c9bc`
test(site/src/pages/AgentsPage/components/ChatMessageInput): restore
per-source loading stories for the slash menu.
- `830b0466982` fix(coderd/x/chatd): fail turn preparation when
organization skills cannot load. Main now returns database errors from
turn preparation (#30492); organization skills still logged and dropped
the failure, so a failed read silently hid them from the model. The new
`TestFetchOrganizationSkillMetadataListFailure` covers the returned
error.
- `84c135f8801` and `a6f8f63ad13` merge the updated settings-ui layer,
which brings in main; they resolve `generation_preparer.go` against
main's turn preparation error handling.
- `19b7880840f` merges #30612's member page test change; no changes of
its own.
- `c74d597f3c5` merges #30612's read-only status change; no changes of
its own.
- `f89c232c081` merges #30612's skills test changes; no changes of its
own.
- `17d5b9bfbc5` docs(docs/ai-coder/agents): describe the member skills
organization picker and read-only Enabled status.
- `f97b1710f71` merges the updated settings-ui layer (main at
`e7b76872098`); no changes of its own.

## Size

Production +558/-213 (docs +116/-6), tests +895/-262; total +1453/-475.
This is above the 500-line reviewer-friendly limit on purpose: Mike
asked for 4 to 5 PRs instead of the 21 small ones this stack replaces.

## Replaces

#30593 (skill sources), #30594 (chatd), #30601 (alias helper), #30602
(slash menu), #30604 (docs).

## Validation

- Go: all 21 packages under `coderd/x/skills` and `coderd/x/chatd` pass
on `62dbbd2dfd3`, plus the coverage comparisons above;
`coderd/x/chatd/chattool` passes again on `13f03d095ff` after the
truncation fix.
- Site: `pnpm run lint` and Biome clean; Vitest 102 tests; Storybook
Vitest 49 + 4 tests. `pnpm run lint` reran clean on `13f03d095ff`.
- On `a6f8f63ad13`, this PR's head before the test-only `19b7880840f`
merge: `go test` passes for `coderd/x/skills`,
`coderd/x/chatd/chattool`, and the `coderd/x/chatd` skill tests (`-run
'OrganizationSkill|Skill'`); 38 Vitest files covering skills and the
slash menu pass, 542 tests with 2 skipped.
- Docs: markdownlint 0 findings, Vale 0 findings.
- On `17d5b9bfbc5`: Vale clean, markdownlint and `make pre-commit` pass.
On `f97b1710f71`: `go build`, the `coderd/x/skills` and `chattool`
tests, `pnpm check` and `tsc` pass.
- Remote UAT round 7 through Coder Agents: PASS
([chat](https://dogfood.cdr.dev/agents/3426d4d3-2139-4ae1-8e0c-72ddeff413a8)).
- Full run on `62dbbd2dfd3`, including an in-place upgrade from main
`50c785cc6d7`: personal skills kept their IDs, names, content hashes and
timestamps, came up enabled, and worked in chats (`read_skill`) and the
`/` menu; create, edit, download and delete worked after the upgrade.
The admin page, sharing, the Enabled toggle, the member section, the `/`
menu, chats reading organization skills as the owner, limits, audit,
authorization and scoped keys, and the docs all passed.
- Delta run on `7349eac4389`: an update-only key gets 404 on PATCH with
no content in the response; saving permissions refreshes Updated without
a reload; smoke of the admin page, auditor View and Download, the `/`
menu and `read_skill`.
- Delta run on `0f84a30ec69` for `aaa356f0fd5`: read-only View, as an
org auditor and as a member, shows no Name hint, focuses no field and
starts on Close, and Escape or Close returns focus to the row menu
button; Add skill and Edit still show the hint and focus the import
field, and a saved edit persisted; smoke of the admin page, the `/` menu
organization group and `read_skill`. Screen-reader behavior was not
tested.
- Delta run on `13f03d095ff` for `698dd83946d`, `f82573cd1c2`,
`0e55137de7b` and `fb37822f985`: for an organization admin of org B who
is a plain member of org A, the sidebar Skills link drops `?org=` for
org A on seven AI settings pages and opens org B's skills, keeps `?org=`
for org B, and a typed `/ai/settings/skills?org=` for org A still shows
the permission error. A skill deleted and recreated with the same name
loads a fresh sharing list showing only Everyone, with no stale grant. A
1,500-character description appears in the chat's skill index as 1,023
characters plus an ellipsis, while `read_skill` returns all of it. Smoke
of the admin toggle, the member section, the `/` menu organization group
and `read_skill`.
- Low findings: the read-only View hint and field focus, fixed in
`aaa356f0fd5` and verified by the delta run above. The member section's
always-on disabled Enabled switch (also in the old stack) is replaced by
a read-only status icon in #30612's `63bb42754e3`, which has no UAT run.
Not fixed: `/` shows the `/personal/` prefix while organization skills
fail to load (the unknown-list rule above); the shared Switch
component's disabled styling (unchanged from main). A user who can read
skills in only one organization sees no organization name on the Skills
page, because the picker is hidden for a single organization, as on the
MCP servers page on main.
- Not tested: the workspace skills group (no running agent on the test
deployment), the 100-per-user personal cap, and behavior without a
Premium license.
- Remote UAT round 8 through Coder Agents: PASS on `b03c14c86de` (same
chat). The remote grader marked the unknown `read_skill` item FAIL only
because the prompt asked for the literal phrase "skill not found"; the
result names the skill once and says "not found" once, the same format
as main, so the runner graded it PASS on intent.
- Member page: starts on the default organization, shows one
organization's table with the read-only note once, Export all downloads
only the selected organization's skills, the picker is hidden with one
organization, and an organization without enabled skills isn't listed.
- Personal and admin tables: create, edit, toggle and delete each
refetch the list; reopening the editor shows the new content even with
the detail request delayed; a toggle changes state once, with no
flicker. A skill deleted and recreated with the same name shows only
Everyone in sharing.
- The sidebar Skills link keeps `?org=` only for an organization whose
skills the user can read. A create over 64 KiB returns 400 with the
detail "got 70000 bytes, maximum is 65536 bytes: skill is too large",
the cause once. The `/` menu shows Commands, Personal skills and
Organization skills, and invoking an organization skill loads its body
through `read_skill`.
- New low findings, not fixed: the member page picker's listbox is
labeled "Suggestions" (likely from the shared
`OrganizationAutocomplete`); a typed body over 64 KiB shows only the
size counter and a disabled Create until Import; `/ai/settings?org=`
redirects to the models page without `?org=`. The first and third were
not checked against main.
- `9002dc60382`, `1a32d05c9bc`, `830b0466982`, `ac3e62c83b7`,
`63bb42754e3`, `82b2487ad17`, `17d5b9bfbc5` and the main merges came
after `b03c14c86de`; they have tests but no UAT run.
- Every UAT round ran the migration as `000615_skills`. The heads ship
the same up and down SQL as `000621_skills`, so the upgrade path from
main's 000620 to 000621 has no UAT run.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-13 17:13:36 +00:00
Michael Suchacz 2c432a12a3 feat: add organization skill sharing ACL routes (#30611)
## Stack

Part of
[CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents).
This stack adds organization skills to Coder Agents: admins publish a
skill once per organization, share it with groups or members, and chats
in that organization load it next to personal and workspace skills. It
replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to
top:

1. #30609 data model: one migration, queries, RBAC and dbauthz
2. #30610 organization skill API routes and the scope-neutral SDK
3. **#30611** sharing ACL routes (this PR)
4. #30612 settings UI: shared skills table, admin page, member section
5. #30613 chats: chatd loading, `/` menu, docs

## What changes

This PR adds the routes that let admins choose who can read an
organization skill. It reuses the MCP server ACL machinery rather than
adding a second implementation.

Routes under
`/api/experimental/organizations/{organization}/skills/{skillName}`, all
behind `httpmw.ExtractOrganizationSkillParam` with `share`, so a caller
without share gets 404:

- `GET /acl` returns the resolved user and group grants.
- `GET /acl/available` returns the organization's users and groups that
can be granted, with the same `q`, `after_id`, `limit` and `offset`
paging as the MCP endpoint.
- `PATCH /acl` takes a sparse `UpdateOrganizationSkillACLRequest`: only
the listed principals change, `read` grants and `""` removes. It returns
204 and writes a `write` audit entry with the old and new ACL.

**Concurrency (please review closely):** the PATCH transaction first
locks the granted users' `organization_members` rows `FOR KEY SHARE`
(new query `LockOrganizationMembersByUserIDsForShare`, run as system),
then locks the skill row `FOR UPDATE`, then validates the update on the
transaction handle. Member removal takes the same locks in the same
order (its membership row, then the skill rows through the #30609
trigger), so the two can't deadlock, and a removal can't miss a grant
written by a concurrent PATCH. A skill deleted between the middleware
read and the row lock stays a 404.

Grants are users and groups, the same as MCP server configs (Mike's
decision, for parity). Only the `read` role exists; any other role is a
validation error, as are users or groups outside the organization.

Supporting changes:

- `coderd/mcp_acl.go`: the MCP ACL handlers' resolution, candidate
listing and validation move into generic helpers
(`writeOrganizationACLAvailable`, `validateOrganizationACLUpdate`,
`resolveACLUsers`, `resolveACLGroups`) shared by both resources;
`acl.Validate` accepts a named `acl.Role` constraint that now includes
`codersdk.OrganizationSkillRole`.
- `codersdk/skills.go`: `OrganizationSkillRole`, `OrganizationSkillACL`
with its user and group entries, `UpdateOrganizationSkillACLRequest`,
and client methods.
- New sqlc query `UpdateOrganizationSkillACLByID` with its dbauthz
wrapper (requires `share`).

## Commits

The first commit reproduces the reviewed code from the old stack; each
later commit is a labeled trim or fix.

- `b1c85560b69` feat: add organization skill sharing ACL routes.
Reproduction: every file this commit changes equals the reviewed old
`acl-routes` head `80c1c3594e6`; the rest of the tree differs only by
#30609's migration merge and main.
- `b5d2ee265c8` test(coderd): merge duplicate organization skill ACL
route tests (+49/-111). The removed cases repeated
`TestMCPServerConfigACL`, `TestMCPServerConfigACLAvailable` or
`TestRolePermissions`; with the MCP tests included, the covered block
set of `organizationskills_acl.go`, `mcp_acl.go` and
`rbac/acl/updatevalidator.go` is identical before and after. Kept:
`TestOrganizationSkillACL` (`OrgAdminManagesACL`, `ReadersGetNotFound`,
`SparseUpdateChangesReaders` with the audit diff,
`RejectsUnsupportedRole`, `ScopedAPIKeys`),
`TestOrganizationSkillACLConcurrentDelete`, and the lock order tests
`TestOrganizationSkillACLMemberRemoval` (`RemovalBeforeGrant`,
`RemovalAfterGrant`). `OrgAdminManagesACL` now also checks that users
from another organization are not candidates (mutation checked). Sharing
the param extractor with MCP was skipped: it would save about 10 lines
of security-sensitive code.
- `e09c4dba66b` Merge the updated org-api layer into sharing. Brings in
#30610's `ad0a7a37504`; the merge has no changes of its own.
- `1d3817dc639` and `c8c88f84d18` merge the updated #30610 layer,
including main; no changes of their own.
- `c4d22c47990` merges the updated #30610 layer (main at `e7b76872098`);
no changes of its own.

## Size

Production +431/-102, tests +377/-0, generated +918/-0; total
+1726/-102. This is above the 500-line reviewer-friendly limit on
purpose: Mike asked for 4 to 5 PRs instead of the 21 small ones this
stack replaces.

## Replaces

#30591 (ACL queries), #30592 (ACL routes).

## Validation

- `go build` and `go vet` passed; `make gen` exited 0 with no diff.
- `go test` passed for `coderd` (skill, MCP and ACL tests),
`coderd/database`, `dbauthz`, `migrations`, `gentest`,
`coderd/rbac/...`, `coderd/httpmw`, `coderd/x/skills/...`,
`coderd/x/chatd/...`, and the audit and skill tests in
`enterprise/audit` and `enterprise/coderd`.
- The covered block comparison above, before and after the test merge.
- On `c4d22c47990`: the same checks as #30610's head, plus `go test
./coderd/ -run 'Skill|MCP|ACL'`, pass.
- Remote UAT round 7 through Coder Agents passed on the whole stack; see
#30613's Validation for scope and findings.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-13 16:43:15 +00:00
Michael Suchacz db7f5a7703 feat: add organization skill API routes (#30610)
## Stack

Part of
[CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents).
This stack adds organization skills to Coder Agents: admins publish a
skill once per organization, share it with groups or members, and chats
in that organization load it next to personal and workspace skills. It
replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to
top:

1. #30609 data model: one migration, queries, RBAC and dbauthz
2. **#30610** organization skill API routes and the scope-neutral SDK
(this PR)
3. #30611 sharing ACL routes
4. #30612 settings UI: shared skills table, admin page, member section
5. #30613 chats: chatd loading, `/` menu, docs

## What changes

This PR adds the organization skill CRUD routes and makes the skill SDK
types owner-neutral so personal and organization skills share them.

**API contract change (experimental API, personal and organization
skills):** skill list and get responses gain `enabled`, and `PATCH
.../skills/{skillName}` now takes `{content?, enabled?}`, so `content`
is optional and a toggle doesn't resend it. A PATCH with neither field
returns 400; `content`, when present, is validated as before. In the
SDK, `UserSkill`, `UserSkillMetadata`, `CreateUserSkillRequest` and
`UpdateUserSkillRequest` become `Skill`, `SkillMetadata`,
`CreateSkillRequest` and `UpdateSkillRequest` (`codersdk/userskills.go`
moves to `codersdk/skills.go`), and `UpdateSkillRequest.Content` is now
`*string`. The personal client method names are unchanged. The site
changes are the handwritten edits that rename forces (`api.ts`,
`queries/userSkills.ts`, `testHelpers/skills.ts` and three call sites),
with no UI change.

Organization routes under
`/api/experimental/organizations/{organization}/skills`, not gated by an
experiment or license, like the personal ones:

- `GET` lists with the caller's own permissions, so admins, the site
owner and org auditors see every skill and members see only skills
shared with them. `GET /{skillName}` goes through the new
`httpmw.ExtractOrganizationSkillParam` and admits only read, so a
missing skill and a denied one both return 404.
- `POST` checks `create` on the organization before reading the body,
then seeds `group_acl` with the Everyone group (whose ID is the
organization ID) so a new skill reaches every member until an admin
narrows it. The 100-skill cap and a duplicate name return 409.
- `PATCH` admits only callers who can read the skill, because its
response includes the content; `DELETE` admits callers who can read or
delete it. A reader who can't change the skill gets a write-specific 403
from the database wrapper, and everyone else gets 404. PATCH locks the
row as system, updates as the caller in the same transaction, and audits
the locked row only after the transaction commits.
- Audit: `database.AuditableOrganizationSkill` records resource type
`organization_skill` with the organization ID; personal skills keep
`user_skill`.

Personal and organization handlers share one create and update path in
`coderd/userskills.go` (`createSkill`, `updateSkill`, and a
`skillWriteErrors` table per owner and action).

## Commits

The first commit reproduces the reviewed code from the old stack; each
later commit is a labeled trim or fix.

- `49cb2c84276` feat: add organization skill API routes. Reproduction:
every file this commit changes, including the SDK layer's site edits,
equals the reviewed old `write-routes` head `aa597100f28`; the rest of
the tree differs only by #30609's migration merge and main.
- `dc893bc60d6` refactor(coderd): share the skill create and update path
across owners (production +164/-188). It keeps the organization create
check before the body read, the lock as system then update as caller
order, and audit assignment after the transaction. No swagger,
`codersdk` or `typesGenerated.ts` change; `make gen` is clean.
- `f94925425ef` test(coderd): merge duplicate personal and organization
skill tests. `TestSkillsCRUD`, `TestSkillValidationAndConflicts` and
`TestSkillAudit` are table-driven over Personal, OrgAdmin and SiteOwner,
alongside `TestOrganizationSkillAuditsDeniedWrites` and one
`ScopedAPIKeys` subtest; `TestPatchUserSkill` is deleted because none of
its blocks were unique. Coverage of `userskills.go`,
`organizationskills.go` and `httpmw/organizationskillparam.go` is 101 of
117 blocks before and after, the same block set.
- `ad0a7a37504` fix(coderd): require read access to patch an
organization skill. The organization PATCH route now admits only `read`,
because its response carries the skill's content: a key scoped to
`organization_skill:update` without read gets 404, like GET, instead of
200 with the content. Readers without update still get 403, and DELETE
is unchanged. `TestOrganizationSkillAccess/ScopedAPIKeys` asserts the
404, that the response has no content, and that the skill is unchanged;
against the old route it fails with 200.
- `69d00ded0b6` and `f67b79343ec` merge the updated #30609 layer,
including main; `69d00ded0b6` resolves a conflict in the personal skills
page with main. No other changes of their own.
- `18373ffd59e` merges the updated #30609 layer (main at `e7b76872098`)
and reapplies the `AuditableOrganizationSkill` entry in
`enterprise/audit/table.go` alongside main's new
`AuditableChatProjectInstructions` entries.

## Size

Production +838/-308, tests +733/-322, generated +801/-274; total
+2372/-904. This is above the 500-line reviewer-friendly limit on
purpose: Mike asked for 4 to 5 PRs instead of the 21 small ones this
stack replaces.

## Replaces

#30587 (scope-neutral SDK and personal enabled state), #30588 (read
routes), #30589 (write routes and audit).

## Validation

- The coverage comparison above, before and after the test merge.
- `make gen` leaves no diff after the shared create and update refactor.
- On `18373ffd59e`: `make gen` is clean; build, vet, the `coderd` and
`enterprise/coderd` skill tests, `rbac`, `httpmw`, `dbauthz` and
`enterprise/audit` pass; `pnpm check`, `pnpm lint` and `make pre-commit`
pass.
- Remote UAT round 7 through Coder Agents passed on the whole stack; see
#30613's Validation for scope and findings.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-13 16:24:45 +00:00
Michael Suchacz b67587c875 feat: add the organization skills data model (#30609)
## Stack

Part of
[CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents).
This stack adds organization skills to Coder Agents: admins publish a
skill once per organization, share it with groups or members, and chats
in that organization load it next to personal and workspace skills. It
replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to
top:

1. **#30609** data model: one migration, queries, RBAC and dbauthz (this
PR)
2. #30610 organization skill API routes and the scope-neutral SDK
3. #30611 sharing ACL routes
4. #30612 settings UI: shared skills table, admin page, member section
5. #30613 chats: chatd loading, `/` menu, docs

## What changes

This PR adds the data model and authorization for organization skills.
Personal skills keep working unchanged: `git diff origin/main --
codersdk site` shows only generated RBAC and API key scope additions
(`apikey_scopes_gen.go`, `rbacresources_gen.go`,
`rbacresourcesGenerated.ts`, the RBAC and scope unions in
`typesGenerated.ts`) and `codersdk.ResourceTypeOrganizationSkill` with
its friendly string. No personal route, SDK type or site component
changes, and `coderd/userskills_test.go` changes only by mechanical
constant renames.

**One migration, `000621_skills`** (please review it closely):

- renames `user_skills` to `skills` with its constraints, index and
triggers, and recreates the cap function and
`delete_deleted_user_resources`, whose PL/pgSQL bodies name the table;
- adds `organization_id` and `project_id` (`ON DELETE CASCADE`),
`enabled` (default true), and `group_acl` / `user_acl` (`jsonb`, default
`{}`), with CHECKs for exactly one owner, ACLs that are JSON objects,
and ACLs that stay empty unless an organization owns the row, plus
partial unique name indexes per owner kind;
- replaces the cap trigger with `enforce_skills_per_owner_limit()`: 100
rows per owner, disabled rows included, locking the owner row `FOR NO
KEY UPDATE` so unrelated foreign key checks are not blocked;
- adds a `BEFORE DELETE` trigger on `organization_members` that removes
a departing member from `user_acl` on that organization's skills, so
rejoining doesn't restore a direct grant;
- adds the `organization_skill` `resource_type` and `api_key_scope` enum
values last; nothing in the migration uses them.

The down migration undoes the three old migrations in reverse order: it
drops organization and project rows and restores `user_skills`, its
names, the cap function and `user_id NOT NULL`.

Authorization: a new RBAC resource `organization_skill` (create, read,
update, delete, share). Owner and org admin get it; site and org
auditors get read; members read only through the skill's ACL, which
applies only to members of its organization. `Skill.RBACObject()`
returns the organization object with both ACLs; project rows stay denied
to everyone. New sqlc queries cover organization insert, get, list
(SQL-filtered through `GetAuthorizedOrganizationSkillMetadata` and a
`regosql` converter), update and delete, with dbauthz wrappers and the
audit table entries. No routes yet.

## Commits

The first commit reproduces the reviewed code from the old stack; each
later commit is a labeled trim or fix.

- `baa3150e3b7` feat: add the organization skills data model.
Reproduction: every non-site file this commit changes equals the
reviewed old `authz` head `1e16b3004c4`, except that the old stack's
migrations 000615, 000616 and 000617 are merged into one migration (then
`000615_skills`, now `000621_skills`) with one fixture
(`testdata/fixtures/000621_skills.up.sql`) and one migration test (now
`testMigration000621Skills`). At that commit, `dump.sql` was
byte-identical to the old head's.
- `1b84e83463a` test(coderd/database): keep only the SQL paths of the
org skill matrix. `TestOrganizationSkillAuthorization` becomes
`TestOrganizationSkillQueries` instead of being deleted, because
coverage showed it is this PR's only run of the
`GetAuthorizedOrganizationSkillMetadata` row scan (80% to 65% without
it) and of the organization update and delete queries (100% to 0%).
Swapping `group_acl` and `user_acl` in the converter makes
`GrantedMember` fail. The removed cases are decided by
`TestRolePermissions`, `TestSkillRBACObjectOrganizationACL` and the
dbauthz method suite.
- `e2931b412e9` test(coderd/database): fold the skill cap cases into
existing coverage. The user case is covered by `TestUserSkillLimit` and
`TestUserSkillLimitConcurrentCreates`; the organization case folds into
`ConcurrentOrganizationInsertsHonorCap`, which now also checks that
disabled rows count and another organization is unaffected (mutation
checked); the project case stays.
- `be62aea9021` test(coderd/rbac): drop
`TestOrganizationSkillReadScope`, covered by `TestExternalScopeNames`
and the generated scope file.
- `0c275ba37de` merges main at `454a2fcc697`, which took 000615 to
000618: it renumbers the migration to 000619 and resolves `dump.sql`,
`check_constraint.go` and the generated API docs. `8f2b4b87ee2` merges
main at `ad85646164e`, which took 000619: it renumbers the migration to
000620.
- `5248d56b64f` merges main at `e7b76872098`, which took 000620
(`000620_audit_chat_project_instructions_resource_type`): it renumbers
the migration, fixture and migration test to `000621_skills` (step
`Migration000621Skills`, version 621), and resolves
`enterprise/audit/table.go` (keeping main's
`AuditableChatProjectInstructions` entries and the `Skill` entry),
`dump.sql` and `models.go` (the `resource_type` enum gets
`chat_project_instructions`, then `organization_skill`) and the
regenerated `schemas.md`. The up and down SQL are unchanged by every
renumbering.

## Size

Production +549/-67, tests +728/-33, generated +881/-314; total
+2158/-414. This is above the 500-line reviewer-friendly limit on
purpose: Mike asked for 4 to 5 PRs instead of the 21 small ones this
stack replaces.

## Replaces

#30584 (rename `user_skills` to `skills`), #30585 (skill owners), #30586
(organization skill authorization).

## Validation

- Migration test: up keeps the existing personal row and its ID; down
restores the `user_skills` names, cap function and `NOT NULL`, and drops
organization rows; soft-deleting a user removes their skills after up,
after down, and after re-up.
- `go test` passed on `be62aea9021`, before the main merges, for
`coderd` (skill tests), `coderd/database`, `dbauthz`, `migrations`,
`gentest`, `coderd/rbac/...`, `coderd/x/chatd`, `coderd/x/skills`,
`enterprise/audit` and `enterprise/coderd`; `make lint/check-scopes`
passed; the four personal skills Vitest files pass unchanged.
- On `5248d56b64f`: build, vet, `TestMigrate`, `TestMigrationChain`, the
`migrations`, `database`, `dbauthz`, `gentest`, `rbac` and
`enterprise/audit` packages pass; `make gen` run twice is clean, and
`make pre-commit` passes.
- Remote UAT round 7 through Coder Agents passed on the whole stack; see
#30613's Validation for scope and findings.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-13 15:51:31 +00:00
97c5260adb feat: add chat project instructions API (#30368)
Adds the experimental API for chat project instructions: `GET`, `PUT`,
and `DELETE
/api/experimental/organizations/{organization}/chats/projects/{project}/instructions`,
behind the `chat-projects` experiment.

- `GET` returns 200 with empty instructions and null
`updated_by`/`updated_at` when nothing is set, so clients don't need a
404 path.
- `PUT` requires update access only (the read-back runs under a system
context, so a custom role with update but not read can save). It
sanitizes the text with `SanitizePromptText`. It rejects blank text with
400, pointing the caller to `DELETE`, and rejects text over the chat
prompt byte limit. It records the caller as `updated_by`. The upsert and
the read-back that returns the editor's profile run in one transaction.
- `DELETE` returns 204, including when nothing is set.
- Users without access to the project get 404, like the other project
routes.
- `PUT` and `DELETE` are audited as a new `chat_project_instructions`
resource (`write` and `delete`), with the project as the resource ID and
its name as the target. The diff tracks only the instruction text.
Saving unchanged text and deleting unset instructions are not audited. A
per-project advisory lock serializes reading the previous value with the
write, so concurrent edits produce accurate diffs. Migration `000615`
adds the enum value.

The codersdk client gains `ChatProjectInstructions`,
`UpdateChatProjectInstructions`, and `DeleteChatProjectInstructions`.

**What to scrutinize:** writes are last-writer-wins with no version
precondition, matching the deployment, user, and organization prompt
settings.

**Generated:** `coderd/apidoc/docs.go`, `coderd/apidoc/swagger.json`,
and `docs/reference/api/schemas.md` come from the Swagger annotations;
`site/src/api/typesGenerated.ts` comes from `codersdk`;
`docs/admin/security/audit-logs.md`, `coderd/database/dump.sql`, and
`coderd/database/models.go` come from the audit table and migration.
Together they are about 400 of this PR's lines.

**Validation:** `make gen` leaves no diff. `go test ./coderd -run
TestChatProject` and `go test ./enterprise/coderd -run
TestChatProjectInstructions` pass. `TestChatProjectInstructionsAudit`
checks the audit entries for create, update, and delete, and that no-op
saves and deletes are not audited. It covers unset GET, create, update,
blank and oversized PUT, delete, access denial, and cascade on project
delete.

---

Stack (merge top to bottom). #30377 through #29583 are f0ssel's split of
the original #29583, and this stack sits on #29583:
- #30377 data layer and project dialog (f0ssel)
- #30378 new-chat form locked to a project (f0ssel)
- #30379 project page route (f0ssel)
- #29583 sidebar folders (f0ssel), the base of this stack
- #30291 composer frame
- #30292 project chat list caches
- #30293 project page header
- #30294 project chats list
- #30367 instructions storage and prompt injection
- #30368 instructions API 👈
- #30369 instructions editor dialog
- #30370 instructions Details panel

---------

Co-authored-by: Garrett Delfosse <delfossegarrett@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 19:15:16 +07:00
Michael Suchacz b2c8798e0e fix: replay stateless OpenAI reasoning across chat turns (#30298)
OpenAI Responses chats default to `store=false`, and chatd only replayed
reasoning as an `item_reference`, which needs provider-side storage.
Reasoning was therefore dropped between tool steps and turns, so
reasoning models lost their chain of thought mid-task.

This pins coder/fantasy `coder_2_33` at 6ba8681, which includes fantasy
#63 (and #64, #65, and #66). #63 captures each completed reasoning item
(ID, `encrypted_content`, summary) and replays it in full regardless of
`store`. In chatd:

- Every Responses call requests `reasoning.encrypted_content`. The
`store` default and configured includes are unchanged, and Chat
Completions requests are untouched.
- Prompt rebuilds keep reasoning parts without visible text when they
carry finalized encrypted state, so reasoning survives durable history
and coderd restarts.
- The provider-switch sanitizer also strips OpenAI reasoning from
history produced by a different or unknown provider instance, since
reasoning IDs and encrypted content only resolve where they were issued.
- A compaction model override on a different provider instance, or with
a different model, gets a compaction prompt without the chat's OpenAI
reasoning.
- Each saved reasoning part records the provider instance, model, and
credential (the deployment's or the user's personal API key) that
produced it, and the sanitizer keeps it only when all three match the
current turn, because another model, or the organization behind another
credential, can reject the encrypted content. Switching models, adding
or removing a personal key, or editing a config's model or provider
drops the earlier reasoning; switching back restores it. The record is
never returned by the API. Reasoning saved before this change has no
record and follows its model config.
- If OpenAI rejects replayed reasoning it cannot decrypt
(`invalid_encrypted_content`), for example after the deployment's key is
replaced with one from another organization, the request is retried once
without reasoning, and the reasoning that step produces is saved with
the provider's next rejection epoch. Epochs are kept separately for each
provider, model, and credential. Later steps and turns leave out
reasoning from earlier epochs, on any replica and after restarts, so
only the first request after the change is rejected and newer reasoning
keeps replaying. A retried step that returns no reasoning saves its
epoch on its first part. The advisor's prompt never includes OpenAI
reasoning, as on `main`.

## Testing

Rebased onto main at 98332c739b as 5735acb36ed (same tree as merging
the pre-rebase head a378071862a with main; older SHAs below refer to the
pre-rebase history). 3c17d807a33 then adds the model to the reasoning
record, 21156b2b32e pins Fantasy d55a867, which treats a response that
echoes `store: false` as unstored, bf795daa923 adds the retry and the
advisor change, 7eb8b1d029a moves the pin to 88cffb5 (`coder_2_33`
merged into #63; its OpenAI provider code is identical to d55a867), and
f4f40734a41 pins `coder_2_33` at 6ba8681 once #63 merged. That adds
fantasy #66, which replays the Responses message phase on assistant
text. abccede81f7 makes the retry remember rejected reasoning in the
process. The branch was then rebased onto main at abc2faed60, so every
SHA above refers to the pre-rebase history. 5fb3ce1689 replaces that
memory with the credential and a rejection epoch saved on each reasoning
part, and 62b5cb532a keeps epochs per provider, model, and credential
and saves the epoch when the retried step returns no reasoning.
1057beb327 hides the four internal fields from the Swagger spec and API
reference, and 29a705e099 merges main at d68b2c8b25. Its only
conflicts were the fantasy pin (kept at 6ba8681, whose `coder_2_33`
history includes main's 3510552) and test imports.

- Tests in `chatprompt`, `chatprovider`, `chatsanitize`, the
provider-switch sanitizer, and `integration_responses_test.go` cover
tool-step and durable replay, legacy and incomplete metadata, provider
switches, and a web search chat switching from `store=false` to
`store=true`. Integration tests also cover the retry, a later tool step
after it, the next turn on a fresh server against the same database
(with and without reasoning in the retried step), a personal key added
mid-chat, and the advisor prompt. Red-green verified, including a config
whose model is edited in place. The compaction override's model check
has unit tests and UAT coverage but no integration test. `go vet`,
targeted race tests, and the pre-commit hook (gen, fmt, lint, build)
pass. Local pre-push hooks were skipped; CI covers them.
- Remote UAT on dogfood with real gpt-5-mini and gpt-5.4 behind a
logging proxy that checks every request body. Rounds 1 and 2 failed on
dangling item references (both fixed). Rounds 3 to 5 passed
(ee3c0bf3d60, 0580870b5ff, a378071862a): tool-step and durable replay,
restarts, interrupts, provider switches and config repoints, web search
history, and compaction overrides. Both test providers shared one
upstream account, so cross-provider checks cover request shape, not a
real rejection. Round 6 at 21156b2b32e (model record) with gpt-5-mini
and gpt-5-nano: PASS. 151 Responses requests, all 200; no request
carried reasoning from a different model except an older, unrecorded
part that follows its config by design. Same-model config switches,
switching back, edits to a config's model, a mid-turn switch, restarts,
and a same-provider compaction override with a different model all
behaved as described. Round 7 at bf795daa923 (retry and advisor): PASS.
Through AI Gateway, a corrupted saved blob is rejected with HTTP 400
`invalid_encrypted_content`. All 14 rejected requests were retried once
without reasoning and returned 200, a different 400 was not retried, and
advisor requests on the chat model, a same-provider override, and
another provider carried no reasoning.
- f4f40734a41 changes only the pin. Its new OpenAI code is fantasy #66,
which UAT rounds 1 to 7 did not run. A live check of fantasy 6ba8681
through AI Gateway, two turns with tool calls on gpt-5.5 and gpt-5-mini
with `store` false and true: all 18 requests returned 200, and the
gpt-5.5 requests replayed encrypted reasoning followed by assistant
messages with the `commentary` and `final_answer` phases. `go test
./coderd/x/chatd/... ./codersdk/` passes (21 packages) on the same
fantasy tree.
- UAT round 8 at abccede81f7 (remembered rejection, and fantasy #66
through chatd) with gpt-5.5, gpt-5-mini, and gpt-5-nano: PASS. 110
requests; the 6 rejections were 5 corrupted saved blobs, each retried
once without reasoning and answered 200, and one unrelated `temperature`
error that was not retried. After the first rejection, later steps and
turns in that chat were never rejected again and replayed newer
reasoning. A coderd restart cost one more rejection, which also dropped
reasoning saved after the first one, and a second chat was unaffected.
gpt-5.5 requests replayed assistant messages with the phases the model
returned, with `store` false and true.
- UAT round 9 at 62b5cb532a (saved rejection epoch, personal key
switch) with gpt-5.5, gpt-5-mini, and gpt-5-nano: PASS. 158 Responses
and 7 Chat Completions requests; the 6 rejections were corrupted saved
blobs, each retried once without reasoning and answered 200, and an
unrelated `temperature` error was not retried. After a coderd restart,
and on a second coderd against the same database, the chat was not
rejected again and kept replaying the reasoning saved after the
rejection. Adding a personal key left out the deployment key's reasoning
before sending, and removing it brought that reasoning back. A retried
step without reasoning, two chats rejected at once, compaction, a model
switch after a rejection, and a switch to Chat Completions and back all
behaved as described.

## Review record

- Independent full-branch test audit at bbe1a1b27a: PASS.
- Codex through a378071862: compaction overrides kept foreign reasoning
and configs could be repointed to another provider (both fixed); a
request for documentation TODOs and two behaviors that already exist on
`main` (declined).
- Codex on 5735acb36e: two findings. Reasoning replayed after a
same-provider model switch: fixed in 3c17d807a33. A provider's base URL
edited in place keeps its reasoning: covered by the retry in
bf795daa923.
- Codex on 21156b2b32, bf795daa92, and 7eb8b1d029: no major issues.
Codex on f4f40734a4: rejected reasoning stayed in the chat's history, so
each later request in that chat was rejected and retried again without
any reasoning. Fixed in abccede81f7. Codex on abccede81f: that memory
was lost on a restart or another replica, where the next rejection also
dropped newer reasoning, and it grew without bound. Fixed in
5fb3ce1689. Codex on 5fb3ce1689: a rejection advanced the epoch for
every model and credential on the provider, and a retried step without
reasoning lost the new epoch. Both fixed in 62b5cb532a. Codex on
62b5cb532a: keep reasoning out of a config switched from Responses to
Chat Completions (declined: the Chat Completions converter already skips
reasoning parts, confirmed with a probe test). Codex on 29a705e099: no
major issues.
- ethanndickson: retry without reasoning on `invalid_encrypted_content`,
and keep OpenAI reasoning out of the advisor prompt. Both fixed in
bf795daa923. Then: in-process memory is not enough because dogfood
restarts on every merge and runs several replicas. Fixed in 5fb3ce1689
with his proposed rejection epoch in chat history, plus the credential
record, and tested with the next turn on a fresh server. Then: hide the
internal fields from Swagger, fixed in 1057beb327. Approved on
62b5cb532a.
- coder/fantasy#63 review: honoring an echoed `store: false` is fixed in
d55a867; replaying web searches that had no preceding reasoning is a
pre-existing gap, tracked in #30528.
- CI on 5735acb36ed: green after rerunning two failures outside this
change (a `cli` `TestSSH` data race, and a `test-go-pg` job timeout that
also fails on `main`). CI on 21156b2b32e: all checks pass. CI on
bf795daa923: all checks pass. CI on 7eb8b1d029a: all checks pass. CI on
f4f40734a41: all checks pass. CI on abccede81f7 did not run, because the
branch conflicted with `main` (#30492 changed the provider-switch
sanitizer to return database errors). CI on 62b5cb532a: all checks
pass. CI on 29a705e099: all checks pass.

> Xum, an AI coding agent, implemented, tested, and opened this PR on
behalf of @ibetitsmike.
2026-10-13 13:49:12 +02:00
ad85646164 feat(coderd): add chat project instructions storage and prompt injection (#30367)
Adds storage for project-level instructions in Coder Agents chat
projects and injects them into every chat in the project. This is the
first of four stacked PRs that replace #30349.

A new `chat_project_instructions` table (migration 000613) holds at most
one row per project, scoped to the project's organization. It records
`updated_by` and cascades on project delete. The dbauthz wrappers
authorize against the parent project: read access to get the
instructions, update access to upsert or delete them. No new RBAC
resource is added; project RBAC comes in a later PR.

chatd loads the instructions on every turn, so edits reach existing
chats on their next turn. They are added to the system prompt as a
`<project-instructions>` block after the memory guidance and before
personal instructions, so a user's personal instructions still win a
conflict. Subagent chats carry no `project_id`, so they use their root
chat's project. A load error other than no rows is logged, and the turn
continues without instructions.

**What to scrutinize:** the prompt ordering in `buildSystemPrompt`, and
the choice to inject per turn rather than snapshot at chat creation.
Per-turn injection costs one prompt-cache miss after each edit; the org
instructions in #30272 use a creation-time snapshot instead.

**Generated** (`make gen` from the migration and
`queries/chatprojectinstructions.sql`): `dump.sql`, `models.go`,
`querier.go`, `queries.sql.go`, `dbmetrics/querymetrics.go`,
`dbmock/dbmock.go`, and the `*_constraint.go` files.

`coderd/x/chatd/ARCHITECTURE.md` gets two TODOs for the PR author to
replace with the real description, per AGENTS.md.

**Validation:** `make gen` leaves no diff. `go test
./coderd/database/dbauthz -run TestMethodTestSuite/TestChats`, `go test
./coderd/x/chatd -run 'SystemPrompt|ProjectInstructions|Memory'`, and
`go test ./coderd/database/migrations/...` pass.

---

Stack (merge top to bottom). #30377 through #29583 are f0ssel's split of
the original #29583, and this stack sits on #29583:
- #30377 data layer and project dialog (f0ssel)
- #30378 new-chat form locked to a project (f0ssel)
- #30379 project page route (f0ssel)
- #29583 sidebar folders (f0ssel), the base of this stack
- #30291 composer frame
- #30292 project chat list caches
- #30293 project page header
- #30294 project chats list
- #30367 instructions storage and prompt injection 👈
- #30368 instructions API
- #30369 instructions editor dialog
- #30370 instructions Details panel

---------

Co-authored-by: Garrett Delfosse <delfossegarrett@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 16:45:44 +07:00
Michael Suchacz 7517301ffa chore: add golangci new-code quality gates (#30622)
Stack context: second layer of the Go lint gate stack, built on the
exclusion fix below.

Why: adds commented-out code detection and a `log/slog` ban to the main
config, plus a new-code-only pass requiring reasons on lint
suppressions, flagging ignored errors (including `_, _ = w.Write(...)`),
and banning `time.Sleep` in tests. Fixing the hits surfaced a real bug:
GitLab warnings went to the wrong logger. The new-code pass resolves
symlinked checkout paths so it does not silently report nothing.

Validation: both configs report 0 issues; each seeded violation is
caught; `make pre-commit` passed.

> Xum acted for Mike to open this PR.
2026-10-13 07:57:56 +00:00
Michael Suchacz e8dcf9a3f1 fix: anchor golangci .git exclusion and fix hidden findings (#30621)
Stack context: two-layer stack tightening Go lint gates. This bottom
layer fixes the lint exclusion; the layer above adds new rules.

Why: a `.git` path exclusion in the golangci config was hiding Go files,
masking 70 findings. This fixes the exclusion, fixes the findings
(`agent/agentgit` now runs git through `agentexec.Execer`), and adds a
`make lint/go` check that fails when an exclusion hides Go files. `make
lint/go` now waits for a concurrent golangci-lint run instead of
failing. The exhaustruct exclusion rule for `scripts/*` had the same
problem: as a regex it also matched `agent/agentscripts` and
`coderd/agentapi/scripts.go`, so it is now `^scripts/`.

Validation: full-tree lint reports 0 issues; the new check fails on the
old entry and passes on the new one.

> Xum acted for Mike to open and update this PR.
2026-10-13 07:57:56 +00:00
Bobby Ho dae7967bd1 feat: persist script resource addresses and dependencies (#30625)
feat(coderd): persist workspace agent script dependencies

Persist Terraform resource addresses and script dependencies supplied by
the provisioner (#30451, #30491), and expose them through the SDK and
REST API.

- Add migration 000618 with `resource_address` and `dependencies`
  columns, empty defaults, and a JSON-array constraint. API responses
  always include the address and omit empty dependency lists.
- Preserve `success` and `completion` requirements, including scripts
  on devcontainer subagents. Legacy scripts and older provisioners
  remain compatible through empty defaults.
- Reject empty prerequisite addresses and unsupported requirements,
  rolling back the entire job. Invalid stored dependencies return HTTP
  500 rather than silently dropping ordering.
- Add persistence, validation, rollback, devcontainer, API, and migration
  coverage; regenerate API docs and TypeScript types.

Unordered templates retain their existing execution behavior.
Dependency lists remain empty until #30556. Agent manifest wiring and
the agent API version bump are deferred to a follow-up.

Refs PLAT-548.
2026-10-13 03:43:55 +00:00
Ehab Younes 2f2169ed19 fix: deduplicate template insights family metrics (#30497)
coderd_insights_applications_usage_seconds summed each family's per-app
minutes, so a user with VS Code and Cursor open in the same minute was
counted as two VS Code minutes. GetTemplateInsightsByTemplate now takes
the registry's app-to-family mapping as jsonb, groups the raw agent
stats by family, and counts each minute once per user and family.

- Keep the metric's four built-in series (Visual Studio Code, JetBrains,
  Web Terminal, SSH) and their labels; only their values change. The
  HELP text now says built-in apps are grouped by family.
- Add AppFamilyName.DisplayName() and remove the duplicate
  TemplateBuiltinAppDisplayName* constants from codersdk. The template
  insights handler returns the same display names.
- Pin how the migration 000596 rollback approximates family minutes
  from per-app rows.

Family values can drop wherever the old per-app sum double-counted
overlapping apps. No migration or storage changes.
2026-10-12 23:58:35 +03:00
Michael Suchacz 41f5caafec fix: fold web searches, answer reasoning, and OpenAI narration into working blocks (#30529) 2026-10-12 22:53:07 +02:00
Ehab Younes 5b060beeb1 feat: separate connection log methods from app names (#30466)
Connection logs now record how a connection was made
(`connection_method`) separately from which app made it (`app_name`).
Storage, the HTTP API, search filters, and the UI move to the new
fields. Legacy agent reports and the deprecated `type` field keep
working.

- Add `connection_method` (`ssh`, `reconnecting_pty`, `workspace_app`,
  `port_forwarding`, `tunnel`) and optional `app_name` and
  `app_display_name` to `GET /api/v2/connectionlog`. Deprecate `type`
  and make it a plain string in the API and the Go SDK.
- Add `method:` and repeatable `app:` search filters. `type:` keeps its
  old results but can't be combined with the new filters.
- Replace the Type menu with a Method menu, show the app next to the
  method in each row, and warn when a filter uses `type:`.
- Change the Active SSH connections preset to `method:ssh`, so it also
  includes SSH sessions from VS Code and JetBrains apps.
- Generate `codersdk.ConnectionLogMethod` from the database enum with
  `typegen dbenums`.
- Migration 000617 rewrites `connection_logs` under an ACCESS EXCLUSIVE
  lock, which takes about 50 seconds for 10 million rows. `vscode` and
  `jetbrains` rows become `ssh` rows with that app. Rolling back folds
  apps into the old types and loses per-app names.
- Document the new filters and how to replace each `type:` value.

Closes #27412
2026-10-12 15:56:40 +03:00
Danielle Maywood cf6ab8739e fix(coderd/x/gitsync): attach subagent git refs to the root chat (#30647) 2026-10-12 12:55:20 +01:00
Ethan 4afd834ad2 fix(coderd/x/chatd): return database errors from turn preparation (#30492)
Relates to CODAGT-1330

When chatd prepares a turn, it reads a bunch of things from the
database: the user's custom prompt, plan mode instructions, personal
skills, the advisor config, the user's compaction threshold override,
MCP server configs, inline MCP servers, MCP user tokens and the pinned
workspace MCP tools. If any of those reads failed, we'd log a warning
and run the turn without them, so the model would quietly ignore the
user's instructions or be missing tools for that turn.

To fix, we'll just return the error instead, so the prepare retry runs,
and the chat shows an error if the reads keep failing. The provider
switch history sanitizer and the cancel of deleted tool calls also
return their database errors now. A model config that's unavailable,
e.g. deleted, is still treated as foreign by the sanitizer like it is
today.

An inline MCP server with invalid stored headers still shows up as a
failed connect in the chat debug panel, but a failed read of the inline
MCP servers themselves now fails the turn.

This PR is in a stack on top of #30449.
2026-10-12 09:53:45 +11:00
Ethan 807878c1ab fix(coderd): return database errors at chat create and subagent spawn (#30449)
Relates to CODAGT-1330

chatd stores the deployment and organization system prompts in a chat
when it's created. If either read failed, we'd log it and use the
built-in default, or skip the organization prompt, so the chat ignored
the admin's instructions for its whole life. The subagent model override
had the same problem, and the compaction and advisor overrides quietly
fell back to the chat model for that turn.

To fix, we'll just return the database error instead. Overrides that
can't be used, e.g. the model was deleted or the user has no API key for
its provider, still fall back like they do today.
2026-10-12 09:53:44 +11:00
Thomas Kosiewski f923f4a525 chore!: remove the chat automations experiment (#30412)
Removes the experimental Chat Automations feature (`chat-automations` experiment) from main. This reverts the 16-PR stack that added it and adds two forward migrations that undo its schema and credential changes.

> [!WARNING]
> **Upgrade consequences**
>
> - **Automation data is deleted.** Migration 000615 drops the `chat_automations` table and deletes every queued message an automation delivered. Chats, their messages and ordinary queued messages stay. Messages lose their automation provenance (`automation_id`, `input_id`).
> - **Automation-only credentials are revoked.** Migration 000616 deletes API keys, OAuth2 codes and OAuth2 tokens whose only scopes (or allow-list entries) were `chat_automation:*`. Mixed credentials keep their other scopes. Custom roles lose their `chat_automation` permissions. OAuth2 apps that allowed only `chat_automation` scopes get the whitespace allowlist that grants nothing, as in 000595.
> - **A coordinated restart is required.** Stop all old replicas before the new version runs the migrations. Old replicas read the dropped table and columns, so a rolling deploy is not supported.
> - **Remove `--chat-max-automations-per-owner` before upgrading.** The flag, the YAML key `chat.maxAutomationsPerOwner` and the env var `CODER_CHAT_MAX_AUTOMATIONS_PER_OWNER` are removed. A deployment that still sets the flag or the YAML key will not start (`unknown flag` / `unknown option`). The env var is ignored. This includes dogfood. No release or tag contains the flag.
> - **Down migrations restore the schema only.** 000616 down and 000615 down recreate the 000611 schema with empty tables. They do not restore deleted automations, webhook secrets, provenance, queued deliveries, revoked credentials, stripped permissions or the deleted experiment rule.

## What is removed

- The `chat-automations` experiment and its stored runtime rule (`experiment_rule:chat-automations`).
- The `--chat-max-automations-per-owner` deployment option.
- The `chat_automation` RBAC resource, its member and chatd grants, and the `chat_automation:*` API key scopes.
- The automation sqlc queries, dbauthz wrappers and audit table entries.
- The management routes under `/api/experimental/organizations/{organization}/chat-automations`, and the webhook route `POST /api/experimental/chat-automations/{automation}/events` with its rate limiter.
- The chatd management service, webhook publish, schedule loop, the `manage_automations` tool, and the chatstate admission seam and queue promotion guard that only automations used.
- The Agents Automations page, editor, nav entry, chat labels, the Manage automations toggle and the API client.
- The `archived:any` chat search value that the automations "View chats" dialog added.
- `docs/ai-coder/agents/automations.md`, its manifest entry, the glossary entry and the related rows.

Reverted PRs: #30085, #30096, #30112, #30117, #30127, #30134, #30160, #30166, #30167, #30171, #30187, #30207, #30214, #30217, #30219, #30220.

## What stays, and why

- Migrations 000609, 000610 and 000611, their fixture, and the #30297 lock fix with `migration000609_test.go` stay unchanged. Deployments such as dogfood already ran them.
- New migration **000615** reverses the schema changes of 000609 and 000611 against the current schema. It uses the same short-wait lock-and-retry block as 000609, and it rebuilds `chats_expanded` from main's current definition minus the two automation columns. New migration **000616** follows the 000595 Tasks removal for credentials and recreates `api_key_scope` without the five `chat_automation:*` values. It keeps the `chat_project_memory:*` values from 000612.
- The `chat_automation` value stays in the `resource_type` enum, and `codersdk.ResourceTypeChatAutomation` stays. Old audit log rows stay readable, as with `task` after the Tasks removal.
- The 000615 delete fires the existing queue-version trigger, so `queue_version` moves to `snapshot_version` for each affected chat. `snapshot_version` is not bumped: the coordinated restart restarts every stream loop from an empty state, and the site does not read the version fields.
- `coderd/x/chatd/ARCHITECTURE.md` drops the sections the stack added and gets back its pre-stack wording, merged with the later main edits to the same file.

## Notes for reviewers

- #30379 (chat project page) merged while this PR was in progress. It passed `manageAutomationsEnabled` in `AgentCreatePage.test.tsx` and listed an `automations` route in `AgentsPageLayout.test.tsx`. Later main commits also added automation props in `ConversationTimeline.test.tsx` and `ConversationTimelineWorkingBlocks.stories.tsx`. This PR removes all of them.
- Remaining `automation` matches under `coderd`, `codersdk`, `enterprise`, `site/src` and `docs` are intended:
  - migrations 000609 to 000611, their fixture and `migration000609_test.go`;
  - the new 000615 and 000616 migrations and `migration000615_test.go`;
  - the `chat_automation` resource type in `dump.sql`, `models.go`, `codersdk/audit.go`, the generated API docs, `swagger.json` and `typesGenerated.ts`;
  - unrelated uses of the word "automation" (docs, `chatlabels_test.go`, `ratelimit.go`, `autostop.go`, `subagent_catalog.go`, `WorkspaceSharingSection.tsx`, `AgentChatPage.stories.tsx`).

Remote UAT on an isolated dev deployment passed. It upgraded a populated pre-removal database, then checked chats, queued messages, tokens, the 404 routes, the removed page and nav entry, and the experiments list.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

![Agents sidebar after the upgrade, with no Automations entry](https://github.com/user-attachments/assets/bd446035-7fc8-4ca4-a735-3a4f36380738)
2026-10-11 19:57:49 +02:00
Michael Suchacz c28ae0dc9b feat: trigger compaction independently for the chat and compaction models (#28568)
> [!NOTE]
> Xum (an AI agent) authored this pull request on Mike's behalf.

## Summary

Automatic compaction now fires at whichever of two independent triggers
is reached first, and the UI shows when the organization override's
trigger applies.

- **Chat trigger**: the chat owner's personal threshold (or the chat
model's) against the chat model's window. A threshold of 100 turns off
only this trigger.
- **Organization trigger**: a fixed 80% of the organization compaction
override model's window. The override model's own compression threshold
is ignored. The other 20% is a target margin for the summary prompt and
the summary, not a guarantee, because the trigger reads the previous
step's prompt size.

On main, the chat threshold was applied to the smaller of the two
windows, and nothing in the UI showed it. Example: with a 20K chat model
at 70% and an 8K override, main compacts at 5,600 tokens; this PR
compacts at 6,400 (80% of 8K, shown as 32%).

## Changes

Backend (`coderd/x/chatd`):

- The trigger with the lower token point binds (ties go to the chat
trigger). It drives the automatic compaction gate and the
still-over-limit check, and every summary, automatic or manual, records
it.
- An override that is unset, deleted, disabled, on a disabled provider,
or without credentials for the chat owner is treated as absent: the chat
model summarizes and only the chat trigger applies.
- The summary's output cap is bounded by the summarizer's own window,
not the trigger's.
- The still-over-limit error names what can clear it: the compaction
threshold (chat trigger), an override model with a larger window
(organization trigger), or both when usage is over both points.
- The still-over-limit check compacts once more when the latest summary
was recorded under a different threshold or trigger window. The
`chat_summarized` result gains `trigger_context_limit_tokens`, and
`context_limit_tokens` now records the chat model's window (main
recorded the smaller window). On main, lowering a threshold after a
compaction ended every later turn with the error.
- The still-over-limit check skips a 0% threshold ("always compact"),
which on main ended the run with the error after every compaction.
- `ARCHITECTURE.md` documents the two triggers, the summary cap, and the
still-over-limit rules.

Frontend (`site`), through a shared `compactionTriggers` helper that
mirrors the backend rules (no new API):

- Coder Agents settings: the compaction override row warns when the
organization trigger undercuts chat models' default triggers ("the
organization override compacts chats at 80% of {model}'s {N}-token
window").
- Personal thresholds page: an Effective column shows the trigger that
applies, with a popover when the organization override binds. "Compacts
at" follows the same trigger, and a point the chat model's window cannot
reach shows as Off. The header's 100% sentence, and the row warning when
a member types 100, say an organization override may still compact chats
when one is set or its organization's overrides failed to load (the
header also while they load).
- Chat context gauge: the compact-at percent follows the binding trigger
and is annotated when the organization override binds. If the overrides
request fails, it shows the chat-only value with "(organization override
not loaded)", or "Compaction off (organization override not loaded)"
when the chat trigger is off.
- An override whose provider the user cannot use is ignored on the gauge
and the personal page, matching chatd.
- The shared `formatContextLimit` prints counts below 1,000 exactly (an
800-token compaction point showed "~1K") and 999,500 to 999,999 as
"1.0M" instead of "1000K". It also labels the model pickers; their other
values are unchanged.
- `HelpPopoverIconTrigger` lets callers set their own `aria-label`; only
the Effective column's popover does.

Known v1 limits: the Coder Agents settings alert is organization-scoped,
so it can warn about an override that some members fall back from for
lack of provider credentials. The gauge checks the viewer's provider
access, so on a shared chat its label can differ from the owner's
runtime trigger (Codex P2, declined). Compaction itself is unaffected.

## Screenshots

From remote UAT round 13 on 093076f402: an isolated test deployment
with a local fake LLM. The models are test fixtures named for their
context windows: Chat model 20K (own threshold 70%), Chat model 5K
(40%), and Chat model 8K (50%). Chat model 8K is also the organization
compaction override, so the organization trigger is 80% of 8,000 = 6,400
tokens, which is 32% of Chat model 20K's window. Chat model 8K's own 50%
applies only to chats that use it as the chat model; changing it to 10%
or 100% left every value below unchanged.

**Coder Agents settings:** the organization trigger undercuts Chat model
20K's 70% default.

![Override warning: 80% of Chat model 8K's window undercuts Chat model
20K](https://github.com/user-attachments/assets/188c92e6-faaa-4e9a-a563-567cbf98af85)

**Personal thresholds:** the organization trigger binds Chat model 20K
at 32%. Chat model 5K (40%) and Chat model 8K (50%) keep their own chat
triggers.

![Effective column: 32% from the organization trigger on Chat model
20K](https://github.com/user-attachments/assets/9949be4e-9417-4cfc-a683-f1367a1dd77a)
![Organization override popover: 80% of Chat model 8K's window, about
32% of this model's
window](https://github.com/user-attachments/assets/4bc3e002-0be1-46fd-91f0-aa27503034af)

**Personal thresholds, organization settings request failed** (the first
red line is the injected error). Rows fall back to chat-only values.

![Organization settings load failure above usable chat-only
rows](https://github.com/user-attachments/assets/dcf48546-aa47-4f82-a6eb-85f106b4bf45)

**Chat context gauge:** organization trigger binds (Chat model 20K),
chat trigger binds (Chat model 5K), overrides request failed (injected).

![Gauge: Compacts at 32% (organization
override)](https://github.com/user-attachments/assets/cd5402d9-9849-4254-a7a7-6623c4006a2a)
![Gauge: Compacts at
40%](https://github.com/user-attachments/assets/f372c9e9-72ef-41c8-950c-e77d74965f65)
![Gauge: Compacts at 70% (organization override not
loaded)](https://github.com/user-attachments/assets/ab829746-2ab5-4743-848f-dce4e806de3a)

**Still-over-limit errors from real runs:** organization trigger, chat
trigger, both.

![Organization-only over-limit
error](https://github.com/user-attachments/assets/f210807a-a2cc-47a0-807c-f1404373bb28)
![Chat-only over-limit
error](https://github.com/user-attachments/assets/eecec38e-97de-49b6-8670-b32090103bbb)
![Combined over-limit
error](https://github.com/user-attachments/assets/459a6d08-7215-492d-93f5-96a4f767a09e)

## Testing

- Go: `TestBindingCompactionTriggerSource`,
`TestCompactionStillOverLimitError`, `TestCompactionStatusFromHistory`,
and `TestActiveServer_CompactionModelOverride` cover the 80% point
(fails if the override's own threshold is read again), user threshold
100 leaving the organization trigger on, the summary cap for automatic
and manual compaction, the over-limit messages, and recompaction after a
trigger change. The site binding tests mirror the Go table row for row.
- Site: `compactionTriggers` unit tests and Storybook stories for the
three surfaces, including the overrides fetch failure.
- On c6f8750479: `go vet`, the `chatd` and `chatloop` tests, and the
AgentsPage, AISettingsPage, and HelpPopover unit tests (2,274 passed, 2
skipped) pass, and CI is green. On 987cefe399, tsc, the five affected
story files (109 passed), and `make pre-commit` also passed. 093076f402
only changes `ARCHITECTURE.md`; the pre-commit hook's markdown format
and lint checks pass on it.
- On 3cd5b08f1f: tsc, the new `formatContextLimit` test (fails on the
old helper for 800 and 999,999), the `compactionTriggers` and `aiModels`
unit tests (224 passed), and the affected story files pass, and the
pre-commit hook passed on all three commits.

## Review record

- Earlier heads: 12 Codex passes and 3 human reviews, all threads
answered. Rebased onto main several times, most recently ff3fa22f68
(kept #30274, #29892, and #29890 in conflicted files).
- coder-agents-review rounds 1 to 18: findings fixed; approved since
round 15. The round 1 `ARCHITECTURE.md` finding was left for the
maintainer, who asked for the update in the last commit.
- coder-frontend-review (eight roles): all pass. Splitting the PR (RO6)
was declined because it is reviewed as one unit; the
`organizationTriggerFor` story helper stays because its three fields
must match the model.
- Design change after UAT round 11: the organization trigger is a fixed
80% of the override window instead of the override model's own threshold
(171d5f568e, 21d2ce7fce, 987cefe399).
- coder-agents-review round 19: copy, naming, and an unreachable
zero-window gate fixed (6e8efb9054, c6f8750479). A summary-time fit
check with chat-model fallback (CRF-156) was declined: the previous-step
lag applies to every trigger, including main's. Round 20 approved
c6f8750479 with no findings.
- Codex: one P2 on 987cefe399 declined (see known limits); no major
issues on c6f8750479.
- Remote UAT on dogfood (fake LLM, isolated test deployment): rounds 8
to 13 PASS. Round 12 on c6f8750479 first failed one criterion that was
written wrong (the admin alert correctly lists an override model that is
also a chat model at 100%); the reviewer endorsed PASS on the same
evidence after the correction. Round 13 on 093076f402 retook the
screenshots with neutral model names and confirmed that the override
model's own threshold (10%, 50%, 100%) does not move the organization
trigger.
- UAT round 13 notes: the header after a failed or pending overrides
load and the sub-1K labels are fixed in 4e172ab5ff and d5888ce15dc;
coder-agents-review round 21 found the same gap in the row warning
(CRF-167, fixed in 3cd5b08f1f). Left as on main: a summary's
`usage_percent` is measured against the binding trigger's window, so it
can exceed 100 (nothing reads it), and the 100% help text shows only for
an unsaved edit.
- Codex: no major issues on d5888ce15d and 3cd5b08f1f.
coder-agents-review round 22 approved 3cd5b08f1f with no open findings.
- coder-frontend-review on those commits: all roles pass after the
fixes. Declined: moving the formatter fix to its own PR (RO5; the owner
asked for it here), adding "tokens" to sub-1K labels (TN6; "~800" reads
like "~16K"), and splitting the component (RP6).
2026-10-11 12:10:59 +02:00
McKayla はな 86ebc72236 chore: remove unused autofill-parameters endpoint and experiment (#30525) 2026-10-09 16:42:41 -06:00
Michael Suchacz d28a433b23 perf(coderd/database): index chat message pagination (#30562)
Loading a chat's history can take seconds when its messages sit far from
the end of the `chat_messages` id space, because no index returns one
chat's live messages in id order. Given the `LIMIT`, the planner chooses
`chat_messages_pkey` instead and filters by `chat_id`, so it reads other
chats' messages until it reaches this chat's.

On dogfood, the newest page of one idle long chat took 21 s: `Index Scan
Backward using chat_messages_pkey` discarded 614,776 rows and did 78,749
disk reads. Over the past 7 days, 108 of 33,745
`GetChatMessagesByChatIDDescPaginated` calls took more than 1 s, and 10
took more than 10 s. `GetChatMessagesByChatIDAscPaginated` has the same
problem in the other direction: for `after_id = 0, limit 1`, which chatd
uses as a first-turn probe, dogfood's plan scans forward from the
table's oldest row. 305 of its 2,863 calls took more than 1 s.

This PR adds migration 000614: `CREATE INDEX idx_chat_messages_chat_id
ON chat_messages (chat_id, id DESC) WHERE deleted = false`. No queries
change.

**Migration cost:** a plain `CREATE INDEX` inside the migration
transaction, which blocks writes to `chat_messages` until the upgrade
commits. Reads continue. On a 1.01M-row fixture (641 MB heap) the build
took 0.8 s and the index is 39 MB. Dogfood has 5.85M rows and a 5.7 GB
heap, so expect seconds and about 230 MB.

## Testing

Remote UAT through Coder Agents (round 1 PASS) on the 1.01M-row fixture,
Postgres 13.21. It ran base f8e053e385, upgraded in place to
9abea171ba, then ran the same queries. Result rows and HTTP bodies were
identical.

| Query (fixture chat) | Without the index | With the index |
|---|---|---|
| Newest page, idle chat | pkey backward, 83,471 buffers, 391 ms | new
index, 13 buffers, 0.13 ms |
| Ascending `after_id=0, limit 1`, recent chat | pkey forward, 80,843
buffers, 404 ms | new index, 5 buffers, 0.06 ms |
| `POST /messages` to a default-title chat | about 0.5 s | about 0.02 s
|

- A sweep of every `chat_id`-filtered `chat_messages` query in
`chats.sql`, plus the two soft-delete updates and the chat-list
`has_unread` checks, found no query more than 2x worse.
- The UI loaded older pages of long chats, an empty chat, and a chat
with soft-deleted messages correctly.
- Running the down migration restores the base plan.
- Local tests: `TestMigrate`, `TestMigrateUpWithFixtures`,
`TestMigrationChain`, `TestCheckLatestVersion`,
`TestGetChatMessagesByChatIDOrdersByID`,
`TestGetChatMessages_Pagination`, `TestGetChatMessages_TurnStartID`, and
the pre-commit hook (`make gen` changed only `dump.sql`).
- No plan test is added. The repo has none, and the bad plan needs
production-shaped data: on a 200k-row local fixture with rows under
about 250 B, the planner kept the pkey even with the index. Dogfood rows
average about 1 KB, which by the same cost model favors the index for
any chat under about 3% of the table (the largest is under 0.4%).

## Review record

- Polish pass: no changes.
- Codex review: no findings on 9abea171ba.
- Test audit: no tests in scope. The audited tree is the head commit
9abea171ba.
- Remote UAT round 1: PASS on 9abea171ba. Two informational notes: the
write lock described above, and no `IF NOT EXISTS` (only 11 of 111 index
migrations use it).

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-09 19:23:25 +02:00
Michael Suchacz 782fb30f09 feat: load whole chat turns per history page (#30524)
Closes
[CODAGT-1372](https://linear.app/codercom/issue/CODAGT-1372/load-whole-turns-per-history-page-so-collapsed-working-blocks-fill-in).

With collapsed working blocks, a long agent turn renders as one row, but
chat history pages hold a fixed 50 messages. A 400-step turn took about
eight sequential page loads to fill, and its block showed partial labels
("at least", "or more") until then.

`GET /api/v2/chats/{chat}/messages` now returns `turn_start_id` on
newest-first pages: the ID of the user prompt that starts the turn
containing the page's oldest message. It comes from a new
`GetChatTurnStartID` query on the existing
`idx_chat_messages_user_prompts` index, so there is no migration.
Soft-deleted prompts and model-only user rows never start a turn.

The history query then fills each page back to that prompt with requests
of up to 200 messages, capped at 1000 messages per page, so a page holds
whole turns and a complete block renders without partial labels. Past
the cap, or when a fill request fails or returns no messages, the page
keeps what loaded and ends mid-turn, and the next page continues it, as
before. Because the fill runs inside the page fetch, a chat now waits
for its fill requests (one to five) before it first renders: with an
injected 500 ms per-request delay, a chat whose newest turn hits the cap
appeared after 3.8 s instead of 1.1 s.

## Screenshots

From the dogfood UAT, the same 200-step turn with its next history
request held, so each label shows only what loaded before it:

**Before**, on the base commit `29e876df4da`, after two 50-message
pages: "Worked for at least 1m 35s (48 steps or more)", partly hidden by
the loading indicator.

![Before: partial label for a 200-step
turn](https://github.com/user-attachments/assets/6b4e4abf-4bc6-4476-8ed5-52c39c8eebe6)

**After**, on `186a9b053a0`, after the first page: "Worked for 6m 39s
(200 steps)".

![After: exact label for the same 200-step
turn](https://github.com/user-attachments/assets/22ec1900-1e7b-4c98-87a9-0463e1d852c2)

A 750-step turn, larger than one 1000-message page, is exact once the
next page loads:

![After: 750-step turn,
expanded](https://github.com/user-attachments/assets/f927a848-62b1-4eb7-a548-fbdff61fc9c4)

A malformed fill response keeps the loaded messages and the partial
label instead of showing "Failed to load chat", as `ecdfb27ec29` did:

![Malformed fill response: partial label, no error
screen](https://github.com/user-attachments/assets/6d393df9-15a2-471e-9bb6-71e919aca570)

## Testing

- Go: `TestGetChatMessages_TurnStartID` (pages spanning turns, deleted
prompts, model-only user rows, polling and empty pages) and the dbauthz
suite entry.
- Vitest: fill request parameters, the 1000-message cap, failed,
malformed, and empty fill responses, and servers without
`turn_start_id`.
- Dogfood UAT through Coder Agents: PASS on `186a9b053a0` (round 4):
whole-turn first pages, the 1000-message cap and its continuation, fill
failures, a live turn, and API checks for deleted prompts and model-only
user rows. Round 3 on `ecdfb27ec29` failed: a malformed fill failed the
chat load, and an empty fill reporting `has_more` repeated forever.
`7266f468290` fixes both, with regression tests.

## Review record

- Test audit with mutation checks: the Go test now covers a page
spanning two turns, which catches a lookup keyed on the newest message.
A re-audit of later test changes changed nothing; the one surviving
mutation drops the `!older.has_more` stop, costing one extra request
when the prompt is deleted mid-fill.
- Frontend review (8 roles, 5 rounds): fixed doc, naming, loose-type,
and test findings. Its suggestions to catch only the fill request (DF5)
and drop the empty-page stop (SD4) caused the round 3 failures and are
reverted. Kept: SD1 (`MAX_MESSAGES_PER_REQUEST` is used once, named like
`MESSAGES_PAGE_SIZE`) and round 5 SD4 (`!older.has_more` is the
endpoint's pagination signal). Declined DF6: the `queued_messages`
refetch race predates this change, and a failed refetch fill only
unloads older history that reloads on scroll.
- Codex: no findings on `186a9b053a0`.
- Known: a hung fill delays the first render (no client timeout); fills
are not cancelled when the user leaves; the top of history costs one
extra empty request; injected malformed fill rows break the page as they
do on the first page.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-09 13:32:39 +02:00
Mathias Fredriksson befda9b29f docs: mention chat reasoning_tokens is part of output_tokens (#30500)
Document that reasoning_tokens is included in the output_tokens on
ChatMssageUsage, which is the case for all known providers.

Refs CODAGT-1350
2026-10-09 12:12:08 +03:00
Atif Ali 261269aff0 fix: support path-aware OAuth discovery for remote MCP (#30168)
## Problem

VS Code discovers Coder's MCP endpoint as a path-qualified OAuth
resource, but Coder advertises the deployment root. Accepting MCP-bound
tokens also requires keeping REST-backed tool calls within the
originating MCP resource rather than granting those tokens direct REST
access.

## Fix

- Publish path-aware protected resource metadata and authentication
challenges; keep authorization-server metadata at the root.
- Validate the MCP audience and delegate tool requests through a private
in-process transport that retains identity, scopes, expiry, revocation,
and streaming cancellation.
- Reject unsupported MCP path aliases and validate SDK path components
before URL resolution.
- Add discovery, audience, authorization, endpoint-isolation, and
transport lifecycle regression tests and update integration docs.

## Validation

VS Code completed OAuth and returned the authenticated user through a
real MCP tool. Official MCP SDK 1.30.1 completed discovery,
registration, PKCE, tool execution, and refresh. These live checks
preceded the final path hardening; the hardened paths are covered by
automated regressions.

Affected Go package tests, targeted race tests, backend/SDK lint, and
targeted documentation checks passed during self-review. The exact PR
commit was also checked separately from the uncommitted consent-scope
fix.

> [!NOTE]
> Tokens are MCP-audience-bound, but grants still include `coder:all`;
least-privilege scopes remain a separate requirement
([PLAT-639](https://linear.app/codercom/issue/PLAT-639)).

Preview:
https://coder.com/docs/@fix/mcp-internal-delegation/ai-coder/mcp-server

<details>
<summary>Implementation decisions and review context</summary>

1. Treat `/api/experimental/mcp/http` and its single-trailing-slash form
as supported MCP resource paths; reject other aliases and descendants
before route authentication.
2. Preserve deployment-root discovery and authorization-server metadata
for existing REST integrations.
3. Bind private delegation to the authenticated API-key ID. Revalidate
the original credential and enforce normal RBAC/scopes on every internal
REST request; do not derive delegation from HTTP headers.
4. Keep internal HTTP, streaming, and WebSocket traffic in-process with
no network fallback. Close the transport on outer-request cancellation
and normal completion.
5. Validate workspace owner/name path segments before SDK URL
construction, including tool-level tests that malformed arguments cannot
select a different REST endpoint.
6. Keep compatibility and delegated tool execution together:
discovery-only changes would allow authentication while leaving real MCP
tool calls broken.

</details>

> 🤖 This PR was created with the help of Coder Agents, and needs a human
review. 🧑💻
2026-10-09 00:58:39 +05:00
cdrciandgithub-actions[bot] 315789c6bf chore: refresh AI model price book (#30498)
## Price book changes

27 models added, 8 models removed, 46 models changed.

<details>
<summary>Added</summary>

- anthropic/claude-haiku-5-5
- azure/claude-haiku-5-5
- azure/grok-4.6
- bedrock/anthropic.claude-haiku-5-5
- bedrock/au.anthropic.claude-haiku-5-5
- bedrock/eu.anthropic.claude-haiku-5-5
- bedrock/eu.anthropic.claude-sonnet-5-5
- bedrock/global.anthropic.claude-haiku-5-5
- bedrock/global.zai.glm-5.3
- bedrock/in.anthropic.claude-haiku-4-5-20251001-v1:0
- bedrock/in.anthropic.claude-opus-5
- bedrock/in.anthropic.claude-sonnet-5
- bedrock/jp.anthropic.claude-haiku-5-5
- bedrock/us.anthropic.claude-haiku-5-5
- bedrock/us.anthropic.claude-sonnet-5-5
- bedrock/us.zai.glm-5.3
- copilot/claude-haiku-5.5
- openrouter/anthropic/claude-haiku-5.5
- openrouter/apodex/apodex-1.1-mini:free
- openrouter/google/gemini-nano-banana-2.1
- openrouter/inclusionai/ling-3.1-flash
- openrouter/mistralai/mistral-large-4-0
- openrouter/unbiased/pareto-26.10-preview
- vercel/anthropic/claude-haiku-5.5
- vercel/google/gemini-nano-banana-2.1
- vercel/mistral/mistral-large-4
- vercel/stealth/glyph-cluster
</details>

<details>
<summary>Removed</summary>

- openrouter/kwaipilot/kat-coder-pro-v2.5
- openrouter/qwen/qwen3.8-27b:free
- openrouter/stealth/space-bunny-alpha
- vercel/deepseek/deepseek-v3.1-terminus
- vercel/inclusionai/ling-3.0-flash-sante-free
- vercel/liquid/d1
- vercel/moonshotai/kimi-k2-thinking
- vercel/stepfun/step-3.5-flash
</details>

<details>
<summary>Changed</summary>

- anthropic/claude-sonnet-5-5
- bedrock/anthropic.claude-sonnet-5-5
- bedrock/global.anthropic.claude-sonnet-5-5
- copilot/claude-sonnet-5.5
- openrouter/anthropic/claude-sonnet-5.5
- openrouter/deepseek/deepseek-v3.1-terminus
- openrouter/deepseek/deepseek-v3.2
- openrouter/deepseek/deepseek-v4-flash
- openrouter/deepseek/deepseek-v4-flash-0731
- openrouter/deepseek/deepseek-v4-pro
- openrouter/deepseek/deepseek-v4.1-flash
- openrouter/inclusionai/ling-3.0-flash-fin
- openrouter/meta-llama/llama-4-maverick
- openrouter/meta/muse-glimmer-30b
- openrouter/moonshotai/kimi-k2.6
- openrouter/moonshotai/kimi-k3
- openrouter/nvidia/nemotron-3-nano-30b-a3b
- openrouter/nvidia/nemotron-3-super-120b-a12b
- openrouter/nvidia/nemotron-3-ultra-550b-a55b
- openrouter/nvidia/nemotron-3.5-lightning
- openrouter/openai/gpt-5.6-sol-pro
- openrouter/qwen/qwen3-235b-a22b-2507
- openrouter/qwen/qwen3-next-80b-a3b-instruct
- openrouter/qwen/qwen3.5-27b
- openrouter/qwen/qwen3.5-35b-a3b
- openrouter/qwen/qwen3.5-397b-a17b
- openrouter/qwen/qwen3.6-27b
- openrouter/qwen/qwen3.8-27b
- openrouter/z-ai/glm-5.1
- openrouter/z-ai/glm-5.2
- openrouter/z-ai/glm-5.3
- openrouter/~anthropic/claude-haiku-latest
- openrouter/~anthropic/claude-sonnet-latest
- openrouter/~deepseek/deepseek-flash-latest
- openrouter/~deepseek/deepseek-pro-latest
- openrouter/~deepseek/deepseek-v4-flash-latest
- openrouter/~moonshotai/kimi-latest
- openrouter/~z-ai/glm-flash-latest
- openrouter/~z-ai/glm-latest
- vercel/anthropic/claude-sonnet-5.5
- vercel/arcee-ai/trinity-large-thinking
- vercel/google/gemini-3.8-flash-lite-tts
- vercel/google/gemini-3.8-flash-tts
- vercel/inclusionai/ling-3.0-flash-sante
- vercel/poolside/laguna-s-2.1
- vercel/xiaomi/mimo-v2.6-flash
</details>

## Review notes

Regenerated by `make gen/aibridge-prices` from the live
[models.dev](https://models.dev) catalog. Both artifacts come from one
snapshot, so they ship together:

- `coderd/aibridge/prices/data/prices.json`
- `site/src/modules/aiModels/knownModels/knownModelsGenerated.json`

These are customer-visible cost numbers taken from upstream data, so
this PR is never merged automatically. The summary above lists what
moved; check the diff for exact figures before approving.

Opened automatically by the [aigateway-prices-refresh
workflow](https://github.com/coder/coder/actions/runs/37754651010).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-08 15:20:44 +01:00
Mathias Fredriksson 788a8bc1ba test: skip agent tool call tests until netstack TIME_WAIT fix (#30465)
Skip `TestAgent_ToolCall` and `TestEditMessageCancelsToolCallsOnAgent`
until the agent netstack fix in PLAT-717 lands.

The agent's gVisor netstack accepts connections through `tcp.Forwarder`
and has no listening endpoint, so it drops a SYN that reuses a source
port it still holds in TIME_WAIT (google/gvisor#15013). Agent API
requests open a new connection each and the agent closes first, so a
client that reuses a port within 60s hangs for about 63s. Both tests
make enough requests to one agent to hit it (CODAGT-1336, CODAGT-1331).
The same stall was reproduced against a production agent on dogfood, so
this is a product bug, tracked in PLAT-717.
2026-10-08 10:42:17 +03:00
Sas Swart ef590afda7 fix: coalesce AI session metadata in list query (#30351)
Update `ListAIBridgeSessions` to return `{}` when selected
session/interception metadata is SQL NULL, regenerate database code, and
add a regression test that inserts an interception with NULL metadata
and verifies the sessions list succeeds with empty metadata. No schema
migration is included; existing insert-path normalization remains
unchanged.

Fixes AIGOV-694
2026-10-08 08:27:43 +02:00
dylanhuff-at-coderandNick Vigilante b4d51012da feat: allow template version selection for workspace builds (#30199)
Add named template version selection to `coder start` and `coder stop`,
and independent stop/start selection to `coder update` with
`--stop-template-version` and `--template-version`. Stop still defaults
to the workspace's current version, and update starts with the active
version by default. Explicit selections use the selected version for
parameter preparation and builds, keep server permissions authoritative,
and never silently fall back to the active version.

Classic stop builds retain previous parameter values omitted by the
selected stop version, so a later start can reuse them across separate
commands or an interrupted update. Declared parameters still follow
normal validation, and start builds still discard removed parameters.
Update now returns an error and retry guidance when a start apply fails
after stopping, including unflagged updates that previously returned
success.

Closes #29100


[PLAT-616](https://linear.app/codercom/issue/PLAT-616/cli-allow-specifying-a-template-version-for-stopstart-builds-and-both)

---------

Co-authored-by: Nick Vigilante <nickvigilante@users.noreply.github.com>
2026-10-07 13:17:12 -07:00
Jakub Domeracki b4fb7901e6 docs: clarify that disable-password-auth also applies to owners (#30362)
The `--disable-password-auth` help text says owners can still sign in
with a password "to avoid potential lock out", and a comment in
`coderd/userauth.go` says the same. Neither is true. #6193 removed the
owner bypass on purpose, so the flag blocks password login for everyone.
The recovery advice was also wrong. It named a command that doesn't
exist (`create-admin`), and any admin that `create-admin-user` creates
while the flag is on still can't sign in.

This PR changes docs and comments only:
- Rewrites the flag description to say it applies to owners too and how
to recover access. Regenerated the CLI and config reference docs and the
golden files.
- Fixes the misleading comment in `coderd/userauth.go`.
- Adds the same warning and recovery steps to
`docs/admin/users/password-auth.md`.

> [!NOTE]
> Generated by Coder Agents on behalf of @jdomeracki-coder.
2026-10-07 19:03:46 +02:00
Paweł Banaszewski f08bfddfa7 feat: validate AI Gateway actor and credentials in proxy mode (#30225)
Extends the AI Gateway `forwardingHandler` with actor check, provider
credential resolution, and initial interception metadata.
Missing provider credentials return 403
Credential resolution failures return 500 with details kept in logs.
AWS-signed bridged credentials remain unsupported.

Generated by Coder Agents on behalf of @pawbana.
2026-10-07 16:51:19 +00:00
Michael Suchacz 28fe61ffa1 feat(coderd): evaluate read_template parameters for the chat owner (#29561)
The chat `read_template` tool returned the parameter defaults recorded
at template import. Defaults that depend on the workspace owner, such as
a region chosen from the owner's groups, were therefore wrong for many
users, and an agent that omitted the parameter in `create_workspace`
built a workspace with a different value than the tool had shown.

`read_template` now evaluates the active version's parameters for the
chat owner with no inputs, through `dynamicparameters.Prepare` and the
same `Render` call `wsbuilder` makes for a first build, via a
`RenderTemplateParametersFn` hook wired from coderd like the other chat
hooks. The result carries a `parameters_note` naming the source:
owner-evaluated build values, import rows for templates on the classic
parameter flow (which `wsbuilder` also builds from), a retry note while
the version is still importing, or import rows with a warning when
evaluation fails. A parameter whose evaluated default fails its own
validation is reported as `error` instead of a usable `default`. Only an
evaluated default is checked: without one, preview and the static
renderer validate an empty value, so required parameters and unevaluated
defaults are never reported as broken, and their remaining errors
surface at build time. Fields preview cannot evaluate before the build
are shown as evaluated rather than filled from the import row, because
preview's output does not distinguish a default, option set, or
validation bound that is unknown (it depends on
`data.coder_provisioner`) from one that is null for this owner: an
unevaluated default carries a `default_note` saying so, an option with
an unknown value is omitted, with preview's invalid-options error
surfaced as `error` when the parameter has an evaluated default, and
bounds appear exactly as rendered. The one import-row fill is for
parameters a `module_not_loaded` render dropped, keyed on the same
diagnostic code `wsbuilder` uses. Import rows carry no module
provenance, so that fill also restores root-level rows the render
omitted for this owner; every restored row carries a `note` marking it
an import-time estimate. The build-values note is scoped to a request
that omits every parameter and matches no preset, since a default that
depends on another parameter is re-evaluated against the values
`create_workspace` passes, a preset can match without a `preset_id`, and
a claimed prebuilt workspace can keep its own values. When evaluation
falls back, the warning logs only diagnostic codes, because diagnostic
text can quote owner attributes.

Measured on dogfood, evaluation adds about 4 ms for a two-parameter
template and about 70 ms for the 6-parameter, 19-module `coder` template
per `read_template` call, matching the workspace create form's evaluate
endpoint, with no file cache growth across repeated calls.

> Xum acted for Mike on this pull request.
2026-10-07 15:55:04 +02:00
Michael Suchacz 9cfe27063d test(coderd): reduce chat test fixture and timer waits (#29523)
Reduce repeated database/server fixtures and fixed timer waits in chat
tests without changing production code or assertions.

`TestTransitionMatrix_AllCombinations` shares one database and seeded
dependencies across 272 leaves, with a unique chat and recording
publisher per leaf. `TestPostChatFile` shares one authenticated client
across 20 upload leaves. `TestResolveFallbackModelConfigID` shares one
database with independently seeded model configurations. The
requires-action test closes its server after the expected state
persists; the stopped-workspace test advances a trapped Quartz
validation timer; two tool-filtering tests no longer start unused
workspace agents.

| Test | Before | After |
|---|---|---|
| `chatstate/TestTransitionMatrix_AllCombinations` (272 leaves) | 94.06s
| 16.86s |
| `coderd/TestPostChatFile` (20 leaves) | 31.23s | 0.95s |
| `chatd/TestRequiresActionChatPersistsWaitingStatusLabel` | 11.21s |
1.25s |
| `chatd/TestSubagentChatExcludesWorkspaceProvisioningTools` | 7.90s |
2.84s |
| `chatd/TestPlanModeSubagentChatExcludesAskUserQuestion` | 8.25s |
3.00s |
| `chatd/TestStoppedWorkspaceWithPersistedAgentBindingDoesNotBlockChat`
| 6.91s | 1.91s |
| `chatd/TestResolveFallbackModelConfigID` (12 leaves) | 7.58s | 0.34s |

Independent review measurements on September 18, 2026: median summed
leaf time from two runs per side in A/B/B/A order, comparing base
`0ea777a341` with head `e378f9e936`. Every run used
`-tags=testsmallbatch -count=1 -v -p 1 -parallel 8`, PostgreSQL 17, and
the same test selection per package. Other agents shared the host, so
absolute times varied. The speedups reproduced, but not the original
ratios: the matrix improved 5.58x and uploads 32.87x. Summed leaf time
excludes shared parent fixture setup and cleanup; it is not package wall
time. Median package elapsed was 19.26s to 9.76s for the full
`chatstate` package, 4.47s to 1.28s for `coderd` with only
`TestPostChatFile`, and 11.43s to 3.36s for the five targeted `chatd`
tests. Compilation time is excluded.

<details>
<summary>Investigation notes</summary>

- Transition matrix: the shared user, organization, membership,
provider, and model are not mutated by any leaf. Each leaf seeds its own
chat, messages, and queue, and copies the fixture with a new recording
publisher (`transitions_matrix_test.go:617,728,762`).
`ChatMachine.Update` still opens real transactions on the shared
database store; no outer transaction masks rollback assertions
(`chatstate/machine.go:161`, `transitions_helpers_test.go:891`). Parent
cleanup runs after every leaf.
- Requires-action: the old idle hook reached its 10s timeout because the
runner remains active while awaiting dynamic-tool results
(`testhooks.go:14`, `worker.go:114`). The test now waits for the
persisted status and label, closes and drains the server, and retains
the no-web-push assertion (`chatd_test.go:3072`).
- Stale binding: the test injects a new Quartz mock, traps the 5s
validation timer, waits for `MustRelease` to finish timer registration
and for the dial to start, then advances the clock
(`chatd_test.go:4907`). The one-dial, model-call, and
unavailable-workspace tool-result assertions remain intact.
- Tool filtering: both tests create templates but no workspace, and
their mocked model only invokes `spawn_agent` before returning text. The
removed workspace agents could not authenticate and were never used by
the tested flow; their shutdown added about 5s
(`chatd_test.go:229,384`).
- Fallback models: configurations and providers are independently
inserted. Most leaves use unique organizations; the two
default-organization cases insert distinct models, and only one inserts
a default. No leaf mutates another leaf's model or provider
(`chatd_internal_test.go:3512`).

Validation: all changed tests passed with `-count=5` and `-race
-count=2`; the full `chatstate` package passed both checks (2,350 and
940 test executions, including parents). All 272 matrix, 20 upload, and
12 fallback leaves remain present. `gofmt -l`, `git diff --check`, and
`golangci-lint run` on the three changed packages were clean. The full
`coderd` package was not run. No correctness changes were needed during
review. `ARCHITECTURE.md` was read; this test-only PR does not affect
the documented architecture, so no TODO or architecture edit is needed.

</details>

_Investigated, implemented, and validated by Coder Agents on behalf of
@ibetitsmike._
2026-10-07 13:42:42 +02:00
Ehab Younes fc01bee831 fix: keep new API keys within the caller's scopes and allow list (#30256)
A scoped key holding api_key:create could mint a broader key, e.g. a token scoped to [api_key:create user:read] could mint coder:all.

- New keys must be covered by the caller's scopes and allow list, and inherit them when none are requested
- Requests beyond the caller return 403
- New key expiry is capped at the caller's
- Covers POST /users/{user}/keys/tokens, POST /users/{user}/keys, app auth-redirect, and /oauth2/authorize (invalid_scope on excess grants or a restricted session allow list)
- coder:all sessions, including browser logins, behave as before
- Not addressed: workspace start still mints a coder:all key for the owner
2026-10-07 11:43:19 +03:00
Michael Suchacz 67a781ce1d feat: remove model_intent from the execute, process_output, and advisor tools (#30397)
The built-in `execute`, `process_output`, and `advisor` tools offered an
optional `model_intent` argument whose text replaced the tool row's
default label. Models fill it inconsistently, so most chats mixed intent
labels with plain command labels. On dogfood over three days, Claude
Opus 5.5 sent it on 17% of `execute` calls through Bedrock and 30%
through the Anthropic API. The rate follows reasoning effort: the few
chats at max or xhigh effort sent it on every call, while chats at high,
medium, or default effort sent it on about a third of calls or fewer.

This removes the argument from the three tool schemas and always renders
the command-based labels ("Ran ...", "Started ... in the background",
"Checked ...", "Consulted the advisor"). It also deletes the frontend
helpers, tests, and stories that existed only for the intent labels.

- Old chats keep working. chatd's decoders and its tool input check
ignore undeclared keys, so calls that still carry `model_intent` run as
before and their rows show the default label.
- The per-server `model_intent` option for MCP tools is unchanged. That
field is required in the schema, and Opus 5.5 fills it on 96 to 99.7% of
those calls.
- The MCP workspace `execute` and `process_output` tools now match the
Coder Agents contract apart from their `workspace` argument, so the
golden test drops its Agents-only exception.
- Pixel diffs from this change: the deleted intent stories and the new
default labels in `AllToolIconsTranscript`,
`PolicyBadgeCoversEveryRenderer`, and
`StreamedAdvisorResultOverlaysDurableCall`.

## Testing

- `go test` for `chattool`, `chatadvisor`, `toolschema`, `coderd/mcp`,
and `coderd/x/chatd -run Advisor`; `go vet ./coderd/x/chatd/...
./coderd/mcp/...`.
- Red check: re-adding the field to `ExecuteArgs` fails
`TestWorkspaceToolsGolden`.
- Vitest unit tests for the tool utils, the generic tool row, and chat
parsing (5 files) and the affected Storybook files (4 files); `biome`,
`oxlint`, and `tsc`.
- `make pre-commit` passed on the rebased head. The pre-push hook was
skipped per the maintainer's standing preference; CI covers it.

## Review record

- `frontend-review` (FE1 to FE10): all PASS or N/A.
- `coder-frontend-review` (8 parallel reviewer roles): no findings. Two
optional side notes were declined: a nested ternary for `ExecuteTool`'s
label (SD3 discourages them) and a story for an old call that still
carries `model_intent` (TS7: it shows the same state as the existing
default-label stories).

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-06 22:50:28 +02:00