mirror of
https://github.com/coder/coder.git
synced 2026-10-31 11:02:50 +08:00
2f996d765af540a807de46b33ebbd4953b41dac0
1581
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
24b83dc727 |
feat: add organization picker to MCP server settings pages (#28150)
Adds an organization picker to the MCP server settings pages so
multi-org deployments can manage every organization's MCP servers
through the UI, not just the default organization's. Closes CODAGT-714
(scoped to MCP servers only; model configs stay deployment-wide until
CODAGT-709 lands).
## Stack Context
Top of the CODAGT-711/717/712/806 stack: MCP server configs are
org-scoped with RBAC, audit logging, per-config ACLs, and owner-scoped
user tokens. This layer makes the admin UI org-aware, resolving the
"default org only" limitation documented in the bottom PR.
## Summary
- The MCP servers list and add pages resolve the working organization
from an `org` search param (falling back to the default org), and render
an organization picker built on the existing `OrganizationAutocomplete`.
The picker only appears when more than one organization is visible, so
single-org deployments see no change.
- Selecting an organization rewrites the search param, refetches the
list for that org, and threads through to the add page so creation posts
to the selected organization.
- The update page now loads the server by ID (new organization-scoped
`GET /api/experimental/organizations/{organization}/mcp-servers/{id}`
frontend binding) instead of listing the default org and searching
client-side, so servers from any organization can be edited. Post-save
navigation returns to the owning organization's list via the server's
`organization_id`.
- Removes the now-unused `getDefaultOrganizationId` helper.
- Review hardening: organization display labels are disambiguated with
the unique org name when effective display names collide (picker
options, trigger text, and accessible name). Access is no longer
restricted to deployment admins: the pages, sidebar item, and nav entry
accept org-scoped `mcp_server_config` permissions, the picker lists only
organizations the user can view, content and affordances (add, edit,
delete) are gated on the selected organization's permissions, and
deployment admins keep full access everywhere.
## Testing
Storybook interaction stories cover: picker hidden with one org,
switching orgs refetches and swaps list contents, add posting to the
URL-selected org, add posting after an in-form org switch (asserting
typed form state survives the switch), and update loading by ID.
Red-green verified by breaking the search-param resolution and observing
the new stories fail.
> Mux acted on Mike's behalf for this PR.
|
||
|
|
8203b2ebaf | feat!: hide Coder Tasks behind the enable-ai-tasks flag (#28008) | ||
|
|
7ca7c30f40 |
feat: add group and user ACLs to MCP server configs (#27944)
Adds group and user ACLs to org-scoped MCP server configs so
organizations can restrict specific MCP servers to subsets of members,
mirroring the template ACL pattern.
## Summary
- Migration adds `group_acl`/`user_acl` JSONB columns (nested `{"<id>":
{"permissions": [...]}}` shape) and seeds every existing config with its
organization's Everyone group read entry (the Everyone group ID equals
the org ID), so member access is unchanged by default. Creation seeds
the same entry.
- The blanket org-member read grant from the base PR is replaced by ACL
evaluation: Rego requires org membership for every ACL grant, and
`GetAuthorizedMCPServerConfigs` compiles ACL-aware SQL filters.
- New `ActionShare` (org admins) gates `GET|PATCH
/api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl`
(nested under the organization like the rest of the config surface).
PATCH validates principals against the config's organization, merges
sparse updates under a row lock, stamps `updated_by`/`updated_at`, and
is audited as a Write with `Old` captured before authorization. A config
deleted concurrently between the middleware fetch and the locked
re-fetch is concealed as 404, matching the update and delete handlers.
ACL columns are tracked in the audit table.
- ACL management is available in all editions (no enterprise
entitlement), documented in the MCP servers page. Revoking an ACL does
not retro-strip already-selected configs from existing chats; new
selection is blocked at chat create.
- Force On respects the ACL: the forced set is loaded as the chat owner,
so a `force_on` server whose ACL denies the owner never attaches at
create, send, or generation time.
- No rolling-upgrade machinery: upgrades run in scheduled maintenance
downtime, so the migration only backfills existing rows; the API sets
the Everyone read grant explicitly on every insert.
Stacked on #27943. Part of the MCP org-separation stack.
Closes https://linear.app/codercom/issue/CODAGT-712
UAT: verified on a dogfood instance with two members and a custom group:
Everyone-seed default visibility, group grant with Everyone removal
(non-member loses list/fetch/selection), foreign-principal rejection,
non-admin share denial (audited 403 / concealed 404), user_acl restore,
and audited ACL diffs.
> Mux (AI agent) authored this PR on Mike's behalf.
<!-- mux-attribution: model=claude-fable-5 thinking=high -->
|
||
|
|
299e72ad30 |
feat: audit MCP server config changes (#27943)
Adds enterprise audit logging for MCP server config create, update, and
delete, with strict secret redaction. MCP configs hold credentials
(OAuth2 client secrets, API keys, custom headers), so admin changes to
them need an audit trail.
## Summary
- `enterprise/audit/table.go` gains an `MCPServerConfig` entry
enumerating every column: `oauth2_client_secret`, `api_key_value`, and
`custom_headers` are `ActionSecret` (never appear in diffs); dbcrypt
`*_key_id` bookkeeping, IDs, and timestamps are ignored; the remaining
config fields, including the endpoint URL fields, are tracked so
auditors can see which endpoints a config points at.
- Type registration in `coderd/audit` (diff, request, resource target
with org attribution), `codersdk/audit.go`, and a `resource_type` enum
migration.
- Handlers wire `audit.InitRequest`: create records `New`; update and
delete record `Old` from the param middleware before the
write-authorization check, so a readable-but-not-writable caller
produces an audited 403 while read-denied callers stay concealed as
unaudited 404s.
- Tests: create/update/delete audit entries, write-denied and
delete-denied 403 auditing, cross-org concealment producing zero
entries, and a serializer-level regression test proving none of the
three secret classes can reach a serialized diff.
- Review round: MCP config audit entries link to
`/ai/settings/mcp-servers/{id}`, audit table comments are trimmed per
review, and a fault-injection test pins that a config row surviving a
failed post-discovery credential update still gets its creation audit
entry.
Stacked on #27942 (org-scoped MCP configs). Part of the MCP
org-separation stack.
Closes https://linear.app/codercom/issue/CODAGT-717
UAT: verified on a trial-licensed dogfood instance: audit entries for
the full CRUD lifecycle with correct actor/org/target, redacted secrets
in the update and OAuth2 create diffs, and a full plaintext scan of the
audit dump finding zero secret leaks.
> Mux (AI agent) authored this PR on Mike's behalf.
<!-- mux-attribution: model=claude-fable-5 thinking=high -->
|
||
|
|
443e3b9b80 |
feat!: org-scope MCP server configs with RBAC (#27942)
Moves `mcp_server_configs` from deployment scope to organization scope
so each organization fully controls the MCP servers its members can use
with Coder Agents.
## Summary
- Migration: adds `organization_id` (NOT NULL, FK) and keeps existing
rows as the default organization's originals with credentials intact.
Other organizations start with no MCP servers and configure their own;
nothing is copied across organizations. Chats outside the default
organization keep any now-cross-organization `mcp_server_ids` entries;
the runtime already ignores IDs that do not resolve in the chat's
organization, so no data rewrite is needed. Slug uniqueness becomes
`(organization_id, slug)`.
- RBAC: new org-scoped `ResourceMCPServerConfig` with regosql converter
and `GetAuthorizedMCPServerConfigs`; org admins get in-org CRUD, org
members get read (replaced by ACL evaluation in the follow-up ACL PR in
this stack).
- API: all config routes nest under the organization, matching
templates: `POST|GET
/api/experimental/organizations/{organization}/mcp-servers` and
`GET|PATCH|DELETE .../mcp-servers/{mcpserverconfig}` (plus
`oauth2/connect`), resolved by a read-only param middleware that
conceals read-denied and cross-organization access as 404. Two routes
stay on the frozen `/api/experimental/mcp/servers/{mcpServer}` block:
the OAuth2 callback (the redirect URI baked into existing AS-side client
registrations) and `oauth2/disconnect`, which must remain reachable by
users removed from the organization so they can still revoke their token
grant.
- Chat runtime: selection validation and generation resolve configs
strictly by IDs, enabled state, and the chat's organization in SQL;
requested duplicates are normalized; invalid or cross-org IDs are
rejected with the precise ID list. IDs already persisted on a chat are
exempt from message-time rejection so disabling a selected server never
blocks sends; generation skips servers that are no longer usable.
- Frontend: API layer and admin settings pages target the new endpoints.
The admin page manages the default organization's servers; the org
picker is tracked separately (CODAGT-714).
- Security hardening from review: OAuth user grants are additionally
bound to `oauth2_revocation_url` (changing it invalidates grants, and a
racing OAuth callback gets 409 instead of recreating a grant).
Stack-wide SSRF protection for MCP config-directed traffic was split
into its own PR at the top of this stack (#28242) to keep this diff
reviewable; this PR keeps main's existing discovery IP-range guard.
OAuth2 auto-discovery now completes before the config row is inserted: a
failed discovery persists nothing, and there is no provisional row that
concurrent updates could race against.
Two follow-up PRs in this stack were split out to keep this diff
reviewable: #28064 completes the swagger annotations for the moved
routes (main already ships these experimental MCP handlers unannotated),
and #28065 carries hardening fixes and regression pins on top of the
cutover.
- Force On enforcement (landed on main mid-review) is org-scoped: the
forced set is read per chat organization
(`GetForcedMCPServerConfigsByOrganization`), so another organization's
`force_on` server never attaches to a chat.
## Breaking changes (experimental API)
The MCP server config endpoints move from the deployment-scoped
`/api/experimental/mcp/servers` block to organization-nested paths:
`POST|GET /api/experimental/organizations/{organization}/mcp-servers`
and `GET|PATCH|DELETE
/api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}`
(plus `oauth2/connect`). The old paths are removed, so API consumers
must supply an organization. Two routes intentionally stay on the frozen
`/api/experimental/mcp/servers/{mcpServer}` block: the OAuth2 callback
(its redirect URI is baked into existing AS-side client registrations)
and `oauth2/disconnect` (must remain reachable by users removed from the
organization). These endpoints are under `/api/experimental`, so no
deprecation window is provided.
## Rolling upgrades
During a rolling deploy, an old replica creating an MCP config can fail
the new `NOT NULL organization_id` constraint until it is upgraded
(reads are unaffected: old binaries' generated queries select their own
column lists). This matches the repo's existing precedent for additive
NOT NULL migrations (000562) and affects only the admin config-create
path in the upgrade window.
Upgrades are expected to run in scheduled maintenance downtime with the
database locked during migration, so the migration ships no
rolling-upgrade compatibility machinery. The down migration deletes
organization-created configs (their chat references are cleaned by the
000510 delete trigger) and restores deployment-wide slug uniqueness.
Part of the MCP org-separation stack (CODAGT-711 -> CODAGT-717 audit ->
CODAGT-712 ACLs -> CODAGT-806 token RBAC).
Closes https://linear.app/codercom/issue/CODAGT-711
UAT: validated end to end on a two-org dogfood deployment, including a
real pre-migration to post-migration upgrade, cross-org isolation
(404s), same-slug-two-orgs, chat selection gating, and a live MCP tool
call through the org-scoped generation path. The migration was later
revised to keep existing rows in the default organization only (no
per-organization copies); that revision is covered by the migration test
suite.
> Mux (AI agent) authored this PR on Mike's behalf.
<!-- mux-attribution: model=claude-fable-5 thinking=high -->
---------
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
|
||
|
|
8405bbb26c | feat(site): show best-effort model pricing on the model form (#28290) | ||
|
|
663f41ffa9 |
feat: derive OAuth2 client type from token_endpoint_auth_method (#28043)
Adds an OAuth2 client type (public vs confidential, RFC 7591 §2) derived from the requested auth method instead of hardcoded confidential. The type is stored and guarded here, but no endpoint enforces on it yet; public behavior at the token endpoint follows in the next PR in the stack. - Client type is derived once and reused by both registration and redirect URI validation, so they can't disagree - IsPublic() fails closed: an unrecognized or missing value reads as confidential - RFC 7592 update (PUT) now rejects moving a client between public and confidential (400) instead of silently flipping it when the auth method is omitted - Discovery still doesn't advertise "none"; follows once the token endpoint honors it ### Behavior by client shape `client_type` is derived from `token_endpoint_auth_method` at POST and pinned at PUT. RFC 7592 GET/PUT authenticate with the registration access token, not the client secret, so neither endpoint reads a secret. | Registered with | Stored `client_type` / method | GET reports | PUT that flips the method | |------------------------------------|---------------------------------------|-----------------------|-----------------------------------------| | omitted, or `client_secret_basic` | `confidential` / `client_secret_basic` | `client_secret_basic` | `none` → 400 `invalid_client_metadata` | | `none` (new) | `public` / `none` | `none` | `client_secret_*` → 400 `invalid_client_metadata` | | `none` (before this PR) | `confidential` / `none` | `none` | either → 200, type stays `confidential` | - PUT still replaces every other RFC 7591 field. `client_type` is the only pinned one; the method may move within a type (`client_secret_basic` ↔ `client_secret_post`). - Row 3 is the only shape where the two columns disagree. The guard fires only on a method change that crosses the type line, so those clients keep managing themselves instead of being locked out of their own configuration endpoint. - The token endpoint does not consult `client_type` yet, so every client still authenticates with a secret and registration still issues one. Split out of #27873, second in the stack (on top of #28041). Refs https://linear.app/codercom/issue/ENG-3029/oauth2-support-public-client |
||
|
|
821d91fabd | fix: log tailnet tunnel authorization decisions (#27819) | ||
|
|
d3f08b1983 |
feat: audit chat system instructions changes (#27668)
Adds an audit record for administrative events on the deployment-wide
chat instruction settings (system prompt, the include-default toggle,
and the plan-mode instructions), per CODAGT-719 and operator decision
D5. Each endpoint records under a stable identity: resource type
`chat_instruction_settings`, a fixed resource ID and a human-readable
target ("System prompt", "Plan mode instructions"), so two changes to
one setting share an ID and history-by-setting works. A real change
exports a Write entry with the old-to-new text visible; a
value-identical PUT still upserts and still returns 204 but records
nothing.
Attempts are recorded, not only transitions. Identity is assigned before
the authorization check, so a denied PUT exports a 403 row with an empty
diff (no request content reaches it), a validation failure exports a 400
row, and a write failure exports a 500 row, each with an empty diff; an
operator can tell "nothing changed" from "something changed and capture
degraded" by the status code.
The write path stays authoritative. The advisory lock and, on plan-mode,
the transaction exist only to serve change-detection; if any of that
machinery fails (lock, begin, commit, rollback), the handler runs main's
idempotent write path directly and derives the response from it, so a
member-visible failure of audit-only infrastructure can never replace
main's successful response. Accepted consequence: when the lock cannot
be taken, two concurrent identical writes can produce two rows instead
of one. That is audit degradation, which is allowed; changing a member's
response is not. Write failures keep the exact response the endpoint
produced before this wiring (transaction error for the system prompt,
which was always transactional; the raw write error for plan mode, which
was not), and the full transaction error is logged so rollback failures
cannot vanish.
<details>
<summary>CODAGT-66 plan entry: S1 (verbatim)</summary>
**S1 `feat: audit chat system instructions changes`** (CODAGT-719; base:
main)
- Struct: `database.ChatSystemPromptSettings{ID uuid.UUID; SystemPrompt
string; IncludeDefaultSystemPrompt bool; PlanModeInstructions string}`
in `coderd/database/types.go` (ticket-sketched shape; one struct, both
endpoints).
- Registration: union entry (diff.go), table.go entry (`id`
ActionIgnore, other three ActionTrack), `AuditActionMap` Write-only;
four request.go cases (`ResourceTarget` "", `ResourceID` from struct,
`ResourceType` new enum value `chat_system_prompt_settings`,
`ResourceRequiresOrgID` false with the "Artificial ID / deployment
singleton" comment convention).
- Migration: `ALTER TYPE resource_type ADD VALUE IF NOT EXISTS
'chat_system_prompt_settings';` comment-only no-op down (000558 shape);
number picked at push per the numbering constraint.
- codersdk: constant + prose `FriendlyString` ("chat system prompt
settings"); `TestAuditDBEnumsCovered` forces both. `coderd/audit.go`
presentation switches: rely on safe defaults (no link, generic
description); no FE changes (filter label falls back to capitalized
value; acceptable per precedent).
- Wiring `putChatSystemPrompt` and `putChatPlanModeInstructions`:
InitRequest with Action Write; artificial `ID: uuid.New()` on `New` only
when a change is detected; no-op suppression by leaving both aReq sides
unset (nil resource IDs skip the log, request.go skip rule); the write
path itself stays byte-identical (upserts still run unconditionally).
- `putChatSystemPrompt` (writes two keys conditionally in one existing
tx): inside that tx, read the pair via `GetChatSystemPromptConfig` for
`Old`, perform the conditional writes exactly as today, then RE-READ the
pair for `New`. The re-read is load-bearing:
`include_default_system_prompt` is computed from the toggle row AND the
prompt, so a prompt-only write can flip the effective value without the
request carrying the pointer. `PlanModeInstructions` stays zero on both
sides.
- `putChatPlanModeInstructions` (no tx exists today): wrap its
read-upsert in `InTx` (behavior-preserving: same single write);
`Old`/`New` populate only `PlanModeInstructions`; the two system-prompt
fields stay zero on both sides; no cross-key reads.
- Change detection compares the populated payload fields only (never the
artificial ID).
- Tests: handler-level coderdtest with `audit.NewMock()` asserting Write
entry on change and NO entry on a value-identical PUT, for both
endpoints (this also exercises `ResourceRequiresOrgID` end to end); the
fallback-flip case (no explicit include-default row, nonempty prompt set
to empty, effective boolean flips: entry emitted with the boolean diff);
diff assertions (old->new prompt text tracked, not secret) in
`enterprise/audit/diff_internal_test.go`; `TestAuditableResources`
passes by construction.
- Bookkeeping at PR open: correct CODAGT-719's no-op premise ("matches
the existing 204-on-unchanged behavior" does not exist on main;
suppression is new, write path unchanged).
- Review focus: Old capture and the New re-read inside the tx (three of
four existing singletons never set Old; do not copy them; and the
computed include-default value makes a naive New construction wrong);
the skip-on-no-op mechanism; prompt text deliberately visible in diffs.
</details>
Note: the plan excerpt above predates operator decision D5 (2026-07-30),
which this PR implements: the resource type is
`chat_instruction_settings` (not `chat_system_prompt_settings`), each
setting carries a stable ID and a display-name target (not a per-write
artificial ID and an empty target), no-op suppression runs through
`InitRequestWithCancel` (not the nil-ID skip), and attempts (denied,
failed, capture-degraded) record rows with real statuses and empty
diffs. Ticket bookkeeping for CODAGT-719 was corrected on Linear at
kickoff: the ticket's "matches the existing 204-on-unchanged behavior"
premise does not exist on main; suppression is new, and the write path
is unchanged.
> 🤖 This PR was created with the help of Coder Agents, and _will be_
reviewed by a human. 🏂🏻
---------
Co-authored-by: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
|
||
|
|
affeeaf9c8 |
feat: extend agent chat MCP tools for remote UAT evidence loops (#28233)
Extends the Agent-chat MCP tools so an unattended UAT evidence loop can
fetch artifacts, monitor long runs, and find prior runs without burning
model context.
## Backend
- New `chat_files_token` crypto key feature (migration 000571) with
rotator support and a dedicated signing keycache on coderd.
- `POST /api/experimental/chats/files/{file}/download-url`
(authenticated) mints a short-lived (5 min) signed URL and returns it
with `sha256`, `size_bytes`, `name`, `mime_type`, and `expires_at`.
- `GET /api/experimental/chats/files/{file}/download?token=` (no session
token) redeems the signed URL: verifies the JWS, requires the token's
`file_id` to match the path, and re-checks the minting user's RBAC
access live at redemption. Clients can `curl -o` artifacts with zero
credentials in the URL consumer.
- `ChatFileMetadata` gains `size_bytes` (via `octet_length`, no bytes
fetched).
## MCP tools (`codersdk/toolsdk`)
- `coder_download_chat_file`: by `file_id` or `chat_id`+`file_name`;
returns the signed URL plus checksum and size instead of base64.
- `coder_await_chat`: blocks (bounded `wait_secs`, 1-120) until a chat
leaves `running`/`interrupting`, using the existing watch stream with
subscribe-before-read.
- `coder_list_chats`: label, query, and limit filtering; chat
projections now include labels.
- `coder_get_chat_messages`: `after_id` forward cursor with
`next_after_id` (exact incremental reads), plus per-message `files`
metadata so artifact-bearing messages are identifiable.
- `coder_get_chat`: file listings now include `size_bytes` and
`created_at`.
- `coder_list_templates`: exposes `agents_allowed` for pre-flight
checks.
## Testing
- coderd: mint/redeem happy path with an unauthenticated client,
expired/tampered/file-mismatched tokens, auth still required on the
plain file endpoint, non-owner mint rejection.
- toolsdk: harness + integration coverage for all new/changed tools,
including signed-URL redemption with checksum verification,
forward-cursor exactness, await transition/timeout paths, and label
filtering.
- Remote dogfood UAT (dev.coder.com Coder Agent) passed all six
acceptance scenarios end to end over both MCP transports.
Note: `go test ./codersdk/toolsdk/` has a pre-existing goleak flake on
main (leaked `agentssh` non-PTY session goroutines from SSH exec tests;
reproduced 3/3 on clean `b4971bc49f1`). It is unrelated to this diff.
> Mux acted on Mike's behalf to create this PR.
<!-- mux-attribution: model=claude-sonnet-4-6 thinking=high -->
|
||
|
|
7724ee281a |
feat: defer MCP tool schemas behind a find_tools search (#28225)
## Summary When the `mcp-tool-search` experiment is enabled, chatd stops inlining connected MCP tool schemas into every generation. It instead exposes a built-in `find_tools` tool whose description carries a compact catalog of the deferred tools, and only ships full JSON schemas for tools the model has activated by searching or by calling them directly. Closes [CODAGT-760](https://linear.app/coder/issue/CODAGT-760). ## Problem Tool-heavy agent configurations (GitHub, Linear, Notion, and dev-tooling MCP servers) inline over 100k tokens of tool schema definitions into every generation. Initial uncached requests reached ~216k tokens with time-to-first-token close to nine minutes, while the model typically invokes only a handful of tools per turn. ## How it works - `decideMCPToolSearch` defers external and workspace `.mcp.json` MCP tools whenever the experiment is enabled. Native, dynamic, provider, skill, and transport tools are never deferred. - `find_tools` embeds a server-grouped catalog in its tool description (degrading to names-only, then counts-only, then a constant-size summary past a context-scaled size cap) and scores keyword matches across tool names, descriptions, parameter schemas, and server metadata. Queries can scope to one server with a `server:` prefix, and exact `names` arguments always activate. - Activation state is ephemeral: it is re-derived each generation from surviving chat history (`find_tools` results and direct calls to deferred tools), so activations naturally lapse when compaction summarizes them away. Aggregate activated schema weight is capped at 10% of the context window, shedding the least recently activated schemas first; `find_tools` shares that budget across parallel calls in one step. No new persistence. - Deferred tools stay registered for execution, so the model can call a cataloged tool directly without searching first; the schema is activated for subsequent steps. - Fail-open: the experiment being disabled, an empty candidate set, or an MCP tool named `find_tools` all disable deferral, leaving today's behavior byte-identical on the wire. - Prometheus counters/histograms track `find_tools` calls, matches, activations, and deferred token weight. - The conversation timeline renders `find_tools` calls with a collapsed search summary and expandable match list, falling back to the generic renderer on malformed payloads. ## Validation - Unit tests for the catalog, matcher, experiment-gated decision, and activation derivation; end-to-end chatd generation tests covering search-then-call, direct-call activation, experiment-off wire parity, compaction lapse, and subagent tool gating. - Storybook interaction tests for the timeline rendering and malformed-payload fallback. - Remote dogfood UAT on dev.coder.com passed: deferral with a real MCP server and Anthropic model, direct calls without prior search, activation persistence across turns, experiment-off parity, and clean UI/console. > Disclosure: Mux (AI agent) authored this PR on Mike's behalf. |
||
|
|
119f2b1dd9 |
feat: limit concurrent chat agents with pooled admission (#27902)
Limits concurrent chat generation on capped deployments to 5 root chats and 10 delegated subagent chats. The pools are deployment-wide and independent, so delegated work can continue while root capacity is full. The default caps live in AGPL code. Enterprise contributes only a licensing unlock, so unlicensed deployments stay capped and cannot fail open. Licensed deployments are uncapped while Agent Hours usage stays below an explicit hard limit. Deployments without a hard limit remain uncapped, and reaching the Agent Hours allocation only triggers warnings. Admission happens before a worker takes chat ownership. Capped deployments serialize admission across replicas with a transaction-scoped advisory lock and derive active and queued state from current ownership plus fresh runner heartbeats, rather than persisted queue markers or per-replica state. The acquisition query returns a bounded, pool-interleaved candidate set instead of ranking the whole backlog; a migration replaces the acquisition index with a pool-aware one. Refused chats stay running but unowned, and interrupt requests bypass admission so users can stop queued or over-cap chats. The single-chat API derives `queued_for_capacity` from live pool state; list endpoints do not report it. The UI polls that value every 5 seconds while a chat is running and shows a callout when the chat is waiting for capacity. Updates the administrator documentation and deployment-wide Prometheus gauges for active and queued agents. Replica-level values must be aggregated with `max`, not `sum`. > Mux updated this PR on Mike's behalf. |
||
|
|
995d7fe31b |
feat: add per-license Products section with Coder Agents price gates (#28051)
<img width="1100" height="312" alt="Screenshot 2026-08-17 at 3 43 51 PM" src="https://github.com/user-attachments/assets/30d21467-93ec-4880-a430-ccd4b494b8f5" /> Each license card now always expands to a **Products** section: a Coder Workspaces box showing active seat usage, and, on Premium licenses, a Coder Agents box driven by the `agent_runtime_hours_*` license claims and the merged `agent_runtime_hours` entitlement. The card header gains a **Type** column (`Trial`/`Standard`), and the left header label now shows the feature set only (`Premium`/`Enterprise`). The Coder Agents box renders five states: no allocation (dashed purple upgrade CTA), unlimited allocation (`-1` sentinel), normal usage, allocation exceeded (red border and red "Agent hours exceeded" status; concurrent chats stay Unlimited), and hard limit exceeded (red "Hard limit exceeded" status; concurrent chats capped at 5, mirroring the backend's `maxConcurrentRootAgents`, which is not exposed via the API). Usage and overage indicators only render on the license whose allocation matches the merged entitlement and which is currently effective, following the existing AI Governance winning-license pattern via a generalized `isLicenseApplicableForFeatureUsage` helper; AI Governance add-on behavior is unchanged. Stacked on #27985 (base branch `runtime-hours-entitlements`); do not merge before it. Notes for review: - #27985 now grandfathers claim-less Premium licenses into a zero-hour `agent_runtime_hours` allocation, so the merged entitlement (disabled, `limit: 0`) and its measured `actual` are always present for Premium deployments. The upgrade card's "Agent hours used" row therefore renders universally; the `Premium` story pins that state. - Agent hours usage now renders with exactly one decimal (e.g. `16,264.3`, `42.0`), derived from #27985's new `actual_ms` field and floored to tenths with integer math. The same floored value drives the exceeded checks, so the displayed number and the red state flip at the same instant; a fraction past the allocation now trips "Agent hours exceeded" (`20,000.1 > 20,000`), pinned by the `PremiumWithAgentHoursExceededByFraction` story. The allocation denominator stays whole (it comes from the whole-hour license claim). |
||
|
|
fa8ffe4eda |
feat: report agent runtime hours usage in entitlements (#27985)
Populate `FeatureAgentRuntimeHours.Actual` on every entitlements refresh for licenses that grant the feature. A new `GetTotalUsageHBAgentRuntimeV1` query sums `runtime_ms` over the license's usage period, reading `usage_events` directly: `hb_agent_runtime_v1` is exactly one row per hourly bucket deployment-wide with `created_at` at the bucket start, enforced by the unique partial index introduced in #27983. The measurement reuses the shared `measureUsage` policy from #27984 through a new `AgentRuntimeMsFn` closure (usage publisher subject): failures publish the stable `LicenseAgentRuntimeUsageUnavailableErrorText` and log the cause. Usage is floored to whole hours, matching the unit of the `agent_runtime_hours_*` claims, and at most one warning is emitted per refresh: reaching the allocation supersedes the advisory soft limit. The dashboard renders the soft-limit advisory muted without a sales link and treats the runtime usage-unavailable text as a diagnostic. **Precise usage.** `Feature.ActualMs` (JSON `actual_ms`), set only for `agent_runtime_hours`, carries the exact stored milliseconds backing the floored `Actual` so clients can render fractional hours (e.g. `10.3`). It has the same freshness as `Actual`; the whole-hour warning thresholds are unchanged. **Unlimited licenses.** A license minted with the unlimited (`-1`) allocation decodes to an enabled feature with a nil `Limit` (#27984), so the warning write-back now guards the allocation dereference: no thresholds can exist for an unlimited license, so no runtime hours warning is ever emitted, while `Actual` is still measured and published. `Feature.Compare` is unchanged; for usage-period features the issued-at/end dates decide first, so a metered feature outranks an unlimited one only on an exact timestamp tie, an edge pinned by a `TestFeatureComparison` case and documented on `decodeAgentRuntimeHours`. **Grandfathered premium licenses.** Premium licenses without `agent_runtime_hours_*` claims are now granted the feature disabled with a zero limit over the license term, identical to an explicit `allocation: 0`: usage is measured and published for every Premium deployment, and chatd's pooled admission (#27902) caps concurrent agentic chats until a license with a positive allocation is added. The default carries a fixed early `UsagePeriod.IssuedAt` (2026-08-01, the same mechanism as the managed-agents default) so any license actually carrying the claims outranks it in the `AddFeature` merge regardless of the licenses' relative issue dates; the constant must stay earlier than the earliest legitimately issued claim-bearing license. Zero allocations (explicit or grandfathered) emit no deployment-wide warning banner: those deployments are steered by the in-page upgrade CTA and the concurrency cap. Enterprise licenses are unchanged. Part 3 of a 3-PR stack splitting up #27796 (see there for review history). Stack: #27983 → #27984 → this PR. Closes CODAGT-852. |
||
|
|
d15800b494 |
feat: tolerate unusable runtime hours claims and decode -1 allocation as unlimited (#27984)
Two coupled changes to the license/entitlements layer, preparing for runtime-hours usage reporting. **Tolerate unusable runtime hour claims.** Unusable `agent_runtime_hours_*` claim combinations no longer reject the whole license: rejecting a signed license over a cosmetic threshold claim would drop the deployment to unlicensed. `decodeAgentRuntimeHours` drops the unusable claims, surfaces the stable `LicenseAgentRuntimeHoursClaimsIgnoredWarningText` (deduplicated across licenses), and logs the affected license and claims through the new `FeatureArguments.Logger`; `validateAgentRuntimeHours` and its license-invalidating errors are removed. The dashboard recognizes the stable diagnostic text and renders it muted, with a "License notices" heading instead of the exceedance heading and without a sales link. **Unlimited allocation.** An `agent_runtime_hours_allocation` claim of exactly `-1` (`AgentRuntimeHoursUnlimitedAllocation`, mirrored in coder/license) is reserved to mean unlimited: it decodes to an enabled feature with no `limit` in `/api/v2/entitlements`, the shape the UI already renders as "Unlimited". Threshold claims alongside it have nothing to threshold against, so they are dropped with the claims-ignored warning, and any other negative allocation remains unusable. The issuer-side counterpart (refusing to mint `-1` together with threshold claims) is coder/license#49. The managed agent measurement path is intentionally untouched: managed agents are deprecated and slated for removal, so the shared usage-measurement failure policy (`measureUsage`) now lands in #27985 next to its runtime-hours consumer instead of converting a doomed call site here. Part 2 of a 3-PR stack splitting up #27796 (see there for review history). Stack: #27983 → this PR → #27985. |
||
|
|
b5d18bb9c9 | feat: add redirect URL override for external auth (#28082) | ||
|
|
521c383f6b |
fix: repair stale chat agent bindings after workspace rebuild (#28152)
## Problem When a chat is bound to a workspace, chatd persists `chats.agent_id` pointing at a specific workspace agent, and it only rebinds on the next chat turn. A workspace stop/start creates a new agent with a new ID in the latest build, so the chat page resolves the stale agent ID to `undefined` and the right sidebar silently drops Terminal, Desktop, Browser, apps, and ports even though the workspace is running. The existing read-time enrichment only filled nil agent IDs and skipped stale non-nil ones, so refreshing did not help until the user sent another message. ## Fix - `coderd/exp_chats.go`: single-chat reads now repair agent IDs that no longer resolve in the workspace's latest build, using the same `agentselect.FindChatAgent` selection chatd uses. A repaired binding also carries the latest build's ID so the response never pairs the new agent with the previous build. Bindings that still resolve are preserved, and repair stays best-effort and response-only (no write-on-read). List reads keep the previous nil-fill-only behavior because validating existing bindings would cost a per-workspace authorization lookup per listed chat. - `site/src/pages/AgentsPage/AgentChatPage.tsx`: the workspace watch update handler detects when a running workspace's latest build no longer contains the chat's bound agent and invalidates the chat query once per chat/build/binding key for immediate recovery, and the chat query polls every 30 seconds while the binding remains unresolved so a transiently failed repair retries even when an idle workspace publishes no further watch events. The watch stream replays the current workspace on every (re)connect, so this covers rebuilds that happen while the page is open or disconnected; page loads are covered by the server-side repair. The workspace-watcher bailout now also keys on `latest_build.id` so a rebuild propagates while the page is open. - `site/src/api/queries/chats.ts`: chat watch events replay the persisted (pre-repair) binding, so the summary merge adopts a snapshot's `build_id` only when the snapshot agrees on `agent_id`, keeping the repaired agent/build pair atomic in the caches. ## Testing - `go test ./coderd -run TestEnrichChatAgentIDs` covering repair, keep-valid, selection-error, list-mode-skips-bound, and no-workspaces cases. - Storybook interaction story `RecoversSidebarAfterWorkspaceRebuild` exercising the watch-event to chat-refetch to sidebar-recovery flow (verified red without the invalidation, green with it). - `pnpm test AgentChatPage.test.ts` covering the binding-resolution predicate. > Mux created this PR on Mike's behalf. |
||
|
|
2d9b6eda8f |
feat: add experimental CLI to price unpriced AI models (#27926)
## Description AI Gateway computes the cost of an interception from `ai_model_prices`, which is seeded on every server start from a price book embedded in the binary. A model the price book does not cover records a NULL cost, so its spend is invisible to cost reporting and is not enforced against budgets. The only fix was to wait for a Coder release that added the model. This adds an experimental CLI, backed by an experimental HTTP endpoint, for pricing those models. Models the price book already covers are rejected, because the seeder re-applies the book on every start and would overwrite an operator price. Support for custom pricing will be handled in https://linear.app/codercom/issue/AIGOV-589/extend-experimental-cli-command-to-set-custom-ai-model-prices. ## Commands ``` coder exp ai-model-prices list [--provider] [--model] coder exp ai-model-prices update [file|-] [--provider] [--model] [--input-price] [--output-price] [--cache-read-price] [--cache-write-price] [--yes] ``` ## Changes - Add `GET` and `POST /api/experimental/ai/model-prices`, gated behind the AI Bridge entitlement and the existing `ai_model_price` RBAC resource. - Add a `GetAIModelPrices` query with optional `provider` and `model` filters applied in SQL. - Validate the whole request before writing anything, so one bad entry cannot leave the table half updated, and report every problem at once. - Reject prices for models the embedded price book already covers, through a new `prices.IsDefaultPriced`. - Add the `coder exp ai-model-prices` command with `list` and `update`. `update` accepts a JSON document or the single-model flags and prints a plan, asking to confirm unless the document is piped in or `--yes` is passed. - Consolidate the supported provider list into `coderd/aibridge/prices/providers` so the price generator and the server share one definition. - Add `codersdk` types and client methods for both endpoints, and bound the request body at 1 MiB. - Document the command in the AI Gateway cost controls page. Closes https://linear.app/codercom/issue/AIGOV-567/experimental-cli-command-to-set-prices-for-unpriced-ai-models > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira |
||
|
|
d509e1e6a0 |
fix(site): include owner context in Agents org picker permission check (#28076)
Fixes the Agents org picker and workspace attach menu for users whose only chat grant comes from the member-scoped "Coder Agents User" (`agents-access`) org role (PRODUCT-552). ## Problem The per-org authcheck behind the org picker checked `chat:create` with only `organization_id`. The `agents-access` role grants chat permissions at org-member scope, which requires the checked object to be owned by the caller (`policy.rego` `org_member` requires a non-empty owner matching the subject). With no `owner_id`, every org check returned `false`, so: - the org picker never rendered (`permittedOrgs.length > 1` gate), - the form stayed pinned to the default org, - the workspace attach menu, filtered to that org, showed "No workspaces found" even though the user had workspaces in another org. The page-level `createChat` check already includes `owner_id: "me"`, which is why the same user could load the page and create chats via the API. ## Fix Pass `owner_id: "me"` in the `permittedOrganizations` seed check in `AgentCreateForm`, matching the page-level check's semantics. The `permittedOrganizations` helper spreads the check object through, so each per-org check now carries owner context and the backend substitutes the caller's user ID. The other `permittedOrganizations` callsites (`organization_member:create`, `template:create`) check org-scoped admin permissions and correctly omit `owner_id`. Adds a regression story whose `checkAuthorization` mock only allows checks carrying `owner_id: "me"` (mirroring the RBAC member-scope behavior); it fails without the fix and passes with it (red-green verified). ## Validation - Red-green verified regression story: fails without the fix (picker not found), passes with it; all 31 stories in the file pass. - `pnpm -C site check` and `pnpm -C site lint:types` clean. - Remote dogfood UAT (dev.coder.com chat [97be7f39](https://dev.coder.com/agents/97be7f39-4688-45ee-be3d-24bc4f6f8046)): PASS on all acceptance criteria at this exact commit. Reproduced the bug scenario end to end (two orgs, non-admin user with only the "Coder Agents User" role in both, workspace only in the second org): the org picker renders, the second org's workspaces appear in the attach menu, and chat creation succeeds with a real model. Single-org and admin behaviors unchanged. Authcheck probe documents the backend semantics: `chat:create` with `owner_id: "me"` returns true, without it returns false. > Mux acted on Mike's behalf for this PR. |
||
|
|
1458d27d78 |
fix: allow manual chat compaction from the error state (#28022)
A chat that fails generation with a context overflow (for example `Input
length 262625 exceeds the maximum allowed input length of 262112
tokens`) is stuck in a catch-22: `POST /chats/{id}/compact` returns 409
because the `RequestCompaction` transition is only allowed from the
waiting state, and the only other way out of the error state is sending
or editing a message, which re-runs generation with the same oversized
prompt and fails again. Compaction is exactly the recovery a
context-overflowed chat needs, and it is unreachable exactly when it is
needed.
Three semantic changes:
- Allow `RequestCompaction` from the error states: `E0 -> R0` and `E1 ->
R1` (queued messages are preserved and processed after the compaction
turn).
- Clear `last_error` in `Tx.RequestCompaction`, matching the
architecture rule that transitions leaving `E0`/`E1` clear the stored
error. Without this a successful compaction would land in waiting with a
stale persisted error.
- Grant the compaction turn a fresh history epoch: a
`grant_history_epoch` flag on `UpdateChatExecutionState` sets
`history_version = snapshot_version`, resets `generation_attempt`, and
clears `retry_state` in the same atomic update that clears `last_error`
(mirroring the `chat_messages` trigger postcondition). The transition
inserts no history, so without this the turn inherits the failed turn's
spent retry budget, and resetting the counter alone could collide with
message part episode keys still retained on the erroring replica.
No frontend change is required: the chat input is already enabled in the
error state and `/compact` submission already handles both the success
and 409 paths. Also updates ARCHITECTURE.md (transition matrix,
endpoint, and manual compaction sections), the endpoint's swagger
description, and SDK comments.
> Mux created this PR on Mike's behalf.
<!-- mux-attribution: model=claude-sonnet-4-6 thinking=high -->
|
||
|
|
c424a76a12 | feat: wire chat search box to full-text search (#27973) | ||
|
|
87fdd2b59b |
feat(site): add server-side search and pagination for groups page (#27994)
Repeat of #27604, which I had mistakenly manually merged into #27603, instead of letting Graphite handle it after #27603 merges into main. Reattempting with GitHub stacks now instead of Graphite! See #27604 for details/review. |
||
|
|
6e07e2610f |
feat: add paginated API endpoint for groups (#27603)
backend-only changes from #27271; see that PR for summary of changes + implementation details |
||
|
|
9a57dfa642 |
feat: include agent metadata in workspace list responses (#27934)
Closes #27933. Related: #27897 (single-agent GET). Agent metadata is only readable via a per-agent watch stream, so reading it across N workspaces costs N+1 requests. This adds a batch read to the list endpoint: ```text GET /api/v2/workspaces?q=param:"pool=demo" include_agent_metadata:task_status ``` - New `include_agent_metadata` search key, repeatable and key-scoped. It expands the response, it does not filter workspaces. - `GetWorkspaces` aggregates the requested keys as JSON behind a `CASE`: without opt-in the response is unchanged and the subquery never runs. Runs only for the returned page, inside the same authorized query. - Agents in the response gain `metadata` (`[]codersdk.WorkspaceAgentMetadata`, `omitempty`), mapped by the `workspace_agent_id` each element carries. The collection script is omitted; it can be long. - `codersdk.WorkspaceFilter` gains `IncludeAgentMetadata []string`. - No wildcard, no schema change, no migration. --- Authored by Coder Agents on behalf of @Emyrk. |
||
|
|
192842c8f2 | fix(site/src): treat chat deleted watch events as archive instead of eviction (#27921) | ||
|
|
19fdc23d63 | fix(site/src): fan chat message upserts out to every containing page (#27912) | ||
|
|
f9047e5836 |
feat(site/src): reconcile chats-by-workspace cache across archive and watch paths (#27901)
Implements Phase 2 item 5 of the chats query architecture: by-workspace cache reconciliation. Stacked on #27892; base branch `feat/chat-search-invalidation`. ## Problem `chatsByWorkspace` mappings (flat `Record<workspaceId, chatId>`, IDs only, archived chats filtered server-side) go stale on archive/unarchive, workspace binding changes, and watch events (FINDINGS 2.3). Because the client cannot re-derive archived state from the cached map, the only correct repair for a stale mapping is synchronous removal plus family invalidation. ## Fix - New `removeChatFromChatsByWorkspace(queryClient, chatId)` in `site/src/api/queries/chats.ts`: value-match removal across the by-workspace family, reference-preserving when nothing is removed (mirrors the `patchChatMessages` no-op pattern). - `archiveChat.onSuccess` and the AgentsPageLayout archive-and-delete `onSuccess` synchronously remove the mapping; the existing `onSettled`/explicit invalidations then reconverge. - Watch handler: `deleted` branch removes then invalidates (remove-before-invalidate ordering); `created` root branch invalidates; merge path invalidates behind a new `shouldInvalidateChatsByWorkspace` predicate (`status_change`, `action_required` only; `created`/`deleted` have their own branches, title/summary/diff/context events do not move `updated_at` ordering); `onOpen` reconnect invalidates for convergence. - `useChatToolInvalidations`: workspace-binding tool completion (`create_workspace`) also invalidates by-workspace; this is the only reconciliation path on the embed route. ## Confirmed exclusions - No optimistic patch in `updateChatWorkspace.onMutate`; the awaited `onSettled` invalidation already converges. - No cancellation guard for the by-workspace family (Phase 2 item 9 territory). - No `createChat` change (already invalidates); no ACL or pin changes. ## Known constraints - Cascade archives remove only the event's own chat ID from the mapping; per-family-member `deleted` events plus family invalidation repair the rest. - The REST `patchChat` workspace-rebind branch publishes no watch event server-side, so cross-session manual rebinds converge only via the acting session's `onSettled` or a WorkspacesPage remount; not fixable client-side. ## Testing - `chats.test.ts`: removal scoping and reference-preservation tests; `it.each` wiring tests (archiveChat/unarchiveChat/updateChatWorkspace onSettled, createChat onSuccess invalidate by-workspace); synchronous-removal assertion for `archiveChat.onSuccess`; negative assertions for `updateChatTitle` and `createChatMessage`. - `AgentsPageLayout.test.ts`: exhaustive `ChatWatchEventKind` table for `shouldInvalidateChatsByWorkspace`. - `useChatToolInvalidations.test.tsx`: create_workspace regression test extended with a seeded by-workspace bystander. PR generated by Coder Agents. |
||
|
|
632eecc090 |
feat(site/src): reintroduce chat search cache invalidation (#27892)
Stacked on `feat/chat-cache-semantic-ops`. ## Bug Chat search results (`chatSearch` queries) were never invalidated, so the search dialog served stale results after archives, renames, deletions, new chats, message edits, and watch-driven status changes. ## Fix Reintroduces `invalidateChatSearches`, a prefix invalidation over the module-private `chatSearchFamilyKey`, and wires it into: - `chats.ts`: `archiveChat.onSettled`, `unarchiveChat.onSettled`, `updateChatTitle.onSettled`, `editChatMessage.onSettled`, `createChat.onSuccess` - `useChatStore.ts`: `upsertCacheMessages` (unconditional; assistant message bodies are indexed too) and `replaceCacheMessages` - `AgentsPageLayout.tsx`: the `deleted` and root `created` watch branches, the merge watch branch (gated by a new exported `shouldInvalidateChatSearches` helper), the `has_unread` clearing effect, the `onOpen` reconnect convergence, and `archiveAndDeleteMutation.onSuccess` The merge-branch gate only invalidates for search-affecting event kinds (`title_change`, `status_change`, `diff_status_change`, `action_required`). `summary_change`, `chat_summary_change`, and `context_dirty` are excluded: stale `last_turn_summary` subtitles are accepted until reconciliation lands. ## Backend constraint Message bodies only enter full-text search via the dbpurge backfill (`search_tsv` starts NULL and is populated every 10 minutes). Frontend invalidation fixes removals, ordering, and rendered fields immediately, but a chat that newly matches on message body will not appear until the next backfill. This is a server-side eventual-consistency limit we accept. ## Scope decisions (confirmed) - No invalidation in `createChatMessage.onSuccess`: the send path already routes through `useChatStore.upsertCacheMessages`; adding both would double-invalidate every send. - No invalidation for `pinChat`/`unpinChat`/`reorderPinnedChat` (ordering-only, self-heals). - ACL mutations out of scope. - No coalescing/debouncing; that belongs to a later reconciler PR. ## Tests - Prefix invalidation: multiple distinct `q` params invalidated, bystanders (list, by-workspace, entity, messages, cost tree) untouched. - Mutation wiring: settlement of `archiveChat`, `unarchiveChat`, `updateChatTitle`, `editChatMessage`, and `createChat` invalidates a seeded search key; `createChatMessage` asserted NOT to. - `shouldInvalidateChatSearches` unit-tested over all `ChatWatchEventKind` values. PR generated by Coder Agents. |
||
|
|
f96671d588 | refactor(site/src): centralize chat cache operations in semantic cache functions (#27869) | ||
|
|
6c8a8647f6 |
fix(site): name template versions in promote/archive dialogs and toasts (#27633)
> [!NOTE] > These were previously returning as typed values in the `api.ts`, however, they were not actually typed in this way and updating them wouldn't have worked for the `promotion` as it would fall back to needing to validate against the template. Archive toasts showed `"undefined"` because the API does not return a `TemplateVersion`. Toast/dialog copy now comes from the selected version. - Hold the full `TemplateVersion` for promote/archive confirms (global pattern elsewhere) - Name the version in confirm dialogs and success/error toasts - Type archive/unarchive API helpers as `Promise<void>` <img width="772" height="152" alt="image" src="https://github.com/user-attachments/assets/0063ebdc-09a3-4041-bfa0-d4c70b57159b" /> |
||
|
|
d2f9280138 |
chore: remove legacy chat template allowlist (#27515)
Relates to CODAGT-713 Depends on #27514 Removes the legacy deployment-wide allowlist now that the API and frontend use per-template `agents_allowed`: the experimental `/template-allowlist` routes, SDK methods and generated types, site config queries, frontend bindings, and the now-unused `xjson` utility. Migration `000563` deletes the obsolete `agents_template_allowlist` value. It's irreversible for deployments that configured an allowlist, which I think is fine, since `000562` already drops `agents_allowed` on the way down, and this release ships `000548` and `000555` with the same property. Two side effects of the model change worth writing down, both from #27514 rather than here. The value used to need `ActionRead` on `ResourceDeploymentConfig` to read and deployment config update to write. `AgentsAllowed` is now a plain field on the template response, readable by anyone who can read the template, and it's set with a template update, so org admins manage it themselves. That's the delegation we wanted, and it's tracked in the audit log. The rest of the stack adds `--agents-allowed` to the CLI and updates the platform controls docs. |
||
|
|
01c00ce1bf |
feat(site): add per-template Coder Agents controls (#27514)
Relates to CODAGT-713 Depends on #27285 This replaces the deployment-wide template allowlist UI with per-template Coder Agents controls. Each template's General Settings page exposes `agents_allowed` to organisation and template administrators through the existing template-update permission, whilst AI Settings > Templates gives deployment administrators a bulk management page. The bulk page requires both deployment-config and template-update permissions, reuses the same URL-backed server-side filter as `/templates`, tracks pending updates independently for each template, and surfaces failures through template-specific toasts. The copy refers to new workspace creation because disabling a template does not revoke an existing Coder Agent workspace. This completes the frontend migration away from the experimental `/template-allowlist` API. #27515 removes the now-unused legacy storage, routes, SDK methods, generated types, and JSON utility. Later PRs add CLI flags and update the platform controls documentation. The stack is intended to merge as a unit. |
||
|
|
0ac23e3ee1 |
feat: add per-template Coder Agents access control (#27285)
Relates to CODAGT-713 Depends on #27284 This makes the per-template `agents_allowed` field authoritative in the API and chatd. It adds optional create and metadata update fields with the intended default and omission semantics, supports `agents-allowed:` template search, includes the value in telemetry, and makes `list_templates`, `read_template`, and `create_workspace` read the template row directly. Existing-workspace retries remain idempotent, and blocked same-organisation templates return an actionable message. The experimental `/template-allowlist` routes remain temporarily because the shipped AI Settings page still calls them, but they no longer control chatd enforcement. #27514 moves that page to per-template metadata, #27515 removes the legacy storage, routes, SDK types, and utility, #27517 adds the CLI flags, and #27518 updates the platform controls documentation for the per-template model, directly addressing CRF-5 and CRF-6. The stack is intended to merge as a unit. |
||
|
|
4b9880afa6 |
feat: add --chat-hook-allow-insecure to allow plain HTTP chat hook URLs (#27896)
Adds a hidden `--chat-hook-allow-insecure` / `CODER_CHAT_HOOK_ALLOW_INSECURE` deployment option (default `false`) that allows the chat lifecycle hook URL to use plain HTTP for any host. The HTTPS requirement is enforced at two points, and the flag relaxes both: `DeploymentValues.Validate()` rejects `http` hook URLs at startup, and the hook dispatcher's `validateHookURL` allows `http` only for loopback hosts. With the flag set, any-host `http` is accepted; the host, fragment/userinfo, secret, and timeout checks are unchanged, and non-http(s) schemes still fail. This removes the need for an HTTPS reverse proxy when testing a hook consumer on a trusted network. Following security review feedback, the flag description and docs state that plain HTTP lets an on-path attacker forge hook responses (which control agent execution), and `coder server` logs a startup warning (with a redacted hook URL) when hooks run over plain HTTP. Docs, generated API types, and the server config golden are updated accordingly. > Mux acted on Mike's behalf to create this PR. |
||
|
|
0c88c2accc | refactor(site): migrate chats query keys to collections/entities taxonomy (#27841) | ||
|
|
9dcb75cd56 |
chore: add docs inline-HTML linter and backtick generated placeholders (#27399)
## What Adds CI enforcement that fails when docs Markdown contains invalid inline HTML the docs site silently drops or mangles, and fixes the remaining generated-doc placeholders at their source. This is the tooling half of the docs-HTML audit. The hand-written fixes it guards landed in #27298 (kept small and separate so it reviewed fast); this PR carries everything that touches code, CI, or generated output. ## Changes **Linter (`scripts/docshtmlcheck`), wired into `make lint` via `lint/docs-html`.** Markdown-aware: parses each file with goldmark and inspects only raw-HTML nodes, so angle brackets in fenced code blocks, inline code, HTML comments, and `<https://…>` / `<user@host>` autolinks are ignored. Flags swallowed placeholders (`<region>`), void-element end tags (`</br>`), unregistered or incorrectly capitalized component tags (`<Image>`), and unclosed container tags (a `<div class="tabs">` that leaks its wrapper). The one intentional renderer component, `<children>`, is allowed but still balance-checked. **Generator-source placeholder fixes (regenerated via `make gen`).** - `codersdk/chats.go`: backtick `<server>__` in the `ChatContextTool.Name` doc comment (it becomes the Swagger description, so it was swallowed in `reference/api/{chats,schemas}.md`). - `codersdk/deployment.go`: backtick `<region>` in the AWS Bedrock region flag help (swallowed in `reference/cli/server.md`); also updates `coder server --help` output and the golden files. **Temporary allowlist.** `docs/reference/cli/agent-firewall.md`'s `<host>` / `<glob>` come from the external `github.com/coder/boundary` CLI help (still `v0.10.0` on `main`), so they are suppressed on that one file. The suppression is self-clearing: if an allowlisted tag stops appearing on a scanned file, the linter reports `stale-allowlist-entry` and fails until the dead entry is removed, so a dead entry cannot silently mask a later regression of that tag on that page. (An entry whose file is deleted outright is never rescanned, but a missing file yields no findings, so nothing hides behind it either.) ## Review feedback addressed This tool + generator work was reviewed by Coder Agents Review while it was bundled into #27298. Addressed here: - **P1:** tokenize each raw-HTML node as a whole instead of per source line, so a tag whose attributes wrap across lines is no longer torn in half. This fixes both the missed multi-line unclosed `<div>` (a leaked wrapper that passed with exit 0) and the spurious `stray-end-tag` on valid multi-line tags. Each token maps back to its own source line. - Normalize allowlist lookup/report paths to a canonical repo-relative form, so the escape hatch no longer silently misses under absolute / `./` paths. - Route generated-page findings to the generator source. - Add `<search>` to the allowed set; reword the unknown-element message to note that a real element can be added to `allowedElements`. - Self-clearing allowlist guard (above); rename `optionalEndTag(s)` and `kindUnclosed(Tag)`; adopt `slices`/`maps` idioms; move the lint banner to the Makefile recipe; stop aliasing the input slice in `filterAllowed`. - New tests: multi-line tokenization (both classes), interleaved nesting, a pinned line number, `collectMarkdown`, and the stale-allowlist guard. ### Round 2 (Coder Agents Review on this PR) A second `/coder-agents-review` pass on this PR raised 16 findings; addressed in `fix(docshtmlcheck): catch self-closing containers and capitalized tags`: - **P2:** self-closing container tags (`<div class="tabs"/>`) were ignored by the HTML5 parser and leaked their wrapper like the open spelling; the balance check now tracks self-closing tokens too (CRF-1). - **P2:** a capitalized component tag whose lowercase name is a real element (`<Table>`, `<Section>`) slipped through on the `allowedElements` lookup. The tokenizer lowercases tag names, so the check now reads the raw token and reports any capitalized name as a component reference (CRF-2). - Narrowed the `:` / `@` autolink skip to a real URI scheme or a dotted `local@domain`, so `<region:id>` and `<user@host>` stay checked (CRF-3). - Stale-allowlist findings now report against the linter source with no line, and count separately from invalid-HTML issues in the footer (CRF-7, CRF-11). - Comment / README / Makefile wording synced to the honest capitalized-tag behavior; added the deleted-file allowlist caveat and a note that `allowedElements` is hand-maintained against the renderer (CRF-14, CRF-17, CRF-9). - Internal cleanups (`pop` -> `matchEndTag`, extracted `unclosedFinding`) and new tests: self-closing, capitalized open/close, colon/at placeholders, a non-first-token line assertion, `isGeneratedDoc`, and the stale message (CRF-12, CRF-13, CRF-1/2/3/4/5/16). Two findings resolved without a code change: - **CRF-8** (also wire `lint/docs-html` into `lint-light`): declined. `lint-light` is the Go-free fast path; `lint/docs-html` needs the Go toolchain, so it stays in the full `make lint`, which CI runs. Adding it would pull Go into the light path for no coverage gain. - **CRF-9** (`allowedElements` <-> renderer coupling): documented with a maintenance note in the `allowedElements` comment and tracked in DOCS-597 for a cross-repo sync/check decision. Deferred (note, no current trigger): raw-text element interiors (`<script>` / `<style>`) are not scanned for nested tags. No docs page relies on this today; noted for follow-up. ## Merge order #27298 (the hand-written fixes this PR guards) has merged, and this branch is rebased on `main`, so `make lint/docs-html` now reports 0 findings and the `lint` check passes. The two PRs are independent (disjoint files, no stacking). ## Verification - `go test ./scripts/docshtmlcheck/`, `go vet`, `gofmt -l`, `golangci-lint run`: clean. - `make lint/docs-html` (branch rebased on `main`): 0 findings. ## Linear - DOCS-584: https://linear.app/codercom/issue/DOCS-584/add-ci-check-that-fails-on-invalid-inline-html-in-docs - DOCS-551: https://linear.app/codercom/issue/DOCS-551/backtick-placeholder-syntax-in-generated-reference-docs-cli-help - DOCS-597 (follow-up, from CRF-9): https://linear.app/codercom/issue/DOCS-597/track-docshtmlcheck-allowedelements-drift-vs-docs-renderer-component > This PR was created with AI assistance (Coder Agents). |
||
|
|
d23d0d5313 |
refactor(site): replace MUI update-check snackbar with <UpdateCheckNotice/> (#27728)
Replaces the MUI `Snackbar` / `Link` update-check toast in `DashboardLayout` with a Tailwind `UpdateCheckNotice`, and adds Storybook coverage for the layout and notice. The notice stays declarative (not Sonner), offsets above the deployment banner when the user can view deployment stats, and otherwise sits at the normal bottom margin. | Old | New | | --- | --- | | <img width="460" height="158" alt="UPDATE_NOTICE_OLD" src="https://github.com/user-attachments/assets/d7ac9e5a-f69a-47de-89d5-9b6c6df4e1aa" /> | <img width="440" height="157" alt="UPDATE_NOTICE_NEW" src="https://github.com/user-attachments/assets/a5eee05b-0232-4668-bc3e-fd3ffdbad9b8" /> | |
||
|
|
6b8f820493 |
feat: remove native chat cost tracking in favor of AI Gateway cost data (#27330)
## Stack Context This stack makes AI Gateway data and budgets the source of truth for AI spend controls. 1. Re-back the per-chat cost endpoint with AI Gateway data (#27328, merged). 2. Remove native chat usage limits (#27329, merged). 3. **This PR, now based on `main`:** remove native chat cost tracking and its dedicated admin UI. ## Summary Removes native per-message price calculation, model pricing fields, cost persistence, aggregate cost queries, and admin cost API types. It also deletes the Analytics and Spend pages plus their legacy redirects. The AI Gateway-backed per-chat cost row and compact budget indicators remain. The spend documentation is renamed to `spend-management.md` and updated for the remaining surfaces, group budget APIs, CSV export, upgrade handling for native pricing and cost history, and the absence of a deployment-wide spend dashboard. The per-chat cost API documents that data follows AI Gateway retention and reports zero after all matching requests are purged. No schema is dropped in this release. `chat_messages.total_cost_micros` remains nullable and unwritten so replicas from the previous release can continue inserting messages during rolling upgrades. #27600 tracks removal after the compatibility window. > Mux prepared this PR on Mike's behalf. |
||
|
|
f0e6ac64b3 |
feat: remove native chat usage limits in favor of AI Gateway budgets (#27329)
## Stack Context This stack makes AI Gateway data and budgets the source of truth for AI spend controls. 1. Re-back the per-chat cost endpoint with AI Gateway data (#27328, merged). 2. **This PR:** remove native chat usage limits. 3. Remove native chat cost tracking and its dedicated admin UI (#27330). ## Summary Removes the native usage-limit API, SDK types, SQL, and chat enforcement for deployment, user, and group chat limits. Compact AI Gateway budget indicators remain in the Agents sidebar, user menu, and group settings. Gateway budget rejections and provider quota failures continue to classify as usage-limit errors, including a 409 response for synchronous title generation. Budget-period labels now use the API's UTC boundaries, so users see the same dates in every browser timezone. The documentation explains the AI Gateway replacement, its licensing requirements, and the differences from native limits. No schema is dropped in this release. The usage-limit table, index, user and group columns, constraints, audit mappings, and generated scan fields remain for mixed-version rolling upgrades. #27600 tracks their removal after the compatibility window. ## Breaking change Native day, week, and month chat spend limits are removed and are not migrated. AI Gateway budgets are month-based, group-scoped with per-user overrides, and require the AI Gateway entitlement. Deployments without that entitlement no longer have chat spend enforcement. > Mux prepared this PR on Mike's behalf. |
||
|
|
c6cee10e8b |
feat: add per-model OpenAI Responses API toggle (#27683)
chatd hardcoded `WithUseResponsesAPI()`, so the provider SDK's static known-model list decided whether an OpenAI model spoke the Responses API or Chat Completions. A model absent from that list silently fell back to Chat Completions until the fantasy fork was patched. This exposes the SDK's `WithResponsesAPIFunc` hook as a per-model setting, `openai_config.use_responses_api`, stored in the existing `chat_model_configs.options` JSONB. Unset keeps the known-model list, `true` forces Responses, `false` forces Chat Completions. There is no migration. It sits in a new construction-time `openai_config` section rather than in `provider_options.openai` because it selects the API when the client is built, while `provider_options` holds per-request parameters. That placement is also load-bearing: a config setting only this field would otherwise materialize an OpenAI request-options struct and turn on provider-side response storage, since `Store` defaults to true there. Three places independently decided the transport and would silently disagree with the client actually built: | Site | Effect when it disagrees | | --- | --- | | `ModelFromConfig` | the transport being overridden | | `AcceptsFilePartMediaType` | text attachments dropped, since Responses natively accepts only images and PDFs | | `UsesResponsesOptions` | the SDK type-asserts the concrete options struct, so every OpenAI option is discarded | They share one predicate here, `chatopenai.UsesResponsesAPI`, with the override threaded to each. The rest of the stack removes that threading by resolving the transport once and carrying it. Compaction overrides and the quickgen debug model built clients without `ConfigOptions`, so they now pass it and pick up both this setting and the existing Anthropic beta headers. The toggle also makes transport-conditional option handling admin-switchable, so two hardening changes ride along. `ServiceTierFromChat` now maps every tier the codersdk enum advertises (`auto`, `default`, `flex`, `scale`, `priority`); it previously returned nil for `default` and `scale`, so flipping a model to Responses silently dropped a configured `service_tier` that the API accepts (fantasy forwards the value unchanged). And a new `TestProviderOptionsTransportParity` pins, per `provider_options.openai` field, which transport honors it, against a table in ARCHITECTURE.md, so a field honored on one transport and silently ignored on the other fails the test unless recorded as intentional. Review rounds also caught two lifecycle gaps around the new field. `isZeroChatModelCallConfig` now inspects `OpenAIConfig`, so a stored options blob whose only setting is this toggle survives into GET/list responses instead of reading as `model_config: null`; `TestIsZeroChatModelCallConfigCoversEveryField` sets each config field in isolation and fails if any field is invisible to the zero check. And the model editor's update path sends an explicit empty `model_config` when an edit clears the last field, since an omitted property preserves the stored options server-side; covered by the `EditClearingLastOptionSendsEmptyConfig` story. Azure keeps following the known-model list, because the Azure provider exposes no equivalent hook. The model editor renders Azure with the OpenAI option schema, so instead of shipping a visible but inert control, the option schema generator gains a `providers` struct tag that it emits as `visible_for_providers`. Gating uses the raw provider type rather than the alias table, so the control appears only for openai-typed providers. No hand-written frontend field: the editor renders it from the generated schema. Closes https://linear.app/codercom/issue/CODAGT-874/add-completionsresponses-api-toggle-in-model-editor > Mux prepared this PR on Mike's behalf. |
||
|
|
52423eb87b |
feat: promote MinimumImplicitMember experiment to GA (#27472)
Promotes the `minimum-implicit-member` experiment to GA and removes it.
## What changes
- The `minimum-implicit-member` experiment constant, its
`RoleOptions.MinimumImplicitMember` toggle, and the global
`rbac.MinimumImplicitMember()` accessor are deleted. The minimal-member
behavior is now the only behavior: `organization-member` and
`organization-service-account` carry only the floor (read-self records,
notifications, and similar) and grant **no workspace permissions**.
Workspace access lives exclusively on the
`organization-workspace-access` role.
- The experiment gate on customizing `default_org_member_roles` (`PATCH
/organizations/{org}`) is removed; the built-in-roles-only validation
remains.
- The dashboard's Default Roles section and the implied-roles display on
the members page are no longer experiment-gated.
- Admin docs: new "Default member roles" section in
`docs/admin/users/organizations.md`, cross-linked from
`groups-roles.md`.
## Why this is safe for existing deployments
Migration `000516` (shipped earlier) backfilled
`default_org_member_roles` with `['organization-workspace-access']` on
every organization. Members therefore keep exactly the effective
permissions they had with the experiment off; the workspace elevation
flows through the default role instead of being baked into
`organization-member`.
**Rollback caveat:** rolling back past this release restores the bundled
elevation, silently re-granting workspace access to members of
organizations that cleared their default roles.
## Review
Deep-review R1 findings are addressed in `chore: address deep-review
findings` (copy fixes, read-only Default Roles for viewers, removable
overlapping explicit grants, RBAC prose restoration, test
de-tautologizing, docs). Point-by-point disposition is in the PR
comments.
---
Generated by Coder Agents on behalf of @Emyrk.
|
||
|
|
df1c0f9710 |
feat: show what a chat lifecycle hook changed (#27655)
## Stack Context Follow-up fixes from live UAT of the merged chat lifecycle hooks stack (#27430). Two PRs: 1. **This PR**: make hook effects visible and correctly attributed in the transcript. 2. [`mike/chat-hooks-uat/dispatch-capacity`]: reserve dispatch capacity so an admission burst can't fail running turns. ## Why? UAT found three ways the transcript misrepresented what a lifecycle hook did. All three are user-visible and share the same surface (`chathooks/effects.go`, `codersdk.ChatMessagePart`, the conversation timeline), so they're reviewed together. **A prompt `input_override` silently discarded attachments.** `ComposeUserPromptContent` replaced the entire submitted part list with one text part, dropping `file` and `file-reference` parts along with their `chat_file_links`. The user saw their attachments vanish with no explanation. The override now replaces submitted *text* parts only and preserves non-text parts in order. A consumer that wants to block attachments uses `deny`, which is the documented mechanism for refusing a submission. **Every user-visible `system` row was labelled "Lifecycle hook".** The timeline keyed the notice off `role === "system"`. That was correct only by accident, because the hook `user_message` was the sole client-visible system row. The backend now emits the notice as a typed `hook-notice` part and the timeline renders on that, so a future system row can't be mislabelled as a policy notice. **Nothing marked a tool call the hook had rewritten.** A consumer could replace tool input via `input_override` and the transcript showed the rewritten input as if the model had produced it. `ChatMessagePart` gains `hook_rewritten`, set from `preflight.Overrides` on the same path that already carries `ToolCallCreatedAt`, and the tool row renders a "Modified by policy" badge. `ToolCall.PolicyProvider` renders the badge itself, at four wrap sites: the `Tool` dispatch wrapper, the `ReadFilesTool` aggregate and its per-file rows, and `ReadFileTimelineBlock` (grouped and single `read_file` rows bypass `Tool`). Renderer props do not include the flag; descendants consume it through the provider context. The badge is emitted by the provider rather than by the shared header because several renderer branches return early without one, including the auth-required `execute` card, a completed `ask_user_question`, and an empty question payload. Those branches would drop the attribution with no type or runtime error, and the gap is not greppable: every renderer file contains a header somewhere, only individual branches do not. Emitting at the provider removes the possibility instead of enumerating the cases. A rewritten call is wrapped in a group labelled by its badge, so one rewritten file inside a merged read is attributed on its own rather than inheriting the group's badge. `HeaderButton` still appends the policy wording to an explicit `ariaLabel`, since an explicit `aria-label` replaces the name computed from descendants. Provider-executed calls are excluded from attribution. Hooks never see them, and duplicate tool-call ID rejection deliberately skips them, so a reused ID would otherwise mark a provider-executed call as policy-rewritten. ## Testing Go: `coderd/x/chatd/...`, `coderd/x/agenthooks/...`, `codersdk/...`, and `coderd -run 'Hook|Chat'`. Frontend: `tsc` plus every `AgentsPage` story; the only failures are `MCP Tool Completed` and `Scroll To Bottom Button Works With Inverse Scroll`, both of which fail on trunk. A registry-wide story asserts every registered renderer shows the badge, verified against three inverted toggles: removing the badge, hiding it with `display:none`, and skipping the provider for one renderer (which names that renderer). Storybook also covers the rewritten subagent spawn, a completed empty question payload, a non-hook system message, and a failed `read_file` guarding the accessible name. > Mux opened this PR on Mike's behalf. |
||
|
|
4245e4e378 |
feat: expose dynamic client registration in deployment settings (#27480)
Adds the admin-controlled OAuth2 Dynamic Client Registration setting landed by #27316 (`GET`/`PUT /api/v2/oauth2-provider/settings`) to the OAuth2 Applications deployment settings page, since it was previously only reachable via the API or `coder oauth2-provider dcr enable|disable`. The page is now tabbed, **Applications** and **Settings**, so DCR has a home that further OAuth2 settings can share (an Initial Access Token setting is a likely next one). The active tab is backed by a `tab` search param, so `?tab=settings` links straight to it, and an unpermitted deep link falls back to **Applications** rather than selecting nothing. On the Settings tab, DCR renders as a titled section with a description, an `Enabled` badge when active, and an Enable/Disable button. Enabling opens a confirmation dialog, since it lets any OAuth2 client self-register against the deployment without prior admin approval (RFC 7591). Disabling is immediate, no confirmation. The control is a button rather than a switch on design feedback: a switch reads as an immediate on/off flip, which conflicts with a confirmation dialog standing in front of it, and it left the only explanation of the risk inside a dialog that disappears. A button carries the confirmation step without misrepresenting what a click costs, the always-visible description explains the setting on the page, and the `Enabled` badge gives the active state a persistent indicator. The layout follows Tracy's mockup on `tj/oauth2-apps-pagination`; the apps-table pagination work that shares that branch is deliberately not included here. Visibility and editability are gated on the same `ResourceDeploymentConfig` RBAC checks the endpoint itself enforces (`viewDeploymentConfig` / `editDeploymentConfig`), not a separate hardcoded check. The view takes the settings values as one optional `settings` prop, absent when the viewer lacks `viewDeploymentConfig`, so "cannot view" is the shape of the prop rather than a flag the caller keeps consistent with the values beside it, and the tab is not rendered at all. Closes https://github.com/coder/coder/issues/27432 ## Where this sits in the request path ```mermaid sequenceDiagram autonumber actor Admin participant View as OAuth2AppsSettingsPageView<br/>(Tabs + Enable/Disable + Dialog) participant Page as OAuth2AppsSettingsPage<br/>(React Query) participant S as coderd Note over Page: On mount Page->>S: GET /api/v2/oauth2-provider/settings S-->>Page: { dynamic_client_registration_enabled } Page-->>View: settings: { dynamicClientRegistrationEnabled, canEdit, ... } Note over Admin,View: Admin opens the Settings tab and enables DCR Admin->>View: click "Enable" View->>View: open confirmation dialog<br/>(no request sent yet) Admin->>View: click Confirm View->>Page: settings.onDynamicClientRegistrationChange(true) Page->>S: PUT /api/v2/oauth2-provider/settings<br/>{dynamic_client_registration_enabled: true} S-->>Page: 200 OK (audited) Page->>S: GET /api/v2/oauth2-provider/settings (refetch) S-->>Page: { dynamic_client_registration_enabled: true } Page-->>View: section shows the "Enabled" badge and a Disable button Note over Admin,View: Admin disables DCR Admin->>View: click "Disable" View->>Page: onDynamicClientRegistrationChange(false)<br/>(no dialog, disable is immediate) Page->>S: PUT ... {dynamic_client_registration_enabled: false} S-->>Page: 200 OK (audited) ``` ## Files changed All 10 files are hand-written; nothing in this PR is `make gen` output. | File | What changed | |---|---| | `site/src/api/api.ts` | New `getOAuth2ProviderSettings`/`putOAuth2ProviderSettings` methods, thin typed wrappers around the two endpoints #27316 added to `main`. | | `site/src/api/api.test.ts` | Covers both methods against the request they issue and the error they propagate. | | `site/src/api/queries/oauth2.ts` | A `getSettings` query and a `putSettings` mutation that invalidates the settings key on success. Both the app and settings keys now derive from a shared `oauth2ProviderKey` constant. | | `site/src/api/queries/oauth2.test.ts` | 4 tests: the key nesting, both delegations, and that a successful update invalidates the settings key without touching app queries. | | `.../OAuth2AppsSettingsPage.tsx` | Wires query and mutation into the page and passes the settings values down as one object, or omits it entirely without `viewDeploymentConfig`. The apps error stays its own prop, since the view gates the applications empty state on it. | | `.../OAuth2AppsSettingsPageView.tsx` | `Tabs` splitting Applications from Settings. The settings tab distinguishes loading, failed, and a value the server omitted rather than rendering nothing, and the header's "Add application" action is scoped to the applications tab. | | `.../OAuth2AppsSettingsPageView.stories.tsx` | 14 stories, covering the tab wiring, both permission boundaries, the header action's scope, and the settings tab's loading, fetch-error, update-error, and value-omitted states. | | `.../DynamicClientRegistrationSetting.tsx` | The section itself: heading, description including what disabling does not undo, `Enabled` badge, a permission explanation when the viewer cannot edit, and one button that confirms only in the enable direction. | | `.../DynamicClientRegistrationSetting.stories.tsx` | 11 stories, including focus surviving an in-flight request and the dialog ignoring a value that changes underneath it. | | `docs/admin/integrations/oauth2-provider.md` | Adds the web UI route to the DCR section, which previously enumerated only the CLI and the management API. | ## Suggested review order Follows the direction data actually flows, from the raw HTTP call up to the rendered section. 1. **`site/src/api/api.ts`**: the two new methods. Confirms they match the `codersdk.OAuth2ProviderSettings` shape #27316 landed and sit next to the existing OAuth2 app methods they mirror. 2. **`site/src/api/queries/oauth2.ts`**: the query/mutation pair. The mutation's `onSuccess` → `invalidateQueries` is the one detail worth double-checking: it's what makes the on-screen state catch up with what was just saved, rather than trusting the PUT payload. 3. **`OAuth2AppsSettingsPage.tsx`**: the container. Check the two separate permission gates (`viewDeploymentConfig` on the query's `enabled` option, `editDeploymentConfig` on the button's editability) match the RBAC the backend enforces. 4. **`OAuth2AppsSettingsPageView.tsx`**: the tabs and the settings tab's four states. The `settings` prop being optional is what hides the tab; the error inside the tab is deliberately separate from the page-level `error`, which gates the applications empty state. 5. **`DynamicClientRegistrationSetting.tsx`**: the section. Two things worth reading closely: the enable path opens the dialog while the disable path calls straight through, and lacking permission uses the native `disabled` attribute while an in-flight request uses `aria-disabled`, so a keyboard user is not blurred mid-flip. 6. **The two story files**: read last, as they exercise everything above without a real server. The dialog stories query `canvasElement.ownerDocument.body` rather than `canvasElement`, since the dialog renders into a portal attached to `<body>`. ## Deliberately not in this PR - **ENG-3116**: the applications list cannot distinguish self-registered clients from admin-created ones. Surfacing that needs a new field on `codersdk.OAuth2ProviderApp`, which is an API addition this PR does not need. - **ENG-3118**: reusing the shared `EnabledBadge` and `SettingsHeader` primitives for this section. Both hinge on what the mockup intends, and the badge in particular is a visible change either here or on the four other pages that share it. ## Screenshots Default (disabled): <img width="1676" height="497" alt="image" src="https://github.com/user-attachments/assets/cfa60266-8678-410e-9577-16ef474491e3" /> Enabling (confirmation dialog): <img width="1661" height="558" alt="image" src="https://github.com/user-attachments/assets/a7d54fdd-f65d-4fec-9ed9-3bfdcfdae5be" /> Enabled: <img width="1666" height="559" alt="image" src="https://github.com/user-attachments/assets/d39251c2-771c-4608-81c2-dda151b35c3d" /> --------- Co-authored-by: Tracy Johnson <tracy@coder.com> |
||
|
|
8886a5749a |
feat: add network calls list to AI session threads API (#27425)
The AI session threads API returned only a network call *summary* (total/blocked counts + top domains). This adds the per-call list so the session detail can render individual Agent Firewall network calls. `ListAIBridgeSessionNetworkCalls` reuses the same sequence-number windowing as the existing summary and includes all protocols. The list is exposed as `network_call_logs` on the threads response and is capped server-side at 100 rows. The summary (`network_calls.total`/`blocked`) remains authoritative for whole-session totals: the list length and its blocked count equal the summary only when a session has at most 100 calls, and are truncated beyond that. ### PR map (merge strictly bottom-up) This change is a 4-PR stack. Each PR depends on all the ones below it, so merge in this exact order: 1. #27417 — backend network summary 2. #27418 — frontend summary rows 3. #27425 — backend per-call list `network_call_logs` 4. #27426 — frontend network-calls panel Refs AIGOV-464 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3f3fd1c4d7 |
feat: show network request summary on AI session detail card (#27418)
Frontend for the AI session network summary. Adds Network calls, Blocked network requests, and Top domains rows to the Session summary card on the individual AI session detail page, driven by the network fields on the session threads response. Renders "Disabled" when network monitoring was not active and "No activity" when there were no calls. Covered by Storybook stories for each state. ### PR map (merge strictly bottom-up) This change is a 4-PR stack. Each PR depends on all the ones below it, so merge in this exact order: 1. #27417 — backend network summary 2. #27418 — frontend summary rows 3. #27425 — backend per-call list `network_call_logs` 4. #27426 — frontend network-calls panel Refs AIGOV-463 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Cian Johnston <cian@coder.com> |
||
|
|
841a1765f7 |
feat: add network calls summary to AI session threads API (#27417)
Backend for the AI session network summary. Exposes total/blocked
network calls and top destination domains on the session threads
endpoint (`GET /api/v2/ai-gateway/sessions/{id}`).
Total and blocked reuse the existing Agent Firewall aggregation from the
sessions list query, so the numbers match the sessions table. Top
domains are a new server-side aggregation
(`GetAIBridgeSessionTopDomains`) over boundary logs, using the same
interception-window correlation. There is no network-error state,
matching the current data model.
Frontend consuming these fields is in a separate stacked PR.
### PR map (merge strictly bottom-up)
This change is a 4-PR stack. Each PR depends on all the ones below it,
so merge in this exact order:
1. #27417 — backend network summary (base `main`)
2. #27418 — frontend summary rows (base #27417)
3. #27425 — backend per-call list `network_call_logs` (base #27418)
4. #27426 — frontend network-calls panel (base #27425)
Refs AIGOV-463
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cian Johnston <cian@coder.com>
|
||
|
|
95a2c2ba02 |
feat: back the per-chat cost endpoint with AI Gateway data (#27328)
## Stack Context
This stack removes native chat cost tracking and native chat usage
limits, making the AI Gateway the single source of AI spend data and
budget enforcement.
1. **This PR:** re-back the per-chat cost endpoint with AI Gateway data.
2. Remove native chat usage limits end to end, rewiring the sidebar
indicator to gateway spend.
3. Remove native chat cost tracking end to end, deleting the
Analytics/Spend cost UI.
## What?
`GET /api/experimental/chats/{chat}/cost` summed
`chat_messages.total_cost_micros`, which native chat cost tracking
maintained. It now aggregates AI Gateway interception data instead, and
has no native fallback.
- New `GetAIBridgeChatCost` query, authorized through the root chat so
members can read their own chat's cost without gaining access to raw
interception rows.
- Response fields renamed: `priced_message_count` -> `request_count`,
`unpriced_messages_having_usage_count` -> `unpriced_request_count`.
- The chat summary sidebar keys its cost cache by root chat, and hides
the cost row where the AI Gateway is off or unlicensed. The root cost is
invalidated when a chat leaves an active status and when a generated
title lands, since title generation bills its own gateway request.
`GetChatModelUsageCostByChatID` and the rest of native cost tracking are
untouched here; PR 3 removes them.
## Why?
Native cost tracking duplicates what the AI Gateway already records, and
the two disagree. Repointing the endpoint first means the cost UI keeps
working while the native implementation is deleted later in the stack.
Two behaviour changes follow from gateway semantics and are intentional:
- **Requests, not messages.** The gateway records interceptions, so
counts are requests. Title-generation traffic now counts.
- **Whole-tree totals.** The gateway records the *spawning* chat's ID as
the interception session ID, so a subagent's requests are attributed to
its immediate parent, not always the root. Only a whole chat tree can be
summed, so the query resolves the root and aggregates the tree, and
every chat in a tree reports the same total. Native returned per-subtree
totals.
## Attribution and counting semantics
The aggregate groups token usage per interception before counting, so
the reported numbers are per request even though a request records one
usage row per provider response:
- `RequestCount` counts finished `Coder Agents` interceptions in the
tree, including unpriced ones.
- `UnpricedRequestCount` counts requests with at least one usage row the
gateway could not price. It is a subset of `RequestCount`.
- `TotalCostMicros` omits only unpriced usage, so a partially priced
request still contributes its priced portion. The sidebar therefore says
`Excludes unpriced usage from N request(s)` rather than claiming whole
requests were dropped.
A recorded cost of zero is a free request, not an unpriced one. Usage
without an effective group is excluded, matching what never reached
`ai_user_daily_spend`.
## Authorization
Reads go through `ExtractChatParam` plus `ResourceChat`, with no
cost-specific RBAC widening. `TestGetChatCost/MemberCanReadOwnChat`
covers a scoped `agents-access` member reading their own chat's cost,
and `MemberCannotReadOtherUsersChat` still asserts 404 for a non-owner.
Plain members without `agents-access` cannot create or read chats at
all, so they never reach this endpoint.
## Known limitation
AI Gateway data has its own retention period, 60 days by default and
configured independently of chat retention, so spend for requests older
than that is no longer reported. A chat whose gateway records have all
been purged reports zero cost, which is indistinguishable from genuinely
free usage under this contract. The endpoint documents the caveat;
#27330 documents it on the Spend Management page.
In-flight interceptions are excluded, since cost is only known once the
response is recorded. A chat's cost therefore lags the active turn by
one request.
## Rebase note
Rebased onto `main` after #27579 removed the `ai-gateway-cost-control`
experiment. The per-chat cost row is now gated on the `aibridge` feature
alone, matching how #27579 degated the other cost-control surfaces.
> Mux prepared this PR on Mike's behalf.
|
||
|
|
6c42309ccb |
feat(site): modernize OAuth2 applications settings UI (#27562)
Follow up to #27561 Modernizes the Deployment Settings OAuth2 Applications create/edit/list UI to match the AI Providers pattern: fat page views, card layout, and a Formik + Yup form using shared field primitives. - Rework CreateOAuth2AppPageView / EditOAuth2AppPageView into fat views with provider-style layout (back link, avatar + title, bordered cards, cancel/submit footer) - Rewrite OAuth2AppForm with Formik/Yup, FormField descriptions, and IconPickerField (live header avatar on create/edit) - Order edit page as settings → endpoints (Client ID / Auth / Token via CodeExample) → secrets - Align list page row styling and add a Callback URL column - Update Storybook stories for the new fat-view + form validation behavior | Old | New | | --- | --- | | <img width="2936" height="1802" alt="old-oauth2-application-create" src="https://github.com/user-attachments/assets/98c1dec1-c273-43a7-a517-a31ae48bc17c" /> | <img width="2936" height="1802" alt="new-oauth2-application-create" src="https://github.com/user-attachments/assets/9d2e1d6b-6905-469f-b9e0-cf8ae3b14f3d" /> | | <img width="2936" height="1802" alt="old-oauth2-application-list" src="https://github.com/user-attachments/assets/3d38daab-1d93-4acb-bff0-d7299da884fd" /> | <img width="2936" height="1802" alt="new-oauth2-application-list" src="https://github.com/user-attachments/assets/b3cc56ac-a810-4742-9ea7-27e20caa9bb7" /> | | <img width="2936" height="2030" alt="old-oauth2-application-update" src="https://github.com/user-attachments/assets/544ad92b-ae2b-4b10-907d-c94bcc3d12c4" /> | <img width="2936" height="3470" alt="new-oauth2-application-update" src="https://github.com/user-attachments/assets/6e4d228d-8deb-4934-989a-b9c98b633509" /> | |
||
|
|
4bf9b9d1e6 |
feat(site): surface chat lifecycle hook outcomes in the chats UI (#27430)
Surfaces chat lifecycle hook outcomes in the chats UI. Final PR of the lifecycle hooks stack (#27401, #27428, #27429), all now merged. - Show hook notices attached to their user message as timeline notes (`role="note"` so historical notices stay out of the screen reader's assertive live region), and show an info tooltip for notices on queued messages. - Cache the full inserted message batch from send and edit responses so hook-inserted messages survive stream reconnects and queue promotion. - Reconcile the promoted queue head after sending to an errored chat so a missed or delayed queue update neither duplicates nor hides messages, and clear the stale error status so the Thinking indicator appears before the websocket status event. - Ignore an authoritative queue snapshot that still contains a just-promoted message: queued messages are delete-only, so such a snapshot predates the promotion and would both re-show the promoted message and drop messages queued since. Fresh snapshots apply in full and clear the suppression. - Cache the store's reconciled queue on `queue_update` instead of the raw event, so a stale update cannot re-show a promoted message after REST re-hydration. - Refresh chat details when a send or edit fails, because a failed hook dispatch can move the chat to the error state. - Surface tool result error text in the tool rows: the execute failure tooltip shows the actual error instead of a hardcoded "Command failed", and a failed `write_file` renders an error label with the result error text instead of "Wrote <file>" with an args-derived diff of content that was never written. This makes hook tool denials legible in the timeline, and benefits every failed execute or write. - Label a tool call blocked by `pre_tool_use` as failed instead of `Ran <command>`, matching what the write and edit tools already do. The wording derives from the tool-result error flag, so a command that ran and exited non-zero is unaffected. - Render a hook notice below the message it annotates rather than above it, which reads correctly for a "your prompt was rewritten" card. - Give both hook outcomes their own treatment on the create path, where they previously fell through to the generic error alert and an expected policy decision appeared with a stack trace, response data, and a workspaces action. Classification keys on the structured response body rather than the status code, so ordinary permission errors keep their existing rendering. - Unrelated to the hooks work, de-flake `SchedulePage.test.tsx`. Its `fillForm` helper wrapped an already-retrying `findByLabelText` in `waitFor`, so the two 1s budgets raced and a slow first render failed `test-js` with "Timed out in waitFor". This is separable from the rest of the PR if you would rather it land on its own. > This PR was written by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
e3a5a697ab |
fix(site): refresh group member budgets after overrides (#27553)
Group member budget rows remained stale after saving or deleting a user override because the cached member-spend query was not refreshed. Invalidate the affected user's override query and only cached group-member spend queries whose user ID list contains that user. This refreshes relevant rows without invalidating unrelated groups. |