Commit Graph
4086 Commits
Author SHA1 Message Date
Ethan eeb2624549 fix: pin workspace agent API client to intended agent (#26600)
## Summary

The control-plane HTTP client used to talk to workspace agents followed
HTTP redirects and trusted the redirected host, letting a malicious
workspace agent bounce a coderd request onto a different agent on the
shared tailnet. Because the agent HTTP API on port 4 is unauthenticated
(it relies on tailnet reachability plus control-plane authorization),
this allowed cross-tenant file read/write and remote code execution.
This PR refuses redirects and pins every dial to the intended agent.

Closes CODAGT-668.

## Problem

`agentConn.apiClient` in `codersdk/workspacesdk/agentconn.go`
constructed an `http.Client` with no `CheckRedirect`, so Go's default
policy followed up to 10 redirects. Its custom `Transport.DialContext`
parsed the host from the (post-redirect) request URL and dialed that IP
over the shared tailnet, validating only that the port was
`AgentHTTPAPIServerPort` (4). It never pinned the connection to the
intended `AgentID` / `agentAddress()`.

A workspace owner (any regular org member, not just admins) controls
their own agent and can make its port-4 handler return a `3xx`
`Location` pointing at a victim agent's tailnet IP. When a control-plane
action (for example a chat tool or the HTTP MCP server) sends an agent
API request to the attacker's agent, coderd acts as a confused deputy
and replays the request against the victim:

- `301/302/303` rewrite POST to GET, but `307/308` preserve method and
body when the body is replayable. The real callers pass replayable
bodies, so a redirected `POST /api/v0/write-file` writes
attacker-controlled content into the victim workspace and a redirected
`POST /api/v0/processes/start` executes it, giving RCE on the victim
agent.

The dangerous callers run server-side on coderd's single deployment-wide
`ServerTailnet`, which is authorized to tunnel to any agent, so the
blast radius is cross-tenant / cross-organization (limited in practice
to victim agents coderd currently has a live tunnel to).

## Fix

In `agentConn.apiClient`:

- Set `CheckRedirect: http.ErrUseLastResponse` so the client never
follows a redirect. A `3xx` is surfaced to the caller as the response
(which the existing `ReadBodyAsError` path turns into an error) instead
of being replayed against another host.
- Capture the intended agent address once from `AgentID` (`agentAddr :=
netip.AddrPortFrom(c.agentAddress(), AgentHTTPAPIServerPort)`), reject
any dial whose host or port does not match it, and always dial that
pinned address rather than the URL-derived host.

In `coderd/aitasks.go`, the task app proxy client (`taskAppHTTPClient`)
also now sets `CheckRedirect: http.ErrUseLastResponse`. This client
dials through `agentConn.DialContext`, which already pins the host to
the originating workspace's agent (it takes only the port from the dial
address), so it was never cross-agent. The change is hardening for
parity so a malicious app cannot bounce the request to a different port
on the same agent.

## Hardening and defense in depth

The two layers are independent. `CheckRedirect` removes the
redirect-following behavior entirely, and the dial pinning guarantees
that even a request constructed with a foreign host can only ever reach
the intended agent. Removing either one in the future cannot, on its
own, reintroduce the cross-agent vector.

## Tests

- `codersdk/workspacesdk/agentconn_redirect_test.go` builds a three-peer
tailnet (client, attacker, victim). The attacker agent redirects to the
victim's port-4 URL, and the test asserts that `GET` `302`, `POST`
`307`, and `POST` `308` all return an error and that the victim is never
contacted.
- `coderd/aitasks_internal_test.go` adds
`TestTaskAppHTTPClient_RejectsRedirect`, which verifies the task app
client surfaces a `307` instead of following it to a stand-in victim.

## Why this closes the whole vulnerability class

`apiClient` is the only HTTP chokepoint to the agent port-4 API, so
fixing it covers every server-side caller:

- Every agent HTTP API method in `agentConn` funnels through
`apiClient`, either via `apiRequest`, a direct `apiClient(ctx).Do(...)`
(`ExecuteDesktopAction`), or as the websocket `HTTPClient`
(`WatchContainers`, `WatchGit`, `ConnectDesktopVNC`). The websocket
handshake matters here: `coder/websocket` follows `3xx` during the
handshake by default and only requires `101` on the final hop, but it
honors the underlying client's `CheckRedirect`, so reusing `apiClient`
closes the websocket paths too.
- The HTTP MCP server coderd hosts at `/api/experimental/mcp/http`
registers tools (`coder_workspace_bash`, `_write_file`, `_read_file`,
`_edit_files`, etc.) that reach the agent through
`workspacesdk.AgentConn` methods, so they go through `apiClient` and are
covered. The same is true for agent-hosted MCP, which coderd reaches
only via `agentConn.CallMCPTool` / `ListMCPTools`. coderd never opens an
MCP client connection directly to an agent over the tailnet.
- Raw-TCP agent services (reconnecting PTY, SSH, speedtest, generic
`DialContext`) speak non-HTTP protocols and have no redirect surface.
The workspace apps reverse proxy targets user app ports, not port 4,
forwards `3xx` to the browser rather than following them, and pins its
transport to the request's agent.
- `provisionerd` does not talk to the agent HTTP API at all.

No other server-side client follows redirects to an agent-controllable
tailnet host, so no further redirect changes are required for this
class.
2026-06-23 23:06:44 +10:00
Leon ZhangandSas Swart e044a4265d fix(coderd): only send prebuild claim reinit for the claim build (#26548)
## Problem

#23108 made prebuild claim delivery durable: when an agent connects to
`/api/v2/workspaceagents/me/reinit?wait=true`, the handler checks
whether the workspace's first build was created by the prebuilds system
user and whether its latest build succeeded, and if so pre-seeds a
`prebuild_claimed` reinitialization event in case the original pubsub
event was missed.

The check does not verify that the latest build is the claim build, so
it keeps firing for the rest of the workspace's life. Any workspace that
was claimed from a prebuild receives a spurious "prebuild claimed"
reinit every time its agent (re)opens the `/reinit` connection: after
every agent restart, every coderd deploy or replica restart, and every
dropped SSE connection. Each one shuts the agent down and reinitializes
it, killing SSH/IDE sessions and re-running startup scripts. In our
deployment, where most workspaces are claimed from prebuilds, this
caused fleet-wide "agent disconnected" blips whenever a coderd replica
restarted, and a few workspaces whose container exits when the agent
restarts went into a restart loop every 15-60 minutes. The agent-side
dedup (`lastOwnerID` in `cli/agent.go`) only suppresses the second event
within one agent process, so every new agent process takes at least one
spurious restart.

## Fix

Only seed the reinitialization event while the latest build is the claim
build itself, determined from the build job's input
(`prebuilt_workspace_stage`), the same signal `provisionerdserver` uses
when publishing the claim event:

- Latest build is the claim build: behavior unchanged (seed when the job
succeeded, 409 when it failed permanently, wait on pubsub while it is in
progress).
- Latest build is still a prebuilds-initiated build (claim build not
created yet): fall through to the pubsub subscription, which delivers
the claim event when the claim build completes.
- Latest build is any later user-initiated build: the claim was already
handled, so return 409 and the agent stops polling, the same as a
regular workspace.

`dbfake` gains a `MarkPrebuiltWorkspaceClaim()` builder option so tests
can model claim builds' job input, and the existing `TestReinit` claim
subtests now use it. A new subtest covers the long-claimed workspace
case.

One deliberate behavior change worth calling out: if a claim build fails
and the owner retries with another start build, the handler now returns
409 for that retry build rather than seeding a reinit. This matches the
existing treatment of failed claim builds as terminal for the reinit
poller.

## Verification

- `go test ./coderd/ -run TestReinit` against Postgres 17: all subtests
pass, including the new `workspace claimed in the past gets 409` case.
- `gofmt`, `go vet`, and `golangci-lint` (v1.64.8) are clean on the
touched packages.
- The fix mirrors behavior validated by hand against an affected
deployment: for a long-claimed workspace, `/reinit?wait=true` returned
the seeded `prebuild_claimed` event on every connection before the
change and a 409 afterwards.

Note: this branch was prepared in an environment without the full local
toolchain, so the repo's pre-commit hook (`make pre-commit`) was not run
locally; relying on CI for the full gen/fmt/lint suite. Opening as a
draft mainly to report the issue and propose a fix; happy to rework it
to the maintainers' preferred approach.

---------

Co-authored-by: Sas Swart <sas.swart.cdk@gmail.com>
2026-06-23 13:22:06 +02:00
Susana Ferreira 970bd73691 feat: add /api/v2/ai-gateway API route aliases (#26475)
## Description

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

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

## Changes

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

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

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

> Generated with the assistance of Coder Agents (@ssncferreira)
2026-06-23 12:15:10 +01:00
Kyle Carberry 27ecd17991 refactor: consolidate agent MCP onto a single persistent engine (#26599)
Two MCP code paths both spawned the servers declared in a workspace's
`.mcp.json`: the persistent engine in `agent/x/agentmcp` (which owns
tool-call execution via `CallTool`) and an ephemeral one-shot runner in
`agent/agentcontext` (`mcprunner.go`) that connected, listed tools, and
immediately closed each server purely for discovery. Every declared
server was launched twice, and the discovery path duplicated the
engine's `.mcp.json` parse, transport-build, env-resolve, and connect
logic.

This makes `agent/x/agentmcp` the single persistent MCP engine. The
`agentcontext` manager now reads that engine's per-server catalog
in-process through an injected `MCPCatalog` option and surfaces each
server as a `KindMCPServer` resource. The engine wires `SetOnReload` to
the manager's `Trigger`, so a reload (startup connect or `.mcp.json`
edit) re-resolves and re-pushes the pinned resources. Tool-call
execution is unchanged: it still flows through the engine's `CallTool`
over `POST /api/v0/mcp/call-tool`.

The now-dead HTTP discovery surface is removed: the agent `GET
/api/v0/mcp/tools` route with `agentmcp.API.handleListTools`, and
`workspacesdk.AgentConn.ListMCPTools` with `ListMCPToolsResponse` (mock
regenerated). The change nets roughly `-1370` lines, mostly the deleted
duplicate runner and its tests.

<details>
<summary>Decision log</summary>

The merge of #26585 made pinned `chat_context_resources` the sole source
of workspace context, which surfaced the duplicate spawning. Two options
were considered:

- **Option A + dependency injection (chosen):** keep `agent/x/agentmcp`
as the single persistent engine; `agentcontext` consumes its catalog
in-process and stays the orchestrator/owner at the API boundary (it
still pushes `KindMCPServer` resources). This is low-risk because
`agentcontext` already exposed the `resolver.MCPResources` seam, so the
change just rebinds it from the ephemeral runner to the shared engine.
- **Option B (rejected):** reimplement persistent pooling, reconnect,
singleflight, and race handling inside `agentcontext` and delete
`agentmcp`. Too broad, and it discards the engine's tested lifecycle for
no behavioral gain.

`agentcontext`'s discovery was never what kept servers alive; its runner
closed each server immediately after listing tools. The component
holding persistent connections was always `agentmcp`, which is why
execution already lived there. Consolidating onto it removes the
duplicated stack rather than a whole package: both packages survive with
distinct roles (`agentmcp` is the engine, `agentcontext` is the
orchestrator/owner).

</details>

Coder Agents generated on behalf of @kylecarbs
2026-06-22 22:21:58 -06:00
Kyle Carberry cd56ab9e33 refactor: remove legacy live-read and injected-history chat context paths (#26585)
This PR makes the agent-pushed pinned snapshot
(`chat_context_resources`) the sole source of workspace context for
chats, completing the "Release 5" cleanup. It removes legacy mechanisms
now superseded by the snapshot that agents push over dRPC
(`PushContextState`) and refresh via `chat-context/refresh`.

Removed:

- **Live-read at turn time.** MCP tool discovery, skill live-body reads,
and the instruction/skill history fallback that dialed the workspace on
every turn.
- **Context injected as message history.** The
`persist_workspace_context` generation action and its decision-loop
guard.
- **The legacy write path.** `POST`/`DELETE
/api/v2/workspaceagents/me/experimental/chat-context`, the agentsdk
`AddChatContext`/`ClearChatContext` methods, and the CLI one-shot
writer.
- **The `chats.last_injected_context` column** and all of its plumbing
(migration `000529`, queries, `db2sdk`, `dbauthz`, audit table, and the
frontend `ContextUsageIndicator` fallback).

Subagent context inheritance no longer copies parent context messages;
children now hydrate the parent's pinned `chat_context_resources` on
create, which yields an identical pin for the same workspace and agent.

What stays (still served by the live agent connection, not the
snapshot): `read_skill_file` supporting-file reads, `read_skill`
supporting-file listing, and MCP tool execution.

> [!NOTE]
> Migration `000529` drops `chats.last_injected_context` and recreates
the `chats_expanded` view without it. The down migration restores both.

<details>
<summary>Decision log (D1-D5)</summary>

- **D1 (subagent inheritance):** Re-point inheritance from the legacy
message copy to a pinned hydrate. Children call
`hydrateChatContextOnCreate` instead of copying parent context messages.
- **D2 (`persist_workspace_context`):** Remove the generation action
entirely along with the decision-loop guard it existed to satisfy, since
context is never injected into history anymore.
- **D3 (legacy HTTP + CLI):** Remove the experimental `chat-context`
POST/DELETE endpoints, the agentsdk methods, and the CLI one-shot. The
dRPC push + `chat-context/refresh` replace them.
- **D4 (frontend fallback):** Remove the `last_injected_context`
fallback in `ContextUsageIndicator`; pinned `resources` are the sole
source.
- **D5 (sequencing):** Ship as a single PR rather than a stacked pair.

</details>

---
Coder Agents generated on behalf of @kylecarbs.
2026-06-22 19:26:34 -06:00
Spike Curtis 73acb2d5b4 test: accept 0 duration in TestAwaitDeliveryExactCount (#26582)
fixes https://github.com/coder/internal/issues/1603

On Windows you can get 0 duration from subsequent time.Now() calls.
2026-06-22 16:15:17 -04:00
Kyle Carberry 78c5ab96c9 feat(coderd/x/chatd): serve workspace MCP tools and read_skill from pinned context (#26581)
This wires the last two consumer-side gaps of the agent-pushed workspace
context refactor. coderd already hydrates each chat's pinned context
(`chat_context_resources`), and `resolveTurnWorkspaceContext` already
prefers the pin for instruction files and skill metadata. This change
extends that preference to workspace MCP tools and the `read_skill`
body.

Workspace MCP tools are now built from the chat's pinned `mcp_server`
resources instead of a live `ListMCPTools` pull.
`resolveWorkspaceMCPTools` prefers the pin and falls back to live
discovery for chats whose agent has not reported context yet, gated the
same way as the instruction/skills pin: the pin wins whenever the chat
has any pinned rows, so a workspace with no MCP servers contributes no
tools rather than resurrecting stale ones. Because the agent reports
tool names unprefixed, each tool is re-prefixed to the
`{server}__{tool}` form and the pushed JSON Schema is split into
`properties` and `required` so the result matches what live discovery
produced. Calls still proxy through the workspace agent connection; the
snapshot carries tool definitions, not a way to execute them.

`read_skill` now serves a workspace skill's `SKILL.md` body from the
pinned snapshot (`SkillMeta.Meta`) instead of dialing the agent, so a
pinned chat keeps returning the same instructions even when the
workspace is unreachable. The supporting-file list stays a best-effort
live lookup, since the snapshot carries only the meta file per the agent
push contract.

The legacy live paths remain as the fallback for agents that have not
pushed context; RFC Release-5 cleanup of those paths is out of scope
here.

<details>
<summary>Implementation plan and decisions</summary>

### Background

The agentcontext refactor is mostly shipped across earlier PRs (#25983,
#26526, #26533, #26577, #26570, #26573): the agent resolves instruction
files, skills, and MCP servers into a snapshot, pushes it via
`PushContextState`, and coderd hydrates each chat's pinned context
(`chat_context_resources`). This PR closes the two remaining
consumer-side gaps.

### Key facts established from the code

- Pushed MCP tool names are **unprefixed** (`mcprunner` stores
`tool.Name`); the agent MCP proxy and `CallMCPTool` expect the
`{server}__{tool}` form (`agentmcp.ToolNameSep`). The pinned path
reconstructs the prefix for execution, matching the model-facing names
the legacy path produced.
- Legacy `agentmcp` sets `MCPToolInfo.Schema = InputSchema.Properties`
and `Required = InputSchema.Required` separately. The pushed
`input_schema` is the full JSON Schema object, so the pinned builder
extracts `properties` and `required` to match that shape.
- `SkillMetaBody.meta` is the verbatim SKILL.md. The supporting-file
list is **not** in the snapshot, so it is fetched live on demand
(best-effort).
- Gating mirrors `resolveTurnWorkspaceContext`: the pin wins when the
chat has any pinned rows; otherwise the live path is used.

### Changes

1. `chattool/skill.go`: add `SkillMeta.Meta []byte`; extract
`listSkillFiles` from `LoadSkillBody`; in `readWorkspaceSkillBody`, when
`Meta` is present, parse the body from it without dialing and list files
best-effort, else use the legacy live read.
2. `context_prompt.go`: populate `SkillMeta.Meta` in
`contextResourcesToPrompt`; add `workspaceMCPToolInfosFromResources`
(pinned `mcp_server` rows to `[]workspacesdk.MCPToolInfo` with prefixed
names and split properties/required) and `splitMCPInputSchema`.
3. `chatd.go`: add `pinnedWorkspaceMCPTools` (build tools from the pin,
ok-gated) and `resolveWorkspaceMCPTools` (pin-first, fall back to
`discoverWorkspaceMCPTools`).
4. `generation_preparer.go`: call `resolveWorkspaceMCPTools` instead of
`discoverWorkspaceMCPTools`.

### Tests

- `chattool/skill_test.go`: read_skill serves the pinned body without
dialing, lists files via LS, and still returns the body when the
workspace is unreachable.
- `context_prompt_internal_test.go`: `SkillMeta.Meta` is populated;
`workspaceMCPToolInfosFromResources` prefixing/properties/required/skip
behavior; `pinnedWorkspaceMCPTools` ok-gating and fallback dispatch.

</details>

---

*Opened by Coder Agents on behalf of @kylecarbs.*
2026-06-22 13:13:27 -06:00
McKayla はな 116a220ff6 fix(coderd): let admins change their own workspace sharing role (#26559) 2026-06-22 12:27:16 -06:00
Kyle Carberry 966dd89537 feat: add chat context source CLI and agent-token refresh (#26577)
Adds the `coder exp chat context` CLI for managing workspace context
sources, plus the agent-token refresh endpoint the in-workspace refresh
relies on. Part of breaking the "Workspace Context Sources for Coder
Agents" RFC (#26466) into small, reviewable PRs.

## What this adds

**CLI (`coder exp chat context`)**, talking to the agent's local IPC
socket from inside the workspace:

- `list` lists the registered scan roots (built-in defaults are not
shown).
- `show <path>` shows a source and the resources the agent resolves from
it, including failures.
- `add <path>` registers a path as an additional context source. With
`--chat`, it keeps the legacy one-shot behavior (read context from the
path once and inject it into a single chat).
- `remove <path>` unregisters a source.
- `refresh [<chat>]` re-pins chat context to the agent's latest
snapshot.

**Agent-token refresh path** for the no-argument `refresh`:

- `refresh <chat>` uses the existing user-facing
`ExperimentalClient.RefreshChatContext` (already on main) and works from
anywhere.
- `refresh` with no argument runs inside the workspace: it re-resolves
the agent's sources over the context socket (catching freshly-cloned
repos and startup-script writes), then asks the agent, authenticating
with its own token, to re-pin every drifted chat. No `coder login`
required.
- This adds `agentsdk.RefreshChatContext` and `POST
/api/v2/workspaceagents/me/experimental/chat-context/refresh`
(`workspaceAgentRefreshChatContext`), mirroring the existing clear
endpoint's agent-token auth model.

## Testing

- `go test ./cli` (`TestExpChatContextAdd`, `TestParseChatID`,
`TestResolveContextSourcePath`)
- `go test ./coderd/x/chatd -run TestChatContextRefreshFromAgentToken`
(end-to-end: echo-provisioned agent pushes a snapshot, drifts a bound
chat, the agent-token refresh re-pins it, and an agent-less chat stays
untouched)
- `go build ./...`, `go vet`, `golangci-lint`, `make gen` (no generated
changes; experimental commands are excluded from CLI golden/doc
generation)

<details>
<summary>Design notes</summary>

This is **Split 4** of #26466. Split sequence:

1. #26558 - prompt pin consumption (merged)
2. #26570 - `codersdk` context resource types (merged)
3. #26573 - the context indicator UI (merged)
4. **This PR** - the CLI + agent-token refresh.
5. The context diff (`changes`, `ChatContextResourceChange`, the changes
dialog, `buildContentPatch`) - last.

Key points:

- The agent-local context subsystem (`agent/agentsocket` IPC for source
CRUD, snapshot, resync), the user-facing
`ExperimentalClient.RefreshChatContext`, and the per-chat
`chatd.RefreshChatContext` all already exist on main, so this split is
the CLI surface plus the small agent-token refresh endpoint that fans
out per-chat refresh across an agent's drifted chats.
- `add <path>` resolves relative paths to absolute before handing them
to the agent (which requires canonical paths) but preserves a leading
`~` for the agent to expand against its own home.
`TestResolveContextSourcePath` covers this.
- The agent endpoint is annotated `@x-apidocgen {"skip": true}`,
matching the other agent-token chat-context endpoints.
- No diff/changes rendering is involved; that lands in the final split.

</details>

*This PR was created by Coder Agents on behalf of @kylecarbs.*
2026-06-22 12:15:27 -06:00
Kyle Carberry c0f854c289 feat: report pinned chat context resources on chat API (#26570)
Surfaces a chat's pinned workspace-context resources on the single-chat
GET and refresh responses, so clients can show *what* context the prompt
was built from, not just whether it drifted.

## What's included
- **codersdk**: `ChatContextResource` (plus `ChatContextResourceKind`
and `ChatContextResourceStatus`) and `ChatContextMCPTool`, and a new
`Chat.Context.Resources` field (metadata only, no bodies). It is
populated only on the single-chat GET/refresh response; list and watch
payloads stay nil to remain lightweight.
- **coderd/x/chatd**: `Server.ContextResources`, which builds the
metadata-only list from the chat's pinned `chat_context_resources` rows.
Non-OK resources (invalid / unreadable / oversize / excluded) are
reported with their status and error so the UI can explain why a
resource was dropped from the prompt instead of silently omitting it.
The shared protojson body decoders are extracted so the prompt and
detail paths reuse them.
- **coderd**: `getChat` and `refreshChatContext` enrich the response
with the resource list. Failures are non-fatal (the chat stays usable
without the detail).

## Scope / what's deferred
This is an incremental split from #26466. This PR reports only the
**resource inventory**. The pinned-context drift *diff* (the per-source
`changes` set and the "View changes" dialog) is intentionally deferred
to a later split; the existing `dirty` bit already signals that context
changed. MCP resources are reported for display only; they are not
injected into the prompt (a future RFC item).

<details>
<summary>Design notes</summary>

- The resource list is the chat's full pinned inventory (instruction
files, skills, and MCP configs/servers), preserving the query's `source
ASC` order. OK-but-empty instruction files, OK skills with no name, and
untracked kinds (reserved plugin/hook/subagent/command) are skipped.
- MCP tool names are reported with the agent's `"<server>__"` prefix
stripped so they read as the server exposes them.
- The detail is computed on read and attached only on the single-chat
GET and refresh responses; list and watch payloads omit it to stay
lightweight.
- `refreshChatContext` enriches its own response (mirroring `getChat`)
so the client reflects a refresh immediately, without a full reload.
</details>

<details>
<summary>Testing</summary>

- `go test ./coderd/x/chatd/ -run
'TestPinnedContextResources|TestContextResources|TestChatContextDirtyFromAgentPush'`
(unit + integration on embedded Postgres) passes. The integration test
exercises the GET and refresh enrichment end-to-end.
- `go build`, `go vet`, `golangci-lint`, and `gofmt` are clean.
- `make gen` regenerated `apidoc`, `swagger.json`,
`docs/reference/api/*`, and `typesGenerated.ts`.
</details>

---
*This PR was created by Coder Agents on behalf of @kylecarbs.*
2026-06-22 10:00:15 -06:00
Jon Ayers 401aa58eeb feat: add schema changes for autostop notification (#26417) 2026-06-22 10:59:43 -05:00
Kyle Carberry 46916bf899 feat(coderd/x/chatd): consume the pinned chat context in prompt generation (#26558)
## What

`prepareGeneration` now builds the system-prompt instruction block and
workspace skills from a chat's **pinned context copy**
(`chat_context_resources`, populated in #26438) instead of re-scanning
per-turn history, when the chat has a pinned copy. This is the first
production reader of the pin.

Selection is **presence-based, no experiment**: a chat with pinned rows
builds its prompt from the pin; a chat without them falls back to the
existing per-turn history path. The two paths are mutually exclusive, so
older agents that never report context keep their current behavior and
the per-turn pull stays as the fallback.

## How

- `contextResourcesToPrompt` maps the protojson resource bodies
(instruction files and skills) into the instruction block and skill
metadata, skipping non-OK statuses, non-prompt body kinds, and malformed
bodies (the malformed count is logged so a proto/encoding regression
cannot silently drop context).
- `pinnedWorkspaceContext` reads the pin and reports `ok=false` (history
fallback) when there are no pinned rows; read errors propagate. The
bound agent only decorates the instruction header with OS and directory,
so the pin still resolves when the workspace is unreachable.
- `resolveTurnWorkspaceContext` dispatches between the pinned and
history paths; `prepareGeneration` calls it.

## Testing

- `go test ./coderd/x/chatd/` for `TestContextResourcesToPrompt`,
`TestPinnedWorkspaceContext` (incl. `...FromHydratedPin` against real
Postgres), and `TestResolveTurnWorkspaceContext`: pass.
- `make gen` (no drift), `golangci-lint`, `gofmt`, emdash scan, and `go
build`/`go vet` on `./coderd/x/chatd/...`: all clean.

## Scope

This is the foundational backend slice split from #26466 (the full-stack
staging PR). It changes no API surface, schema, proto, or generated
files. The remaining pieces land as follow-ups in dependency order:

1. `ChatContext` drift/diff API (`resources` + `changes`,
`ContextDetail`). This also extracts the body decoders inlined here so
they are shared with the diff path.
2. Context-ring drift indicator, changes dialog, and refresh (UI).
3. In-workspace `coder exp chat context` source CRUD and `refresh`
(CLI).

<details>
<summary>Why this is the first split</summary>

The coderd hydration, the `PUT /chats/{id}/context` refresh endpoint
(#26389), the `chat_context_resources` table (#26430), and the
copy-into-pin logic (#26438) are already merged, as is the agent-side
push (#26526, #26533). Consuming the pin in prompt building is the step
#26438 explicitly deferred, and it is the bottom of the remaining
dependency stack: the drift/diff API, the UI indicator, and the CLI are
only meaningful once the chat actually builds its prompt from the pin.
Keeping it presence-based means it is independently revertable and
leaves the per-turn pull intact as a fallback, matching the RFC's
Release 3 rollout.

The files are taken verbatim from the reviewed #26466 boundary commit
(before the diff-API work began), so the deep-review feedback already
applied there (CRF-1 through CRF-10) is preserved.

</details>

---

*This PR was created by Coder Agents on behalf of @kylecarbs.* Split
from #26466.
2026-06-22 08:51:57 -06:00
Sas Swart adad5bdd49 feat: surface agent firewall correlation in AI Bridge sessions API (#26416)
Add `agent_firewall_session_id` and `agent_firewall_sequence_number`
fields to `AIBridgeThread` in the `GET
/api/v2/aibridge/sessions/{session_id}` response. These fields link each
thread to its agent firewall confinement session so the frontend can
discover the boundary session and compute sequence ranges for
interleaving firewall events within the thread timeline.

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

Depends on #24814

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

</details>

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

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

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

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

Depends on #24814

> [!NOTE]
> This PR was authored by Coder Agents.
2026-06-22 13:56:29 +02:00
8b970e7ff3 docs: clarify Agents vs Chats API reference pages (#26021)
## Problem

The REST API reference page at
[`/docs/reference/api/agents`](https://coder.com/docs/reference/api/agents)
is confusing: by the name alone, a reader looking for the *AI Coder
Agents* programmatic API would assume this is the right page. In fact,
those endpoints are for the *workspace agent daemon* (the `coder_agent`
Terraform resource / `workspaceagent` daemon). The actual AI Coder
Agents API is documented at
[`/docs/reference/api/chats`](https://coder.com/docs/reference/api/chats).

Both pages compound the confusion by being rendered with a bare `#
Agents` / `# Chats` heading and no descriptive intro. The sidebar
entries are similarly ambiguous (`Agents` and `Chats` with no
descriptions).

## Root cause

The reference pages are generated by `scripts/apidocgen/generate.sh`
(swag → widdershins → postprocess). The widdershins template
(`scripts/apidocgen/markdown-template/main.dot`) already renders
`data.resource.description` directly under each section heading:

```
<!-- APIDOCGEN: BEGIN SECTION -->
{{= data.tags.section }}# {{= r}}

{{? data.resource.description }}{{= data.resource.description}}{{?}}
```

…but the swag annotations in `coderd/coderd.go` never declared
`@tag.name` / `@tag.description` for any tag, so the descriptions were
always empty.

## Changes

- `coderd/coderd.go`: add `@tag.name Agents` / `@tag.description …` and
`@tag.name Chats` / `@tag.description …` annotations next to the
existing `@title` / `@version` block.
- `docs/manifest.json`: rename the sidebar entry `Agents` → `Workspace
Agents` and add `description` fields to both API sidebar entries (every
other top-level section in the manifest has descriptions; the API
children did not).
- Regenerate `coderd/apidoc/swagger.json`, `coderd/apidoc/docs.go`,
`docs/reference/api/agents.md`, and `docs/reference/api/chats.md` via
`scripts/apidocgen/generate.sh` + `pnpm exec markdownlint-cli2 --fix` +
`pnpm exec markdown-table-formatter` + `scripts/biome_format.sh`
(matching the Makefile's `coderd/apidoc/.gen` pipeline).

Resulting diff is intentionally minimal — 6 files, 35 insertions / 3
deletions.

## After this PR

The Agents page will render:

> # Agents
>
> Workspace agent endpoints. These power the workspace agent daemon
defined by the `coder_agent` Terraform resource (sometimes called the
workspace daemon). This API is NOT the AI Coder Agents API. For
programmatic access to AI Coder Agents (formerly Tasks), see the Chats
API.

The Chats page will render:

> # Chats
>
> Programmatic API for Coder AI Agents (the user-facing "Coder Agents" /
"Chats" product). Experimental. Use these endpoints to create, list, and
manage AI coding agent sessions. For background and migration from the
Tasks API, see the AI Coder docs.

And the sidebar entry for the workspace-agent endpoints becomes
`Workspace Agents` instead of `Agents`.

## Out of scope (potential follow-ups)

- `docs/reference/api/chat.md` is a 7-byte stub — likely dead. Could be
deleted in a follow-up.
- Larger rename of the `Agents` Swagger tag (and/or the `coder_agent`
Terraform resource) to something like `Workspace Agents` /
`workspace_daemon` would more thoroughly fix the naming collision, but
that's a much bigger change.

Created on behalf of @mattvollmer.

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: Matt Vollmer <matthewjvollmer@outlook.com>
Co-authored-by: Atif Ali <atif@coder.com>
2026-06-22 07:43:13 +00:00
Cian Johnston d5ec26beac chore: replace testing.Testing with flag lookup (#26552)
In our codebase we have an existing convention of using
`flag.Lookup("test.v")` instead of `testing.Testing()`. This avoids
pulling in the entire `testing` package. Another consequence: some of
our custom linters trigger upon import of the `testing` package which
can lead to unexpected linter errors.
2026-06-19 19:59:54 +01:00
Cian Johnston a12b051834 chore: skip failing azureidentity test while under investigation (#26545)
Skips the failing azureidentity test while under investigation.
Ref: https://github.com/coder/internal/issues/1602
2026-06-19 12:49:49 +01:00
Marcin Tojek a2b680ab29 fix: sanitize MCP tool names to satisfy LLM provider constraints (#26539)
Fixes #26325
2026-06-19 11:48:30 +02:00
Susana Ferreira 19aa9f5616 refactor: separate aibridge provider and interceptor configs (#26092)
## Description

Separates the aibridge provider configuration from the per-request configuration an interceptor actually needs, and introduces a single `Credential` type that each provider resolves per request. Previously a provider handed its full config to the interceptor (including fields the interceptor didn't use) while other request data was passed as loose arguments, and authentication was spread across config fields and arguments.

## Changes

- Add `intercept.Config`: the per-request, provider-agnostic configuration an interceptor needs (`ProviderName`, `BaseURL`, `APIDumpDir`, `SendActorHeaders`).
- Introduce a single `Credential` interface (`BYOK` and `Centralized`) that each provider resolves per request in `resolveCredential`, and have interceptors route on the credential kind.
- Fail fast with `ErrNoCredential` when a request is neither BYOK nor backed by a centralized key pool.
- Remove unused provider config fields (`Key`, `BYOKBearerToken`, `ExtraHeaders`).

Closes: coder/aibridge#266
Closes: https://linear.app/codercom/issue/AIGOV-221/refactor-separate-provider-and-interceptor-configs

> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
2026-06-19 08:48:03 +01:00
Hugo Dutka ba860271b0 chore(coderd/x/chatd): clean up the message part buffer comments and implementation (#26508)
Address deferred review feedback for `messagepartbuffer` by documenting
its episode lifecycle, extracting the repeated episode lookup and
finalization helpers, and documenting subscriber channel buffering
decisions.

Addresses these PR #26109 comments:

- https://github.com/coder/coder/pull/26109#discussion_r3379915812
- https://github.com/coder/coder/pull/26109#discussion_r3379988015
- https://github.com/coder/coder/pull/26109#discussion_r3380010948
- https://github.com/coder/coder/pull/26109#discussion_r3380029684
2026-06-18 22:45:01 +02:00
Hugo Dutka 56eb705b8c fix(coderd/x/chatd): reset auto archive ticker after runs (#26512)
Prevents slow chat auto-archive runs from causing a constant archival
loop by resetting the ticker only after each run completes.

Also documents the UTC midnight cutoff used for archive eligibility so
chats with activity on the same UTC calendar date stay eligible or
ineligible for the full day.

Addresses deferred review comments:
- https://github.com/coder/coder/pull/26109#discussion_r3380197310
- https://github.com/coder/coder/pull/26109#discussion_r3380219922

Generated by Coder Agents and closely reviewed by Hugo.
2026-06-18 22:44:02 +02:00
Zach 76fa1d4577 fix(coderd): show correct deletion time in dormancy notification (#26488)
The dormancy notification's "will be automatically deleted in X"
sentence rendered the dormancy threshold instead of the auto-delete
duration. A 30-day threshold rendered as "4 weeks" even when auto-delete
was 90 days; a 60-day threshold rendered as "1 month" with a 7-day
auto-delete. Render the countdown from the auto-delete setting, and skip
the deletion sentence entirely when auto-delete is disabled so the
notification no longer promises a deletion that will never happen.
2026-06-18 20:17:37 +00:00
Sas Swart 491a75294e feat: GET /api/v2/agent-firewall/sessions/{id} (#24814)
Add a GET endpoint at `/api/v2/agent-firewall/sessions/{id}` that
returns agent firewall session metadata (`id`, `workspace_id`,
`owner_id`, `confined_process`, `started_at`). The handler authorizes
against the `boundary_log` resource with `ActionRead` via dbauthz.

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

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

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

Depends on #24810

**RBAC behaviour:**

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

> [!NOTE]
> This PR was authored by Coder Agents.
2026-06-18 20:50:17 +02:00
Hugo Dutka c0c0d1e353 chore(coderd/x/chatd): improve runner logging (#26522)
Follow up to https://github.com/coder/coder/pull/26412.

- Ensure that each `errTaskExpectedExit` and any other error is joined
with a descriptive reason. This will enable
[`runChatWithRetry`](https://github.com/coder/coder/blob/5601ea18ed650deafa4a2da7286a144f9f382330/coderd/x/chatd/tasks.go#L113)
to log a descriptive message when a task exits.
- Pass additional task information like chat id into `runTaskWithRetry`
so logs can be more descriptive.
2026-06-18 19:02:42 +02:00
Hugo Dutka 61a7b800e2 fix(coderd/x/chatd): always commit a workspace context marker (#26520)
Closes
[CODAGT-629](https://linear.app/codercom/issue/CODAGT-629/agents-can-get-stuck-and-ignore-stop-or-nudge).
A stuck chat had these logs associated with it:

```
1781735334322	2026-06-17T22:28:54.322Z	2026-06-17 22:28:54.322 [debu]  coderd.chatd.processor: workspace context build: workspace agent not resolvable  chat_id=d4524ebb-4494-47df-b258-d933c0248942  owner_id=d96bf761-3f94-46b3-a1da-6316e2e4735d
1781735334298	2026-06-17T22:28:54.298Z	2026-06-17 22:28:54.298 [debu]  coderd.chatd.processor: plan path instruction: agent not reachable  chat_id=d4524ebb-4494-47df-b258-d933c0248942  owner_id=d96bf761-3f94-46b3-a1da-6316e2e4735d  chat_id=d4524ebb-4494-47df-b258-d933c0248942 ...
    error= workspace has no running agent: the workspace is likely stopped. Use the start_workspace tool to start it:
               github.com/coder/coder/v2/coderd/x/chatd.init
                   <autogenerated>:1
```

"workspace agent not resolvable" is printed by
[`fetchContextForBuild`](<https://github.com/coder/coder/blob/fecc991ac9f9d673d7a772b0042ca821d19b6296/coderd/x/chatd/workspace_context_builder.go#L145>).
this causes
[`buildWorkspaceContext`](<https://github.com/coder/coder/blob/fecc991ac9f9d673d7a772b0042ca821d19b6296/coderd/x/chatd/workspace_context_builder.go#L60>)
to exit with a `errWorkspaceContextUnavailable` error. That in turn is
interpreted by
[`persistWorkspaceContext`](<https://github.com/coder/coder/blob/fecc991ac9f9d673d7a772b0042ca821d19b6296/coderd/x/chatd/generation.go#L838>)
as an "expected exit" scenario. That's a bug: because the task exits
without changing the chat state, the runner never issues another task to
process the chat any further. But even if it did, it'd go through the
same code path and exit again. We need to ensure that
`persistWorkspaceContext` commits a marker file even if it cannot reach
the agent.
2026-06-18 16:08:26 +00:00
Ethan 0fcd9c2005 chore: bump fantasy to sync from upstream (#26440)
Closes CODAGT-572

## Overview

Bumps `charm.land/fantasy` to the head of `coder_2_33`
(`v0.0.0-20260617050554-2e3ddbca75dd`) and adapts `chatd` to it.

The fantasy bump:
- Syncs upstream `charmbracelet/fantasy` main (v0.31.0) into
`coder_2_33` (coder/fantasy#42).
- Mirrors the request region when prefixing cross-region inference
profiles, so a legacy (un-qualified) Bedrock model ID is prefixed for
the same region the request is actually signed for.

Pulling in the new fantasy version propagates its required transitive
dependency upgrades (aws-sdk-go-v2, OpenTelemetry, google genai,
`golang.org/x/*`, etc.) through MVS, which accounts for the bulk of the
`go.mod`/`go.sum` churn.

## chatd changes

- Thread a per-provider `Region` through `ConfiguredProvider` and
`ProviderAPIKeys` (`RegionByProvider`), and merge/prune/resolve it
alongside API keys and base URLs.
- Source the Bedrock region from AI provider settings in `chatd` and
pass `fantasybedrock.WithRegion` when a region is configured.
- Migrate the runtime Bedrock title-generation model ID to a
fully-qualified `global.anthropic.*` ID.
- Emit a `finish_reason` in the test OpenAI streaming server so streams
close on a terminal event, matching fantasy's fail-closed stream
handling.

## Heads-up: most of this is short-lived

Almost all of the `chatd` code in this PR only executes on the **direct
(non-gateway) routing path** — the branch taken when
`AIGatewayRoutingEnabled` is `false`. That flag was a transition crutch
for AI Gateway routing, and it (plus the entire direct path /
`x/chatd/chatprovider` package that backs it) is slated for removal in
CODAGT-598. Under AI Gateway routing — which is the path every
deployment is expected to run — the Bedrock region is resolved by
aibridge directly from provider settings (`cli/aibridged.go` builds
`aibridge.AWSBedrockConfig{Region: settings.Bedrock.Region}`), so none
of the region plumbing added here is reached.

Concretely, expect the following to be deleted alongside the direct
path:
- The `RegionByProvider` map, the `Region()` accessor, and the region
preservation in merge/resolve plus the region pruning in
`PruneDisabledProviderKeys` (`chatprovider.go`).
- The `fantasybedrock.WithRegion(region)` branch in `ModelFromConfig` —
only reachable on the direct path; the gateway path builds a
`fantasyanthropic` client with no region key.
- Reading `settings.Bedrock.Region` in `aiProviderConfigFromKeys`
(`chatd.go`).
- The region-specific tests in `chatprovider_test.go`, and the
`chattest`/`model_coverage` adjustments that support direct-path
testing.

What survives the cleanup (independent of routing):
- The `charm.land/fantasy` bump and its `go.mod`/`go.sum` transitive
churn.
- The fully-qualified `global.anthropic.*` Bedrock title-generation
model ID in `quickgen.go` (a runtime-valid model identifier, not
direct-path-specific).

We're landing the full change anyway so the direct path stays correct
for the remaining transition window; just don't be surprised when
CODAGT-598 reclaims most of it.

## Notes

Depends on coder/fantasy `coder_2_33` already containing the upstream
sync and Bedrock region fix (merged via coder/fantasy#42 and
coder/fantasy#43).
2026-06-19 00:53:59 +10:00
Cian Johnston a5a4c49a6f chore(coderd/httpapi): deflake TestWSWatcher (#26495)
Fixes ENG-2930
Fixes coder/internal#1597

Refactors TestWSWatcher to reduce flake occurrences.
The flaky tests were using polling-based assertions which may flake
based on goroutine scheduling.
Fixed by using fake connections and channel synchronization where
appropriate.

Note: No coverage of ProbeCanceled, pre-existing.

> Generated by a human, spot-checked by several robots.
2026-06-18 10:08:29 +01:00
Hugo Dutka 803daaa8b7 fix(coderd/x/chatd): deflake TestStreamPartsDialerDialsPartsEndpoint (#26504)
Closes https://github.com/coder/internal/issues/1601. Fixes a stream
parts WebSocket close race. If the peer closed first, the session read
loop could close the connection before `StreamPartsSession.Close()` ran,
causing cleanup to return a wrapped `net.ErrClosed`. The fix treats
expected transport close errors as successful cleanup.
2026-06-18 08:28:17 +00:00
Hugo Dutka 91543c391d chore: add chatd ARCHITECTURE.md and mention it in AGENTS.md (#26478)
Closes
[CODAGT-610](https://linear.app/codercom/issue/CODAGT-610/add-an-architecturemd-file-to-chatd).
Adds an ARCHITECTURE.md file which describes the architecture of the
chatd subsystem. It's meant for reading by both humans, who would like
to understand chatd better, and agents. It's an edited version of the
chatd stabilization RFC.
2026-06-18 10:08:48 +02:00
Jaayden Halko bc44cdda75 feat: rank chat workspace templates (#25037)
closes CODAGT-203

## Summary

`list_templates` now returns a ranked shortlist with a recommendation,
so the chat agent can pick the right template the way a colleague would:
prefer what matches the request, what the user already uses, and what
the rest of the organization uses. Instead of teaching the model an enum
protocol in prompts, every result carries a fixed `next_step`
instruction telling the agent what to do.

## How list_templates works

1. **Fetch**: active, non-deprecated templates in the chat's
organization, filtered by the admin template allowlist, authorized as
the chat owner (no system escalation).
2. **Query relevance** (optional `query` argument): each template
receives the highest tier any of its fields matches, and a higher tier
always outranks a lower one regardless of usage:

   | Tier | Match |
   |------|-------|
   | 4 | name or display name equals the query |
   | 3 | name or display name starts with the query |
   | 2 | name or display name contains the query |
| 1 | description contains the query (checked only when no name field
matched) |
   | 0 | no match; the template is excluded |

Matching is case-insensitive and ignores spaces/hyphens/underscores
(`python gpu` matches `python-gpu`).
3. **Usage signals**: a new `GetTemplateRankingSignalsByOwnerID` query
returns, per template, the owner's active and recently-deleted workspace
counts within a 60-day window, the last in-window usage, and the count
of distinct developers with an active workspace (unclaimed prebuilds
excluded).
4. **Affinity score** (computed in Go, per template, from that
template's signals only):

   ```text
affinity = 10 x (active + 0.5 x deleted) x 0.5^(days_since_last_use /
14)
            + ln(1 + active_developers)
   ```

`active`/`deleted` are the owner's in-window workspace counts,
`days_since_last_use` is measured from the most recent in-window usage
(the personal term is zero without in-window usage), and
`active_developers` is the org-wide count. Personal usage carries 10x
the weight of org popularity; the confidence floor is the score of two
active developers (`ln 3`) and the required lead over the runner-up is
`ln 3 - ln 2`.
5. **Rank**: query tier first (when a query is present), then affinity
score, then name/ID for determinism. Results paginate 10 per page with
`next_page` present only when more exist.

## Recommendation contract

The result tells the agent what to do next instead of describing
confidence levels:

- `recommended_template_id` is present only when the top template is a
clear winner: the only available template, a decisive query match, or an
affinity score that clears a floor and leads the runner-up by a derived
margin.
- `next_step` is always present and is one of four fixed sentences: use
the recommendation, ask the user to choose, retry a query that matched
nothing, or report that no templates are available.

Per-template items carry raw evidence (`active_developers`,
`your_workspace_count`, `last_used_by_you`) rather than derived labels.
When signals fail to load, the tool logs and degrades to asking the user
unless the query alone is decisive.

Prompts and the `create_workspace`/`read_template` descriptions
reference the field through the `chattool.NextStepField` constant, so
the instruction lives in one place and cannot drift. `create_workspace`
remains idempotent and allowlist-enforced.

## Authorization

The signals query runs with the chat owner's permissions: reading the
owner's own workspaces plus a template-metadata read for the cross-user
popularity count. dbauthz rejects the call if any requested template is
not readable by the owner (covered by allow and deny method tests).

## Docs

Adds `docs/ai-coder/agents/tools/` explaining how agent tool calls work,
with `list_templates` ranking and the `next_step` contract as the first
documented tools.
2026-06-18 06:41:47 +01:00
Jon Ayers ea1379d42c chore: add nats benchmarking pkg (#26396) 2026-06-17 17:34:02 -05:00
Asher 9b847cc5ab feat: support "me" with shared_with_user filter (#26494) 2026-06-17 13:35:53 -08:00
Hugo Dutka 684d904c00 fix(coderd/x/chatd): resolve inflight race (#26460)
Using `WaitGroup.Go` must be synchronized with `WaitGroup.Wait`
according to [go docs](https://pkg.go.dev/sync#WaitGroup.Go):

> If the WaitGroup is empty, Go must happen before a
[WaitGroup.Wait](https://pkg.go.dev/sync#WaitGroup.Wait).

There were a couple of places in chatd that violated this principle.
This was caught as a data race in
https://github.com/coder/internal/issues/1599. This PR ensures that all
functions that spawn inflight goroutines synchronize with each other.

I also noticed that inflight goroutines may be spawned after the server
is closed, which was surprising and looked like a bug. This PR therefore
also introduces a mechanism that disallows spawning inflight goroutines
after the server is closed, and ensures that any code that tries doing
it logs an error.

Closes https://github.com/coder/internal/issues/1599.
2026-06-17 18:29:43 +02:00
Jeremy Ruppel 87de6dc23e feat: add base template variables to API (#26425) 2026-06-17 12:22:35 -04:00
Ethan 35af54d6aa test: isolate passive chatd internal tests (#26369)
Make `newInternalTestServer` use option functions for logger, clock, and
worker startup, and make it passive by default so internal chatd tests
only opt into background execution when they need a real worker.

Use the passive server path in `TestAwaitSubagentCompletion` for the
state-driven subtests, keep `ContextCanceled` explicitly active for real
provider cancellation coverage, and keep the fail-fast default AI
provider base URL so accidental provider calls still fail immediately.

Closes CODAGT-586
Closes https://github.com/coder/internal/issues/1549
2026-06-18 00:21:09 +10:00
Danielle Maywood 8d725969bf chore!: remove coder agents insights page (#26457)
Removes the coder agents PR Insights page (`/agents/settings/insights`) and all of its backend support. The page had previously been hidden and was only reachable via deep link. It had previously been hidden due to the dubious value provided in the current iteration.
2026-06-17 14:02:19 +01:00
Hugo Dutka 6d44bfef77 test(coderd): deflake TestAgentChatContext/AddSuccessUpdatesChatState… (#26456)
…VersionsAndPublishes

Closes https://github.com/coder/internal/issues/1592.
2026-06-17 11:53:32 +00:00
Hugo Dutka b3e4a3af0b fix(coderd/x/chatd): ensure runner initializes from the db first (#26455)
Should close https://github.com/coder/internal/issues/1589.
2026-06-17 11:43:29 +00:00
Paweł Banaszewski f1ce1013c4 chore: export AI Gateway metrics under new branding + keep old as alias (#26413)
> AI Tools where used in this request.

Registers `coder_aibridged_*` and `coder_aibridgeproxyd_*` metrics under
new prefixes: `coder_ai_gateway_*` and `coder_ai_gateway_proxy_*`.
Old prefix is still exported. Will be removed in later release.

Also updated the `metricsdocgen` static fixture. Added 4
previously-undocumented metrics `key_pool_state`,
`key_pool_state_transitions_total`, `key_pool_exhaustions_total`,
`key_pool_failover_attempts` added the `client` label to the existing
interception, prompt, and token counter samples.

Updated AI Gateway documentation.
2026-06-17 13:10:53 +02:00
Hugo Dutka 054d0c45de fix(coderd/x/chatd): log retry errors and add a task timeout (#26412)
This PR adds logging when the chat runner retries and exits because of
an error. It also adds a 15-minute task timeout to ensure that stuck
tasks do not hang forever.
2026-06-17 10:20:47 +00:00
Ethan d638b1aaed chore: gate Coder Agents app and port tabs behind experiment (#26395)
The workspace-app and port preview tabs in the Coder Agents right panel
were previously gated behind a `devel` prerelease build check, which
can't be toggled in real deployments.

This replaces that check with a proper `agent-app-tabs` deployment
experiment, registered in `ExperimentsKnown`, so the feature can be
enabled via `CODER_EXPERIMENTS=agent-app-tabs` like any other
experiment. The frontend now reads
`experiments.includes("agent-app-tabs")` from the dashboard instead of
`getPrereleaseFlag(buildInfo) === "devel"`.

Depends on #26208
2026-06-17 17:29:20 +10:00
Kyle Carberry 1c78bd84b7 feat(coderd): copy agent context resources into the per-chat pin (#26438)
## What

Populates `chat_context_resources` (the per-chat pinned copy added in
#26430) by copying from `workspace_agent_context_resources` at the
points where a chat's `context_aggregate_hash` is set, in the same
transaction, so the pinned hash and pinned bodies always agree. No
prompt-building change yet; consuming the pinned copy in
`prepareGeneration` is a later, experiment-gated PR.

## How

- `HydrateAgentChatsContext` now hydrates NULL-hash chats **and** copies
the agent's resources onto them in one statement (a data-modifying CTE),
so the chat-create and agent-push paths need no Go change.
- New queries `InsertAgentContextResourcesIntoChat`,
`DeleteChatContextResources`, `ListChatContextResources`, each with a
hand-written dbauthz wrapper (per-chat update/read) and a
`MethodTestSuite` entry.
- `RefreshChatContext` re-pins resources via a shared `repinChatContext`
helper (clear-then-copy in a transaction). A dirty chat keeps its old
bodies until refresh.
- On agent rebind (e.g. a workspace rebuild produces a new agent), the
chat's context is re-pinned to the new agent so it stops injecting the
previous agent's resources. Best-effort: a context error never fails the
binding.

## Invariant

A chat's `chat_context_resources` always correspond to its
`context_aggregate_hash`. Bodies are (re)written only when the hash is
set (hydrate, refresh, rebind); a dirty chat keeps its old bodies until
refresh.

## Testing

Extends the context integration test to push real resources and assert
the copy across hydrate, dirty (no re-copy), and refresh. The dbauthz
`MethodTestSuite` covers the three new methods.

<details>
<summary>Why clear-then-copy (two statements)</summary>

The refresh/rebind re-pin clears the chat's rows then inserts the
agent's. It uses two sequential statements inside the transaction rather
than a single `WITH cleared AS (DELETE ...) INSERT ...`, because a
data-modifying CTE cannot see its own delete under snapshot isolation,
so overlapping sources (the common case: the same files re-pinned) would
collide on the `(chat_id, source)` primary key. The hydrate path inserts
into never-pinned (NULL-hash) chats and uses `ON CONFLICT DO UPDATE`
defensively.

</details>

<details>
<summary>Follow-ups</summary>

- `prepareGeneration` consuming the pinned instructions and skills
(experiment-gated).
- `codersdk.ChatContext` resources plus changed diff, and the frontend
indicator/refresh.
- Removing the per-turn pull and `last_injected_context`.

</details>

---

*This PR was created by Coder Agents on behalf of @kylecarbs.* Builds on
#26430.
2026-06-17 00:10:07 -07:00
Kyle Carberry 53a6459ecd feat(coderd/database): add chat_context_resources table (#26430)
Adds chat_context_resources: a per-chat pinned copy of the agent context
resources a chat is hydrated against. The agent-side table
(workspace_agent_context_resources) is last-writer-wins with no history,
so a chat copies its resources at hydration/refresh to keep a stable view
while the agent drifts.

Schema foundation only (no queries/dbauthz/prepareGeneration/SDK yet).
chat_id FK ON DELETE CASCADE for cleanup parity; no agent FK so the pin
survives agent replacement; PK (chat_id, source); reuses the 000522 enum
types.
2026-06-16 14:28:57 -07:00
Steven Masley 0e45ded0ed feat: deployment flag to auto handle changed oidc providers (#26419)
An opt-out flag exists as an escape hatch

closes https://linear.app/codercom/issue/PLAT-343/automatically-reset-user-link-for-affected-users-when-idp-provider
2026-06-16 13:26:04 -07:00
Yevhenii Shcherbina b6fcb9a30a feat: record cost on aibridge token usages (#26229)
Implements
https://linear.app/codercom/issue/AIGOV-286/add-interception-cost-calculation-to-aibridge-token-usages

Adds spend attribution to AI Gateway. After the upstream response, each
token-usage record now captures the user's effective group, the
per-token prices in effect at that moment, and a computed cost — so
spend is recorded as an immutable, point-in-time snapshot.

Concretely, `aibridge_token_usages` gains `effective_group_id`,
`input_price_micros`, `output_price_micros`, `cache_read_price_micros`,
`cache_write_price_micros`, and `cost_micros`. When a usage record is
written, the effective group is resolved (per-user override, else the
deployment budget policy), the `(provider, model)` price is looked up
and snapshotted onto the row, and cost is computed from the
provider-reported token counts. A model that isn't in the price table
records its tokens with a `NULL` cost; any *other* resolution failure
fails the write, so a `NULL` cost unambiguously means "model not priced"
rather than "lookup errored."

All values are stored in micro-units (1 unit = 1,000,000 micro-units;
Phase 1 assumes USD, so 1 micro-unit = $0.000001). Prices are quoted per
million tokens.

This also grants the AI Bridge RBAC subject `read` on `ai_model_prices`
(the per-interception price lookup needs it; it previously only had
`update` for the startup seeder).

## Cost precision

Cost is computed per token category as `tokens × price / 1_000_000` with
integer division, then the four categories are summed. The division is
done **per category** (not once over the summed numerator) on purpose:
it keeps the per-category line items summing exactly to the stored total
— no "the parts don't add up to the whole" in reporting).

Integer division truncates sub-micro-unit fractions. For example, a
cheap model at $0.10 per million tokens is a price of `100_000`; 9
tokens cost `9 × 100_000 / 1_000_000 = 900_000 / 1_000_000 = 0` (the
true 0.9 micro-units floors to 0). At real list prices this rarely bites
— $3/M input is a price of `3_000_000`, so even a single token is 3
micro-units. The per-record under-count is bounded below 1 micro-unit
per category, so under $0.000004 total across the four categories, which
is acceptable for list-price-based cost approximation.

## Overflow safety

`cost_micros` is a `BIGINT` (int64), and the largest intermediate value
is a single category's `tokens × price` before division. int64's ceiling
is ≈ `9.223e18`.

- At a steep $75/M model (price `75_000_000`), overflow would require
~123 billion tokens in one response: `123e9 × 75e6 = 9.225e18`, just
over the limit. `122e9` stays under at `9.15e18`.
- A realistically maxed-out Opus 4.8 response (≈1M input + 128K output
at list prices) costs about $15, with a numerator around `1.5e13` —
roughly six orders of magnitude below the ceiling.

So overflow is unreachable from real token counts.

### Multi-currency support

In the future, we may encounter issues with multi-currency support,
especially when dealing with currencies that have very large exchange
rates relative to USD, for example:

IRR: ~1,300,000 IRR ≈ 1 USD
VND: ~26,000 VND ≈ 1 USD

For currencies with such large denominations, numeric overflow is
technically possible, considering that we have only about six orders of
magnitude of headroom before reaching the limit (see above).

## `effective_group_id` has no foreign key

`effective_group_id` records the group a spend was attributed to, as an
immutable historical fact. It is intentionally **not** a foreign key, so
the record survives deletion of the group.

Alternatives were considered and rejected:

- **`ON DELETE SET NULL`** would mutate an "immutable" record — deleting
a group silently erases that interception's attribution and under-counts
the group's historical spend.
- **`RESTRICT` / `NO ACTION`** would block group deletion entirely
(groups are hard-deleted).
- **`CASCADE`** would delete spend history when a group is deleted — the
worst outcome for an audit record.

There is also no insert-time check that the group still exists: the id
comes from a budget that was just resolved, meaning it was valid at some
point.

## Open question: group name snapshotting

Should we also snapshot the group *name* onto each record? Two options:

- **Denormalize it now** — readable in historical reports even after a
group is deleted, but the snapshot can drift from the current name on
rename, raising a "show point-in-time vs. current name" question.
- **Postpone until needed** — it's a purely additive column later, and
the name is display-only (not correctness-bearing like the price). The
cost: names of groups deleted before the column is added can't be
backfilled.

Leaning toward postponing until a concrete reporting need settles the
drift question.
2026-06-16 20:09:00 +00:00
Steven Masley 1d03e63f4f feat: implement package and cli tool for repairing oidc links (#26418) 2026-06-16 12:46:10 -07:00
Kyle Carberry bca0ce04ca feat: integrate agent context snapshots into chats (#26389)
Makes the chat context foundation from #26385 live. That PR added the
storage columns, writer queries, and a dormant
`agentapi.ContextDirtyMarker` trigger with no production callers; this
PR wires them together end to end.

When a workspace agent pushes a context snapshot, bound chats now
hydrate to that snapshot's hash, and a later push with a different hash
flips already-pinned chats to dirty (emitting a `context_dirty` watch
event after the transaction commits). Chat creation pins the agent's
latest snapshot when one already exists. The experimental chat API
exposes this as `Chat.Context` (`*ChatContext` with `dirty`,
`dirty_since`, `error`), and a new `PUT
/api/experimental/chats/{chat}/context` endpoint re-pins the agent's
latest snapshot and clears the dirty marker.

`context_dirty_resources` stays NULL (the resource-level diff is
deferred to the UI phase) and the live per-turn context pull is
unchanged.

The end-to-end test provisions a workspace agent via the echo
provisioner, connects it over the Agent API v2.10, and exercises the
full path: an initial push hydrates a bound chat (clean), a second push
with a different hash marks it dirty, the API reports the dirty state,
and the refresh endpoint clears it.

<details>
<summary>Decision log</summary>

- **API shape — sub-struct.** Dirty state is surfaced as
`codersdk.Chat.Context *ChatContext { Dirty bool; DirtySince *time.Time;
Error string }` rather than flat fields, matching the RFC's named
`ChatContext` type and leaving room for future fields (resource diff,
sources). `db2sdk.Chat` populates it when the chat is context-tracked
(`len(ContextAggregateHash) > 0`), dirty, or carries a snapshot error,
and leaves it nil (`omitempty`) otherwise. `Dirty` mirrors
`context_dirty_since` being set.
- **Marker wiring.** The chat daemon is injected directly as the
`agentapi.ContextDirtyMarker`. It is unconditionally constructed (only
its background worker is gated), so the marker is always non-nil and the
wiring matches every other `api.chatDaemon` call site. `agentapi` still
treats a nil marker as "chatd absent", so `PushContextState` stays a
pure write path for any future caller that does not wire chatd in.
- **Refresh is atomic.** `RefreshChatContext` reads the agent's latest
snapshot and re-pins the chat in one repeatable-read transaction, so a
concurrent push cannot land between the read and the write and leave the
chat pinned to a stale hash with the dirty marker cleared.
- **Hydrate + dirty run inside the push transaction.** The fan-out
shares the push's transaction so a concurrent refresh cannot interleave
with the version gate; `context_dirty` watch events publish only after
commit. The pinned hash on dirtied chats is intentionally left unchanged
— the refresh endpoint re-pins it.
- **Dirtied chats keep their pinned hash.** Drift is advisory: a dirty
chat stays usable, and refreshing is the only path that advances the
pinned hash.
- **Test binds `chats.agent_id` directly.** In production the binding is
set lazily during a chat turn (`chatd.persistBuildAgentBinding`); the
test sets it via `dbgen` so it exercises the context flow rather than
turn resolution.

Plan: `coderd/x/chatd` context integration + E2E (sub-struct API,
create-time + push-time hydration, refresh endpoint;
`context_dirty_resources` and the per-turn pull untouched).

</details>

🤖 Generated by Coder Agents on behalf of @kylecarbs
2026-06-16 17:46:47 +00:00
Ethan 64289c7388 test: use httptest server.Client() to isolate transport (#26409)
`TestPush/CachesSubscriptionsWithinTTL` could fail with `Post
"http://127.0.0.1:XXXXX": net/http: HTTP/1.x transport connection
broken: http: CloseIdleConnections called` when a sibling parallel
subtest's `httptest.Server.Close()` ran during an in-flight `Dispatch`.

`setupPushTestWithOptions` wired the dispatcher to `http.DefaultClient`,
so every parallel subtest in `TestPush` shared `http.DefaultTransport`.
`httptest.Server.Close()` calls `CloseIdleConnections` on
`http.DefaultTransport`, which could break an in-flight request in any
other subtest using the same transport.

`httptest.Server` already exposes a paired `*http.Client` backed by a
transport dedicated to that server (see `net/http/httptest/server.go`).
Closing one server only touches `http.DefaultTransport` and its own
client's transport, so sibling cleanup can no longer reach into ours.

Same flake class and same isolation principle as #25015, #25407, #25430,
and #25821.

Closes https://github.com/coder/internal/issues/1593
Closes ENG-2926
2026-06-17 00:22:41 +10:00
Sas Swart 894734aa2c chore: remove nopAuditorPtr from dbpurge test setup (#26410)
remove nopAuditorPtr from dbpurge test setup to fix make lint
2026-06-16 13:24:50 +00:00
Hugo Dutka 4f74a7adee fix: enable goleak in chatd tests (#26335)
Enable goleak in chatd tests and fix some leaks. Addresses
https://github.com/coder/coder/pull/26109#discussion_r3380039964
2026-06-16 12:35:40 +00:00