mirror of
https://github.com/coder/coder.git
synced 2026-09-23 14:03:57 +08:00
632eecc09030e4eaff11481a71ea99fc71fe934f
4324
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4b7494be72 |
feat: harden chat generation runtime instrumentation for billing (#27451)
Closes CODAGT-835 ## Summary `chat_messages.runtime_ms` becomes the billing source of truth for Coder Agents runtime (summed hourly by #27312), but it was built for debugging: the June refactor (#26270) silently stopped recording tool-step runtime, compaction was never measured, and interrupted turns lost their partial runtime entirely. This PR defines the billable metric, closes the paths that dropped it, and documents the definition where the data lives. ## The billable definition **`runtime_ms` is the wall-clock duration of the model invocation that produced the persisted message content**, measured from just before the provider stream opens until it is fully consumed. What counts: - Assistant generation steps, in top-level and sub-agent chats (sub-agents are ordinary chats on the same generation path). - Compaction summarization calls, persisted on the compaction assistant message (**new**). - Interrupted attempts: the message-part episode's lifetime is persisted on the partial assistant message committed by `FinishInterruption`, so partial generation time survives interruption (**new**; measured via a new `Buffer.EpisodeDuration`, which works even though the generation goroutine and the interrupt task are different tasks). What deliberately does not count (each is documented in code and docs): - **Local tool execution.** Tool wall time includes idle waits, most importantly `wait_agent` polling a sub-agent chat that already bills its own model invocations; billing the batch would double count, and excluding one tool from a concurrent batch's wall time is ill-defined. Pre-refactor instrumentation did include tool time; this makes the exclusion an explicit product definition instead of a silent regression. - **Failed model calls whose output is discarded** (retried attempts, terminal errors, content-filter refusals). They persist no content, so they bill nothing; billing errs toward undercounting. Notably a stream-silence timeout can burn 10 idle minutes before a retry, which should not be billable "active generation". If product later wants failed attempts billed, that needs a place to persist runtime on error turns (`FinishError` inserts no rows today) and is a deliberate follow-up, not instrumentation drift. - **Ancillary calls that produce no chat messages** (title generation, advisor, turn summaries) and all idle/parked time (`requires_action`, queueing). The definition is documented as `COMMENT ON COLUMN chat_messages.runtime_ms` (migration 000551, surfacing as a Go doc comment on `ChatMessage.RuntimeMs`), on `chatloop.PersistedStep.Runtime`, in the chatd architecture doc, and in the Spend Management docs page. ## Index for the hourly scan None needed: `GetTotalChatMessageRuntimeMsInRange` (#27312) filters an hour-wide `created_at` range, which the existing `idx_chat_messages_created_at` b-tree already serves; the residual `runtime_ms IS NOT NULL` filter applies to one hour of rows. A partial index would add permanent write amplification for a query that runs once an hour. > [!NOTE] > Migration 000551 is also claimed by #27312; whichever merges second renumbers via `fix_migration_numbers.sh`. ## Tests - End-to-end: the existing full-server generation test now asserts `RuntimeMs.Valid` on the committed assistant row (it previously read `.Int64` without checking `.Valid`, so it passed on NULL). - Interrupted turn: full task-level test (real DB, mock clock) asserting the partial assistant message persists the attempt's runtime. - Errored stream: asserts a failed invocation yields no step and no runtime. - Tool-using turn: asserts runtime lands on the assistant row only and tool rows stay NULL. - Compaction: asserts the summarization call duration is recorded and lands on the compaction assistant message only. - `messagepartbuffer.EpisodeDuration` unit coverage. Blocks: CODAGT-843 (B3), CODAGT-838 (D8). --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Hugo Dutka <hugo@coder.com> |
||
|
|
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. |
||
|
|
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. |
||
|
|
b3485d9b3a |
chore: add agents_allowed to templates (#27284)
Relates to CODAGT-713 This adds `templates.agents_allowed` as a default-true, auditable template attribute, along with nullable database filtering. Migration `000562` translates the effective legacy `agents_template_allowlist` state for existing templates: a valid nonempty list allows matching templates and blocks the rest, missing or empty values leave templates allowed, whilst corrupt values fail closed by blocking all existing templates. As per the linear issue, new templates deliberately default to allowed under the per-template model. This is the database-only first PR in the stack. #27285 makes the field authoritative in the API and chatd whilst temporarily retaining the compatibility routes needed by the shipped frontend. Later PRs migrate the UI, remove the legacy storage, routes, SDK types, and utility, then add CLI flags. |
||
|
|
0d0c6e53ba | fix(coderd): document HTTP 201 for workspace and build creation (#27903) | ||
|
|
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. |
||
|
|
97c4031526 |
feat!: resolve agent external auth by template, not config order (#27854)
## TL;DR
**Problem.** A template can declare which external auth provider it
wants via `data "coder_external_auth" { id = "..." }`, and that
declaration is honored at every stage of the build. It was ignored at
runtime. Any git operation going through `GIT_ASKPASS` supplies only a
hostname, never a provider ID, and the handler scanned *every* provider
configured on the deployment and returned whichever matched the hostname
**last in config order**, with no reference to what the requesting
workspace's own template declared. Reordering
`CODER_EXTERNAL_AUTH_<N>_*` silently redirected a plain `git clone` from
one OAuth client's token to a completely different one.
**Fix.** For hostname-only requests, resolve the calling agent's
workspace and build *before* selecting a provider, then narrow
candidates to the providers declared by that build's template version.
Exactly one match wins regardless of config order. No matching declared
provider falls back to today's deployment-wide scan, so a template that
declares only a GitHub provider can still clone an unrelated host. Two
or more matching declared providers return `409` naming them, rather
than picking one arbitrarily: `external_auth_providers` is stored sorted
by ID, so HCL declaration order is already unavailable and no principled
tie-break exists.
Requests supplying an explicit provider ID are untouched. Server-side
only: no wire protocol, proto, manifest, or database schema change, so
already-running agents get the corrected behavior on their next askpass
call with no restart.
Refs #23718
<details>
<summary><b>Call flow</b></summary>
```mermaid
flowchart TD
subgraph Push["1. Template import: coder templates push"]
A1["Terraform extracts coder_external_auth id/optional attrs"]
A2["CompleteJob(TemplateImport) validates each id<br/>against deployment config"]
A4["template_versions.external_auth_providers persisted"]
A1 --> A2 --> A4
end
subgraph PreBuild["2. Pre-build and workspace build (unaffected)"]
B1["User authenticates declared provider(s), exact-ID lookup"]
B2["Build resolves token by exact ID<br/>(provisionerdserver.go)"]
A4 --> B1 --> B2
end
subgraph Runtime["3. Workspace running: a credential is needed"]
B2 --> C0{"Caller supplies id or match?"}
C0 -->|"id (explicit)"| D1["Exact-ID match<br/>UNCHANGED, already deterministic<br/>(coder external-auth access-token)"]
C0 -->|"match only (GIT_ASKPASS)"| C1["git needs credentials for a hostname<br/>GIT_ASKPASS invoked, unchanged"]
C1 --> C2["coder gitaskpass sends ExternalAuthRequest{Match: host}<br/>unchanged (cli/gitaskpass.go)"]
C2 --> C3["workspaceAgentsExternalAuth<br/>(coderd/workspaceagents.go)"]
C3 --> C4["CHANGED:<br/>1. resolve workspace/build BEFORE matching<br/>2. read that build's declared provider IDs<br/>3. filter: declared AND regex matches host"]
C4 --> C5{"how many candidates?"}
C5 -->|"exactly 1"| C6["use it, regardless of config order"]
C5 -->|"0"| C7["fall back to deployment-wide scan<br/>(unchanged legacy behavior)"]
C5 -->|"2 or more"| C8["409 naming every matching ID"]
end
D1 --> E1["Token returned"]
C6 --> E1
C7 --> E1
style C4 fill:#1f4d2e,stroke:#4caf50,color:#fff
style C6 fill:#1f4d2e,stroke:#4caf50,color:#fff
style C8 fill:#1f4d2e,stroke:#4caf50,color:#fff
style D1 fill:#333,stroke:#888,color:#fff
```
</details>
## Verification
Two test functions were added in `coderd/workspaceagents_test.go`, and
the behavior no unit test can reach was verified against a local dev
cluster with two real GitHub OAuth Apps whose regexes both match
`github.com`.
| Behavior | Unit | Manual |
|---|---|---|
| Declared provider wins over a colliding one | yes | yes |
| Outcome independent of deployment config order | yes | yes |
| No declared match falls back to the full scan | yes | yes |
| Host the template never declared still resolves | yes | via fallback |
| Two declared providers matching one host return `409` | yes | not run
|
| Declared but unauthenticated provider returns its auth URL | yes | not
run |
| Two templates resolve independently and concurrently | yes | no |
| Explicit-ID path unaffected | no | yes |
| Running agent corrected with no restart | **no** | **yes** |
| Declared ID since removed from config falls back | **no** | **yes** |
| Recomputed per build after a template update | **no** | **yes** |
The last three are properties a unit test cannot express: they involve
swapping the server binary underneath a live agent, removing deployment
configuration, and rebuilding a workspace against a new template
version.
<details>
<summary><b>Unit test detail</b></summary>
`TestWorkspaceAgentsExternalAuthTemplateScoped` builds a deployment with
two providers sharing a regex, a template declaring one of them, and a
seeded token for **every** provider, so a mis-selection returns a valid
token with the wrong identity rather than an error. Subtests:
- `DeclaredProviderLast` / `DeclaredProviderFirst`: the declared
provider wins in both config orders. Only the `First` arm is
discriminating, since the pre-change loop had no `break` and returned
the last regex match, which the `Last` arm happens to agree with.
- `NoDeclaredProvidersFallsBackToFullScan`: a template declaring nothing
keeps today's behavior exactly, pinning the legacy last-match rule.
- `UnrelatedHostStillResolvesViaFallback`: a template declaring only a
GitHub provider still resolves a GitLab host.
- `AmbiguousDeclaredSetReturnsError`: `409` whose message names both
colliding provider IDs.
- `OptionalUnauthenticatedDeclaredProviderReturnsAuthURL`: returns the
auth URL for the *declared* provider, not for an unrelated one the user
happens to hold a token for.
`TestWorkspaceAgentsExternalAuthMultipleTemplates` runs two workspaces
from two templates, each declaring a different provider, issuing
requests concurrently. Each resolves to its own template's provider.
</details>
<details>
<summary><b>Manual verification detail</b></summary>
Local dev cluster, two GitHub OAuth Apps both defaulting to
`^(https?://)?github\.com(/.*)?$`, both authorized by the workspace
owner so a wrong selection yields a usable token rather than an error.
Workspace built from a template declaring only `github-dotfiles`. Tokens
redacted.
**Order independence.** Same workspace, never rebuilt, config order
reversed between runs:
| Deployment config order | Token returned |
|---|---|
| `[github-broad, github-dotfiles]` | `gho_<dotfiles>` |
| `[github-dotfiles, github-broad]` | `gho_<dotfiles>` |
**A/B against the pre-fix binary.** Everything held constant except the
coderd build, with `/api/v2/buildinfo` checked on both sides so the
comparison rests on verified binary identity. The workspace was never
stopped, rebuilt, or re-authorized:
| coderd | buildinfo | Token | Honors declaration |
|---|---|---|---|
| pre-fix | `v2.35.3-devel+11e03cfb3a` | `gho_<broad>` | no |
| this branch | `v2.35.3-devel+e8b87d0333` | `gho_<dotfiles>` | yes |
This doubles as the demonstration that a coderd-only upgrade corrects
behavior on a live agent's next askpass call.
**Declared provider removed from config.** `github-dotfiles` deleted
from deployment configuration while the workspace's template still
declared it. Result: `HTTP/2 200` with `gho_<broad>` via the fallback.
No `500`, no fail-closed `404`. The orphaned `external_auth_link` row
remained in the database throughout and correctly had no effect.
**Recomputation after a template update.**
| Workspace state | Build's declared provider | Token returned |
|---|---|---|
| new version pushed, workspace not updated | `github-dotfiles` |
`gho_<dotfiles>` |
| after `coder update` | `github-broad` | `gho_<broad>` |
The pair is what makes it conclusive: the first rules out following the
template's newest version, the second rules out a cached value.
**Explicit-ID path.** `coder external-auth access-token github-broad`
returned that provider's result even though the template declared only
`github-dotfiles`, and did not substitute the declared provider's
already-valid token.
Raw traces were captured with `GIT_CURL_VERBOSE=1 git -c
credential.helper="" ls-remote <private repo>`, reading the unredacted
`== Info: Server auth using Basic with user '<token>'` line. A private
repo is required, since a public one never triggers a `401` and
therefore never invokes `GIT_ASKPASS`.
</details>
|
||
|
|
d458fe4941 | fix(coderd/database): match group name case-insensitively in search (#27894) | ||
|
|
dae41eb711 |
fix: detect out-of-range AI Gateway costs instead of wrapping silently (#27602)
Implements: https://linear.app/codercom/issue/AIGOV-448/use-decimal-for-cost-computation Follow-up to https://github.com/coder/coder/pull/26229 Follow-up to the AI Gateway cost-control work. Cost is computed per token category as `tokens × price / 1_000_000` in `int64`, then summed. This change makes an unrepresentable result a defined outcome instead of an accident of integer wrap-around. ## Motivation The intermediate `tokens × price` can exceed `int64`. Real usage cannot get there: at a $75/M model the product overflows at roughly 123 billion tokens in a single response, about six orders of magnitude above a maxed-out Opus request, so this is not a live incident. The problem is what happens if it ever does, because the sign of the wrapped value silently selects between two different failure modes, neither of which was chosen: 1. **Wraps positive.** A plausible-looking cost is stored, incremented into the user's daily spend, and enforced against their AI budget. No error, no signal, wrong number. 2. **Wraps negative.** The value violates `CHECK (cost_micros >= 0)`, the insert fails, the surrounding transaction rolls back, and `RecordTokenUsage` returns a Postgres constraint error that says nothing about overflow. The token usage record is lost entirely, along with its token counts. So the same class of bad input either corrupts budget accounting or discards an audit record, depending on arithmetic that nobody reasoned about. That is the undefined behaviour. ## Decision **An unrepresentable cost is treated as bad input, not a large bill.** Since real usage cannot produce one, it can only mean a wrong price row or implausible provider-reported token counts. In both cases the true cost is unknowable, so no number is stored. **Detect rather than avoid.** `computeCost` now evaluates in `decimal`, so nothing wraps, and range-checks the total against `[0, MaxInt64]` before converting back. Out of range returns `errCostOutOfRange`. Rejecting negatives in the same check also keeps them away from the non-negative column constraint, which would otherwise discard the record. **Log, do not block.** The error is swallowed at the call site: the record is written with token counts intact and `cost_micros` NULL, the spend update is skipped, and the condition is logged at ERROR. **Per-category truncation is unchanged.** Each category is still truncated independently rather than the total being rounded once, so a per-category breakdown recomputed from the snapshotted price columns sums exactly to the stored total. Every existing `computeCost` test case passes unmodified. |
||
|
|
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). |
||
|
|
d814dfad88 |
feat(coderd): support public OAuth2 client tokens at the schema layer (#27712)
Layer 1 of a multi-PR split of #27195 (public/secretless PKCE-only OAuth2 clients), broken up for easier review: **database schema (this PR)** → oauth2provider handler logic → API/e2e integration tests. ## Goal Coder's OAuth2 provider only works correctly for confidential clients today. Public clients — native apps that can't safely hold a shared secret, such as the CLI's browser-based login flow, IDE plugins (VS Code, JetBrains), desktop apps, and MCP clients — cannot complete a real OAuth2 flow against Coder, even though OAuth 2.1 §2.1 explicitly defines this client type and RFC 8252 §8.5 requires PKCE alone to be sufficient authentication for it. Every MCP client, CLI login flow, and IDE plugin is a public client by construction, and none of them can complete a secretless flow against Coder today: dynamic registration always classifies a client as confidential regardless of what it asks for, the token endpoint unconditionally requires a `client_secret`, and discovery metadata never advertises `"none"` as a supported auth method. Full write-up: [ENG-3029](https://linear.app/codercom/issue/ENG-3029/oauth2-support-public-client) ### Overall design (end state across the full PR stack) `[PR2]` marks handler-layer changes landing in the next PR in this stack. The green box is what this PR implements. ```mermaid sequenceDiagram autonumber participant C as Public Client (CLI/MCP/IDE plugin) participant S as coderd (chi router) participant H as oauth2provider handlers participant DB as PostgreSQL Note over C,S: Discovery C->>S: GET /.well-known/oauth-authorization-server S->>H: GetAuthorizationServerMetadata() Note over H: [PR2] add "none" to<br/>the returned auth methods list H-->>C: [PR2] 200 { token_endpoint_auth_methods_supported:<br/>[..., "none"] } Note over C,S: Dynamic Client Registration C->>S: POST /oauth2/register<br/>{redirect_uris, token_endpoint_auth_method: "none"} S->>H: CreateDynamicClientRegistration() Note over H: [PR2] client type now reads<br/>the request -> "public" Note over H: [PR2] skip secret generation<br/>for public clients H->>DB: [PR2] INSERT app row<br/>(client_type = 'public') DB-->>H: app row Note over H: [PR2] skip secret insert entirely H-->>C: [PR2] 201 { client_id }<br/>(no client_secret field) Note over C,S: Authorization Code + PKCE flow C->>S: GET /oauth2/authorize?client_id=...&code_challenge=... C->>S: POST /oauth2/tokens (grant_type=authorization_code)<br/>no client_secret S->>H: extractTokenRequest() Note over H: [PR2] client_secret no longer required<br/>for public clients H->>H: authorizationCodeGrant() Note over H: [PR2] skip secret lookup for public clients Note over H: PKCE verification — already mandatory, unchanged rect rgb(198, 239, 206) Note over H,DB: [THIS PR] oauth2_provider_app_tokens.app_id<br/>column added (NOT NULL, populated at insert<br/>time from app.ID) and app_secret_id loosened<br/>to nullable. Revocation now checks app_id<br/>directly. Confidential-client behavior is<br/>unchanged — no public client can be created yet. H->>DB: [PR2] INSERT refresh token row<br/>(no secret reference, for public clients) end DB-->>H: token row H-->>C: 200 { access_token, refresh_token } ``` ## This PR: database schema A public client has no `client_secret`, so it has nothing to put in `oauth2_provider_app_tokens.app_secret_id`, which was `NOT NULL`. This PR makes that column nullable and instead attributes a token to its owning app through a new, always-populated `app_id` column — so ownership checks (e.g. revocation) work identically for public and confidential clients without joining through a secret that may not exist. | Column | Before | After (this PR) | |---|---|---| | `app_secret_id` | `uuid NOT NULL` | **nullable** | | `app_id` | — | **new**: `uuid NOT NULL`, `FOREIGN KEY → oauth2_provider_apps(id) ON DELETE CASCADE`, backfilled for every existing row and populated on every new insert from that point on | This is a single, complete migration — not staged across multiple PRs. An earlier version of this branch deferred `app_secret_id`'s nullability and the insert-time population of `app_id` to a later PR, keeping this PR's diff limited to `coderd/database`. [Automated review](https://github.com/coder/coder/pull/27712#discussion_r3686851911) correctly flagged that as unsafe: the migration would backfill existing rows once, but nothing would populate `app_id` for rows written afterward, so the moment this PR merged, new tokens would start accumulating a permanently `NULL` app_id — and if a release happened to be cut before the follow-up PR landed, that gap could ship to customers and would need a second, later backfill to close. Doing the full migration now avoids that: `app_id` is correct from the first row written, and the promised `NOT NULL` constraint requires no data repair because it's already enforced. Closing that gap requires a few mechanical, non-branching touches outside `coderd/database`: - `revoke.go`'s two ownership checks now compare `dbToken.AppID` directly instead of looking up the app through `app_secret_id` — a genuine simplification (and slightly less code), not a temporary shim. - `tokens.go`'s two `InsertOAuth2ProviderAppToken` call sites supply the new `app_id` column and wrap `app_secret_id` as a `NullUUID`. - `oauth2_test.go`'s one direct-insert test fixture does the same. None of these introduce client-type branching or new capability — every client today is still confidential-only, still always presents a secret, and behavior is unchanged. The full repo builds, vets, and all existing tests pass unmodified in behavior. ## Coming next - **PR2 (handler layer)**: `codersdk`'s `DetermineClientType()` reading the requested `token_endpoint_auth_method`; `registration.go` skipping secret generation for public clients (and wrapping the app+secret insert in a single transaction, fixing a pre-existing orphan-row/visibility-race gap); `tokens.go` making the secret check conditional so PKCE alone authenticates a public client; `metadata.go` advertising `"none"` in discovery. No further migration is needed — the schema this PR ships is already final. - **PR3 (API/e2e layer)**: integration tests through the real HTTP API (`coderd/oauth2_test.go`), the MCP OAuth2 e2e flow (`coderd/mcp/mcp_e2e_test.go`), and the manual test script (`scripts/oauth2/test-mcp-oauth2.sh`). Depends on: #27195 (original combined PR, being superseded by this stack) --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
c62079c053 |
refactor(coderd): optimize chatdebug (#27129)
Adds a bund of optimizations to chatdebug:
In `coderd/x/chatd/chatdebug`:
- Adds a benchmark (excluding LLM and database stuff)
- Replaces string concatenation with strings.Builder when accumulating
stream parts (~105,000ns -> ~50,00ns)
- Removes double JSON encode in RecordingTransport (114,000ns ->
64,000ns)
In `coderd/util/strings`:
- Adds a benchmark for Truncate
- Removes unnecessary allocations in Truncate (~110,000ns -> 1,550ns in
truncation case, 1 alloc -> 0 allocs in no truncation case)
> 🤖 Claude helped with this.
|
||
|
|
1702bbb816 |
test(coderd/x/chatd): accept query cancellation in subagent wait (#27818)
Closes CODAGT-877 Closes https://github.com/coder/internal/issues/1437 The linked flake happens because the context deadline can be observed as `context.DeadlineExceeded`, or as a PostgreSQL query cancellation when a database call is in flight. The fix is just to assert that the deadline expired and the returned error is a recognised query cancellation, rather than depending on which layer notices it first. |
||
|
|
11427066a1 |
fix: require bedrock model fields for the invoke-model protocol (#27846)
Implements: https://linear.app/codercom/issue/AIGOV-564/aibridge-bedrock-provider-skipped-404-on-all-routes-when-settings-omit Improves validation when creating and updating AI providers: a Bedrock provider using the `invoke-model` protocol now requires `model` and `small_fast_model`. This brings API validation in sync with the UI, which already required both fields. |
||
|
|
8f5f15a92f |
fix: remove unbound Client() method from aibridged.Server (#27845)
Adds client context to `Client()` method in `aibridged.Server`, effectivly renaming `ClientContext()` method as `Client()`. Similarly `aibridged.ClientFuncWithContext` became `aibridged.ClientFunc`. `aibridged.Server.Client()` acquired a DRPC client with `context.Background()`, callers in theory could wait indefinitely for the daemon to connect to coderd. Every call site already had a context except the recorder callback. `aibridge.NewRecorder` takes a `func(context.Context) (Recorder, error)` and acquires against the record call's context. |
||
|
|
db88ec3f6a |
fix: price AI usage by configured provider type (#27836)
## Problem AI Gateway records the aibridge provider on each interception, which is the upstream wire format and only ever `anthropic`, `openai`, or `copilot`. Prices are matched on exact provider and model equality, so a provider configured as Azure, Bedrock, Google, OpenRouter, or Vercel is priced as if it were native OpenAI or Anthropic, matching either the wrong price or no price at all. ## Changes - Resolve the configured provider type from `ai_providers` by provider name, which is unique among live providers, and key the price lookup on it instead of the aibridge provider. No schema change is needed. - Label `unpriced_token_usage_records_total` with the same provider value used for the lookup, so it names a provider an operator actually configured. - Treat a provider that cannot be resolved as unpriced, consistent with how a missing price is handled today. Closes https://linear.app/codercom/issue/AIGOV-570/resolve-ai-model-prices-using-the-configured-provider-type Depends on the follow-up that extends the shipped price book to the remaining provider types: https://linear.app/codercom/issue/AIGOV-571/ship-prices-for-all-ai-governance-provider-types > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira |
||
|
|
60161fd375 |
chore: update model prices to include more providers (#27837)
Adds the full set of supported provider types to `scripts/aibridgepricesgen` and updates stored model prices. Notes: * We need to rename a few keys from models.dev JSON to match our internal provider types. * `prices.json` is now marked as generated. --------- Co-authored-by: Susana Ferreira <susana@coder.com> |
||
|
|
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. |
||
|
|
404bb2f663 |
refactor(coderd/x/chatd): own provider option construction in one function (#27705)
Stacked on #27704. Provider option conversion and reasoning effort injection both create OpenAI option structs, so each of the four call sites had to pair them in the right order and pick the same transport for each. `chatprovider.ProviderOptionsForCall` now owns both steps, and the two helpers it wraps are unexported. The advisor, main generation, compaction override, and quickgen paths each collapse to one call. The ARCHITECTURE section on transport selection is updated to match: `ProviderOptionsForCall` is described as the only entry point in `chatprovider` that builds provider options for a call, delegating OpenAI conversion to `chatopenai.ProviderOptionsFromChatConfig`, and it now records that the quickgen turn status label and chat summary paths deliberately send no provider options. > Mux prepared this PR on Mike's behalf. |
||
|
|
0e16e356b0 |
refactor(coderd/x/chatd): read the OpenAI transport from the model (#27704)
Stacked on #27703. Provider option conversion, reasoning effort injection, and file part acceptance each recomputed the OpenAI wire format from `(provider, modelID, override)`. They now read it from `chatprovider.Model`, so a decision cannot drift from the client it was built for. `ProviderOptionsFromChatConfig` takes a `Transport`, `ApplyReasoningEffort` takes a `Model`, and `AcceptsFilePartMediaType` becomes a `Model` method. `UsesResponsesAPI` and `UsesResponsesOptions` are deleted. The override extraction is unexported and reachable only from `ModelFromConfig`, which now takes the model's `ChatModelOpenAIConfig` directly, removing the six scattered extractions at call sites. That also resolves the computer-use mismatch. The computer-use model is a hardcoded default with no config row of its own: its client was built without an override while request preparation applied the chat model's. Preparation now reads the computer-use model's own transport, so the two agree without one model's client settings following a different model. Passing the chat model's `openai_config` into the computer-use client would have made them agree on the wrong value. `TestModelTransportConsumersAgree` pins the invariant in one test: the HTTP path the client actually hits, the concrete provider option struct type, the type created by reasoning effort, and text/image file acceptance. > Mux prepared this PR on Mike's behalf. |
||
|
|
b5c9e8e471 |
refactor(coderd/x/chatd): carry the resolved OpenAI transport on a model wrapper (#27703)
Stacked on #27683. The OpenAI wire format is decided when the client is built, then thrown away, so downstream sites recompute it from `(provider, modelID, override)`. Any disagreement fails silently: the SDK type-asserts the concrete provider options struct and discards every OpenAI option, and text attachments are dropped because Responses natively accepts only images and PDFs. This adds `chatprovider.Model`, which pairs a fantasy client with the transport resolved from that client's own identity. Its fields are unexported and only the constructor sets the transport, deriving it from the client, so no caller can pick a transport that disagrees with the client it wraps. `chatopenai.Transport`'s zero value is invalid and panics when read rather than defaulting to a wire format, following the existing precedent for construction invariants. `Model` is threaded through construction, the resolve paths, and the four struct fields that store a model for later request preparation. Terminal call sites keep taking `fantasy.LanguageModel` and receive `LanguageModel()`, which avoids a new package edge from `chatloop` and `chatadvisor` into `chatprovider`. No decisions move yet. The consumers still recompute the transport, and `UsesResponsesAPI` now delegates to `TransportFor` so the two agree by construction. #27704 makes the consumers read it from the model. > 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. |
||
|
|
9bab7a8e06 | fix(coderd): preseed AI providers for title tests (#27564) | ||
|
|
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.
|
||
|
|
76eb43a78e |
test(coderd): drain dormancy audit log before reset in StartWakesUpDormantWorkspace (#27541)
Closes PLAT-219 / [coder/internal#1532](https://github.com/coder/internal/issues/1532). The reported failure (`should have 2 item(s), but has 3`) was the stop build's outcome audit landing after `auditor.ResetLogs()`; that path was already deflaked by #26232 and the pre-reset drain added in #25037. This closes the remaining hole in the same class: the dormancy update's audit log is exported in a middleware `defer` after the API response, so it can also land after `ResetLogs` and break the exact `len == 2` assertion. Extend the pre-reset poll to drain the dormancy write entry alongside the stop build entry. Verified with `go test ./coderd -run TestWorkspaceDormant -count=5` and `-race`. > Generated by Coder Agents on behalf of @Emyrk. |
||
|
|
fc24c27dfd |
fix: reserve chat hook dispatch capacity for running turns (#27656)
## Context Follow-up fix from live UAT of the merged chat lifecycle hooks stack (#27430). Its companion UAT fix (#27655) has merged, so this targets `main` directly. ## Why? UAT measured a burst of 1,500 concurrent chat creations against a consumer with 1.2s latency. 255 were admitted and 1,245 got `502 hook_dispatch_failed (over_capacity)`, which is correct fail-closed behavior. The collateral wasn't: the same burst failed 24 `stop` dispatches, parking chats that had already been admitted and had already executed tools. One 256-slot semaphore served every event, so new-work admission could take every slot and kill turns in flight. Callers now classify each dispatch as admission or generation, and admission draws from a 192-slot gate held *before* the shared pool. At least 64 shared slots stay reachable only by dispatches for work a chat already admitted. The dispatcher is per `coderd` replica, so these limits are per replica, not deployment-wide, and the docs say so. **The caller classifies, not the event type.** Event type isn't a reliable proxy in either direction: a subagent spawn dispatches `user_prompt_submit` from inside a running turn, and the edit path dispatches `session_start` at admission time. `CapacityClassUnset` is rejected in `Dispatch`, so a new call site fails closed rather than silently inheriting a share. **Acquisition order is load-bearing.** Admission takes its own gate first. Taking a shared slot first would let admissions queued on the gate occupy the very capacity the reserve protects. `acquireCapacity` is the only path that takes either pool, so the order can't be bypassed. ## What this does not guarantee Nothing bounds how many turns generate concurrently, so the 192/64 split is a judgement call, not a derived ceiling. This stops an *admission* burst from consuming every slot; it does not make the remainder sufficient. A large enough generation load can still exhaust the reserve and error a running chat. The docs say so explicitly rather than promising a guarantee the code doesn't deliver. Generation can now take all 256 slots, so generation traffic starves admission harder than before. That's the intended priority: rejecting a new prompt is recoverable, ending a turn that already ran tools is not. ## Testing Red-green proved both new tests. Removing the release-on-failure path fails `RefusedSharedAcquireReleasesAdmission` deterministically; removing the expired-deadline check fails `ExpiredDeadlineRefusesFreeSlot` in 18/30 runs. That deadline check fixes a real race found in review. `acquire` previously shared one `time.Timer` across both acquires. Because `select` picks a ready case at random, an admission dispatch could take a slot after its capacity deadline had passed. Measured over 300 trials: 135 late acquisitions, worst overshoot 2.1ms. `acquire` now takes an absolute deadline and refuses an expired one before selecting, which measures 0/300. Go: `coderd/x/agenthooks/...` and `coderd/x/chatd/...`, plus `-race -count=3` on the dispatcher. > Mux opened this PR on Mike's behalf. |
||
|
|
7bd9f5ec93 |
fix: correct authorization header spelling in api docs (#27721)
Corrects the misspelled `Authorizaiton` Swagger header name to `Authorization` in the source annotation and checked-in generated API documentation. This prevents generated API specs and SCIM examples from documenting the wrong HTTP header name. |
||
|
|
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. |
||
|
|
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> |
||
|
|
2acfe7e829 |
feat(coderd/externalauth/gitprovider): use conditional requests for GitHub JSON reads (#27628)
Fixes #27627. ## What The chat diff-status gitsync worker polls open pull requests on a fixed 10s interval and re-downloads the full JSON body every tick, even when nothing changed, because the GitHub client never sends `If-None-Match` / ETag. This adds a small, concurrency-safe, bounded in-memory ETag+body cache (`coderd/externalauth/gitprovider/conditional.go`) and wires it into `githubProvider.decodeJSON`. When an ETag is cached for a request, we send `If-None-Match`; on `304 Not Modified` we decode the cached body; on `200` we cache `{etag, body}` when an ETag is present and the body is under a size cap. ## Why `304` responses do not count against GitHub's primary rate limit, but full `200`s do. Today every unchanged poll burns quota that the same token also needs for interactive Git and API operations, so busy instances can hit rate-limit errors and stalls elsewhere. Unchanged PRs now revalidate for free with no behavior change; only genuine changes transfer a body. ## Details - Cache key = request URL + a hash of the token, so one token's response is never served under another; raw tokens are not retained. - Bounded by entry count (LRU eviction, default 2048) and per-body size (1 MiB) to cap memory. - Scope limited to the JSON reads through `decodeJSON`; the raw-diff path (`fetchDiff`, up to `MaxDiffSize`) is intentionally left out to avoid caching large bodies. ## Tests `TestConditionalRequestReuse` in `github_test.go` covers: - `NotModifiedReusesCachedBody` — a warm poll sends `If-None-Match` with the prior ETag and reuses the cached body on `304`, yielding the same result with exactly two upstream requests. - `DifferentTokenDoesNotShareCache` — a different token never sends another token's cached ETag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4959e1f9-f8e6-4e97-a487-f395a0123c79 |
||
|
|
7b104b6a98 |
fix: add info log level to unpriced models message (#27693)
Change the unpriced models log to info level. Refs https://github.com/coder/coder/pull/27678 |
||
|
|
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>
|
||
|
|
3660ffecdd |
fix: add warn log level to unpriced models message (#27678)
When a model is missing from the price table, token usage is still recorded but with a NULL cost, so AI spend for that model goes unattributed. This was logged at debug level, which means it is invisible in a default deployment. Log it at warn level instead so admins can see which provider/model pairs need a price row and act on it. |
||
|
|
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.
|
||
|
|
54d5eb7ec2 |
feat: add hourly hb_agent_runtime_v1 usage events for Coder Agent runtime (#27312)
closes CODAGT-839 closes CODAGT-843 closes CODAGT-773 ## Summary Adds a new heartbeat usage event type, `hb_agent_runtime_v1`, measuring the total agent-loop runtime of Coder Agents (chats) per UTC hour, plus a reconciler that generates one event per hour with self-healing backfill over a trailing 7-day window. Events flow to Tallyman through the existing publisher unchanged. This measures the new Coder Agents (the `chats` tables), not the deprecated Tasks counted by `dc_managed_agents_v1`. Independent of #27508, which fixes the dead ai-seats cron registration. Both PRs carry the identical `usage_event` create permission hunk for the usage-publisher subject (this feature's generator and the ai-seats cron each need it for heartbeat inserts), so they can land in either order and the overlap merges cleanly. > [!WARNING] > **Do not include this in a release until Tallyman accepts `hb_agent_runtime_v1`.** The publisher marks permanently rejected events as done-forever, and the generator then sees those buckets as complete locally, so their usage would be silently and permanently lost. ## Details Each event's payload is `{"runtime_ms": N}`: the sum of `chat_messages.runtime_ms` for messages created in the hour bucket `[H, H+1)`, across all chats (sub-agents, API-created, archived, and soft-deleted messages included). Events use deterministic IDs (`hb_agent_runtime_v1:<bucket start>`) with `created_at` set to the bucket start, so concurrent replicas race safely via `ON CONFLICT (id) DO NOTHING` without locking, and daily rollups attribute backfilled hours to the correct day. Idle hours produce zero-valued events. A bucket becomes eligible 5 minutes after it closes; hours missing for longer than the 7-day window are forfeited, which can only undercount. Note that this makes `usage_events.created_at` explicitly the *event occurrence time* rather than the row insertion time; the two only diverge for backfilled events. It already behaved as the occurrence timestamp (it drives the daily rollup day and is shipped to Tallyman/Metronome as the event timestamp), and the migration now documents this with a `COMMENT ON COLUMN`, which also surfaces as a Go doc comment on `UsageEvent.CreatedAt`. The new `usage.Generator` runs unconditionally in enterprise builds; the `publish_usage_data` license flag continues to gate egress only, so air-gapped deployments still fill their local ledger. The `aggregate_usage_event()` trigger sums `runtime_ms` per day into `usage_events_daily` (unlike `hb_ai_seats_v1`, which takes the daily max). `InsertHeartbeatUsageEvent` now takes an explicit `createdAt` so generators can backfill historical buckets; the cron passes `clock.Now()` to preserve its existing behavior. ## Tallyman follow-up <details> <summary>Prompt for the Tallyman-repo agent</summary> > **Task**: Add support for the new Coder usage event type `hb_agent_runtime_v1` so Tallyman accepts, validates, and forwards it to Metronome. > > **Background**: coder/coder PR (this PR) adds hourly heartbeat events measuring Coder Agent runtime. Events arrive via the existing `/api/v1/events/ingest` endpoint with: `event_type: "hb_agent_runtime_v1"`, `event_data: {"runtime_ms": <int64 >= 0>}`, deterministic `id` of the form `hb_agent_runtime_v1:2026-07-15_14:00:00` (UTC hour bucket start), and `created_at` set to the bucket start (may be up to ~8 days in the past due to backfill; within Metronome's 34-day dedup window). Zero-value events are normal (idle hours). > > **Work**: > 1. Update Tallyman's vendored/imported `coderd/usage/usagetypes` (or equivalent) to the coder/coder commit that adds `UsageEventTypeHBAgentRuntimeV1` and `HBAgentRuntime`. > 2. Ensure ingestion validation accepts the type (`Valid()` switches) and rejects negative `runtime_ms`. > 3. Ensure Metronome forwarding maps the event with transaction ID derived from the event `id` as for existing types, passing `runtime_ms` through as the property for a SUM-aggregated billable metric ("Coder Agent Hours" = `SUM(runtime_ms) / 3,600,000`). > 4. Do NOT permanently reject unknown-but-well-formed future `hb_*` types if avoidable; at minimum confirm current behavior for unknown types (temporary vs permanent rejection) and report it. > 5. Tests: ingest accept/validate, dedup by ID, Metronome payload mapping. > > **Constraint**: this must be deployed to tallyman-prod **before** any coder/coder release containing the event generator; coderd treats permanent rejections as terminal per event. </details> |
||
|
|
2b28515d9b | refactor: migrate story snapshot params to pixel (#26844) | ||
|
|
3deecb481e |
chore: remove ai-gateway-cost-control experiment flag (#27579)
## Description Closes [AIGOV-443](https://linear.app/codercom/issue/AIGOV-443/remove-ai-gateway-cost-control-experiment-flag-once-feature-is-stable). The AI Gateway cost control feature is planned for GA on the upcoming release, so this removes the `ExperimentAIGatewayCostControl` experiment and all of its gating. The cost control API endpoints remain gated by the `FeatureAIBridge` license feature (the AI Governance add-on), so this only drops the experiment layer. ## Changes - **`codersdk/deployment.go`**: remove the `ExperimentAIGatewayCostControl` const, its `DisplayName()` case, and its `ExperimentsKnown` entry. - **`enterprise/coderd/coderd.go`**: remove the `httpmw.RequireExperiment(...)` gating from the AI cost control routes. They keep `RequireFeatureMW(codersdk.FeatureAIBridge)`. Affected endpoints: - `GET /organizations/{organization}/groups/ai/spend` - `GET /organizations/{organization}/groups/{groupName}/members/ai/spend` - `GET /organizations/{organization}/ai/spend/export` - `GET /groups/{group}/members/ai/spend` - `GET /groups/{group}/ai/spend` - `GET/PUT/DELETE /users/{user}/ai/budget/override` and `GET /users/{user}/ai/spend` - **`enterprise/coderd/aibridge_test.go`**: drop the experiment from test setup and remove the now-obsolete `RequiresExperiment` negative-path tests. - **Frontend (`site/src/...`)**: remove the `ai-gateway-cost-control` experiment checks from the cost control UI (Groups pages, user dropdown) and their stories/mocks. The feature is now driven solely by the `aibridge` feature visibility. - **Generated**: regenerated `coderd/apidoc/*`, `docs/reference/api/schemas.md`, and `site/src/api/typesGenerated.ts`. ## Out of scope The dogfood `CODER_EXPERIMENTS` config lives in a separate infra repo, not `coder/coder`. Leaving `ai-gateway-cost-control` there is harmless: unknown experiment values are logged as `"ignoring unknown experiment"` at startup and otherwise ignored, so no ordering dependency or breakage. That cleanup can be a follow-up. <details> <summary>Implementation notes</summary> - Verified how unknown experiments are handled in `coderd/coderd.go` `ReadExperiments`: unknown values produce a warning log and are inert, so removing the definition before the dogfood config is updated is safe. - Noticed the group `ai/budget` routes (`/groups/{group}/ai/budget`) were already gated only by `FeatureAIBridge`, never by the experiment. After this change all cost control routes are uniformly feature-gated, resolving that inconsistency. - Removed an obsolete `RequiresExperiment` subtest in `TestUserAISpendStatus` that only asserted a 403 from the experiment gate; with the gate gone it would no longer be blocked pre-RBAC. </details> --- _This PR was created by Coder Agents on behalf of @ssncferreira._ |
||
|
|
d6a5c8e9f8 |
refactor: make user AI budget and spend endpoints consistent (#27611)
## Description
Makes the user AI cost control endpoints consistent.
## Changes
- Replaces the flat `spend_limit_micros` and `limit_source` fields on
`GET /users/{user}/ai/spend` with a nested `effective_budget`, reusing
the type behind `group_budget`. The flat pair made it possible to encode
a limit without a source.
- Renames `AIGroupBudget` to `AIBudgetLimit`, since it also carries
`user_override` limits and is no longer group-specific. The type name is
not part of the wire format.
- Moves `/users/{user}/ai/budget` to `/users/{user}/ai/budget/override`.
The endpoint only ever managed the per-user override, which the type,
the handlers, and the operation IDs all already said; the path was the
only place that didn't.
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
|
||
|
|
e71249a821 |
fix: ai cost control cap configurable AI spend limit (#27640)
## Problem A configured AI spend limit was only validated as `gte=0`, with no upper bound. The group spend query multiplies the per-member limit by the number of attributed members, so a large enough limit overflows `bigint` and fails the whole query, returning an error for every group in the request rather than just the misconfigured one. ## Changes - Add `MaxAISpendLimitMicros`, $1,000,000 per member per budget period. - Reject group budgets and per-user overrides above the maximum with a 400 naming the limit. - Bound both budget forms in the UI so they show the valid range before submitting. Follow-up https://github.com/coder/coder/pull/27589#discussion_r3668956350 Depends on https://github.com/coder/coder/pull/27589 > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira |
||
|
|
1c722ff969 |
fix(coderd/database): order the chat prompt query and its boundary by id (#27619)
## Stack context Follows #27495 (merged), which gives `chat_messages.id` an append-order guarantee and moves the history reads onto it. This PR applies the same fix to the query that builds the model prompt. ## Why? `GetChatMessagesForPromptByChatID` mixed two orderings. It selected the compaction boundary with `created_at DESC, id DESC`, then applied that boundary with an `id >` comparison, and returned rows with `created_at ASC, id ASC`. `created_at` is `now()`, so it is the transaction start time. Every row in one insert batch shares it, and concurrent transactions can commit in the opposite order to the one they started in. Two consequences, both reaching the provider: - **Malformed prompts.** A tool result could be ordered ahead of the assistant message that requested it. `chatprompt.injectMissingToolResults` does not repair this: it only handles tool rows already contiguous after an assistant row, and adds missing results. It never moves a tool row that precedes its assistant, and nothing re-sorts the rows in Go. - **Wrong compaction boundary.** The boundary is picked by timestamp but compared by id, so a stale compressed summary could be retained while the actual latest one was dropped. ## Changes Both the boundary CTE and the outer query order by `id`. The `id >` predicate is unchanged, which is the point: the ordering now matches the comparison that was always being made. **The boundary index was dead, so it is rebuilt to match.** `idx_chat_messages_compressed_summary_boundary` was created for exactly this lookup, but its predicate requires `role = 'system'` while compaction writes its summary with the user role (`message_conversion.go:334`, the only writer of `compressed = true`). It matched zero rows, and no other query can use it. Migration `000560` rebuilds it as `(chat_id, id DESC) WHERE compressed AND NOT deleted AND visibility = 'model'`, which also matches the new order key. Measured on PostgreSQL 13 with a 20k-message chat, 11 summaries, and 14 sibling chats so `chat_id` is selective: | boundary lookup | plan | buffers | |---|---|---| | old predicate | Index Scan `idx_chat_messages_chat`, 19,989 rows filtered | 267 | | rebuilt index | Index Only Scan | 2 | Not in scope: the outer `SELECT` still inspects every row of the chat, because its `role = 'system' AND compressed = FALSE` disjunct has no lower `id` bound. That predates this PR and needs a query rewrite rather than an index. ## Testing Two subtests, both verified red by reverting the `ORDER BY` and regenerating: - `OrdersByIDWhenTimestampsDisagree` returned `[4,3,2,1]` instead of `[1,2,3,4]`, placing the tool result before the assistant call. - `CompactionBoundaryUsesID` selected the stale summary and leaked the messages between the two summaries into the prompt. Existing subtests pass unchanged. Migration up/down tests pass, and the rebuilt index was verified red-green: restoring the old predicate returns the plan to a 267-buffer scan, and the old predicate matches 0 rows in the fixture. > Opened by Mux on behalf of Mike. |
||
|
|
c17bed25e0 |
feat: wire chat lifecycle hooks into chatd (#27429)
Wires chat lifecycle hooks into chatd, gated by the `agent-lifecycle-hooks` experiment. Part of the lifecycle hooks stack (#27401, #27428, #27430). See `docs/admin/setup/chat-lifecycle-hooks.md` for the consumer-facing contract. ## Summary When a hook URL is configured, chatd dispatches `session_start`, `user_prompt_submit`, `pre_tool_use`, `post_tool_use`, `pre_compact`, `post_compact`, and `stop` events to the consumer and applies its responses. ## Design - **Stateless**: Coder stores no hook dispatch or decision state. Delivery is at least once; consumers deduplicate on stable payload identifiers (chat ID, event type, tool-use ID) and answer duplicates with the same decision. - **Admission-time prompt effects**: `user_prompt_submit` dispatches exactly once per submission (create, send, queue, edit, subagent spawn) and folds its effects into the stored prompt as typed message parts: original-or-overridden user parts, then model-only `hook-context`, then a user-visible `hook-notice`. Hook context is stripped from every client-facing conversion; hook notices are excluded from model prompts. The server rejects hook parts in client-submitted content. - **Tool gating**: `pre_tool_use` allow can override tool input; deny becomes a synthetic denied tool result, with any returned model context persisted as a model-only transcript row so it never reaches clients. The denial text identifies an external policy (the deployment's lifecycle hook) as the source and marks the decision as persistent, so the model explains the denial instead of retrying it or misreporting it as an infrastructure failure. - **Fail closed**: a dispatch failure rejects the triggering request or moves the chat to the error state in the same transaction as the affected step, so a runnable state is never published with unapproved content. - **Admission before persistence**: `pre_tool_use` is dispatched for the calls the model produced, before the assistant message is stored. See "Staged tool admission" below. - **Fresh dispatch per tool call**: every non-provider-executed tool call is decided by its own `pre_tool_use` dispatch; Coder never reuses an earlier decision on the consumer's behalf. Retries re-dispatch the same logical event. ## Structure All hook dispatch flows through one seam: entry points build a `chathooks.Chat` (chat identity) and a `chathooks.Message` (event details) and call `Trigger.Trigger`, the only component that talks to the dispatcher. The integration lives in the `coderd/x/chatd/chathooks` subpackage, split by responsibility: - `trigger.go`: the trigger seam; builds the wire envelope per event, normalizes deny into a typed error, and holds the package's single enabled-check. - `effects.go`: pure conversion of hook results into transcript rows and prompt parts. - `errors.go`: failure classification (dispatch error messages, denial mapping, tool-result dispatch-failure scanning). - `tooluse.go`: the tool-call gate (`pre_tool_use` preflight, `post_tool_use` payloads, applying admitted input to the step). Server-bound glue stays in `coderd/x/chatd/hook_server.go`: the chat-parking dispatch error handlers, the step-commit row insertion wrappers, and the dynamic post-tool-use state loader, which depends on chatd validation types. This PR adopts the `codersdk/x/agenthooks` and `coderd/x/agenthooks/dispatch` import paths introduced at the tip of #27401; intermediate commits still reference the pre-move paths and are not individually buildable. ## Staged tool admission `pre_tool_use` originally ran at tool execution time, which is after the assistant message carrying the tool call was already committed. An `input_override` therefore had to rewrite stored message content in place. @hugodutka pointed out that chatd treats message content as immutable, and that the rewrite was a shortcut rather than a requirement. It was also a correctness problem in its own right: the rewrite only updated the database, so the transcript could show one input while a different one had executed. The hook now runs before the step is persisted: ```text provider stream ends (tool calls complete, in memory) -> pre_tool_use dispatch per call -> ONE transaction: assistant row with admitted inputs, synthetic denials, hook rows -> execute ``` The step is inserted once, carrying the input the tool runs with. `UpdateChatMessageContentByID` and `Tx.UpdateMessageContent` are deleted from #27428, so message content stays immutable. Two consequences, both intentional: - **Clients converge rather than wait.** Tool-call parts still stream live, so a rewritten call briefly shows the model's proposed input before the committed message replaces it. The chat store already clears stream state when an assistant message arrives, so the stored input wins with no frontend change and no added latency before tool cards appear. - **A call already in history was already admitted.** Execution consumes the stored input instead of dispatching a second decision, which keeps one dispatch and one set of hook effects per call. A consumer policy change between admission and execution applies to later calls, not to calls already admitted. The per-chat debug endpoint still records the provider's original tool input. Its purpose is to report provider behavior, and it requires an explicit per-chat debug flag; the invariant here covers the transcript. ## Configuration Adds `chat-hook-url`, `chat-hook-secret`, `chat-hook-timeout`, and `chat-hook-enabled` deployment options with startup validation. The flags are hidden from `coder server --help` while the feature is experimental; the setup guide documents them. ## Tool input validation Built-in tool arguments reach a consumer as raw JSON with key spelling preserved, but the tools decode those bytes with Go, which matches struct fields case-insensitively and keeps the last match. A policy reading `path` could therefore authorize one value while the tool executed another, and a lone case variant such as `{"PATH":"/secret"}` was invisible to a policy checking for `path`. Coder now rejects a built-in tool call whose input repeats a key or spells a schema property with different capitalization, before the `pre_tool_use` dispatch, so a consumer is never asked to authorize bytes whose meaning depends on the reader. Rejected calls produce an error result the model can retry; unambiguous calls in the same batch still run. A consumer-authored `input_override` is rechecked after the dispatch and fails the turn closed, because the model cannot correct it. Dynamic and MCP inputs are excluded because the client and the workspace agent execute those calls rather than coderd. Two paths needed more than a schema check. Execution resolves a deprecated tool name to its canonical tool, so validation resolves aliases first. The `edit_files` decoder also reads `search` and `replace`, which its schema does not advertise, so those aliases are now matched exactly and their case variants ignored. A hook denial now returns a structured 403 carrying `kind: "hook_denied"`, mirroring the dispatch-failure response that already carries its own kind. Without it a client cannot tell a policy decision apart from a generic failure, and the chat UI titled a denial "Request failed". Adding a kind needs no migration: `ChatErrorKind` is persisted only inside the JSONB `chats.last_error` column, whose decoder accepts unknown kinds. The hook docs also correct the tool-input convergence window. A batch dispatches sequentially before the assistant row commits, so the original input stays visible for a span that scales with the number of tool calls in the step rather than a single hook timeout. > This PR was written by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
91c7232d97 |
feat: add chat suffix messages, idle failure, and content update support (#27428)
Adds generic chat state and query capabilities that the lifecycle hooks integration (#27429) builds on. Part of the lifecycle hooks stack (#27401, #27429, #27430). - `chatstate`: `EditMessage` accepts caller-provided suffix messages inserted after the replacement in the same transaction, transitions can carry a typed error kind, and `FinishError` is also allowed from waiting chats so admission-time failures can park an idle chat in error. - `chatstate`: `ValidateToolResults` holds the submitted-tool-result rules (duplicate, invalid JSON, missing, unexpected) in one place, so `CompleteRequiresAction` and API-level prechecks reject the same payloads with the same typed causes. - `database`: `InsertChat` accepts an optional caller-provided ID. No hook-specific state or behavior is introduced here; these primitives are usable by any caller. An earlier revision added a message-content rewrite primitive so a `pre_tool_use` override could update an already-committed tool call. Message content is immutable by design, and @hugodutka pushed back on changing that. The rewrite is gone: #27429 now dispatches the hook before the assistant message is stored, so the stored input is the one that runs and nothing needs updating. > This PR was written by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
5d2a69d85a |
fix(coderd/x/chatd/chaterror): extract plain-text provider error bodies (#27597)
Follows up #27538. Fixes an issue where `chaterror` would not classify a plain-text aibridge budget error 403 as ChatErrorKindUsageLimit. Root cause: Anthropic adapter drops the body from `ProviderError.Message`, and `providerErrorResponseMessage` extracted only JSON. - Parses the dumped response with `http.ReadResponse` (strips headers, de-chunks, leaves non-dump payloads like Google's raw messages whole). - When JSON extraction yields nothing, falls back to the trimmed first line of the body. - Falls back on `Content-Type: text/plain` only, and never on valid JSON as `Detail` is user-facing. - Adds an end-to-end regression test that Anthropic-shaped 403 budget error → `usage_limit`, not retryable (was `auth`). - Adds table-driven test cases: HTML skipped, whitespace skipped, first-line-only, JSON-without-message, chunked, Google raw message. --- > Generated by Coder Agents on behalf of @johnstcn. |
||
|
|
0b4095085e |
fix: report combined member limit in group AI spend (#27589)
## Problem The organization groups page showed each group's AI budget as the group's per-member limit, so the total it displayed was effectively group members × group budget. That ignores per-user budget overrides charged to the group, so a group where one member has an override reported a limit that doesn't match what its members can actually spend. ## Changes - Add `total_spend_limit_micros` to the organization groups AI spend payload, the combined budget of the members attributed to the group, with each member's override replacing their share. - Return `null` for the total when the group has no budget, since its members spend without a cap. - Both the organization groups and single group spend endpoints report the new field, as they share the same query. - Use the total as the denominator on the groups page AI budget column. Depends on #27568 |
||
|
|
e96d8646e2 |
fix(coderd): give chat message ids an append-order guarantee (#27495)
Chat message ordering was derived from `created_at`, which is `now()` and therefore the transaction start time. That makes it unusable as an append-order column for two independent reasons: every row in one `InsertChatMessages` batch shares a single timestamp, and two concurrent transactions can commit in the opposite order to the one they started in. This PR gives `chat_messages.id` a real append-order guarantee and moves the history reads onto it. ## Changes **`InsertChatMessages` had no input-order guarantee.** Callers index the returned slice by input position. That only worked because PostgreSQL happens to evaluate the `BIGSERIAL` default in row order. Ids are now allocated up front and the k-th smallest is assigned to input index k, so the pairing does not depend on where the column default is evaluated. Returned rows are explicitly `ORDER BY id`. **Three history reads now order by `id`.** | Query | Was | Now | |---|---|---| | `GetChatMessagesByChatID` | `created_at ASC` | `id ASC` | | `GetChatMessagesByRevisionForStream` | `created_at ASC, id ASC` | `id ASC` | | `GetLastChatMessageByRole` | `created_at DESC, id DESC` | `id DESC` | `GetChatMessagesByChatID` paginated by `id` while ordering by `created_at`, which is incoherent on its own terms. The other two matter because of who consumes them. The stream query supplies incremental updates on the same socket that emits a full `GetChatMessagesByChatID` snapshot on history reset, so once that snapshot moved to `id` the two disagreed under timestamp skew. `GetLastChatMessageByRole` returns an id that is then used as an id cursor, both as `AfterID` when synthesizing tool cancellations and as `chats.last_read_message_id`, where a stale anchor leaves later assistant messages permanently unread. A tie-breaker would not have fixed either one. It only resolves equal timestamps; leading with `created_at` is the actual defect. **`GetLastChatMessageByRole` loses its index, so this adds one.** `ORDER BY created_at DESC, id DESC` could take an ordered scan of `idx_chat_messages_chat_created`. Nothing in the schema can supply `ORDER BY id DESC LIMIT 1` for a given `chat_id` and `role`, so the planner switches to a backward scan of the primary key and filters every newer row in the table, scanning all of it when the chat has no message in that role, which is the routine case for a fresh chat. Migration `000559` adds `(chat_id, role, id DESC) WHERE deleted = false`, the same shape as the existing `idx_chat_messages_user_prompts`. This matters because the query is hot: it runs on every stream connect and disconnect, and once per turn when synthesizing tool cancellations. `GetChatMessagesForPromptByChatID` has the same defect and is fixed in the stacked PR, because its compaction boundary change is semantic and deserves a separate review. Auto-archive stays timestamp-based deliberately: it measures activity, not order. Wrapping the insert in a CTE (needed because `INSERT` cannot take `ORDER BY`) makes sqlc synthesize `InsertChatMessagesRow`. It is structurally identical to `ChatMessage`, so the call sites use a direct struct conversion that stops compiling if the two ever diverge. ## Testing Behavior tests write `created_at` values inverted against id order, so a reader that leads with `created_at` returns the batch backwards. All three queries were verified red by reverting the `ORDER BY` and regenerating: the stream query returned `[3,2,1]` for `[1,2,3]`, and `GetLastChatMessageByRole` picked id 1 instead of id 3. `TestInsertChatMessagesOrderContract` asserts against the generated SQL, covering what a behavior test cannot: PostgreSQL evaluates the id default in row order anyway, so a batch still looks ordered once the guarantee is removed. `TestChatMessagesSequenceCacheIsOne` guards the cross-batch half of the invariant. Ids follow chat row lock order only while the sequence hands out one value at a time; sequence cache blocks are per session, so with a cache above one a backend holding stale cached values can lock second and still commit lower ids. Bumping a sequence cache is an ordinary throughput tweak, and it would silently corrupt history order. The index was checked on a 200k row fixture. Without it, the zero-match lookup filters all 200,000 rows over 2763 buffers; with it, the plan is an index scan with both `chat_id` and `role` in the index condition, no sort node, and 3 buffers. Note that the within-batch mapping does not depend on the cache size. It is established by `ROW_NUMBER() OVER (ORDER BY id)` over the allocated ids, so it holds regardless of `nextval` evaluation order. ## Note on the deleted subagent hand-sort The subagent history reader's hand-sort stays deleted, but calling it redundant was imprecise. It sorted by `created_at` then `id`, so it is only equivalent to `id` ordering when the two agree. When they disagree the old code selected a different "latest assistant". This is a deliberate behavior change to match the new invariant, not dead-code removal. > Opened by Mux on behalf of Mike. |
||
|
|
06ceb4253d |
feat: add agent runtime hour license claims and entitlement feature (#27459)
Licenses can now carry three agent runtime hour claims:
`agent_runtime_hours_allocation`, `agent_runtime_hours_limit_soft`, and
`agent_runtime_hours_limit_hard` (unit: hours). They surface as the new
usage-period feature `agent_runtime_hours` in `GET
/api/v2/entitlements`, where `limit` carries the allocation and the new
optional `soft_limit` / `hard_limit` fields on `codersdk.Feature` carry
the thresholds.
Invalid combinations reject the entire license via `validateClaims`
(both at upload and when computing entitlements for stored licenses):
soft/hard without allocation, negative allocation, soft outside `0 <=
soft < allocation`, or `hard < allocation`.
Soft and hard limits are not comparison inputs in `Feature.Compare`;
they ride along with whichever license wins (newest `iat`, existing
behavior). None of the three claim names is a feature name, so old
servers ignore them via the existing unknown-claim tolerance, protecting
rollout of licenses minted with the new claims.
The claim name constants defined in `enterprise/coderd/license` are the
canonical contract for `github.com/coder/license` (X1).
Part of
[CODAGT-837](https://linear.app/codercom/issue/CODAGT-837/a1-agent-runtime-license-claims-and-entitlement-feature).
Blocks B4 (usage wiring + warnings), C1 (hard-limit admission gate), F1
(licenses page), A4 (managed-agent coexistence), X1 (licensor).
Out of scope, handled by follow-up issues: `Actual` usage wiring,
threshold warnings, admission gating, premium defaults, and FE surfacing
beyond regenerated types.
<details>
<summary>Implementation plan and decision log</summary>
## Decisions (confirmed by jaayden, 2026-07-23)
1. **Claim names / unit:**
- `agent_runtime_hours_allocation` - allocation (unit: hours, int64)
- `agent_runtime_hours_limit_soft` - soft limit
- `agent_runtime_hours_limit_hard` - hard limit
- None of the three claim names is itself a `FeatureName`; all three map
to the single new usage-period feature `agent_runtime_hours`
(`FeatureAgentRuntimeHours`), mirroring how `managed_agent_limit_soft`
mapped onto `managed_agent_limit`. Old servers therefore ignore all
three claims via the `FeatureNamesMap` check.
2. **Reject-license.** Invalid claim combinations reject the whole
license via `validateClaims` (upload returns 400 via
`ParseClaimsIgnoreNbf`; already-stored licenses produce an `Invalid
license ... parsing claims` entitlements error and contribute nothing).
## Design notes
- `codersdk.Feature` had a `SoftLimit` field until
|
||
|
|
fbac602456 |
feat!: add admin-controlled dynamic client registration toggle (#27316)
`POST /oauth2/register` (RFC 7591 Dynamic Client Registration) has exactly one gate today: `ExperimentOAuth2`, a static, process-lifetime flag that wraps the entire `/oauth2/*` route tree as an all-or-nothing switch. That flag is scheduled for removal at GA, which would leave DCR with zero admin control at all once it is gone. Add a persistent, DCR-specific `oauth2_dcr_enabled` deployment setting, independent of the experiment system, so admin control over DCR survives GA. `POST /oauth2/register` checks the flag and rejects new registrations with an RFC 7591-shaped `403` when disabled; discovery metadata (`GET /.well-known/oauth-authorization-server`) conditionally omits `registration_endpoint`. A new audited `GET`/`PUT /api/v2/oauth2-provider/settings` endpoint lets an owner toggle it live, no restart required. The setting defaults to disabled, matching the canonical design proposal; disabling only stops new self-registrations, clients that already registered continue to authorize and exchange tokens normally. Address issue described in [ENG-3056](https://linear.app/codercom/issue/ENG-3056/oauth2-dcr-admin-configurable-enabledisable). ## Where this sits in the request path ```mermaid sequenceDiagram autonumber participant A as Admin participant S as coderd participant DB as site_configs<br/>(oauth2_dcr_enabled) participant C as OAuth2/MCP Client Note over A,S: Admin toggles DCR (new) A->>S: PUT /api/v2/oauth2-provider/settings<br/>{dynamic_client_registration_enabled: false} S->>S: authorizeContext(ActionUpdate, ResourceDeploymentConfig) S->>DB: UPSERT oauth2_dcr_enabled = false S-->>A: 200 OK (audited) Note over C,S: Client discovery + registration afterward C->>S: GET /.well-known/oauth-authorization-server S->>DB: GetOAuth2DCREnabled (system ctx, every request, no cache) DB-->>S: false S-->>C: 200 metadata, registration_endpoint omitted C->>S: POST /oauth2/register S->>DB: GetOAuth2DCREnabled (system ctx, every request, no cache) DB-->>S: false S-->>C: 403 invalid_request,<br/>"Dynamic client registration is disabled" Note over C,S: A client that registered before the change is unaffected C->>S: GET /oauth2/authorize?client_id=... Note over S: no DCR-enabled check on this path S-->>C: 200 (proceeds normally) C->>S: PUT/DELETE /oauth2/clients/{client_id} (RFC 7592 self-management) Note over S: no DCR-enabled check on this path either S-->>C: 200 (proceeds normally) ``` ## Files changed: manual vs. generated Reviewers should focus on the **manual** files. The **generated** ones are `make gen` output that follows mechanically from the manual changes and don't need direct review. <details> <summary><b>Manual files (26)</b> — click to expand, grouped the same way as "Suggested review order" below</summary> **1. Database** | File | What changed | |---|---| | `coderd/database/queries/siteconfig.sql` | New `GetOAuth2DCREnabled`/`UpsertOAuth2DCREnabled` query pair on the existing generic `site_configs` table. No schema change. | | `coderd/database/dbauthz/dbauthz.go` | RBAC check (`rbac.ResourceDeploymentConfig`) on the two new query methods; extends the `subjectSystemOAuth2` system-actor role with read-only `ResourceDeploymentConfig` access, needed so the public discovery/registration endpoints can read the flag via `dbauthz.AsSystemOAuth2`. | | `coderd/database/dbauthz/dbauthz_test.go` | RBAC assertion coverage for `GetOAuth2DCREnabled`/`UpsertOAuth2DCREnabled` in the method-coverage test suite. | **2. Request gating (the actual feature)** | File | What changed | |---|---| | `coderd/oauth2provider/registration.go` | The actual gate: `CreateDynamicClientRegistration` reads the flag first and returns an RFC 7591-shaped `403` when disabled (defaults disabled if never configured). | | `coderd/oauth2provider/registration_test.go` | New unit test, `TestCreateDynamicClientRegistration_DCREnabled`: calls the handler directly (no HTTP server), covering enabled / explicitly disabled / never-configured. | | `coderd/oauth2provider/metadata.go` | `GetAuthorizationServerMetadata` conditionally omits `registration_endpoint` from discovery metadata when DCR is disabled. | | `coderd/oauth2provider/metadata_test.go` | New unit test, `TestGetAuthorizationServerMetadata_DCREnabled`: same three states, for the discovery handler. | **3. Admin settings endpoint** | File | What changed | |---|---| | `codersdk/oauth2.go` | New `OAuth2ProviderSettings` SDK type plus `Client.OAuth2ProviderSettings`/`PutOAuth2ProviderSettings` methods. | | `coderd/oauth2.go` | New `oauth2ProviderSettings`/`putOAuth2ProviderSettings` admin handlers (audited via `audit.InitRequest`); updates the `GetAuthorizationServerMetadata` call site to pass `api.Database`. | | `coderd/coderd.go` | Registers `GET`/`PUT /api/v2/oauth2-provider/settings`. | | `coderd/oauth2_provider_settings_test.go` | New test file: admin `GET`/`PUT` round-trip, default-disabled-before-any-`PUT`, and `403` for a non-owner on both `GET` and `PUT`. | **4. Audit wiring** | File | What changed | |---|---| | `coderd/database/types.go` | New `database.OAuth2ProviderSettings` audit-only struct (mirrors `NotificationsSettings`). | | `coderd/audit/diff.go` | Adds the new struct to the `Auditable` type union. | | `coderd/audit/request.go` | Adds the new struct to all four dispatch switches (`ResourceTarget`, `ResourceID`, `ResourceType`, `ResourceRequiresOrgID`). | | `codersdk/audit.go` | New API-facing `ResourceTypeOAuth2ProviderSettings` constant and its `FriendlyString` case. | | `enterprise/audit/table.go` | Field-level audit action map (`ActionTrack`/`ActionIgnore`) for the new struct. | | `coderd/database/migrations/000546_audit_oauth2_provider_settings.up.sql` | Adds `oauth2_provider_settings` to the `resource_type` Postgres enum, required for the audit wiring above (`resource_type` is a real enum, not a Go-only value). | | `coderd/database/migrations/000546_audit_oauth2_provider_settings.down.sql` | No-op (`ALTER TYPE ... ADD VALUE` can't be reverted). | **5. Test-suite ripple from the disabled-by-default flip** | File | What changed | |---|---| | `coderd/oauth2provider/oauth2providertest/helpers.go` | New shared test helper, `EnableDCR`, since DCR now defaults to disabled and many pre-existing tests need it turned on to register a client. | | `coderd/oauth2_test.go` | Adds `TestOAuth2DynamicClientRegistrationDisabled` (registers a client, disables DCR, verifies new registration is rejected while the existing client's self-management, authorize, and token exchange all keep working); calls `EnableDCR` in every pre-existing test that registers a client. | | `coderd/oauth2_error_compliance_test.go` | Calls `EnableDCR` in every test that registers a client, so RFC-error-format assertions aren't masked by the new disabled-by-default gate. | | `coderd/oauth2_metadata_validation_test.go` | Same: `EnableDCR` added to every registration-dependent test. | | `coderd/oauth2_security_test.go` | Same. | | `coderd/oauth2provider/validation_test.go` | Same (near-duplicate of `oauth2_metadata_validation_test.go` in a different package). | | `coderd/oauth2provider/provider_test.go` | Same. | | `coderd/mcp/mcp_e2e_test.go` | Same, for the MCP end-to-end dynamic-registration flow test. | </details> <details> <summary><b>Generated files (12)</b> — from <code>make gen</code>, no need to review directly</summary> `coderd/apidoc/docs.go`, `coderd/apidoc/swagger.json`, `coderd/database/dbmetrics/querymetrics.go`, `coderd/database/dbmock/dbmock.go`, `coderd/database/dump.sql`, `coderd/database/models.go`, `coderd/database/querier.go`, `coderd/database/queries.sql.go`, `docs/admin/security/audit-logs.md`, `docs/reference/api/enterprise.md`, `docs/reference/api/schemas.md`, `site/src/api/typesGenerated.ts`. </details> ## Suggested review order ### 1. Database Establishes the persisted setting and its RBAC rule; everything else builds on `GetOAuth2DCREnabled`/`UpsertOAuth2DCREnabled`. 1. `coderd/database/queries/siteconfig.sql` — the two new queries. Same boolean-encoding pattern as the existing `oauth2_github_default_eligible` key right above them in the same file. 2. `coderd/database/dbauthz/dbauthz.go` — the RBAC wrapper for those two queries, plus the `subjectSystemOAuth2` role extension (search this file for `ResourceDeploymentConfig`, it appears in both spots). 3. `coderd/database/dbauthz/dbauthz_test.go` — asserts the RBAC checks from (2) actually fire. ### 2. Request gating (the actual feature) Where `POST /oauth2/register` and discovery metadata change behavior. 1. `coderd/oauth2provider/registration.go` — the primary gate. Read this first; it's the feature. 2. `coderd/oauth2provider/registration_test.go` — its new unit test, exercising the gate's three states directly against the handler. 3. `coderd/oauth2provider/metadata.go` — the same gating pattern applied to the discovery `GET` endpoint. 4. `coderd/oauth2provider/metadata_test.go` — its new unit test. ### 3. Admin settings endpoint How an owner flips the setting live. 1. `codersdk/oauth2.go` — the `OAuth2ProviderSettings` SDK type and `Client` methods first; this is the public contract everything below implements against. 2. `coderd/oauth2.go` — the `GET`/`PUT` handlers themselves. 3. `coderd/coderd.go` — route registration, to see where those handlers get wired in. 4. `coderd/oauth2_provider_settings_test.go` — round-trip and permission tests. ### 4. Audit wiring Plumbing required so step 3's `PUT` is auditable; mechanical except for (3). 1. `coderd/database/types.go` — the audit-only struct; everything else in this layer exists to plumb it through. 2. `coderd/audit/diff.go` — adds it to the `Auditable` type union (the compiler enforces this one). 3. `coderd/audit/request.go` — the four dispatch switches; the one part of this layer worth reading closely. 4. `codersdk/audit.go` — the API-facing resource type constant. 5. `enterprise/audit/table.go` — the field-action map. 6. `coderd/database/migrations/000546_audit_oauth2_provider_settings.{up,down}.sql` — read last; a consequence of needing a new `resource_type` enum value for (1)-(5), not a design decision of its own. ### 5. Test-suite ripple from the disabled-by-default flip 1. `coderd/oauth2provider/oauth2providertest/helpers.go` — the new `EnableDCR` helper. Read first to understand the fix pattern before seeing it applied repeatedly. 2. `coderd/oauth2_test.go` — next, since it also contains the new `TestOAuth2DynamicClientRegistrationDisabled`, not just `EnableDCR` call sites. 3. The rest, in any order, they're mechanical repeats of the same one-line addition: `coderd/oauth2_error_compliance_test.go`, `coderd/oauth2_metadata_validation_test.go`, `coderd/oauth2_security_test.go`, `coderd/oauth2provider/validation_test.go`, `coderd/oauth2provider/provider_test.go`, `coderd/mcp/mcp_e2e_test.go`. ## Explicitly out of scope Per the design proposal: rate limiting on `POST /oauth2/register` (tracked separately), retroactively affecting already-registered clients when DCR is disabled (this only gates new self-registration), and an Initial Access Token requirement (a separate, follow-up ticket). |
||
|
|
1a6a8be96c |
feat: log tailnet tunnels to the connection log (#27423)
Co-authored-by: Chris DiGiamo <cd@anthropic.com> Co-authored-by: Chris DiGiamo <cdigiamo@anthropic.com> |