mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
5f6eeda58832d70b4d88e79f33df6299229e1bf1
2003
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6079c514ee |
fix: follow-up fixes for conditional VCS requests (#27711)
Follow-ups from #27627 - Memoizes `Config.Git()` with a mutex so the provider's ETag response cache survives across calls. Only successful construction is cached; errors are retried. - Moves the HTTP client onto `Config.HTTPClient`, wired through `ConvertConfig`, so `Git()` no longer takes a per-call argument that would be silently ignored after memoization. - `newGitHub` and `newGitLab` now return `(Provider, error)`, eliminating the typed-nil-interface class in `gitprovider.New` rather than the single instance. - Gates the 304 branch on a `haveCached` flag instead of a nil body check. - Only caches bodies that decode successfully, preventing poisoned entries. - Keys the response cache on the full token digest rather than a truncated prefix. - Tests added: `TestConfigGitMemoizesProvider`, `TestConfigGitRetriesOnConstructorError`, `TestGitLabConstructorErrorReturnsNilInterface`, `TestResponseCacheStore`, `TestConditionalRequestReuse/MalformedResponseNotCached`; `TestConvertYAML/CustomScopesAndEndpoint` now asserts `Config.HTTPClient` wiring. Follow-ups tracked in #28139, #28140, #28141, #28142. > 🤖 Generated by Coder Agents on behalf of @johnstcn. |
||
|
|
b5d18bb9c9 | feat: add redirect URL override for external auth (#28082) | ||
|
|
8d4d0b35dd |
feat: add Coder Agents chat tools to the MCP toolsdk (#28025)
Exposes the experimental Coder Agents chats API through the MCP tool registry, so MCP clients (the hosted `/api/experimental/mcp/http` server and `coder exp mcp server`) can start and drive server-side coding agents. New tools in `codersdk/toolsdk`, all thin wrappers over existing `codersdk.ExperimentalClient` methods: | Tool | Wraps | |---|---| | `coder_create_chat` | `CreateChat` (prompt, optional org, model config, labels) | | `coder_get_chat` | `GetChat` (status, last error, last turn summary, workspace, files) | | `coder_get_chat_messages` | `GetChatMessages` (user-facing parts, chronological, cursor pagination, queued prompts) | | `coder_send_chat_message` | `CreateChatMessage` (queue or interrupt busy behavior) | | `coder_interrupt_chat` | `InterruptChat` | | `coder_archive_chat` | `UpdateChat` with `archived: true` | | `coder_list_chat_model_configs` | `ListChatModelConfigs` (enabled configs with default flag) | Both MCP servers register tools from `toolsdk.All`, so no additional wiring is needed. Responses are trimmed to what an MCP caller needs (IDs as strings, user-facing transcripts) rather than full SDK payloads. No new endpoints and no database changes. Also adds MCP [prompts](https://modelcontextprotocol.io/specification/2026-07-28/server/prompts) for the chat workflows, defined once in `codersdk/toolsdk` and registered by both servers: | Prompt | Purpose | |---|---| | `coder_agents_delegate` | delegate a task to a Coder Agents chat and monitor it to completion | | `coder_agents_check` | check the status and recent activity of an existing chat | Each prompt declares the tools its workflow needs; the stdio server skips prompts whose tools are excluded by `--allowed-tools`. Tests run the tools against a chat-enabled coderdtest instance (fake OpenAI-compatible provider plus in-process AI bridge), covering the full lifecycle, an interrupt against a blocked turn, pagination cursors, permission-dependent model config filtering, and argument validation. Prompt coverage spans SDK rendering, the hosted `prompts/list`/`prompts/get` round trip, and the stdio server including allowlist gating. > Mux created this PR on Mike's behalf. |
||
|
|
26fe3f3185 |
feat(cli): migrate exp mcp stdio server to official MCP Go SDK (#28057)
## Stack Context PR 2 of 6 in a stack that migrates every Coder MCP surface from the archived `github.com/mark3labs/mcp-go` library to the official `github.com/modelcontextprotocol/go-sdk` v1.7.0. Stack: #28056 -> #28057 -> #28058 -> #28059 -> #28060 -> #28061 ## Why `coder exp mcp server` (stdio) now uses the official SDK server with `mcp.IOTransport` over the invocation's stdin/stdout, and reuses the shared `coderd/mcp.RegisterSDKTool` helper from PR #28056 so both servers register tools identically. - A `nopWriteCloser` prevents the SDK from closing the invocation's stdout. - Tests send spec-compliant initialize params and `notifications/initialized` before `tools/list` because the official SDK enforces the protocol lifecycle. > Mux created this PR on Mike's behalf. |
||
|
|
f0c17291b3 |
feat: unhide --oidc-redirect-url server option (#28072)
Unhides the `--oidc-redirect-url` / `CODER_OIDC_REDIRECT_URL` server option so it appears in `coder server --help` and the deployment configuration docs. - Removed `Hidden: true` from the option in `codersdk/deployment.go` - Regenerated CLI golden files and docs via `make gen` --- > Generated with Coder Agents on behalf of @Emyrk |
||
|
|
66b065323b |
feat: log rate-limited external auth token validation (#26754)
When `ValidateToken` keeps a token because the external auth validation
endpoint was rate-limited (a `403` with rate-limit headers or a `429`),
it returns `valid=true` without provider confirmation. Previously this
happened silently, so operators couldn't tell a provider-confirmed token
from one kept optimistically during a rate limit.
This adds a `Logger` to `externalauth.Config` and emits a `Warn` (with
`provider_id`, `provider_type`, `status_code`, and `reason`) on those
rate-limit branches. It also adds a
`coderd_oauth2_external_requests_rate_limited_total{name, source,
status_code}` counter, incremented in the instrumented round tripper
whenever a provider returns a rate-limited response. The rate-limit
detection is the shared `xhttp.IsRateLimited` (in `coderd/util/xhttp`),
used by both the tripper and `ValidateToken` so the metric and the
validation decision share one definition; no extra wiring is needed
since `ValidateToken` already routes through the instrumented client
with `source="ValidateToken"`.
One deliberate behavioral change rides along: rate-limit detection now
also recognizes the unprefixed `RateLimit-Remaining` header (GitLab, and
the IETF draft rate-limit headers), so a `403` with
`RateLimit-Remaining: 0` is treated as optimistically valid where it was
previously treated as revoked. All other valid/invalid decisions are
unchanged. `TestValidateToken` asserts the warning's fields on the
rate-limited cases and no warning for revocations, `401`, and confirmed
responses; `promoauth` and `xhttp` tests cover the detector and the new
counter.
<details>
<summary>Manual testing</summary>
The signals fire on the external-auth status check (`GET
/api/v2/external-auth/{id}`), which calls `ValidateToken`. To force a
rate-limited response, point a provider's `validate_url` at a mock that
returns the rate-limit shape:
1. Run a mock returning `429` on one path and `403` +
`X-RateLimit-Remaining: 0` on another.
2. Start `coder server` with `--prometheus-enable` and external auth
providers whose `validate_url` point at those mock paths (e.g.
`CODER_EXTERNAL_AUTH_0_VALIDATE_URL=http://127.0.0.1:5599/429`).
3. Create a stored link, either complete the OAuth flow, or insert a row
into `external_auth_links` with a future `oauth_expiry` (token contents
are irrelevant; the mock rejects regardless).
4. `curl` the status endpoint with a session token, then check:
- coderd logs for the `Warn` (`reason=status_code` for `429`,
`reason=rate_limit_headers` for `403`),
- the metrics endpoint for
`coderd_oauth2_external_requests_rate_limited_total{...,status_code="429"|"403"}`.
Notes: `scripts/testidp -429` only rate-limits `/oauth2/userinfo`, not
the `/external-auth-validate/...` path, so it does not exercise this;
use a mock `validate_url`. The default Prometheus port `2112` may
already be taken on dogfood workspaces, set `CODER_PROMETHEUS_ADDRESS`
to a free port.
</details>
🤖 Generated with the help of Coder Agents on behalf of @jscottmiller.
|
||
|
|
78b5a0f5a2 |
feat(cli): add --agents-allowed to template commands (#27517)
Relates to CODAGT-713 Depends on #27515 This adds `--agents-allowed` to `coder templates create` and `coder templates edit`. Template creation defaults the option to true, matching the per-template API and database default, while template editing only changes the value when the flag is explicitly supplied so unrelated edits preserve the existing setting. The generated CLI help and reference documentation include the new option. #27518 updates the Coder Agents platform controls documentation to describe the completed per-template model. |
||
|
|
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. |
||
|
|
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). |
||
|
|
db68c6c9fe |
fix: add codersdk JSON response decoder for typed API endpoints (#27804)
`coder whoami` and `coder list` can surface low-level JSON decode errors when a reverse proxy, SSO portal, or incorrect Coder URL returns HTML with a successful HTTP status. Add a shared SDK JSON response decoder and use it for the user and workspace list endpoints so these commands return a structured, actionable API response error instead. Refs #27044 Reviewed and updated by Coder Agents on behalf of @dylanhuff-at-coder. |
||
|
|
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. |
||
|
|
a779320d87 |
feat(scaletest): llm-mock tool calls and paced streaming (#26850)
`coder exp scaletest llm-mock` used to return only canned text, so Coder Agents scaletests pointed at it never exercised the path that matters most: the agentic tool-call loop where the model asks for a tool, the workspace runs it, the result is fed back, and the model is re-prompted before it finally answers. This teaches the mock to reproduce that loop deterministically so scaletests actually drive real tool execution and hold streams open the way a live model would. It adds `--tool-calls-per-turn` and `--tool-call-command` so the OpenAI Chat Completions endpoint emits a controllable number of `execute` tool calls per turn (only when the request advertises an `execute` tool, otherwise it falls back to text, so it stays a safe drop-in). It also adds paced streaming with `--min-stream-duration`/`--max-stream-duration` (randomized per response) and `--response-payload-size`, so runs can simulate slow or long-lived responses instead of flushing everything at once. Closes CODAGT-307 Closes GRU-48 |
||
|
|
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. |
||
|
|
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). |
||
|
|
0b2a6cac78 |
feat: add coder secret import for bulk secret files (#27534)
Adds `coder secret import <file>` to bulk-import dotenv, JSON, or YAML secrets through the existing batch API. The command infers the format from the extension or accepts `--input-format`, supports non-interactive stdin, validates files locally before upload, and warns when imported keys cannot be injected as environment variables. Reviewed and updated by Coder Agents on behalf of @dylanhuff-at-coder. |
||
|
|
85984ff142 |
feat: add enable/disable support for user secrets (#27537)
Users can now disable a secret to stop it from being injected into workspaces without deleting it, and re-enable it later. Disabled secrets stay visible and editable everywhere they already appear. An enabled secret must have at least one injection target; a secret with no target can be stored only while disabled. Existing target-less secrets are migrated to disabled to preserve current behavior. Support spans the REST API, SDK, CLI, dashboard, and audit log. |
||
|
|
0e104f38e0 |
fix!: deprecate login_type=none, convert existing users to password login (#26851)
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell. Deprecates `login_type=none` (legacy passwordless machine users) in favour of premium **service accounts**, and migrates existing accounts off the deprecated path while preserving their identity. Resolves [DEVEX-226]. ## What this does - **Creation is gated** — `POST /users` and `coder users create` reject `login_type=none` (and the deprecated `--disable-login`) unless a service account is requested. - **Existing users are converted** — migration `000554_legacy_none_login_to_password` rewrites legacy non-system, non–service-account `login_type='none'` accounts to `login_type='password'`. Email addresses are **preserved** and existing API tokens remain valid. Admins can set a password if interactive login is desired. ## Why convert to `password` and not `is_service_account`? Migration `000433_add_is_service_account_to_users` adds two CHECK constraints: - `users_email_not_empty`: `(is_service_account = true) = (email = '')` - `users_service_account_login_type`: `is_service_account = false OR login_type = 'none'` Turning a real, email-bearing `login_type=none` user into a service account would require **blanking their email**. Converting to `password` instead preserves the account and its email. > ⚠️ **Breaking / one-way.** The `down` migration cannot restore which users originally had `login_type='none'`. Decision log - **Goal:** move existing `login_type=none` users off the deprecated path while preserving their identity/email. - **Constraint discovered:** the `is_service_account` CHECK constraints (migration `000433`) make a literal `none → service account` conversion require blanking emails, so this PR converts to `password` instead to keep emails intact. - **Implementation:** creation-gating in `cli/usercreate.go` and `coderd/users.go`, matching test updates, plus the `000554_legacy_none_login_to_password.{up,down}.sql` migration. - **CI fix:** the branch was behind `main` and its migration originally numbered `000534`, which collided with main's `000534_drop_chat_model_configs_provider`. Merged `main` and renumbered to `000554` (next free after main's `000553`). `make gen` produces no drift (the migration is data-only). > The service-account conversion alternative (#27182, which blanked emails) was closed in favour of this password-preserving approach. > > Docs follow-up: #27333. [DEVEX-226]: https://linear.app/issue/DEVEX-226 --------- Co-authored-by: Sushant P <zenithwolf1000@users.noreply.github.com> |
||
|
|
c351280a37 |
feat: add Prometheus metrics for AI Governance cost control (#27490)
## Description Adds Prometheus metrics for AI budget cost control, emitted by the aibridged server under the `cost_control` subsystem (full names are prefixed `coder_ai_gateway_`). - `blocked_requests_total` (counter) — labels: `group_id` - `blocked_users` (gauge) — labels: `group_id` - `unpriced_requests_total` (counter) — labels: `provider`, `model` - `enforcement_duration_seconds` (histogram) — labels: `outcome` ## Changes - Add `GetOverBudgetUsersPerGroup` query (plus dbauthz/dbmetrics/dbmock wiring) to count over-budget users per effective group. - Add a background collector that refreshes the `blocked_users` gauge on an interval, started only when Prometheus is enabled. - Wire `Metrics` through the aibridged server, coderd API, `cli/server.go`, and the enterprise AI gateway handler; recording is nil-safe when metrics are unset. Closes https://linear.app/codercom/issue/AIGOV-296/add-prometheus-metrics-for-cost-control > [!NOTE] > Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira |
||
|
|
5770085435 |
fix: add prefix to standalone metrics (#27526)
Adds `coder_ai_gateway_` to standalone Gateway metics to match embedded case. |
||
|
|
468b1a27a3 |
fix: remove standalone AI Gateway http listener dependency on loading providers (#27303)
Fixes an issue where the standalone AI Gateway waited for the initial provider load before starting its HTTP server. HTTP serving now starts independently of provider synchronization. `/healthz` becomes available when the HTTP server starts, while `/readyz` requires an active DRPC connection and completed initial provider load. Enables the Helm chart's startup and liveness probes by default because liveness no longer depends on provider loading. |
||
|
|
2b2a5c963a | Revert "fix(coderd): explain default GitHub app org visibility on login rejection" (#27388) | ||
|
|
48e9bb3391 |
fix(coderd): explain default GitHub app org visibility on login rejection (#27374)
## Problem On a fresh deployment with no custom GitHub OAuth app, Coder falls back to the default Coder-managed GitHub app. That app can only see organization memberships in organizations where it has been installed. If `CODER_OAUTH2_GITHUB_ALLOWED_ORGS` is set but the app isn't installed in the allowed organizations, the membership list comes back empty and every login, including the first admin login, is rejected with a bare "You aren't a member of the authorized Github organizations!" with no hint about the actual cause. This leaves fresh deployments in an apparently broken state. ## Fix * Append a remediation hint to the login rejection when the default provider is configured, pointing at the [app installation page](<https://github.com/apps/coder/installations/select_target>) and at configuring a custom GitHub OAuth app. * Log a startup warning when the default provider is combined with `CODER_OAUTH2_GITHUB_ALLOWED_ORGS`, listing the allowed orgs and the install URL. * Document the installation requirement next to the `CODER_OAUTH2_GITHUB_ALLOWED_ORGS` step in the GitHub auth docs. Access-control behavior is unchanged; the org check still rejects logins as before, it just explains why and how to fix it. ## Testing * New `TestUserOAuth2Github/NotInAllowedOrganizationDefaultProvider` asserts the hint appears when `DefaultProviderConfigured` is set; the existing `NotInAllowedOrganization` subtest asserts it does not leak into the custom-app path. Fixes coder/coder#17752 |
||
|
|
66e6f40b8b |
chore: rename agent firewall flags (#27231)
* Alias --boundary-log-proxy-socket-path to --agent-firewall-log-proxy-socket-path * Also alias CODER_AGENT_BOUNDARY_LOG_PROXY_SOCKET_PATH to CODER_AGENT_FIREWALL_LOG_PROXY_SOCKET_PATH * Also Rename related variables and symbols to reflect the new name. <!-- If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting. --> |
||
|
|
101aee8ee0 |
refactor: use Options struct in aibridgedserver.NewServer (#27200)
Refactor `aibridgedserver.NewServer` to take an `Options` struct instead of a long list of positional arguments. Follow-up to review feedback in https://github.com/coder/coder/pull/27117#discussion_r3571535760 |
||
|
|
35ade9e3d2 |
feat: collect workspace logs in support bundles (#26694)
Add workspace-side file collection to `coder support bundle` via repeatable --workspace-file flags. The agent resolves the requested paths or globs inside the remote workspace and streams back a tar with a manifest and the collected files; nothing is read from the machine running the command. - Add POST /api/v0/bundle-files to the agent's agentfiles package. - Expand env vars in the agent's environment; paths must then be absolute or start with ~/ (the agent user's home directory). - Support ** globs and tail oversized files. - Record requested patterns, per-path errors, truncation, and the applied limits in a manifest. - Unpack the archive into the bundle under agent/workspace_files/, recording dropped entries in collection_errors.txt. - Write a manifest-only archive marking collection as unsupported for agents that predate the endpoint. - Bound collection: 64 KB request body, 10000 files, 10 MiB per file, 100 MiB total including archive overhead, 110 MiB client-side read cap, 5 minute timeout. Closes #26020 |
||
|
|
4d4cbd07e6 |
fix: prevent concurrent token refreshes (#26530)
This can cause bad refresh token errors, since it can only be used once. Looks like there was an attempt to fix this by checking the database after a failed refresh, but of course this depends on the first request having updated the database in time, so both that and this fix are required to fully solve. |
||
|
|
ac35e0d3d8 |
test: fix flake in TestServer/Logging (#27263)
<!-- If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting. --> fixes https://github.com/coder/internal/issues/1618 same underlying issue as https://github.com/coder/internal/issues/946 Test flakes because main test ends before the server can connect to postrgres and this causes the CLI invocation to return an error we don't care about. |
||
|
|
4d884c30e7 |
fix: validate bedrock protocol at provider construction (#27234)
Follow-up PR to https://github.com/coder/coder/pull/26745 |
||
|
|
61e52532c0 |
docs: wrap placeholder syntax in backticks in CLI help and swagger annotations (#27194)
## Problem
Generated reference docs (`docs/reference/cli/*`,
`docs/reference/api/*`) contained raw placeholder and JSON syntax that
came straight from Go CLI help strings and swagger annotations. HTML
renderers treat the angle-bracket tokens (`<team-slug>`, `<uuid>`,
`<KEY>`, etc.) as unknown tags and drop them, so readers see
broken/half-missing text today. The same strings also break MDX parsing.
## Fix
Wrap the placeholder/JSON syntax in backticks **at the source** (Go help
strings and swagger annotation comments), then `make gen`. Rendered docs
now show the placeholders as inline code instead of dropping them.
### Source changes
| File | Placeholder wrapped | Surfaces in |
|------|--------------------|-------------|
| `codersdk/deployment.go` | `` `<organization-name>/<team-slug>` `` |
`cli/server.md`, `coder --help`, settings UI |
| `codersdk/deployment.go` | `` `CODER_AI_GATEWAY_PROVIDER_<N>_*` ``, ``
`CODER_AI_GATEWAY_PROVIDER_<N>_<KEY>` `` | `api/schemas.md` |
| `cli/tokens.go` | `` `<type>:<uuid>` `` | `cli/tokens_create.md`,
`coder --help` |
| `coderd/aitasks.go` | `` `owner:<…>` ``, `` `organization:<…>` ``, ``
`status:<status>` `` | `api/tasks.md` |
| `coderd/exp_chats.go` | `` `pr_status:<…>` `` and sibling filter
tokens | `api/chats.md` |
| `coderd/provisionerdaemons.go`, `coderd/provisionerjobs.go` | ``
`{'tag1':'value1','tag2':'value2'}` `` | `api/organizations.md`,
`api/provisioning.md` |
Everything else in the diff (`coderd/apidoc/*`, `docs/reference/**`,
`*.golden`, `site/src/api/typesGenerated.ts`) is `make gen` output.
## Reviewer notes (the "considered pass" from the ticket)
- **Product-visible:** this changes `coder server --help` and `coder
tokens create --help` output, and the `server-config.yaml` reference
comment. Backticks in terminal help are literal but read fine as
placeholder markers.
- **Settings UI:** the `deployment.go` `Description` also renders in the
deployment settings page. If that field is not Markdown-rendered,
literal backticks will show there. Happy to drop the `deployment.go`
change if you'd rather keep the UI text clean and fix `server.md`
another way.
- **Out of scope here:** `docs/reference/cli/agent-firewall.md`
(`<host>`/`<glob>`) is generated from the external
`github.com/coder/boundary` module, not this repo. It needs an upstream
fix + module bump; not included in this PR.
<details>
<summary>Implementation notes / decision log</summary>
- Scope taken from DOCS-551: source-level backtick pass for generated
reference docs only. Hand-written Markdown fixes are tracked separately
(companion ticket).
- Swagger `@Param` descriptions are Go comments, so the existing `\|`
pipe-escaping in the chats `q` filter is preserved inside the new
backticks (still required for the Markdown table cell to render `|`).
- Verified after `make gen`: generated docs render placeholders as code
spans, table pipes intact; `gofmt` clean; changed Go packages build; no
emdash/endash introduced.
- Deliberately left the `AIProviderConfig` type-level doc comment
untouched because it does not surface in any generated doc (kept the
diff to doc-feeding comments).
</details>
Linear: DOCS-551
---
_Opened by Coder Agents on behalf of @nickvigilante._
---
## Evidence: placeholders dropped on the live docs site
Verified **2026-07-14** against the live site (`coder.com/docs`, i.e.
`main`, pre-merge) by loading each affected page in headless Chrome and
reading the post-hydration DOM (confirmed identical in the raw page
payload). Each simple `<token>` placeholder is parsed as an **empty
custom HTML element**, so the browser renders nothing for it and the
placeholder text disappears from the page.
### What readers see today (before this PR)
| Page (live) | Source Markdown | Rendered on the live site |
|-------------|-----------------|---------------------------|
| [`cli/server`](https://coder.com/docs/reference/cli/server) — OAuth2
GitHub Allowed Teams | `Structured as: <organization-name>/<team-slug>.`
| `Structured as: /.` |
|
[`cli/tokens_create`](https://coder.com/docs/reference/cli/tokens_create)
— `--allow` | `Repeatable allow-list entry (<type>:<uuid>, e.g.
workspace:1234-...).` | `Repeatable allow-list entry (:, e.g.
workspace:1234-...).` |
| [`api/tasks`](https://coder.com/docs/reference/api/tasks) — `q` | `...
status:<status>` | `... status:` (nothing after the colon) |
| [`api/schemas`](https://coder.com/docs/reference/api/schemas) —
AIBridgeConfig (`anthropic`/`bedrock`/`openai`) |
`CODER_AI_GATEWAY_PROVIDER_<N>_*` | `CODER_AI_GATEWAY_PROVIDER__*` |
| [`api/schemas`](https://coder.com/docs/reference/api/schemas) —
AIBridgeConfig (`providers`) | `CODER_AI_GATEWAY_PROVIDER_<N>_<KEY>` |
`CODER_AI_GATEWAY_PROVIDER__` |
[`api/chats`](https://coder.com/docs/reference/api/chats) (`q`) drops
five tokens the same way — `title:<substring>`, `diff_url:<url>`,
`pr:<number>`, `pr_title:<text>`, and the trailing `title:<value>`. The
live parameter description reads (note the dangling `title:`,
`diff_url:`, `pr:`, `pr_title:`):
```text
Search query. Supports title: (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status:<draft|open|merged|closed> as repeated or comma-separated values, source:<created_by_me|shared_with_me>, diff_url: (quote values containing colons), pr: (exact PR number match), repo:<owner/repo> (case-insensitive substring match against git remote origin or URL), pr_title: (case-insensitive PR title substring). Bare terms are not supported; use title: for title filtering.
```
<details>
<summary>Raw rendered DOM from the live site (headless Chrome,
post-hydration)</summary>
```html
<!-- reference/cli/server -->
Structured as: <organization-name>/<team-slug>.</team-slug></organization-name>
<!-- reference/cli/tokens_create -->
Repeatable allow-list entry (<type>:<uuid>, e.g. workspace:1234-...).</uuid></type>
<!-- reference/api/tasks : only status:<status> drops; the /-containing tokens are escaped and survive -->
Search query for filtering tasks. Supports: owner:<username/uuid/me>, organization:<org-name/uuid>, status:<status></status>
<!-- reference/api/schemas : anthropic / bedrock / openai rows -->
Deprecated: Use Providers with indexed CODER_AI_GATEWAY_PROVIDER_<n>_* env vars instead.</n>
<!-- reference/api/schemas : providers row -->
Providers holds provider instances populated from CODER_AI_GATEWAY_PROVIDER_<n>_<key> env vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.</key></n>
```
The parser auto-inserts closing tags
(`</team-slug></organization-name>`) and lowercases the tag name (`<N>`
becomes `<n>`), leaving `__` where `<N>_` used to be. Every wrapped
placeholder renders correctly as inline code on the [docs preview for
this
branch](https://coder.com/docs/@vigilante%2Fdocs-551-backtick-placeholder-syntax-in-generated-reference-docs-cli/reference/cli/server).
</details>
### Accuracy note — cases that do *not* drop on live
These render fine today, so they are **not** evidence of dropping (the
PR still wraps them for consistency / MDX-safety):
-
[`api/organizations`](https://coder.com/docs/reference/api/organizations)
and
[`api/provisioning`](https://coder.com/docs/reference/api/provisioning):
`{'tag1':'value1','tag2':'value2'}` renders verbatim — curly braces are
not an HTML tag.
- Tokens containing `/` or `|` are escaped by the renderer and stay
visible (as literal `<...>`): `<username/uuid/me>`, `<org-name/uuid>`,
`<owner/repo>`, `<draft|open|merged|closed>`,
`<created_by_me|shared_with_me>`. Backticks still improve their
readability, but they were never dropped.
|
||
|
|
3126306598 |
feat: add --aigateway-proxy-target flag (#27122)
Adds `--aigateway-proxy-target` option to `deploymentGroupAIGatewayProxy` that defines URL to which intercepted requests should be forwarded to. Forward URL used to be hardcoded to `coderAPI.AccessURL` pointing to embedded Gateway. With addition of standalone AI Gateway this needs to be configurable. Renamed `aibridgeproxyd.Server.coderAccessURL` and `coderAccessPort` -> `gatewayURL` and `gatewayPort` + option to better reflect reality. |
||
|
|
0c3c65d85b |
fix: stabilize latest workspace app status ordering (DEVEX-381) (#27041)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell. Closes [DEVEX-381](https://linear.app/codercom/issue/DEVEX-381/flake-test-tasksendwaitsforworkingappstate). Follow-up to #25648 and #25858, which addressed a different symptom of the same test. ## Symptom ``` task_send_test.go:348: context expired while waiting for trap: context deadline exceeded --- FAIL: Test_TaskSend/WaitsForWorkingAppState (26.02s) ``` Windows-only, on `test-go-pg (windows-2022)`. Reported four times since #25648 landed (2026-06-02, 2026-06-10, 2026-07-01). ## Root cause The test: 1. `setupCLITaskTest` inserts `workspace_app_status(state=idle)` at the end of setup. 2. `WaitsForWorkingAppState` then inserts `workspace_app_status(state=working)` before starting the CLI. 3. Both are persisted via `dbtime.Now()`, which rounds to microseconds. Windows `time.Now()` resolution is coarser than that (often ~1 ms or worse), so back-to-back calls frequently round to the same microsecond. 4. `GetLatestWorkspaceAppStatusesByWorkspaceIDs` has no tiebreaker: ```sql ORDER BY workspace_id, created_at DESC ``` Its sibling `GetLatestWorkspaceAppStatusByAppID` already uses `ORDER BY created_at DESC, id DESC` for exactly this reason. When the two rows collide, Postgres picks either. 5. On the failing runs, the query returned the `idle` row. `waitForTaskIdle` saw idle on the first poll, returned nil, `TaskSend` proceeded, and the CLI completed successfully in ~5 s. 6. But the test was blocked at `resetTrap.MustWait(ctx)` waiting for a **second** `ticker.Reset` that never happened. `WaitLong = 25s` elapsed, line 348 failed. CI log confirms the sequence: only one `Ticker.Reset(5s)` is caught, then `Ticker.Stop([]) call, matched 0 traps` (from `defer ticker.Stop()`), then the trap wait times out. This is the same class of flake Spike documented in #15923 and #21332 ("Windows in particular doesn't have high-resolution timers"), just hidden behind a SQL `ORDER BY`. ## Fix Two changes: 1. **`coderd/database/queries/workspaceapps.sql`**: add an `id DESC` tiebreaker to `GetLatestWorkspaceAppStatusesByWorkspaceIDs`, matching `GetLatestWorkspaceAppStatusByAppID`. Makes the query deterministic when `created_at` collides. 2. **`cli/task_test.go` / `cli/task_send_test.go`**: add a `withoutInitialAppStatus()` option to `setupCLITaskTest` and use it from `WaitsForWorkingAppState`. The test now inserts a single `working` row, so the collision cannot happen in the first place. Belt-and-braces with change 1. Comments in both places reference DEVEX-381 and #21332 so the next agent doesn't have to re-derive this. ## Verification - `go test ./cli -run 'Test_TaskSend' -count=1`: all 12 subtests pass, `WaitsForWorkingAppState` completes in ~5.6 s (was ~16 s previously due to a longer poll loop). - Stress: 20 sequential runs of `WaitsForWorkingAppState` on Linux, race-enabled binary, all pass in ~5.5 s each. - `go test ./coderd -run 'AppStatus|Task' -count=1` passes. - `go vet ./coderd/database/... ./cli/...` clean. - `make lint/emdash` clean. - `gofmt` clean. Not reproducible on Linux (real time between the two patches is orders of magnitude larger than microsecond); the Windows path is fixed by making the ordering deterministic and by not creating the collision in the first place. <details> <summary>Implementation plan & decision log</summary> ### Investigation 1. Pulled the failing job log for run `28483879823/job/84428355669`. 2. Traced the mock-clock trap sequence: one `NewTicker` and exactly one `Ticker.Reset(5s)` were caught, then `Ticker.Stop([]) call, matched 0 traps` fires (the `defer ticker.Stop()` on `waitForTaskIdle` return). This proves `waitForTaskIdle` returned after a single poll, not that the trap machinery hung. 3. The command exited with `<nil>` (`clitest.go:299: command "coder task send" exited with error: <nil>`) and a `POST /send` completed in 5.4 s. So the CLI succeeded; the test's own trap wait is what timed out. 4. The only `waitForTaskIdle` return-nil paths are `Active + CurrentState.State in {Idle, Complete, Failed}` and `Active + CurrentState == nil past 30s grace`. First observation of nil cannot be past 30s. So `TaskByID` must have returned `State == Idle`. 5. Traced `TaskByID` → `taskGet` → `workspaceData` → `GetLatestWorkspaceAppStatusesByWorkspaceIDs`. Found the missing tiebreaker; the sibling query one line above (`GetLatestWorkspaceAppStatusByAppID`) already had it. 6. Confirmed the two `PATCH /app-status` calls in the Windows log happened at `00:26:13.077` and `00:26:13.093`, well within Windows timer resolution. 7. Confirmed `dbtime.Now()` rounds to microseconds; Windows `time.Now()` doesn't have that precision, so `Round(time.Microsecond)` on two calls close together frequently produces equal values. ### Prior art from Spike - #15923: loosened `HeartbeatPeriod * 9/10` to `3/4` for Windows. - #21332: switched `assert.After` to `assert.NotBefore` because timestamps can equal on Windows. Both explicitly cite "Windows doesn't always have high-resolution timers available." ### Considered alternatives - **Only fix the test.** Works today but leaves the SQL query non-deterministic; another test that relies on `GetLatestWorkspaceAppStatusesByWorkspaceIDs` could hit the same collision. - **Only fix the SQL query.** Would give a stable answer but not necessarily the *right* one. If both patches share a `created_at`, `id DESC` picks whichever UUID sorted higher, still random with respect to insertion order. - **Make `dbtime.Now()` monotonic per process.** Cleanest at the source, but affects every timestamp in the database and has broader implications than a targeted flake fix. Going with both the query fix (defense in depth, matches existing pattern) and the test fix (eliminates the collision at the source) is the smallest change that closes the flake and hardens the query. ### Rejected commit-message scopes Changes touch both `cli/` and `coderd/database/`, so per AGENTS.md the scope is omitted for the cross-cutting commit and PR title. </details> |
||
|
|
63ec93a7ce |
feat: add AWS Bedrock mantle endpoint to AI Gateway (#26745)
Implements https://linear.app/codercom/issue/AIGOV-213/add-bedrock-provider # AWS Bedrock mantle support in AI Gateway ## Summary Add support for the AWS Bedrock **mantle** endpoint (`bedrock-mantle.{region}.api.aws/anthropic/v1/messages`) to AI Gateway. Mantle serves Claude through the native Anthropic Messages API. We model it as a `protocol` field on the existing Bedrock provider settings (`invoke-model` default, or `mantle`) rather than as a new provider type, and we treat mantle as a pure passthrough: SigV4-sign and forward, no body translation. ## Background Claude on AWS Bedrock is reachable through two endpoints, each speaking exactly one wire protocol: 1. **InvokeModel** (existing): `bedrock-runtime.{region}.amazonaws.com`. Model id in the URL path, request translated into Bedrock's InvokeModel format, responses returned as a binary AWS eventstream. This is what AI Gateway already supported for Bedrock. 2. **Mantle** (this doc): `bedrock-mantle.{region}.api.aws/anthropic/v1/messages`. Native Anthropic Messages API: model in the body, plain SSE streaming. ## Why a `protocol` field, not a new provider type The alternative is to model mantle as its own `ai_provider_type` (`bedrock-mantle`) alongside `bedrock`. I chose the `protocol` field instead for two reasons: 1. Mantle reads more like a protocol of Bedrock than a separate provider. It is the same AWS account, credentials, region, and IAM, reached over a different wire protocol and host. One Bedrock provider with two protocols (`invoke-model` default and `mantle`) models that more organically than two provider types. 2. It avoids a database migration. The `protocol` field lives in the settings JSON blob (empty resolves to `invoke-model`, so existing providers are unaffected), whereas a new type means an enum value and the `ALTER TYPE ... ADD VALUE` migration that goes with it. ## Why passthrough, not translation The client already emits Bedrock-legal requests in mantle mode: ```sh export CLAUDE_CODE_USE_MANTLE=1 export CLAUDE_CODE_SKIP_MANTLE_AUTH=1 export ANTHROPIC_BEDROCK_MANTLE_BASE_URL=https://<coder>/api/v2/aibridge/<provider-name> ``` So the gateway just forwards the body and SigV4-signs it (service `bedrock-mantle`), and skips all the InvokeModel body-translation (model remap, thinking conversion, beta-flag allowlist, field stripping). This keeps the mantle path thin and avoids a second copy of translation logic to maintain. ## Consequences - Protocol-dependent fields: `model` / `small_fast_model` are used by InvokeModel but ignored by mantle (the client sends the model), and `base_url` is required for mantle but optional for InvokeModel. Validation is protocol-aware. - No central model control on mantle: because it is a passthrough, the operator cannot pin the model. - `region` and the `base_url` host must name the same region (the SigV4 scope must match the endpoint); a mismatch surfaces as `Credential should be scoped to a valid region`. ## Draft UI <img width="1100" height="579" alt="image" src="https://github.com/user-attachments/assets/37bab46d-8958-4a96-9f47-1fef3493e1b6" /> ## Follow-up PRs: - https://github.com/coder/coder/pull/27156 |
||
|
|
ad29777cb2 | feat: NATS mTLS pubsub implementation (#26902) | ||
|
|
6580cdcf7f |
refactor: use AI budget period from deployment config (#27117)
## Description
Read the AI budget period from the deployment config on both the RPC server and the `/users/{user}/ai/spend` endpoint, instead of hardcoding `month`. Drops the `period_start` RPC parameter that was incorrectly introduced in #26915: the period should have been derived from the deployment config from the start.
## Changes
- Add `codersdk.NewAIBudgetPeriodFromString`, mirroring `NewAIBudgetPolicyFromString`.
- `aibridgedserver.Server` takes a `quartz.Clock`, reads `BudgetPeriod` from the deployment config at construction, and computes the period window inside `IsBudgetExceeded`.
- Remove `period_start` from `IsBudgetExceededRequest` and stop sending it from the daemon.
- The `userAISpendStatus` endpoint reads the period from `AIBridgeConfig.BudgetPeriod` instead of hardcoding month.
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
|
||
|
|
81bb8a49c4 |
test(cli): tolerate stray requests in fake agent API (#27127)
Test_TaskSend flaked (coder/internal#1547, coder/internal#1609) when a stray POST /chat/completions hit the fake agent API and the catch-all handler called t.Fatalf. No code under test posts that path to the sidebar app URL; the request most likely came from another test's lingering client after its server's ephemeral port was reused. Fatalf was also called off the test goroutine, which the testing package forbids. Unknown paths now get a 404 and a log line with request details for attribution. Unstubbed known agentapi endpoints still fail the test, via t.Errorf, so a coderd regression is still caught. |
||
|
|
66b42650ae |
fix(cli): respect empty --ssh-host-prefix/--hostname-suffix flags (#27084)
## Problem `coder config-ssh --ssh-host-prefix=""` (or the matching env var, `CODER_CONFIGSSH_SSH_HOST_PREFIX=`) was silently ignored, and the deprecated `Host coder.*` block was written to the SSH config anyway. The merge logic that decides whether to fall back to the server's default prefix checked `user.userHostPrefix == ""`, which is true both when the flag was never passed and when it was explicitly set to empty, so there was no way to distinguish the two. The same issue applied to `--hostname-suffix`. ## How this affects users Anyone who wants to opt out of the legacy prefix-based SSH aliases (`ssh coder.myworkspace`) in favor of the newer suffix-based ones (`ssh myworkspace.coder`) had no way to do so, the `Host coder.*` wildcard block kept reappearing on every `config-ssh` run regardless of the flag. Because that wildcard matches any hostname starting with `coder.`, not just Coder workspaces, it can silently intercept SSH connections to unrelated hosts that happen to share that prefix. It got worse on top of that: even after passing `--ssh-host-prefix=""`, running `config-ssh --use-previous-options` in a later session, a normal way to refresh local config without retyping every flag, would silently bring the block back, because the empty choice was never persisted to the file in the first place. ## Solution Track whether each option (`--ssh-host-prefix`, `--hostname-suffix`) was explicitly set by the user, as opposed to left at its zero value, and only fall back to the server default (or skip persisting the option) when it was genuinely never set. ## How it works Two new fields on `sshConfigOptions`, `userHostPrefixExplicit` and `hostnameSuffixExplicit`, carry this information: - **Live invocation**: they're set from `userSetOption(inv, ...)`, which inspects serpent's `Option.ValueSource` for the flag, right after `header`/`headerCommand` are set in the `Handler`, before any `--use-previous-options`/prompt logic can replace the struct wholesale from a prior run's saved options. - **Persistence**: `sshConfigWriteSectionHeader` now writes the `# :ssh-host-prefix=` comment line even when the value is empty, as long as it was explicit, and `sshConfigParseLastOptions` sets the field back to `true` whenever it parses that line on a later run, regardless of value. `mergeSSHOptions`'s fallback condition changed from `user.userHostPrefix == ""` to `user.userHostPrefix == "" && !user.userHostPrefixExplicit` (and the mirror for suffix). `equal()` and `asList()` were extended to include the two new fields so the `--dry-run` diff and "options differ, use new ones?" prompt stay accurate. ## Why implemented this way - Reuses `userSetOption` (`cli/util.go`), an existing helper already used for this exact "distinguish zero value from unset" problem elsewhere in the CLI (`cli/templateedit.go`), instead of inventing new machinery. - Storing the "explicit" bit as a plain field on `sshConfigOptions`, rather than as extra parameters to `mergeSSHOptions`, keeps that function dependency-free (still plain data in, plain data out, no `serpent.Invocation` coupling), while letting the same bit flow naturally through the SSH config's persisted-options comment, solving the live-flag case and the `--use-previous-options` persistence case with one mechanism instead of two. - A sentinel-value approach was considered and rejected: a self-tracking custom `serpent.Value` doesn't work because serpent applies a flag's default through the same `Value.Set()` call used for real input, so it can't tell the two apart; a plain sentinel string would work but leak into several other code paths (equality checks, diff/prompt text, the persisted comment) that would all need to filter it out. Closes https://github.com/coder/internal/issues/1208 ## Manual verification Every step below was run against a local dev server (`./scripts/develop.sh` + `./scripts/coder-dev.sh`), pointed at a throwaway `--ssh-config-file`, never a real `~/.ssh/config`. ### 1. Baseline: unchanged behavior with no flags ```sh ./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" cat "$TEST_SSH_CONFIG" ``` Both `Host coder.*` and `Host *.coder` are written, unchanged from before this fix (both server defaults are non-empty out of the box). <details> <summary>Output</summary> ```text Updated "/tmp/tmp.9Y7VIeuQoY" You should now be able to ssh into your workspace. For example, try running: $ ssh myworkspace.coder # ------------START-CODER----------- # This section is managed by coder. DO NOT EDIT. # # You should not hand-edit this section unless you are removing it, all # changes will be lost when running "coder config-ssh". # Host coder.* ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR ProxyCommand .../coder-slim ... ssh --stdio --ssh-host-prefix coder. %h Host *.coder ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR Match host *.coder !exec ".../coder-slim connect exists %h" ProxyCommand .../coder-slim ... ssh --stdio --hostname-suffix coder %h # ------------END-CODER------------ ``` </details> ### 2. Explicit empty `--ssh-host-prefix` omits the legacy block (the core fix) ```sh ./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --ssh-host-prefix "" cat "$TEST_SSH_CONFIG" ``` `Host coder.*` is gone, only `Host *.coder` remains. The choice is now also persisted (`# :ssh-host-prefix=`). <details> <summary>Output</summary> ```text Updated "/tmp/tmp.9Y7VIeuQoY" You should now be able to ssh into your workspace. For example, try running: $ ssh myworkspace.coder # ------------START-CODER----------- # This section is managed by coder. DO NOT EDIT. # # You should not hand-edit this section unless you are removing it, all # changes will be lost when running "coder config-ssh". # # Last config-ssh options: # :ssh-host-prefix= # Host *.coder ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR Match host *.coder !exec ".../coder-slim connect exists %h" ProxyCommand .../coder-slim ... ssh --stdio --hostname-suffix coder %h # ------------END-CODER------------ ``` </details> ### 3. Same, via the environment variable instead of the flag ```sh CODER_CONFIGSSH_SSH_HOST_PREFIX="" ./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" grep -c "Host coder" "$TEST_SSH_CONFIG" ``` Confirms the fix isn't flag-only, `userSetOption` checks `ValueSource`, set the same way for `ValueSourceFlag` and `ValueSourceEnv`. <details> <summary>Output</summary> ```text Updated "/tmp/tmp.9Y7VIeuQoY" You should now be able to ssh into your workspace. For example, try running: $ ssh myworkspace.coder 0 ``` </details> ### 4. Explicit empty prefix combined with an explicit suffix ```sh ./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --ssh-host-prefix "" --hostname-suffix mytest cat "$TEST_SSH_CONFIG" ``` Only `Host *.mytest` is written. Both options are correctly recorded in the persisted comment. <details> <summary>Output</summary> ```text Updated "/tmp/tmp.9Y7VIeuQoY" You should now be able to ssh into your workspace. For example, try running: $ ssh myworkspace.mytest # ------------START-CODER----------- # This section is managed by coder. DO NOT EDIT. # # You should not hand-edit this section unless you are removing it, all # changes will be lost when running "coder config-ssh". # # Last config-ssh options: # :ssh-host-prefix= # :hostname-suffix=mytest # Host *.mytest ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR Match host *.mytest !exec ".../coder-slim connect exists %h" ProxyCommand .../coder-slim ... ssh --stdio --hostname-suffix mytest %h # ------------END-CODER------------ ``` </details> ### 5. The explicitly-empty choice survives `--use-previous-options` with no flag repeated This is the persistence half of the fix: confirms the "omit this block" choice, once persisted, doesn't get lost on a later run that reuses previous options without repeating `--ssh-host-prefix`. Before this fix, this exact sequence would bring `Host coder.*` back. ```sh ./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --ssh-host-prefix "" ./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --use-previous-options cat "$TEST_SSH_CONFIG" ``` <details> <summary>Output</summary> ```text Updated "/tmp/tmp.9Y7VIeuQoY" You should now be able to ssh into your workspace. For example, try running: $ ssh myworkspace.coder No changes to make. # ------------START-CODER----------- # This section is managed by coder. DO NOT EDIT. # # You should not hand-edit this section unless you are removing it, all # changes will be lost when running "coder config-ssh". # # Last config-ssh options: # :ssh-host-prefix= # Host *.coder ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR Match host *.coder !exec ".../coder-slim connect exists %h" ProxyCommand .../coder-slim ... ssh --stdio --hostname-suffix coder %h # ------------END-CODER------------ ``` </details> The second command printed `No changes to make.`, and critically, `Host coder.*` did **not** reappear even though that run passed no `--ssh-host-prefix` flag at all, only `--use-previous-options`. ### 6. `--use-previous-options` still wins over this run's explicit empty flag (unaffected by this fix) Confirms this fix didn't change the pre-existing, intentional precedence of `--use-previous-options`: a previously-saved *non-empty* value still wins over an explicit empty flag passed on a later run. ```sh ./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --ssh-host-prefix "custom-test." ./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --use-previous-options --ssh-host-prefix "" cat "$TEST_SSH_CONFIG" ``` <details> <summary>Output</summary> ```text Updated "/tmp/tmp.9Y7VIeuQoY" You should now be able to ssh into your workspace. For example, try running: $ ssh myworkspace.coder No changes to make. # ------------START-CODER----------- # This section is managed by coder. DO NOT EDIT. # # You should not hand-edit this section unless you are removing it, all # changes will be lost when running "coder config-ssh". # # Last config-ssh options: # :ssh-host-prefix=custom-test. # Host custom-test.* ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR ProxyCommand .../coder-slim ... ssh --stdio --ssh-host-prefix custom-test. %h Host *.coder ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR Match host *.coder !exec ".../coder-slim connect exists %h" ProxyCommand .../coder-slim ... ssh --stdio --hostname-suffix coder %h # ------------END-CODER------------ ``` </details> `Host custom-test.*` is preserved verbatim, `--use-previous-options` correctly overrides the explicit empty flag when the saved value is non-empty, the mirror image of step 5's explicit-empty saved value. ### 7. End-to-end sanity check with a real workspace ```sh ./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --ssh-host-prefix "" --hostname-suffix mytest ssh -F "$TEST_SSH_CONFIG" -o ConnectTimeout=15 myworkspace.mytest echo ok ``` <details> <summary>Output</summary> ```text Updated "/tmp/tmp.9Y7VIeuQoY" You should now be able to ssh into your workspace. For example, try running: $ ssh myworkspace.mytest ok ``` </details> `ok` came back from a real, running workspace, confirming the ProxyCommand and Match/exec wiring generated by the suffix-only config actually establishes a working SSH session end-to-end, not just a text-generation check. |
||
|
|
bab8ce9d41 |
feat: setup logging, tracing and metrics in standalone AI Gateway (#27068)
Adds logging, tracing and metrics setup to standalone AI Gateway. Existing options are re-used when possible. |
||
|
|
affb359d13 |
feat: synchronise provider changes with WatchAIProviders (#27091)
## Why PR #26797 was accidentally merged into the stale `graphite-base/26797` branch instead of `main` (Graphite picked the wrong base), so its changes never landed on `main`. This PR re-lands that work as a clean cherry-pick onto the current `main`. ## What Adds a `WatchAIProviders` streaming RPC to the `ProviderConfigurator` service so a running standalone AI Gateway refetches its provider set when the provider configuration changes. The server subscribes to `AIProvidersChangedChannel` (published by the provider CRUD endpoints) and forwards each event as a payload-free signal, plus one signal on subscribe; the gateway calls `GetAIProviders` on each signal to rebuild its pool. The aibridged API is bumped to v1.2. Env-seeded providers don't need a signal: seeding finishes before coderd serves the gateway connection, so the gateway's initial fetch already reflects the seeded set. ## For reviewers The change is split into two commits to make review easy: 1. **`feat: synchronise provider changes with WatchAIProviders`** is a faithful cherry-pick of #26797, identical to the originally reviewed PR. It is committed without pre-commit hooks because it does not build against current `main` on its own. 2. **`fix: resolve cherry-pick conflicts against main`** contains only the deltas needed to re-land on current `main`, and passes the full pre-commit suite: - `coderd/aibridged/proto/aibridged.pb.go` regenerated via the proto make target (the cherry-picked copy was generated against the older proto). - `enterprise/cli/aigatewaystart.go` import block unioned; `main` added `os` and `strings` while the PR added `sync`. - Three `aibridgedserver.NewServer` test call sites that landed on `main` after the original branch diverged now pass the new `pubsub` argument. Refs https://linear.app/codercom/issue/AIGOV-465 *This PR was produced by opencode (agent) using the `anthropic/claude-opus-4-8` model, under human direction and review.* |
||
|
|
ccba3969ab |
feat: add ai-gateway start command (#26605)
> AI Tools were used to produce this PR This PR adds `coder ai-gateway start` command that runs the AI Gateway as an independent process. - Standalone process doesn't have access to DB. Uses DRPC services under `/api/v2/ai-gateway/serve`for auth, recording and provider initialization. - It only handles LLM traffic, other endpoints (eg. `/sessions`) are only available though `coderd`. - The standalone gateway reuses applicable flags from AI Gateway deployment options. Provider-seeding and coderd-only options are excluded. - Only added to fat build, the slim build stub rejects the command. Some wiring used by this new command is added. **`NewWebsocketDialer`** - implements the standalone gateway's connection to coderd's `/api/v2/ai-gateway/serve` endpoint. It upgrades to a WebSocket, multiplexes with yamux, and wires all DRPC services. **`AIGatewayDataPlaneMiddleware`** - extracts the per-request middleware chain (concurrency limiting, rate limiting, BYOK gating) into a shared function used by both the embedded route and the standalone gateway. **`RootCmd.ResolveClientConnection`** - resolve the deployment URL and builds an HTTP transport without requiring a session token. Used in `ai-gateway start`command as it authenticates using different credential type. --------- Co-authored-by: Danny Kopping <danny@coder.com> |
||
|
|
b169f4d8cb |
feat: expose external auth token expiry in agent API and CLI (#26883)
Previously, \`ExternalAuthResponse\` contained no expiry information, so workspace agents and git credential helpers had no way to know when a cached token would stop being valid. Every git operation had to call back to coderd via \`GIT_ASKPASS\` to get a fresh token, adding 1-2 seconds of latency. This PR surfaces \`OAuthExpiry\` from the database as \`ExpiresAt\` in \`ExternalAuthResponse\`, allowing agents to cache tokens with correct eviction timing (compatible with \`git-credential-cache --timeout\` and \`password_expiry_utc\` introduced in git 2.34). \`ExpiresAt\` is normalized to UTC before JSON encoding to avoid sub-minute precision loss that occurs when the PostgreSQL driver applies historical Local Mean Time (LMT) timezone offsets to year-1 AD timestamps. The \`coder external-auth access-token\` CLI command gains \`--output json\` to print the full response including \`ExpiresAt\`, enabling scripts to consume the expiry without parsing heuristics. Closes https://github.com/coder/coder/issues/26036 ## Manual Test <details> <summary>Setup</summary> 1. Create a GitHub OAuth app at https://github.com/settings/developers with: - Homepage URL: `http://127.0.0.1:3000` - Authorization callback URL: `http://127.0.0.1:3000/external-auth/github/callback` 2. Start the dev server with the GitHub provider configured: ```sh CODER_EXTERNAL_AUTH_0_ID=github CODER_EXTERNAL_AUTH_0_TYPE=github CODER_EXTERNAL_AUTH_0_CLIENT_ID=<client-id> CODER_EXTERNAL_AUTH_0_CLIENT_SECRET=<client-secret> ./scripts/develop.sh ``` 3. Log in at `http://127.0.0.1:3000` (use `127.0.0.1`, not `localhost`, so the OAuth state cookie domain matches the callback URL). 4. Go to Account > External Authentication and click **Connect** next to GitHub. Complete the OAuth flow. 5. Create a workspace and SSH into it: ```sh coder create test-workspace coder ssh test-workspace ``` </details> <details> <summary>Flow 1: Token is valid — JSON output includes <code>expires_at</code></summary> Inside the workspace, run: ```sh coder external-auth access-token github --output json echo "Exit code: $?" ``` Expected output (GitHub tokens have no expiry, so \`expires_at\` is the zero value): ```json { "access_token": "<redacted>", "token_extra": null, "url": "", "type": "github", "expires_at": "0001-01-01T00:00:00Z", "username": "<redacted>", "password": "" } ``` ``` Exit code: 0 ``` </details> <details> <summary>Flow 2: Token missing — JSON output includes auth URL, exit code 1</summary> Disconnect GitHub in the Coder UI (Account > External Authentication > Disconnect), then inside the workspace run: ```sh coder external-auth access-token github --output json echo "Exit code: $?" ``` Expected output: ```json { "access_token": "", "token_extra": null, "url": "http://127.0.0.1:3000/external-auth/github", "type": "", "expires_at": "0001-01-01T00:00:00Z", "username": "", "password": "" } ``` ``` Exit code: 1 ``` </details> |
||
|
|
8b60d1d877 |
ci: cache embedded postgres binaries in flake checks (#26986)
flake-go keeps failing every `TestServer` subtest that boots `coder server` with built-in PostgreSQL ([example run](https://github.com/coder/coder/actions/runs/28685288282/job/85134336803)) with `no version found matching 13.21.0`, which is embedded-postgres's error for any non-200 while downloading the Postgres binary archive from Maven. The archive gets cached under the server's config root, which is a fresh temp dir in every test, so with `test-count: 35` one flake run downloads it dozens of times and Maven rate-limits the runner. Using an external Postgres (like coder/terraform-provider-coderd#370 did) would defeat the point, since these subtests exist to exercise the built-in Postgres path. Instead, `startBuiltinPostgres` now honors `EMBEDDED_PG_CACHE_DIR` as the archive cache path (test runs only, data/runtime dirs stay per-test), and flake-go.yaml wires in the existing `embedded-pg-cache` actions the same way the Windows/macOS lanes in ci.yaml already do. All iterations then share a single download, and usually zero once the actions cache is warm. The upload step only saves on `main`. |
||
|
|
d7ad85f7f6 |
feat: support multiple OIDC redirect URIs (#25408)
This PR adds a new opt-in setting, `CODER_OIDC_REDIRECT_ALLOWED_HOSTS`, that lets a single Coder deployment complete OIDC login on more than one hostname. When the allowlist is non-empty, Coder picks the OIDC `redirect_uri` based on the incoming request's Host header (validated against the list) instead of always using the static URL derived from `CODER_ACCESS_URL`. When unset, the (default) behavior is identical to today. The motivation is that a single Coder deployment is frequently reachable via multiple hostnames - for example, an internal hostname for users on a corporate VPN and a different hostname routed through a zero-trust gateway for users off-VPN - but OIDC login today only works on whichever single hostname `CODER_ACCESS_URL` points to, because the `redirect_uri` sent to the IdP is fixed at server startup. Users who reach the deployment on any other valid hostname can see the login page but fail the OIDC callback, since the IdP redirects them back to a hostname they can't reach (or whose cookies they don't have). |
||
|
|
db7f4438b4 |
feat: generate STS external ID for Bedrock role assumption (#26869)
Implements: https://linear.app/codercom/issue/AIGOV-495/add-externalid-to-prevent-confused-deputy-problem When a Bedrock provider assumes an IAM role via STS, the gateway now generates a unique external ID for it and sends that value on every `AssumeRole` call. The external ID guards against the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html) on cross-account role assumption. Per [AWS's recommendation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html), the gateway generates and owns the value rather than accepting one from the operator; that ownership is what makes it effective, since a party who knows another's external ID can't induce the gateway to send it. The external ID is server-owned and read-only over the API. It is generated once, when a provider first has a `role_arn`, and is stable thereafter. Clients cannot set it: create rejects any supplied `external_id`, and update rejects a value that differs from the stored one. An update may echo the stored value back unchanged, so the normal read-modify-write flow (GET the provider, change a field, PATCH the full settings object) keeps working. The value is not a secret and is returned on GET so operators can copy it into the target role's trust policy as an `sts:ExternalId` condition. It is persisted in the existing JSON settings blob, so there is no migration or audit-table change. |
||
|
|
4936ff9808 |
refactor: deprecate AIGatewayRoutingEnabled, remove direct chat routing (#26862)
This PR removes the now-dead direct-routing code: - Deletes the direct routing implementation. - Collapses the resolvedModelRoute discriminated union into aiGatewayModelRoute. - Removes the dead providerKeys cascade. - Deletes the preferredShortTextCandidates quickgen function. - Simplifies the advisor override error handling. - Deprecates the AIGatewayRoutingEnabled deployment option. It is now a no-op so as to not break existing deployments on upgrade. Once direct routing was gone, the AI Gateway became mandatory for chat, which surfaced gaps in how the product behaves with the gateway disabled: - Exposes ai-gateway-enabled to the frontend via embedded page metadata. - Disables the chat composer via the existing AgentSetupNotice when the gateway is disabled, for both new and existing chats. - Fixes nil/typed-nil chatDaemon panics on startup and shutdown when gateway is disabled. - Fixes chat WebSocket from retrying the still-gated stream endpoint forever when the gateway is disabled. |
||
|
|
047c47495b |
refactor: drop chat_model_configs provider column (#26877)
The provider type already lives authoritatively in ai_providers.type, reachable on every active row through ai_provider_id, which the chat_model_configs_ai_provider_required_when_active CHECK makes mandatory. The stored provider string was a denormalized copy the system kept in sync with a startup backfill and no longer needs. Every surface now derives provider type from the linked ai_providers row. Telemetry is the one exception: it keeps emitting provider, now sourced from ai_providers.type via a JOIN, so the BigQuery column and the Nexus dashboards that read it are unaffected. The experimental HTTP/SDK response drops provider and makes ai_provider_id required, since those endpoints return only active configs; consumers resolve provider type from ai_provider_id and the AI providers listing. This ships in a single release with no compatibility window: production reads the table via SELECT *, so a pre-drop binary fails config reads the moment the column is gone. Operators must scale to zero before upgrading, and there is no rollback. Closes CODAGT-599 |
||
|
|
dcb120d6ab |
feat: add --no-wildcard flag to coder config-ssh (#26753)
Add `--no-wildcard` (`CODER_CONFIGSSH_NO_WILDCARD`) to `coder config-ssh` that generates an individual `Host` entry per workspace instead of a single wildcard block (`Host *.coder`). The wildcard approach cannot be enumerated by third-party SSH clients, the VS Code Remote-SSH sidebar, or scripts that parse `~/.ssh/config` to discover hosts. With `--no-wildcard`, each workspace gets its own entry so those tools work without Coder-specific extensions. The flag is persisted in the config section header so re-running without it prompts the user about the option change. Workspaces are fetched with pagination before writing so the diff shows actual hostnames. ## Manual testing **Unit tests (no server needed):** ```sh go test ./cli/ -run TestSSHConfigOptions_writeToBuffer -v go test ./cli/ -run TestConfigSSH_NoWildcard -v ``` **End-to-end with a dev server:** 1. Build: `go build -o ./coder .` 2. Start dev server in a separate terminal: `./scripts/develop.sh` 3. Log in: `./coder login http://localhost:3000` 4. Create two workspaces 5. Run both variants into temp files: ```sh ./coder config-ssh --no-wildcard --hostname-suffix coder --ssh-config-file /tmp/test-ssh-config --yes ./coder config-ssh --hostname-suffix coder --ssh-config-file /tmp/test-ssh-config-wildcard --yes diff /tmp/test-ssh-config-wildcard /tmp/test-ssh-config ``` <details> <summary>Output: <code>--no-wildcard</code></summary> ``` # ------------START-CODER----------- # This section is managed by coder. DO NOT EDIT. # # You should not hand-edit this section unless you are removing it, all # changes will be lost when running "coder config-ssh". # # Last config-ssh options: # :hostname-suffix=coder # :no-wildcard=true # Host coder.myworkspace ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR ProxyCommand <coder> --global-config <config> ssh --stdio --ssh-host-prefix coder. %h Host coder.myworkspace2 ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR ProxyCommand <coder> --global-config <config> ssh --stdio --ssh-host-prefix coder. %h Host myworkspace.coder ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR Match host myworkspace.coder !exec "<coder> connect exists %h" ProxyCommand <coder> --global-config <config> ssh --stdio --hostname-suffix coder %h Host myworkspace2.coder ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR Match host myworkspace2.coder !exec "<coder> connect exists %h" ProxyCommand <coder> --global-config <config> ssh --stdio --hostname-suffix coder %h # ------------END-CODER------------ ``` </details> <details> <summary>Output: wildcard (default)</summary> ``` # ------------START-CODER----------- # This section is managed by coder. DO NOT EDIT. # # You should not hand-edit this section unless you are removing it, all # changes will be lost when running "coder config-ssh". # # Last config-ssh options: # :hostname-suffix=coder # Host coder.* ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR ProxyCommand <coder> --global-config <config> ssh --stdio --ssh-host-prefix coder. %h Host *.coder ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR Match host *.coder !exec "<coder> connect exists %h" ProxyCommand <coder> --global-config <config> ssh --stdio --hostname-suffix coder %h # ------------END-CODER------------ ``` </details> <details> <summary>diff wildcard → --no-wildcard</summary> ```diff 8a9 > # :no-wildcard=true 10c11 < Host coder.* --- > Host coder.myworkspace 17c18 < Host *.coder --- > Host coder.myworkspace2 21a23 > ProxyCommand <coder> ssh --stdio --ssh-host-prefix coder. %h 23c25,31 < Match host *.coder !exec "<coder> connect exists %h" --- > Host myworkspace.coder > ConnectTimeout=0 > StrictHostKeyChecking=no > UserKnownHostsFile=/dev/null > LogLevel ERROR > > Match host myworkspace.coder !exec "<coder> connect exists %h" ``` </details> Closes https://github.com/coder/coder/issues/17153 (Phase 1: CLI flag) |
||
|
|
22d9eaa4e4 |
fix(cli): increase agent log backups (#26863)
The agent log rotation kept only about 55 MiB on disk, which could fall short of the 24h support bundle lookback during high-volume debug logging. Increase the retained `coder-agent.log` rotations from 10 to 19 so the active log plus rotations align with the existing 100 MiB debug logs response cap. Closes #26737 |
||
|
|
d219f96ba5 |
fix(cli): join MCP reporter and watcher goroutines before exit (#26847)
## Problem `TestExpMcpReporter/Reconnect` flakes under the race detector with a data race on the shared `*serpent.Invocation`'s `inv.Stderr` field. The MCP server's reporter and watcher goroutines write status warnings via `cliui.Warnf(inv.Stderr, ...)`, but they were launched fire-and-forget with nothing tying their lifetime to the command handler. On shutdown, `startServer`'s deferred restore of `inv.Stdin/Stdout/Stderr` could run concurrently with a still-running goroutine reading `inv.Stderr`, which the race detector flags. The reporter's error suppression only swallows `context.Canceled`, so a shutdown error from an in-flight `UpdateAppStatus` RPC (a drpc "closed" error, not `context.Canceled`) reaches the `Warnf` call and races the restore. ## Fix Track the reporter and watcher goroutines on a `sync.WaitGroup`. After `startServer` returns, cancel the context, close the queue and socket client, then `wg.Wait()` for the goroutines to exit before returning. All three unblocks are needed: cancel stops the watcher retry loop and a reporter blocked on `Pop`, `queue.Close` also unblocks `Pop`, and `socketClient.Close` unblocks a reporter parked in an in-flight RPC. This also removes the stdin/stdout/stderr save/restore in `startServer`, which only ever wrote back identical values and was the racing write. This mirrors the existing precedent in `cli/ssh.go`, where a `sync.WaitGroup` guards against "logging while closing the log file in a defer." Verified with `go test ./cli -run 'TestExpMcpReporter/Reconnect' -race -count=50` (the reproducer from the issue) plus a 240-execution parallel stress run of the full `TestExpMcp` suite under `-race`, all green. Closes CODAGT-710 Closes https://github.com/coder/internal/issues/1610 |
||
|
|
e5b7e74847 |
test: migrate chatd tests to AI Gateway routing (#26658)
Refs CODAGT-681 Migrates all chatd tests from `AIGatewayRoutingEnabled = false` (direct routing) to AI Gateway routing using the test helpers extracted in #26639. - `coderd/x/chatd/chatd_test.go` — 6 full-server tests migrated to `NewWithAPI` + daemon, `directChatRoutingDeploymentValues` helper deleted, 3 bare-chatd tests renamed - `coderd/x/chatd/context_integration_test.go` — 2 tests migrated - `coderd/exp_chats_test.go` — `chatDeploymentValues` helper deleted, all 5 helper functions now use `NewWithAPI` + daemon internally (no call site changes) - `coderd/exp_chats_acl_test.go` — stale `chatDeploymentValues` reference replaced - `enterprise/coderd/exp_chats_test.go` — 9 sites across 5 `TestChatStreamRelay` subtests migrated - `cli/exp_scaletest_chat_test.go` — 1 test migrated - `coderd/x/chatd/model_routing_internal_test.go` — 1 direct-only test removed - `coderd/x/chatd/chatd_internal_test.go` — 1 direct-only test removed > 🤖 |
||
|
|
56373a09fc |
chore: rename user-facing AI Bridge strings to AI Gateway (#26700)
Rename user-facing "AI Bridge" strings to "AI Gateway" in deployment config, RBAC display names, log messages, error strings, docs style guide, and Grafana dashboard README. Deprecated option names and descriptions (the `--aibridge-*` block) are intentionally kept as "AI Bridge". The `Name` field cannot be renamed because `serpent` uses it as a unique key during JSON serialization; duplicating names causes `UnmarshalJSON` failures (e.g. in the support bundle). Descriptions also stay as "AI Bridge" to avoid confusion between the deprecated and primary options. Refs https://linear.app/codercom/issue/AIGOV-226 > Generated with the assistance of Coder Agents (@ssncferreira) |