mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
8342acee99bdb11157c00dc7c76fc7a1f499191d
15561
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8342acee99 |
chore: bump @types/node from 22.20.0 to 22.20.1 in /offlinedocs (#27740)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.20.0 to 22.20.1. <details> <summary>Commits</summary> <ul> <li>See full diff in <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
599e0ba98e |
chore: bump @chakra-ui/react from 2.10.9 to 2.10.10 in /offlinedocs (#27739)
Bumps [@chakra-ui/react](https://github.com/chakra-ui/chakra-ui/tree/HEAD/packages/react) from 2.10.9 to 2.10.10. <details> <summary>Commits</summary> <ul> <li>See full diff in <a href="https://github.com/chakra-ui/chakra-ui/commits/@chakra-ui/react@2.10.10/packages/react">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
b518713ab7 |
chore: bump sanitize-html from 2.17.5 to 2.17.6 in /offlinedocs (#27738)
Bumps [sanitize-html](https://github.com/apostrophecms/apostrophe/tree/HEAD/packages/sanitize-html) from 2.17.5 to 2.17.6. <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/apostrophecms/apostrophe/blob/main/packages/sanitize-html/CHANGELOG.md">sanitize-html's changelog</a>.</em></p> <blockquote> <h2>2.17.6 (2026-07-10)</h2> <h3>Fixes</h3> <ul> <li>Allow transformTags to emit text when textFilter is set, even if the tag is initially empty. This is consistent with the documentation. Thanks to <a href="https://github.com/spokodev">spokodev</a> for the fix.</li> </ul> <h3>Security</h3> <ul> <li>Fixed an XSS/allowlist bypass in which the contents of a raw-text element (<code>textarea</code> or <code>xmp</code>) nested inside an <code>svg</code> or <code>math</code> root were re-emitted without HTML-escaping. <code>sanitize-html</code> treated that content as inert raw text because <code>htmlparser2</code> 10.x classified raw-text elements by tag name and ignored the namespace, but a real HTML5 parser treats <code>textarea</code>/<code>xmp</code> as ordinary foreign elements inside SVG/MathML and re-parses their contents as live markup. As a result, markup and event-handler attributes that the allowlist never permitted (for example <code><svg><textarea><img src=x onerror=alert(1)></code>) could survive sanitization and execute in the browser. This is now fixed on two fronts: <code>htmlparser2</code> was upgraded to 12.x, which is namespace-aware and parses <code>textarea</code>/<code>xmp</code> inside SVG/MathML as ordinary elements, so their non-allowlisted children (such as the injected <code>img</code>) are dropped by the allowlist instead of being preserved as raw text; and any raw-text content <code>sanitize-html</code> still emits for these tags (at HTML integration points such as <code>foreignObject</code>/<code>mtext</code>, or outside foreign content) is always HTML-escaped. The default configuration is not affected; the precondition is an <code>allowedTags</code> that includes <code>svg</code> or <code>math</code> together with <code>textarea</code> or <code>xmp</code>. Thanks to <a href="https://github.com/khoadb175">khoadb175</a> for responsibly disclosing the vulnerability.</li> <li>Fixed a mutation-XSS / <code>allowedTags</code> bypass affecting configurations that allow the <code>textarea</code> or <code>xmp</code> raw-text tags. <code>htmlparser2</code> 10.x did not recognize an end tag with a trailing solidus (e.g. <code></textarea/></code>) as closing the element, so it kept the following markup as raw text, but a spec-compliant browser treats <code></textarea/></code> as a valid close and parses that markup as a live element. Because raw-text content was re-emitted without escaping, a payload such as <code><textarea></textarea/><img src=x onerror=...></code> could smuggle non-allowlisted, executable markup through the sanitizer. The default configuration was not affected. This is now defended at two layers: <code>htmlparser2</code> was upgraded to 12.x, whose tokenizer closes these end tags correctly, and the raw text sanitize-html emits for these tags is always escaped so no <code><</code> can reopen a tag when the output is re-parsed (<code>textarea</code>, an RCDATA element whose entities <code>htmlparser2</code> decodes, is escaped like normal text, while <code>xmp</code>, a raw-text element, has only its angle brackets escaped to avoid double-encoding already-encoded entities). Because <code>htmlparser2</code> is ESM-only from version 11 onward, <code>sanitize-html</code> now requires Node.js <code>>=22.12.0</code> (the first 22.x release in which <code>require()</code> of an ES module is available unflagged). Thanks to <a href="https://github.com/bibu123456">bibu123456</a> for reporting the vulnerability and <a href="https://github.com/Kayiz-PT">Kayiz-PT</a> for coordinating the disclosure (GHSA-jxwj-j7wr-gfrw).</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li>See full diff in <a href="https://github.com/apostrophecms/apostrophe/commits/HEAD/packages/sanitize-html">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
e0fc756a75 |
chore: bump prettier from 3.9.4 to 3.9.6 in /offlinedocs (#27737)
Bumps [prettier](https://github.com/prettier/prettier) from 3.9.4 to 3.9.6. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/prettier/prettier/releases">prettier's releases</a>.</em></p> <blockquote> <h2>3.9.6</h2> <h2>What's Changed</h2> <ul> <li>Preserve quotes for methods named <code>new</code> (<a href="https://redirect.github.com/prettier/prettier/pull/19621">prettier/prettier#19621</a> by <a href="https://github.com/kovsu"><code>@kovsu</code></a>)</li> <li>Support <code>import defer</code> in <code>typescript</code> parser (<a href="https://redirect.github.com/prettier/prettier/pull/19624">prettier/prettier#19624</a>, <a href="https://redirect.github.com/prettier/prettier/pull/19675">prettier/prettier#19675</a> by <a href="https://github.com/fisker"><code>@fisker</code></a>)</li> <li>Added a new official plugin <a href="https://github.com/prettier/prettier/tree/3.9.6/packages/plugin-yuku"><code>@prettier/plugin-yuku</code> 🚀</a> (<a href="https://redirect.github.com/prettier/prettier/pull/19628">prettier/prettier#19628</a>, <a href="https://redirect.github.com/prettier/prettier/pull/19629">prettier/prettier#19629</a> by <a href="https://github.com/fisker"><code>@fisker</code></a>)</li> </ul> <p>🔗 <a href="https://github.com/prettier/prettier/blob/3.9.6/CHANGELOG.md#396">Changelog</a></p> <h2>3.9.5</h2> <p>🔗 <a href="https://github.com/prettier/prettier/blob/3.9.5/CHANGELOG.md#395">Changelog</a></p> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md">prettier's changelog</a>.</em></p> <blockquote> <h1>3.9.6</h1> <p><a href="https://github.com/prettier/prettier/compare/3.9.5...3.9.6">diff</a></p> <h4>TypeScript: Preserve quotes for methods named <code>new</code> (<a href="https://redirect.github.com/prettier/prettier/pull/19621">#19621</a> by <a href="https://github.com/kovsu"><code>@kovsu</code></a>)</h4> <!-- raw HTML omitted --> <pre lang="tsx"><code>// Input interface Container { "new"(id: string): number; } <p>// Prettier 3.9.5<br /> interface Container {<br /> new(id: string): number;<br /> }</p> <p>// Prettier 3.9.6<br /> interface Container {<br /> "new"(id: string): number;<br /> }<br /> </code></pre></p> <h4>TypeScript: Support <code>import defer</code> (<a href="https://redirect.github.com/prettier/prettier/pull/19624">#19624</a>, <a href="https://redirect.github.com/prettier/prettier/pull/19675">#19675</a> by <a href="https://github.com/fisker"><code>@fisker</code></a>)</h4> <!-- raw HTML omitted --> <pre lang="tsx"><code>// Input import defer * as foo from "foo"; <p>// Prettier 3.9.5<br /> import * as foo from "foo";</p> <p>// Prettier 3.9.6<br /> import defer * as foo from "foo";<br /> </code></pre></p> <h4>JavaScript: Added a new official plugin <code>@prettier/plugin-yuku</code> (<a href="https://redirect.github.com/prettier/prettier/pull/19628">#19628</a>, <a href="https://redirect.github.com/prettier/prettier/pull/19629">#19629</a> by <a href="https://github.com/fisker"><code>@fisker</code></a>)</h4> <p><code>@prettier/plugin-yuku</code> is powered by <a href="https://yuku.fyi/">Yuku</a> (A high-performance JavaScript/TypeScript compiler toolchain written in Zig).</p> <p>This plugin includes two new parsers: <code>yuku</code> (JavaScript syntax) and <code>yuku-ts</code> (TypeScript syntax).</p> <p><strong>To use this plugin:</strong></p> <ol> <li> <p>Install the plugin:</p> <pre lang="bash"><code>yarn add --dev prettier @prettier/plugin-yuku </code></pre> </li> </ol> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/prettier/prettier/commit/8f0c95057cc91d5836409466cd9d9af3bb901e84"><code>8f0c950</code></a> Release 3.9.6</li> <li><a href="https://github.com/prettier/prettier/commit/e9107647d0497d8ff1cacbb0f970d4543df77c1c"><code>e910764</code></a> Update changelog</li> <li><a href="https://github.com/prettier/prettier/commit/ec3f1c7bd74495992bc6954323a1a7fc8368808e"><code>ec3f1c7</code></a> Update typescript-eslint to v8.65.0 (<a href="https://redirect.github.com/prettier/prettier/issues/19675">#19675</a>)</li> <li><a href="https://github.com/prettier/prettier/commit/73d2efc2c6cba6f579585c88ef171132d90834ec"><code>73d2efc</code></a> Update Yuku parser to v0.7.0 (<a href="https://redirect.github.com/prettier/prettier/issues/19664">#19664</a>)</li> <li><a href="https://github.com/prettier/prettier/commit/dd5e24eabeab1f75ad573c79781e5fd408bcfad3"><code>dd5e24e</code></a> Preserve quotes for <code>TSMethodSignature</code> nodes named <code>new</code> (<a href="https://redirect.github.com/prettier/prettier/issues/19621">#19621</a>)</li> <li><a href="https://github.com/prettier/prettier/commit/c03ab4e71c23154d6b11537eee3c938f0d0f67d3"><code>c03ab4e</code></a> Update dependency eslint-plugin-unicorn to v72 (<a href="https://redirect.github.com/prettier/prettier/issues/19633">#19633</a>)</li> <li><a href="https://github.com/prettier/prettier/commit/b74dd53076c7208291a6b2e585c310844b41d35f"><code>b74dd53</code></a> Update Yuku parser to v0.6.5 (<a href="https://redirect.github.com/prettier/prettier/issues/19654">#19654</a>)</li> <li><a href="https://github.com/prettier/prettier/commit/f1b594ea1db1520c383d0e281d623551f671f824"><code>f1b594e</code></a> Update dependency eslint-plugin-simple-import-sort to v14 (<a href="https://redirect.github.com/prettier/prettier/issues/19655">#19655</a>)</li> <li><a href="https://github.com/prettier/prettier/commit/0d9dfb61530986373000dd107ea58ceebb79e233"><code>0d9dfb6</code></a> Update Yuku parser to v0.6.4 (<a href="https://redirect.github.com/prettier/prettier/issues/19650">#19650</a>)</li> <li><a href="https://github.com/prettier/prettier/commit/3bbb8159eb55575d4042653aa99f5f92a1416c19"><code>3bbb815</code></a> Remove <code>typescript-only</code> directory (<a href="https://redirect.github.com/prettier/prettier/issues/19636">#19636</a>)</li> <li>Additional commits viewable in <a href="https://github.com/prettier/prettier/compare/3.9.4...3.9.6">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
79724ab0ba |
chore(site): migrate all <Dialog />s off MUI (#27506)
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell. Removes Material UI from every dialog, moving them onto the internal shadcn/radix `Dialog` primitives. After this change there are **no** `@mui/material/Dialog` usages left in `site/src`. ## Changes - Consolidated `components/Dialogs/*` → `components/Dialog/*` and folded the old `ConfirmDeleteDialog` into `ConfirmDialog` (`type="delete"`). - Rewrote `ConfirmDialog`, `DeleteDialog`, `WorkspaceDeleteDialog`, `ScheduleDialog`, and `AnnouncementBannerDialog` onto the internal primitives (native `Input`/`Label`/`Checkbox`/`Link` instead of MUI). - Finished the migration for the last two MUI hold-outs: `UpdateBuildParametersDialog` and `MissingTemplateVariablesDialog`. - Dialog prop types now compose the rendered component's props (`ComponentProps<typeof Dialog>` / MUI `DialogProps`) instead of hand-rolled `{ open; onOpenChange }` shapes. ## Testing AI-Driven manual dogfood sweep in a live instance (premium license), driven in a browser. Each dialog checked for logical rendering (layout, variant styling, buttons, no overlap/blank/console error) and function (open, primary action, cancel/close, guard states): | Dialog / surface | How tested | Result | | --- | --- | --- | | `ConfirmDialog` (delete / info / success) | Token delete, update-confirm, change-version | ✅ | | `DeleteDialog` (type-to-confirm) | Delete user, group, license, provider, OAuth2 app | ✅ | | `WorkspaceDeleteDialog` | Workspace actions → Delete (+ orphan path via failed workspace) | ✅ | | `ScheduleDialog` | Template schedule → dormancy/deletion warning | ✅ | | `AnnouncementBannerDialog` | Deployment → Appearance → New banner (live preview + color) | ✅ | | `ChangeWorkspaceVersionDialog` | Workspace actions → Change version | ✅ | | `DownloadLogsDialog` | Workspace actions → Download logs | ✅ | | Batch delete (workspaces) | Workspaces list → multi-select → Delete | ✅ | | `TemplatePageHeader` delete | Template → Delete (cancelled) | ✅ | | `FileDialog` (create/rename/delete) | Template editor file tree | ✅ | | `MissingTemplateVariablesDialog` *(migrated)* | Editor → add variable → Build | ✅ | | `PublishTemplateVersionDialog` | Editor → Publish | ✅ | | `UpdateBuildParametersDialog` *(migrated)* | Classic flow + required param → workspace Update (renders + submits) | ✅ | | Update-confirmation (`WorkspaceUpdateDialogs`) | Workspace Update | ✅ | | Suspend/activate confirm | Users → member row | ✅ | | `ResetPasswordDialog` | Users → member → Reset password | ✅ | | Token delete confirm | Settings → Tokens | ✅ | | Create-token confirm | Token create flow | ✅ | | SSH key regenerate confirm | Settings → SSH Keys | ✅ | | Change-login-type confirm | Settings → Security | ✅ | | Secret delete | Settings → Secrets | ✅ | | Group delete | Admin → Groups | ✅ | | Org member remove | Admin → Organization → Members | ✅ | | License remove | Deployment → Licenses (cancelled) | ✅ | | Announcement banner delete | Deployment → Appearance | ✅ | | OAuth2 app delete | Deployment → OAuth2 apps | ✅ | | `ModelFormDialogs` (form + delete) | AI → Models | ✅ | | Provider delete | AI → Providers | ✅ | | Gateway key create/delete | AI → Gateway keys | ✅ | | `MCPServerFormDialogs` delete | AI → MCP servers | ✅ | | Personal skill (create form + delete) | Agents → Personal Skills | ✅ | | Spend user-override (add + delete) | AI → Spend | ✅ | Human tested: - [x] template version promote/archive - [x] external-auth/OAuth2-provider delete - [x] custom-role delete - [x] cancel-provisioner-job Things that have a chance to bleed: - tasks dialogs - dormant inline confirm Should be known that each of these renders through the already-verified `ConfirmDialog`/`DeleteDialog`, so the underlying component is covered even where the specific trigger wasn't reachable. <details> <summary>Plan & decision log</summary> **Goal:** finish the de-MUI migration everywhere and confirm every affected modal renders logically and functions. **Phase 1 - complete the migration** - Confirmed only two files still rendered MUI `Dialog` (`UpdateBuildParametersDialog`, `MissingTemplateVariablesDialog`); ported both to the internal primitives, preserving the `{ open, onClose, ... }` public API (mapped to `onOpenChange` internally) so call sites were unchanged. radix now wires `aria-labelledby`/`aria-describedby`, removing a duplicated element id. **Phase 2 - sighting sweep (live browser, premium license)** - Batch A (workspaces/tasks): 4 PASS, rest state-gated. - Batch B (templates): `MissingTemplateVariablesDialog`, `FileDialog`, `PublishTemplateVersionDialog`, template delete - all PASS. - `UpdateBuildParametersDialog`: reached by enabling classic parameter flow + pushing a version with a required parameter - PASS (renders + submits). - Batch C (users/org/settings): 10 PASS. - Batch D (deployment/AI): 10 PASS. **Decisions** - Kept `ConfirmDialog`-wrapper prop types explicit (composing `DialogProps` there reintroduced a MUI smell). - Dropped the unused `ConfirmDialogType` export (knip) and updated the `WorkspacePage` orphan-delete test: the radix `Checkbox` puts the test id on the `role=checkbox` button itself, so the previous `within(...).getByRole` no longer matched. </details> |
||
|
|
bc9c7855d9 | fix: build actionlint from source to avoid the shellcheck deadlock (#27679) | ||
|
|
e30a7bcd0d | fix(site/src/pages/UserSettingsPage/SecretsPage): prevent Add secret dialog overflow (#27649) | ||
|
|
46d01fca65 |
refactor(site/src/pages/AgentsPage/components/ChatElements/tools): replace label/icon switches with tables and registry invariants (#27706)
Stacked on #27697. Do not merge before it; this diff is against that branch, not main. Replaces the `ToolLabel` and `ToolIcon` string switches with lookup tables, and makes the registry/table agreement a CI failure instead of a manual audit. No behaviour change; every label and icon renders identically. ## Why Three enumerations of the same tool-name set (`toolRenderers`, the label switch, the icon switch) were kept in agreement by convention alone, and drifted repeatedly (#27684, #27687, #27697 each deleted arms a registered renderer had silently shadowed). Switches are unenumerable, so the drift was invisible to both tsc and tests. ## What changed - `genericToolLabels` (ToolLabel.tsx): the four generic-rendered labels (`process_signal`, `process_list`, `attach_file`, `advisor`) as a `Partial<Record<string, FC>>`. `ToolLabel` is now a table lookup plus the MCP/raw-name fallback. - `toolIcons` (ToolIcon.tsx): all 19 built-in icon names as a `Partial<Record<string, LucideIcon>>`, same pattern. Unknown/MCP names still fall to `WrenchIcon`. - `Tool.tsx`: exports `toolRenderers` (it is the single source of truth for dispatch; the tests read it directly). - `toolLabelVisibility.test.ts`: fails if a registered renderer that does not delegate to `GenericToolRenderer` shadows a `genericToolLabels` entry, naming the arm. `process_signal` (known delegator) and `advisor` (consumed directly by `AdvisorTool`) are allowlisted in the test. This is the tripwire requested in review on #27697, as a CI failure rather than a comment. - `toolIconsCoverage.test.ts`: fails if a registered renderer has no dedicated icon, with `read_skill_file` allowlisted for its `read_skill` icon alias. ## Design notes - The invariant checks live in tests, not in the type system. TypeScript cannot assert a runtime object's key set against an independent intent without either re-listing the names (the `satisfies Record<union, ...>` approach, rejected as ugly repetition) or abusing conditional types. The tables are plain objects; the tests own the invariant. A generated union from the Go `chattool` constants is the proper long-term fix and is deliberately out of scope. - No `as const` / union key types: they added annotation without buying safety the tests don't already provide, and nothing consumes `keyof typeof` here. ## Validation - `tsc --noEmit`, `biome check --error-on-warnings`, knip: clean. - Unit (`--project=unit src/pages/AgentsPage`): 1502 passed, 2 skipped (base: 1500/2; +2 are the new invariant tests). - Storybook (`--project=storybook src/pages/AgentsPage`): 949 passed, 2 failed, identical to base: `AgentChatPageView.stories.tsx > Scroll To Bottom Button Works With Inverse Scroll` and `Tool.stories.tsx > MCP Tool Completed`. Both reproduce on unmodified main, so pre-existing and unrelated. One additional flake (`AgentChatPage.stories.tsx > Slash Compact Yields To Personal Skill`) failed once under parallel load and passed in isolation on the final code. - Line delta vs #27697: +148 / -98 across 5 files. Generated by Coder Agents. |
||
|
|
9bc681fa6e |
fix(site/src/pages/AgentsPage/components/ChatElements/tools): delete unreachable switch arms from ToolLabel and ToolIcon (#27697)
Deletes unreachable switch arms from `ToolLabel` (14 arms) and `ToolIcon` (1 arm). No behaviour change; the deleted arms could never be reached, so the rendered output is identical. ## Reachability proof `ToolLabel` has exactly two call sites: 1. `Tool.tsx` `GenericToolRenderer` (line 950), reached when a tool name has no `toolRenderers` entry or when its registered renderer delegates to `GenericToolRenderer`. 2. `AdvisorTool.tsx` (line 72), which hardcodes `name="advisor"`. Dispatch in `Tool.tsx`: subagent names (`spawn_agent`, `wait_agent`, `message_agent`, `interrupt_agent`, plus legacy `spawn_subagent`, `close_agent`) route to `SubagentRenderer`; everything else hits `toolRenderers[name] ?? GenericToolRenderer`. None of the subagent names appear in either switch. Every registered renderer was checked for delegation: | ToolLabel arm | Shadowing renderer | Delegates? | | --- | --- | --- | | `execute` | `ExecuteRenderer` -> `ExecuteTool` | No | | `process_output` | `ProcessOutputRenderer` -> `ProcessOutputTool` | No | | `read_file` | `ReadFileRenderer` -> `ReadFileTool` | No | | `write_file` | `WriteFileRenderer` -> `WriteFileTool` | No | | `edit_files` | `EditFilesRenderer` -> `EditFilesTool` | No | | `create_workspace` | `CreateWorkspaceRenderer` -> `CreateWorkspaceTool` | No | | `start_workspace` | `StartWorkspaceRenderer` -> `StartWorkspaceTool` | No | | `list_templates` | `ListTemplatesRenderer` -> `ListTemplatesTool` | No | | `read_template` | `ReadTemplateRenderer` -> `ReadTemplateTool` | No | | `read_skill` | `ReadSkillRenderer` -> `ReadSkillTool` | No | | `read_skill_file` | `ReadSkillFileRenderer` -> `ReadSkillTool` | No | | `chat_summarized` | `ChatSummarizedRenderer` -> `ChatSummarizedTool` | No | | `propose_plan` | `ProposePlanRenderer` -> `ProposePlanTool` | No | | `computer` | `ComputerRenderer` -> `ComputerTool` | No | Kept arms: - `process_signal`: `ProcessSignalRenderer` IS a registry key but delegates to `GenericToolRenderer`, so the arm stays reachable. Kept. - `advisor`: hardcoded by `AdvisorTool.tsx`. Kept. - `process_list`, `attach_file`: no registry entry, not subagent names, so they fall through to `GenericToolRenderer`. Kept. - `default`: covers MCP tools and any unregistered name. Kept. `ToolIcon` is rendered by `ToolCall.LeadingIcon` / `ToolCall.Header iconName`, and every dedicated per-tool component passes its own fixed name (`execute`, `process_output`, `read_file`, `write_file`, `edit_files`, `list_templates`, `read_template`, `read_skill`, `chat_summarized`, `ask_user_question`, `propose_plan`, `computer`, `start_workspace`, `list_agents`, `create_workspace`, `advisor`), so those arms are reachable and kept. `thinking` is passed directly from `StreamingOutput.tsx` and `ConversationTimeline.tsx`, and `chat_summarized` covers `list_agents` via the shared `BotIcon` arm. `read_skill_file` is the only arm whose renderer (`ReadSkillFileRenderer`) renders `ReadSkillTool` with the hardcoded `iconName="read_skill"`, so nothing ever passes `read_skill_file` to `ToolIcon`. That arm alone is deleted. No exports, helpers, or imports became dead (verified with knip, which passes clean). ## Delta and tests - Line delta: -119 (ToolLabel -118, ToolIcon -1), 0 insertions. - Unit (`--project=unit src/pages/AgentsPage`): 1500 passed, 2 skipped before and after. - Storybook (`--project=storybook src/pages/AgentsPage`): 949 passed, 2 failed before and after, identical failures both runs: `AgentChatPageView.stories.tsx > Scroll To Bottom Button Works With Inverse Scroll` and `Tool.stories.tsx > MCP Tool Completed`. Both reproduce on unmodified main (MCP Tool Completed also fails in isolation on main), so they are pre-existing and unrelated. - `tsc --noEmit`, `biome check --error-on-warnings`, and knip all pass. Generated by Coder Agents. |
||
|
|
218829d444 | refactor(site): use uuid package instead of generateUUID helper (#27709) | ||
|
|
6cfefc0685 |
chore: replace isChromatic with isPixel (#26832)
Swaps `chromatic/isChromatic` for `@coder/pixel-storybook`'s `isPixel()` so the snapshot-determinism gates (fixed workspace name, frozen Spinner, fixed CLI origin, no scroll, font loader) fire under pixel instead of Chromatic. Drops the now-unused `chromatic` dependency. Stacked on #27658 (pixel-storybook 0.3); `isPixel` comes from the new `@coder/pixel-storybook/storyapi` subpath. Verified the app and Storybook builds both bundle `isPixel` cleanly, and `tsc` passes with the dependency removed. This is the last piece of the Chromatic removal; the story params migrated in #26844 and the addon came out in #27353. <details> <summary>Chromatic removal sequence</summary> 1. Remove the Chromatic CI job + scripts + docs reference. (#26777, merged) 2. **This PR** — `isChromatic()` → `isPixel()`; drop the `chromatic` dependency. 3. `data-pixel` + `pixel.exclude`; drop `delay` / `pauseAnimationAtEnd`. (#26778, merged) 4. Migrate story snapshot params (`viewports` / `diffThreshold` / theme modes → `pixel.matrix`); delete `testHelpers/chromatic.ts`. (#26844, merged) 5. Remove the `@chromatic-com/storybook` addon. (done on main in #27353) </details> --- > Generated by Coder Agents on behalf of @aslilac. |
||
|
|
95275d9659 |
chore: upgrade to @coder/pixel-storybook 0.3 (#27658)
includes some fixes to improve performance and reduce the number of false positives |
||
|
|
2acfe7e829 |
feat(coderd/externalauth/gitprovider): use conditional requests for GitHub JSON reads (#27628)
Fixes #27627. ## What The chat diff-status gitsync worker polls open pull requests on a fixed 10s interval and re-downloads the full JSON body every tick, even when nothing changed, because the GitHub client never sends `If-None-Match` / ETag. This adds a small, concurrency-safe, bounded in-memory ETag+body cache (`coderd/externalauth/gitprovider/conditional.go`) and wires it into `githubProvider.decodeJSON`. When an ETag is cached for a request, we send `If-None-Match`; on `304 Not Modified` we decode the cached body; on `200` we cache `{etag, body}` when an ETag is present and the body is under a size cap. ## Why `304` responses do not count against GitHub's primary rate limit, but full `200`s do. Today every unchanged poll burns quota that the same token also needs for interactive Git and API operations, so busy instances can hit rate-limit errors and stalls elsewhere. Unchanged PRs now revalidate for free with no behavior change; only genuine changes transfer a body. ## Details - Cache key = request URL + a hash of the token, so one token's response is never served under another; raw tokens are not retained. - Bounded by entry count (LRU eviction, default 2048) and per-body size (1 MiB) to cap memory. - Scope limited to the JSON reads through `decodeJSON`; the raw-diff path (`fetchDiff`, up to `MaxDiffSize`) is intentionally left out to avoid caching large bodies. ## Tests `TestConditionalRequestReuse` in `github_test.go` covers: - `NotModifiedReusesCachedBody` — a warm poll sends `If-None-Match` with the prior ETag and reuses the cached body on `304`, yielding the same result with exactly two upstream requests. - `DifferentTokenDoesNotShareCache` — a different token never sends another token's cached ETag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4959e1f9-f8e6-4e97-a487-f395a0123c79 |
||
|
|
6df24634fd |
fix: remove nodePort from required service fields of ai-gateway chart (#27696)
Updates schema for ai-gateway chart. Missing piece from https://github.com/coder/coder/pull/27682 |
||
|
|
11e03cfb3a |
fix(site/src/pages/AgentsPage/components/ChatElements/tools): delete dead execute auth_required flow (#27687)
Stacked on #27684. Addresses review note CRF-2 from that PR: the kept `auth_required` execute path is dead by the same premise that PR proved for `wait_for_external_auth`. The chatd execute tool's `ExecuteResult` struct (`coderd/x/chatd/chattool/execute.go:79-88`) has no `auth_required`, `authenticate_url`, or `provider_*` fields, so the execute tool cannot emit the payload this path parsed. The `authenticate_url` matches elsewhere in Go are the unrelated workspace-creation external-auth flow (`codersdk.TemplateVersionExternalAuth`). Per `bb3a363ed4`, the `auth_required` execute payload was written and removed on an unmerged branch before #22290 squash merged, so no server version ever emitted it. Removes: - `ExecuteAuthRequiredTool` and its `ExecuteRenderer` branch - the `authenticateURL`/`providerLabel` chain in `getExecuteRenderData`, and the `Boolean(data.authenticateURL)` disjunct in `shouldRenderExecuteTool` - the `ExecuteAuthRequired` Storybook story - the now-dead `toProviderLabel` helper and its test block - the `auth_required` visibility test case The `providerLabel` identifiers elsewhere under `site/src` (ModelSelector, ModelRow, AISettings) belong to the unrelated AI model/provider selector and are untouched. 🤖 This pull request was created with Coder Agents. |
||
|
|
4003f0086f |
fix(site/src/pages/AgentsPage/components/ChatElements/tools): delete unreachable WaitForExternalAuth tool code (#27684)
The backend never emits a `wait_for_external_auth` tool call (no references in any Go source, the chatd tool registry, or anywhere outside the frontend), so the entire frontend rendering path for it was unreachable. Removes the `WaitForExternalAuthTool` component, its renderer and `toolRenderers` entry, the `ToolIcon` case, and the four Storybook stories, along with the imports that only they used (`CheckIcon`, `LoaderIcon`, `LogInIcon`, and `toProviderLabel` in `Tool.tsx`). Kept the separate, live `execute` auth-required flow: `ExecuteAuthRequiredTool` and the `toProviderLabel` usage in `toolVisibility.ts` belong to the `authenticateURL` path, not this dead tool. Refs #27593 🤖 This pull request was created with Coder Agents. |
||
|
|
3f1973f45c |
docs: document AI Gateway cost controls (#27643)
### Description Adds documentation for AI Governance Cost Control, including how administrators configure budgets, how effective groups are resolved, how enforcement works, and where spend reporting is available. ### Changes - Replace the placeholder cost control page with a full admin guide - Document deployment settings, group budgets, user overrides, and effective group resolution - Explain estimated spend, unpriced models, notifications, enforcement, and spend reporting - Add migration guidance for Coder Agents Cost Control - Add screenshots for group budgets and user overrides Closes [AIGOV-476](https://linear.app/codercom/issue/AIGOV-476/add-documentation-for-ai-bridge-cost-controls). > [!NOTE] > Initially generated by Coder Agents, modified and reviewed by @ssncferreira |
||
|
|
7b104b6a98 |
fix: add info log level to unpriced models message (#27693)
Change the unpriced models log to info level. Refs https://github.com/coder/coder/pull/27678 |
||
|
|
a86e67d3d9 |
fix(site/src/modules/templates/TemplateExampleCard): point Use template to builder (#27663)
## Summary
Updates the "Use template" button on `TemplateExampleCard` to link to
the template builder instead of the legacy create flow.
- `/templates/new?exampleId=${example.id}` →
`/templates/new/builder?base=${example.id}`
This affects everywhere the card is rendered: the Templates page empty
state and the Starter Templates gallery page.
## Notes
The "View all starter templates" button in the empty state keeps its
existing `templateBuilderEnabled` conditional (unchanged).
---
_This PR was generated by Coder Agents on behalf of @jeremyruppel._
|
||
|
|
cf7f876880 |
feat(site): add generateUUID helper (#27661)
Adds a `generateUUID()` helper to `site/src/utils/uuid.ts`. It uses `crypto.randomUUID()` when available, and otherwise falls back to `crypto.getRandomValues()`, setting the version (4) and variant (RFC 4122) bits before formatting the 16 random bytes into the standard `8-4-4-4-12` UUID string. Seriously open to any implementation here, let me know if you have a favorite! --- _This PR was created by Coder Agents on behalf of @jeremyruppel._ |
||
|
|
b3852c707b |
fix(site/src/pages/AISettingsPage/SpendPage): announce cost controls move in v2.36 (#27688)
Corrects the version in the AI settings Spend banner: cost controls features move to AI Governance in **v2.36**, not v2.37. Updates the banner copy in `SpendPageView` and the matching Storybook play assertion. No other changes. The `release/2.36` backport is opened manually as #27690, so this PR does not carry the `cherry-pick` label. > Mux, an AI agent, prepared this PR on Mike's behalf. |
||
|
|
b4eda32a2e |
fix: hide AI budget override controls without permission (#27654)
### Description Setting a user's AI budget override updates both the user and the group its spend is charged to, so it requires `user:update` and `group:update`. Organization admins have group update but only site-wide user read, so they could tick "Override group budget", enter an amount, and then fail on save. The dialog now shows the member's budget as read-only when the viewer can't change it. ### Changes - Gate the override controls on `user:update` (site-wide) in addition to the group permission the page already checks - Replace the form with a read-only view: the group's budget, followed by "To update this limit, contact a Coder administrator." - Swap the whole view rather than disabling the checkbox, since an existing override seeds the form enabled and unchecking it would call the delete endpoint and fail the same way - Add stories for the read-only dialog and for the page-level wiring > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira |
||
|
|
e819dd4af6 |
fix(helm/ai-gateway): render service nodePort with an explicit if guard (#27682)
> Coder Agents generated this commit Replace the with block around the Service nodePort field with an if guard that references .Values.service.nodePort directly. The with form rebinds the dot inside the block, so a later addition that needs .Values or .Release there would break. Extend the default_values fixture to enable ingress and httproute with only the values each one requires, so the golden file covers every template with the minimum viable configuration. Add a mustNotContain list to the render test cases. Golden files are rewritten wholesale by TestUpdateGoldenFiles, so these assertions pin optional fields and resources that each fixture leaves unset, including the Service nodePort. |
||
|
|
dc31791c88 |
test: don't use ptytest for client side of SSH session tests (#27681)
In our initial batches of test refactors, I left the SSH session tests using `ptytest` because I (erroneously) thought that we still needed a client side PTY when the SSH server creates a PTY. This is incorrect and plain in-process IO is fine on the client side. closes https://github.com/coder/internal/issues/1400 (again)<!-- 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. --> |
||
|
|
3f3fd1c4d7 |
feat: show network request summary on AI session detail card (#27418)
Frontend for the AI session network summary. Adds Network calls, Blocked network requests, and Top domains rows to the Session summary card on the individual AI session detail page, driven by the network fields on the session threads response. Renders "Disabled" when network monitoring was not active and "No activity" when there were no calls. Covered by Storybook stories for each state. ### PR map (merge strictly bottom-up) This change is a 4-PR stack. Each PR depends on all the ones below it, so merge in this exact order: 1. #27417 — backend network summary 2. #27418 — frontend summary rows 3. #27425 — backend per-call list `network_call_logs` 4. #27426 — frontend network-calls panel Refs AIGOV-463 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Cian Johnston <cian@coder.com> |
||
|
|
841a1765f7 |
feat: add network calls summary to AI session threads API (#27417)
Backend for the AI session network summary. Exposes total/blocked
network calls and top destination domains on the session threads
endpoint (`GET /api/v2/ai-gateway/sessions/{id}`).
Total and blocked reuse the existing Agent Firewall aggregation from the
sessions list query, so the numbers match the sessions table. Top
domains are a new server-side aggregation
(`GetAIBridgeSessionTopDomains`) over boundary logs, using the same
interception-window correlation. There is no network-error state,
matching the current data model.
Frontend consuming these fields is in a separate stacked PR.
### PR map (merge strictly bottom-up)
This change is a 4-PR stack. Each PR depends on all the ones below it,
so merge in this exact order:
1. #27417 — backend network summary (base `main`)
2. #27418 — frontend summary rows (base #27417)
3. #27425 — backend per-call list `network_call_logs` (base #27418)
4. #27426 — frontend network-calls panel (base #27425)
Refs AIGOV-463
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cian Johnston <cian@coder.com>
|
||
|
|
3660ffecdd |
fix: add warn log level to unpriced models message (#27678)
When a model is missing from the price table, token usage is still recorded but with a NULL cost, so AI spend for that model goes unattributed. This was logged at debug level, which means it is invisible in a default deployment. Log it at warn level instead so admins can see which provider/model pairs need a price row and act on it. |
||
|
|
95a2c2ba02 |
feat: back the per-chat cost endpoint with AI Gateway data (#27328)
## Stack Context
This stack removes native chat cost tracking and native chat usage
limits, making the AI Gateway the single source of AI spend data and
budget enforcement.
1. **This PR:** re-back the per-chat cost endpoint with AI Gateway data.
2. Remove native chat usage limits end to end, rewiring the sidebar
indicator to gateway spend.
3. Remove native chat cost tracking end to end, deleting the
Analytics/Spend cost UI.
## What?
`GET /api/experimental/chats/{chat}/cost` summed
`chat_messages.total_cost_micros`, which native chat cost tracking
maintained. It now aggregates AI Gateway interception data instead, and
has no native fallback.
- New `GetAIBridgeChatCost` query, authorized through the root chat so
members can read their own chat's cost without gaining access to raw
interception rows.
- Response fields renamed: `priced_message_count` -> `request_count`,
`unpriced_messages_having_usage_count` -> `unpriced_request_count`.
- The chat summary sidebar keys its cost cache by root chat, and hides
the cost row where the AI Gateway is off or unlicensed. The root cost is
invalidated when a chat leaves an active status and when a generated
title lands, since title generation bills its own gateway request.
`GetChatModelUsageCostByChatID` and the rest of native cost tracking are
untouched here; PR 3 removes them.
## Why?
Native cost tracking duplicates what the AI Gateway already records, and
the two disagree. Repointing the endpoint first means the cost UI keeps
working while the native implementation is deleted later in the stack.
Two behaviour changes follow from gateway semantics and are intentional:
- **Requests, not messages.** The gateway records interceptions, so
counts are requests. Title-generation traffic now counts.
- **Whole-tree totals.** The gateway records the *spawning* chat's ID as
the interception session ID, so a subagent's requests are attributed to
its immediate parent, not always the root. Only a whole chat tree can be
summed, so the query resolves the root and aggregates the tree, and
every chat in a tree reports the same total. Native returned per-subtree
totals.
## Attribution and counting semantics
The aggregate groups token usage per interception before counting, so
the reported numbers are per request even though a request records one
usage row per provider response:
- `RequestCount` counts finished `Coder Agents` interceptions in the
tree, including unpriced ones.
- `UnpricedRequestCount` counts requests with at least one usage row the
gateway could not price. It is a subset of `RequestCount`.
- `TotalCostMicros` omits only unpriced usage, so a partially priced
request still contributes its priced portion. The sidebar therefore says
`Excludes unpriced usage from N request(s)` rather than claiming whole
requests were dropped.
A recorded cost of zero is a free request, not an unpriced one. Usage
without an effective group is excluded, matching what never reached
`ai_user_daily_spend`.
## Authorization
Reads go through `ExtractChatParam` plus `ResourceChat`, with no
cost-specific RBAC widening. `TestGetChatCost/MemberCanReadOwnChat`
covers a scoped `agents-access` member reading their own chat's cost,
and `MemberCannotReadOtherUsersChat` still asserts 404 for a non-owner.
Plain members without `agents-access` cannot create or read chats at
all, so they never reach this endpoint.
## Known limitation
AI Gateway data has its own retention period, 60 days by default and
configured independently of chat retention, so spend for requests older
than that is no longer reported. A chat whose gateway records have all
been purged reports zero cost, which is indistinguishable from genuinely
free usage under this contract. The endpoint documents the caveat;
#27330 documents it on the Spend Management page.
In-flight interceptions are excluded, since cost is only known once the
response is recorded. A chat's cost therefore lags the active turn by
one request.
## Rebase note
Rebased onto `main` after #27579 removed the `ai-gateway-cost-control`
experiment. The per-chat cost row is now gated on the `aibridge` feature
alone, matching how #27579 degated the other cost-control surfaces.
> Mux prepared this PR on Mike's behalf.
|
||
|
|
54d5eb7ec2 |
feat: add hourly hb_agent_runtime_v1 usage events for Coder Agent runtime (#27312)
closes CODAGT-839 closes CODAGT-843 closes CODAGT-773 ## Summary Adds a new heartbeat usage event type, `hb_agent_runtime_v1`, measuring the total agent-loop runtime of Coder Agents (chats) per UTC hour, plus a reconciler that generates one event per hour with self-healing backfill over a trailing 7-day window. Events flow to Tallyman through the existing publisher unchanged. This measures the new Coder Agents (the `chats` tables), not the deprecated Tasks counted by `dc_managed_agents_v1`. Independent of #27508, which fixes the dead ai-seats cron registration. Both PRs carry the identical `usage_event` create permission hunk for the usage-publisher subject (this feature's generator and the ai-seats cron each need it for heartbeat inserts), so they can land in either order and the overlap merges cleanly. > [!WARNING] > **Do not include this in a release until Tallyman accepts `hb_agent_runtime_v1`.** The publisher marks permanently rejected events as done-forever, and the generator then sees those buckets as complete locally, so their usage would be silently and permanently lost. ## Details Each event's payload is `{"runtime_ms": N}`: the sum of `chat_messages.runtime_ms` for messages created in the hour bucket `[H, H+1)`, across all chats (sub-agents, API-created, archived, and soft-deleted messages included). Events use deterministic IDs (`hb_agent_runtime_v1:<bucket start>`) with `created_at` set to the bucket start, so concurrent replicas race safely via `ON CONFLICT (id) DO NOTHING` without locking, and daily rollups attribute backfilled hours to the correct day. Idle hours produce zero-valued events. A bucket becomes eligible 5 minutes after it closes; hours missing for longer than the 7-day window are forfeited, which can only undercount. Note that this makes `usage_events.created_at` explicitly the *event occurrence time* rather than the row insertion time; the two only diverge for backfilled events. It already behaved as the occurrence timestamp (it drives the daily rollup day and is shipped to Tallyman/Metronome as the event timestamp), and the migration now documents this with a `COMMENT ON COLUMN`, which also surfaces as a Go doc comment on `UsageEvent.CreatedAt`. The new `usage.Generator` runs unconditionally in enterprise builds; the `publish_usage_data` license flag continues to gate egress only, so air-gapped deployments still fill their local ledger. The `aggregate_usage_event()` trigger sums `runtime_ms` per day into `usage_events_daily` (unlike `hb_ai_seats_v1`, which takes the daily max). `InsertHeartbeatUsageEvent` now takes an explicit `createdAt` so generators can backfill historical buckets; the cron passes `clock.Now()` to preserve its existing behavior. ## Tallyman follow-up <details> <summary>Prompt for the Tallyman-repo agent</summary> > **Task**: Add support for the new Coder usage event type `hb_agent_runtime_v1` so Tallyman accepts, validates, and forwards it to Metronome. > > **Background**: coder/coder PR (this PR) adds hourly heartbeat events measuring Coder Agent runtime. Events arrive via the existing `/api/v1/events/ingest` endpoint with: `event_type: "hb_agent_runtime_v1"`, `event_data: {"runtime_ms": <int64 >= 0>}`, deterministic `id` of the form `hb_agent_runtime_v1:2026-07-15_14:00:00` (UTC hour bucket start), and `created_at` set to the bucket start (may be up to ~8 days in the past due to backfill; within Metronome's 34-day dedup window). Zero-value events are normal (idle hours). > > **Work**: > 1. Update Tallyman's vendored/imported `coderd/usage/usagetypes` (or equivalent) to the coder/coder commit that adds `UsageEventTypeHBAgentRuntimeV1` and `HBAgentRuntime`. > 2. Ensure ingestion validation accepts the type (`Valid()` switches) and rejects negative `runtime_ms`. > 3. Ensure Metronome forwarding maps the event with transaction ID derived from the event `id` as for existing types, passing `runtime_ms` through as the property for a SUM-aggregated billable metric ("Coder Agent Hours" = `SUM(runtime_ms) / 3,600,000`). > 4. Do NOT permanently reject unknown-but-well-formed future `hb_*` types if avoidable; at minimum confirm current behavior for unknown types (temporary vs permanent rejection) and report it. > 5. Tests: ingest accept/validate, dedup by ID, Metronome payload mapping. > > **Constraint**: this must be deployed to tallyman-prod **before** any coder/coder release containing the event generator; coderd treats permanent rejections as terminal per event. </details> |
||
|
|
2b28515d9b | refactor: migrate story snapshot params to pixel (#26844) | ||
|
|
d210b311dc |
ci(.github): retry build-tool downloads in Windows signing jobs (#27664)
## Problem The Windows code-signing path downloads two build tools with bare `wget` and no retry, in both `ci.yaml` (`build` job) and `release.yaml` (`release` job): - `rcodesign` from GitHub releases - `jsign-6.0.jar` from GitHub releases A single transient network failure on either fetch fails the whole job. In `ci.yaml` that turns `main` red via the `required` aggregator; in `release.yaml` it fails a release. This has happened. `Install rcodesign` failed on **2026-02-25** (in the since-deleted `build-dylib` job), **2026-03-04**, and **2026-04-30**. ## Root cause Two parts, one structural and one local. **Structural:** GitHub Actions has no per-step retry. This repo already knows toolchain provisioning is network-flaky and has `.github/scripts/retry.sh` (3 attempts, 2s/4s/8s backoff), applied in roughly 20 places. But `retry.sh` is a shell wrapper, so it can only wrap `run:` steps. These four downloads are `run:` steps that were simply never wrapped. **Local:** the failing step's body, under `set -euo pipefail`, is exactly three commands: ```sh wget -O /tmp/rcodesign.tar.gz https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/... sudo tar -xzf /tmp/rcodesign.tar.gz -C /usr/bin --strip-components=1 ... rm /tmp/rcodesign.tar.gz ``` `tar` and `rm` operate on a file that was just written, so they are deterministic. The only nondeterministic command in the step is the network fetch, and a truncated download surfaces as a `tar` failure whose cause is still the network. ### How we know Enumerated failed runs through the GitHub Actions API and extracted, per run, every failed job together with the names of its failed steps. | Scan | Scope | Runs | |---|---|---| | `ci.yaml`, `main` | 2025-08-01 to 2026-07-29 | 934 | | `ci.yaml`, all branches | most recent failures | 150 | | `release.yaml` | all recorded failures | 22 | The 934 is effectively the complete set; the API reports 923 failed `main` runs over that period and the scans overlap slightly. `Install rcodesign` appears **3 times on 3 separate dates**. Being spread across dates rather than clustered, these behave as **independent** events. That distinction is what selects the remedy, and it is why this change is retry rather than removal. For contrast, the `Setup Java` failures in the same jobs are **4 failures inside a single 90-minute window** on 2026-05-28, all from an `api.azul.com` edge failure. That is a correlated outage, where every attempt shares the same degraded dependency and retry provably cannot help. **That defect is not addressed here** and needs a different fix; see "Not addressed" below. ### Limits of the evidence Stating these plainly so a reviewer can weigh them: - **Cause is not directly confirmed.** Logs for all three `rcodesign` failures are past GitHub's 90-day retention. The inference from the step body above is strong but circumstantial. - **Step-level attribution only reaches back about five months.** GitHub prunes per-step detail from the jobs API while keeping job-level conclusions. Probed directly: runs from 2026-03-01 onward return populated `steps` arrays; runs from 2026-02-05 and earlier return empty ones. So the true count over the full period could be higher; it cannot be lower. - **Impact is small.** This whole class of failure is 10 of 800 attributed non-`required` job failures, about **1.25%** of measured `main` CI failure volume. This is not a significant reliability improvement and should not be reviewed as one. The Postgres-backed Go tests alone are over 40%. ## Solution Wrap all four downloads in the existing retry helper: ```yaml - ./.github/scripts/retry.sh -- wget -O /tmp/rcodesign.tar.gz https://... ``` Four lines changed, one per site: `ci.yaml:1287`, `ci.yaml:1321`, `release.yaml:199`, `release.yaml:225`. **How it works.** `retry.sh` runs the command, and on non-zero exit sleeps 2s, 4s, then 8s before re-attempting, up to 3 attempts, then fails with the original command in the error message. On success the first time, behavior is unchanged. **Why it works for these failures.** They are independent events, so each attempt is a fresh trial with an independent chance of success. A GitHub releases CDN blip on one run says nothing about the next 2 seconds. This is exactly the regime retry is for. **Why retry rather than deletion.** These artifacts genuinely are not present on the runner, so the network call is unavoidable. It can only be made survivable. (Where a dependency *is* avoidable, deletion is the better answer, which is the shape the `setup-java` fix will take.) **Why `wget -O` is safe to retry.** `-O` truncates its output file on each attempt, so a partial download from a failed attempt is overwritten rather than appended to. No corruption path. ## Risks Low, and worth naming precisely. | Risk | Assessment | |---|---| | Behavior change on the success path | None. `retry.sh` execs the command directly; a first-attempt success is identical to today. | | A persistently broken URL now takes longer to fail | Yes, by up to 14s of backoff, then it fails exactly as it does today. Negligible against a job that takes tens of minutes. | | `retry.sh` mangling `wget`'s own flags | `retry.sh` parses its own options with `getopt`, so this was the main correctness concern. Verified explicitly, both argument orders used in these workflows. See Verification. | | Relative path `./.github/scripts/retry.sh` resolving wrongly | These steps set no `working-directory`, so cwd is the repo root. Deliberately **excluded** the third `wget` at `release.yaml:713` (`publish-homebrew`), which runs after `cd "$temp_dir"` where a repo-relative path would break. | | Retry masking a real regression | Bounded to 3 attempts over 14s. This is not job-level auto-retry, which would hide regressions and is explicitly not proposed. | ### Verification gap a reviewer should know about **The changed steps do not run on PR CI.** `ci.yaml`'s `build` job is gated on `github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')`, and `release.yaml` runs only on release. So these four steps will execute for the first time on merge to `main`. Verification below is therefore local plus static analysis, not a live run of the modified steps. ## Verification `retry.sh` argument passing, using a stub that prints what it received, for both argument orders present in these workflows: ``` --- form A: -O before URL (rcodesign style) --- argc=3 arg1=[-O] arg2=[/tmp/rcodesign.tar.gz] arg3=[https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-x86_64-unknown-linux-musl.tar.gz] --- form B: URL before -O (jsign style) --- argc=3 arg1=[https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar] arg2=[-O] arg3=[/tmp/jsign-6.0.jar] ``` Order preserved and the `%2F` encoding in the rcodesign URL intact, which was the specific failure mode to rule out. `make lint/actions` (actionlint plus zizmor security audit): ``` ✓ lint/actions/actionlint No findings to report. Good job! (29 ignored, 102 suppressed) ``` `make pre-commit-light`: ``` ✓ fmt/shfmt ✓ lint/markdown ✓ lint/actions/actionlint ✓ fmt/terraform ✓ lint/shellcheck ✓ lint/helm ✓ fmt/markdown ✓ lint/bootstrap ✓ lint/emdash ✓ lint/migrations ✓ lint/typos ✓ lint/mise-versions ✓ pre-commit-light passed (14s) ``` ## Not addressed Deliberately out of scope, listed so the remaining exposure is visible: - **`actions/setup-java` with `distribution: "zulu"`** in both files. This resolves a JDK from `api.azul.com` and downloads it from `cdn.azul.com` on **every** run, confirmed from a successful `main` build's log, because a `Java_Zulu_jdk` tool-cache lookup can never hit the runner's cache. This is the correlated-outage defect from 2026-05-28 and retry cannot fix it. The probe on this branch ([run 30492093096](https://github.com/coder/coder/actions/runs/30492093096)) has now answered what the fix should be. `depot-ubuntu-22.04-8` ships: ``` RESULT: java found at /usr/bin/java OpenJDK Runtime Environment Temurin-11.0.31+11 (build 11.0.31+11) JAVA_HOME=/usr/lib/jvm/temurin-11-jdk-amd64 JAVA_HOME_8_X64 / _11_X64 / _17_X64 / _21_X64 / _25_X64 (all present) tool cache: Java_Temurin-Hotspot_jdk ``` So the job downloads **Zulu 11.0.32+9 over two third-party hosts while Temurin 11.0.31+11 is already on the runner's `PATH`**. The follow-up PR will point `JAVA_HOME` at `$JAVA_HOME_11_X64` and drop the action, which removes both Azul hosts while keeping the Java 11 pin rather than inheriting whatever the image default becomes. - **`storybook`'s `pnpm/action-setup`**, the last direct use in the repo and the same unguarded `registry.npmjs.org` dependency originally reported on the issue. Note `cache: true` does **not** mitigate it: per the action's own `action.yml`, `cache` caches "the pnpm store directory", not the pnpm binary. ## Scope This PR is now a **single commit** (`673162b84e`) containing only the four-line retry change. A throwaway probe workflow briefly lived on this branch to answer the JDK question above. It has served its purpose and the commit was dropped, so nothing diagnostic remains here to review. Its result is quoted in "Not addressed" and will be carried into the follow-up PR. Refs coder/internal#929 |
||
|
|
4e512f786f |
fix: prefetch outdated Coder CLI in e2e setup instead of in the test (#27629)
## Summary `e2e/tests/outdatedCLI.spec.ts` has a 30 second budget in which it must create a template and workspace, start an agent, download an 84 MiB release binary from GitHub, and then exercise the actual thing under test: whether a `v2.8.0` client can still SSH into a workspace served by HEAD. In the run that filed this ticket, `install.sh` spent **20.04 seconds** of that budget on an HTTP request the test does not need, leaving 5.4 seconds for the download. The SSH flow never executed. Worth being precise about the shape, because it changes the fix. The stall is not in the code under test and it is not an SSH problem. `install.sh` resolves the latest stable release version **unconditionally**, even when `--version 2.8.0` is passed explicitly, and on the pinned path that value feeds nothing but a cosmetic post-install advisory string. Two thirds of the test's budget went to producing one sentence of console output that the test discards. Refs: https://github.com/coder/internal/issues/1571 ## Problem ### What the test is for This is a backward-compatibility test, and `v2.8.0` is the compatibility floor it enforces rather than a "supported version" in the release-channel sense. The pin traces to one code comment, `we no longer support versions prior to Tailnet v2 API support`, citing 059e533544; that commit first shipped in v2.8.0, so the pin sits exactly on the boundary it names. Worth stating plainly: this is the oldest client expected to still interoperate, not a version that receives patches. Release support is mainline / stable / n-2 / ESR, all far newer. The test's value is that it runs the *real* historical binary, compiled in Feb 2024, against a current server: `codersdk` REST compatibility, tailnet coordination v2, DERP negotiation, `coder ssh --stdio` as an SSH transport, and the agent accepting a session. Nobody gets to assert what that old client sends over the wire, which is exactly why the binary has to be downloaded rather than faked. The structural defect is that the download shares a timeout with the assertion: ```text ┌─────────────────────────────────────────────────────────────────┐ │ ONE 30-second Playwright test budget │ ├──────────────────────────────┬──────────────────────────────────┤ │ What we want to measure │ Incidental setup │ │ (deterministic, local) │ (network, non-deterministic) │ │ │ │ │ • template + workspace │ • HTTP HEAD to github.com │ │ • agent connect │ • 84 MiB download from │ │ • coder ssh --stdio │ GitHub release CDN │ │ • SSH handshake + exec │ • tar extraction │ │ • workspace stop │ │ └──────────────────────────────┴──────────────────────────────────┘ ~7-14 s, stable 0 s (cached) .. ∞ (unbounded) ``` A test that asserts protocol compatibility should not be able to fail because `github.com` was slow. ### The evidence CI runs Playwright with `DEBUG: pw:api`, and `downloadCoderVersion` passes `TRACE=1` to `install.sh`, which makes it `set -x`. The job log therefore stamps every phase. Reconstructed from [job 80049661296](https://github.com/coder/coder/actions/runs/27124540074/job/80049661296), `t=` relative to test start: ```text t=+0.000 08:17:44.671 browserContext.newPage <- test starts t=+0.882 08:17:45.553 login complete t=+4.228 08:17:48.899 workspace create submitted t=+4.519 08:17:49.190 agent-status-ready visible <- startAgent returns t=+4.526 08:17:49.197 install.sh: parse_arg --version 2.8.0 ... <- downloadCoderVersion t=+4.531 08:17:49.202 curl -sSLI https://github.com/coder/coder/releases/latest : : 20.042 SECONDS OF NOTHING : (agent logs keepalives; the page sits idle) : t=+24.573 08:18:09.244 response= 200 .../releases/tag/v2.33.6 <- probe returns t=+24.575 08:18:09.246 STABLE_VERSION=2.33.6 <- feeds a log line t=+24.582 08:18:09.253 curl -#fL -o .../coder_2.8.0_linux_amd64.tar.gz.incomplete : 5.4 s of an 84 MiB download t=+30.000 08:18:14.671 Playwright kills the test ``` Three observations rule out the originally suspected cause (slow SSH readiness or general runner slowness): - **The SSH flow never started.** `sshIntoWorkspace` is called after `downloadCoderVersion` returns, and it never returned. There is no `coder ssh --stdio` process in the log. - **The agent was healthy.** `agent-status-ready` resolved in 88 ms, and through the entire 20 second stall the agent logs a live DERP connection, successful STUN, and a completed wireguard handshake. - **The runner was fast, not slow.** Login plus template plus workspace plus agent took 4.5 seconds. ### Where the 20 seconds goes ```text install.sh main() ... L431 STABLE_VERSION=$(echo_latest_stable_version) <- ALWAYS runs | +-- echo_latest_stable_version() (install.sh:94) curl -sSLI https://github.com/coder/coder/releases/latest # no --connect-timeout # no --max-time # non-200 => exit 1 (hard failure) L454-461 the only consumers when --version is pinned: if VERSION == STABLE_VERSION: STABLE=1 L148 advisory="To install our stable release (v${STABLE_VERSION}), ..." L159 "Coder ${channel}release v${VERSION} installed. ${advisory}" ``` That is the whole dependency chain. `-sSLI` also follows redirects and `/releases/latest` *is* a redirect, so this is at minimum two round-trips to `github.com` with no timeout ceiling on either. ### Why 30 seconds and not 60 `test.setTimeout(60_000)` used to be on this test. #16236 removed it, and that removal was deliberate: it was itself a flake fix (coder/internal#204, #279) whose thesis was that `go run` compiling inside a resource-constrained test run was the problem. Having pre-built the binary, it consistently stripped the allowances that existed to absorb compile time: | File | Change in #16236 | Was that allowance really compile time? | |---|---|---| | `app.spec.ts` | `setTimeout(75_000)` removed, click timeout `60_000` -> `10_000` | Yes | | `webTerminal.spec.ts` | `setTimeout(75_000)` removed | Yes | | `helpers.ts` | agent-ready wait `45_000` -> `15_000` | Yes | | `outdatedCLI.spec.ts` | `setTimeout(60_000)` removed | **No: also an 84 MiB download** | | `outdatedAgent.spec.ts` | timeout untouched, 60 s survives | n/a | The reasoning was sound and the sweep internally consistent. It had one blind spot: for `app.spec.ts` and `webTerminal.spec.ts` that budget genuinely was the compiler's, but here it covered compile time **plus** a release download, and only the compile half went away. With 60 seconds, the failing run above would have finished in roughly 31 to 43 seconds and passed. ### Budget arithmetic At `t=+24.58` the test still had to do: | Remaining work | Realistic cost | |---|---:| | Download 84 MiB tarball | 2 - 8 s | | `tar` extract | 0.3 - 1 s | | `coder ssh --stdio` cold start | 0.5 - 2 s | | Tailnet dial + SSH handshake | 1 - 3 s | | `stopWorkspace` | 2 - 4 s | | **Needed** | **~6 - 18 s** | | **Available** | **5.42 s** | ## Fix Move the download into the existing `testsSetup` Playwright project, where it gets a 300 second budget and where a failure is attributed to the download rather than to SSH. ```mermaid flowchart TB subgraph BEFORE["BEFORE: one budget, two concerns"] direction TB T1["tests project, timeout 30s"] T1A["outdatedCLI.spec.ts<br/>login / template / workspace / agent<br/><b>downloadCoderVersion <- NETWORK</b><br/>sshIntoWorkspace / exec / stopWorkspace"] T1 --> T1A end subgraph AFTER["AFTER: network work has its own clock"] direction TB S2["testsSetup project, timeout 300s"] S2A["downloadCoderVersions.spec.ts<br/>stable-version probe + 84 MiB + retries<br/>all live HERE"] T2["tests project, timeout 60s"] T2A["outdatedCLI.spec.ts<br/>downloadCoderVersion = cache hit, ~300ms<br/>SSH path gets the whole budget"] S2 --> S2A S2A -- "dependencies" --> T2 T2 --> T2A end BEFORE ~~~ AFTER style T1A fill:#ffe5e5,stroke:#cc0000,stroke-width:2px style S2A fill:#e5ffe5,stroke:#007700,stroke-width:2px style T2A fill:#e5ffe5,stroke:#007700,stroke-width:2px ``` ### Why it works `downloadCoderVersion` was already idempotent and cache-checking: it spawns `<binaryPath> version` first and returns early on exit 0. So the test keeps its existing call and that call simply becomes a no-op costing a few hundred milliseconds. **No test logic changes.** ```mermaid sequenceDiagram autonumber participant S as testsSetup:<br/>downloadCoderVersions participant IS as install.sh participant GH as github.com participant T as tests:<br/>outdatedCLI participant CD as coderd + agent Note over S: budget 300s S->>IS: downloadCoderVersion(v2.8.0) IS->>GH: stable-version probe (unbounded) IS->>GH: fetch 84 MiB asset GH-->>IS: /tmp/coder-e2e-cache/bin/coder-e2e-2.8.0 IS-->>S: binaryPath Note over T: budget 60s, local only T->>T: downloadCoderVersion(v2.8.0) Note right of T: spawn "<bin> version" -> exit 0<br/>returns early, ~300ms, no network T->>CD: coder ssh --stdio, handshake, exec "exit 0" CD-->>T: exit code 0 ``` ### Why the prefetch is non-fatal The obvious implementation raises on failure. That would be wrong here, and I verified why rather than assuming: `tests` declares `dependencies: ["testsSetup"]`, and a failing setup project stops dependent tests from **running at all**. Adding a deliberately-throwing setup spec produced: ```text ✓ 1 [testsSetup] › addUsersAndLicense.spec.ts › setup deployment (11.7s) ✓ 2 [testsSetup] › downloadCoderVersions.spec.ts › download outdated CLI (353ms) ✘ 3 [testsSetup] › zzTempFail.spec.ts › temporary blast radius probe (0ms) 1 failed 1 did not run <- outdatedCLI never ran 2 passed ``` So raising would convert a one-test flake into a whole-suite outage on any GitHub hiccup. Instead the prefetch logs a warning and returns, and the test's own `downloadCoderVersion` call fetches inline as it does today. The failure path is therefore no worse than the status quo, and the success path removes the network from the test entirely. Of the three policies available (fail hard, fall back inline, or skip the test), this is the only one that cannot regress anything: it never blocks the suite, and it never silently drops coverage the way an auto-skip would. ### Restoring the 60 second budget This is the second half of the change, and it exists for the fallback path above. It cannot reintroduce what #16236 fixed: the timeout value has no causal relationship to how the binary is produced, `coderBinary` stays pre-built, `go run` stays gone, and only `outdatedCLI.spec.ts` is touched. It does give back a bounded sliver of the CI-latency goal, and the bound is small. A passing run is unaffected. The cost lands only when this one test hangs, and then it is +30 s once: `--workers 1` so there is no fan-out, `CODER_E2E_TEST_RETRIES` is unset in CI so `retries` is 0 and nothing multiplies it, and the job budget is `timeout-minutes: 20`. ## Measurements Four scenarios, locally on darwin/arm64 against a freshly built `site/e2e/bin/coder`: | Scenario | setup spec | `outdatedCLI` | `install.sh` inside the test? | Result | |---|---:|---:|---|---| | Cold, empty cache | 5.7 s | 10.0 s | **no**, ran in setup | ✓ passed | | Warm cache | 340 ms | 11.9 s | **no**, 0 invocations | ✓ passed | | Prefetch fails, cache empty | 1 ms | 15.4 s | yes, inline fallback | ✓ passed | | Setup spec throws | n/a | did not run | n/a | blast radius above | The cold run is the load-bearing one: `install.sh` is invoked from the setup spec and the test runs local-only in 10.0 s, so the 84 MiB download and the 20 s probe are no longer on the assertion's clock. For context on what "local only" costs, eight consecutive `main` runs where the CI cache already made `install.sh` a no-op: | Job | duration | |---|---:| | 90382498172 | 11.7 s | | 90352398170 | 12.1 s | | 90335547858 | 8.1 s | | 90317232684 | 13.7 s | | 90297156949 | 8.4 s | | 90280065546 | 6.9 s | | 90265047082 | 6.9 s | | 90250803989 | 6.6 s | 6.6 to 13.7 seconds. This change makes that the only path rather than the lucky one. Also checked: the test name is byte identical (`ssh with client v2.8.0`) so flake tracking keeps matching it, `outdatedAgent` remains skipped, and `webTerminal`, `auditLogs`, and `updateTemplate` still pass, so the added setup dependency disturbs nothing. The full 60-test suite was not run locally because the premium tests need `CODER_E2E_LICENSE`. ## Also in this change The pinned versions move to `site/e2e/constants.ts` as `oldestSupportedCLIVersion` and `oldestSupportedAgentVersion`, so the setup spec and the tests share one source of truth, and the comments explaining *why* those particular versions travel with them. The CI cache key follows them there: it previously hashed the two spec files, and now hashes `constants.ts`, so it still invalidates exactly when a pinned version changes. ## Not addressed here The 20 second probe is relocated, not removed. `install.sh` still resolves the latest stable version on every pinned install, with no `--connect-timeout` or `--max-time`, and still treats a non-200 as fatal, so a GitHub hiccup can fail an install whose target tarball is already cached locally. That is a user-facing bug in its own right and wants its own PR, since fixing it means deciding what a pinned install should print when we no longer look up what "stable" currently is. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dc1d6c3f3a |
ci: explicitly specify bash in mise tools installation (#27666)
Should hopefully fix [this issue](https://github.com/coder/coder/actions/runs/30412236628/job/90710968864?pr=27628) |
||
|
|
740f5f7e1e |
chore: drop stale cost control experiment params (#27650)
Removes two stale `experiments: ["ai-gateway-cost-control"]` story parameters from `GroupPage.stories.tsx`. Refs #27579 #27553 |
||
|
|
18128b7b52 |
docs: add standalone AI Gateway docs (#27592)
Documents standalone AI Gateway deployment, Gateway key authentication, monitoring, and the updated embedded vs standalone topology in the AI Gateway docs. --------- Co-authored-by: Cian Johnston <cian@coder.com> |
||
|
|
6c42309ccb |
feat(site): modernize OAuth2 applications settings UI (#27562)
Follow up to #27561 Modernizes the Deployment Settings OAuth2 Applications create/edit/list UI to match the AI Providers pattern: fat page views, card layout, and a Formik + Yup form using shared field primitives. - Rework CreateOAuth2AppPageView / EditOAuth2AppPageView into fat views with provider-style layout (back link, avatar + title, bordered cards, cancel/submit footer) - Rewrite OAuth2AppForm with Formik/Yup, FormField descriptions, and IconPickerField (live header avatar on create/edit) - Order edit page as settings → endpoints (Client ID / Auth / Token via CodeExample) → secrets - Align list page row styling and add a Callback URL column - Update Storybook stories for the new fat-view + form validation behavior | Old | New | | --- | --- | | <img width="2936" height="1802" alt="old-oauth2-application-create" src="https://github.com/user-attachments/assets/98c1dec1-c273-43a7-a517-a31ae48bc17c" /> | <img width="2936" height="1802" alt="new-oauth2-application-create" src="https://github.com/user-attachments/assets/9d2e1d6b-6905-469f-b9e0-cf8ae3b14f3d" /> | | <img width="2936" height="1802" alt="old-oauth2-application-list" src="https://github.com/user-attachments/assets/3d38daab-1d93-4acb-bff0-d7299da884fd" /> | <img width="2936" height="1802" alt="new-oauth2-application-list" src="https://github.com/user-attachments/assets/b3cc56ac-a810-4742-9ea7-27e20caa9bb7" /> | | <img width="2936" height="2030" alt="old-oauth2-application-update" src="https://github.com/user-attachments/assets/544ad92b-ae2b-4b10-907d-c94bcc3d12c4" /> | <img width="2936" height="3470" alt="new-oauth2-application-update" src="https://github.com/user-attachments/assets/6e4d228d-8deb-4934-989a-b9c98b633509" /> | |
||
|
|
659fb48a1d |
fix: demui <OAuth2AppForm /> (#27561)
This pull-request removes the MUI styles from the `<OAuth2AppForm />` and adjacent components. |
||
|
|
0b93731ebf |
fix(site): reflect submitting state during batch update (#27630)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
## Problem
When bulk updating workspaces, the confirmation modal's **Update**
button never entered a submitting/loading state, so there was no
feedback that the update was actually in progress.
## Root cause
The `BatchUpdateModalForm` shows a spinner when its `isProcessing` prop
is `true`. That prop is fed by `batchActions.isProcessing` from
`useBatchActions`. However, the `isProcessing` value was OR-ing together
every mutation's `isPending` flag **except** `updateAllMutation` — the
one that actually performs the batch update:
```ts
isProcessing:
favoriteAllMutation.isPending ||
unfavoriteAllMutation.isPending ||
startAllMutation.isPending ||
stopAllMutation.isPending ||
deleteAllMutation.isPending,
// updateAllMutation.isPending was missing
```
As a result, the button stayed idle for the entire duration of a bulk
update.
## Fix
Include `updateAllMutation.isPending` in the `isProcessing` derivation
so the button spinner and disabled state correctly reflect an in-flight
batch update.
## Testing
- [ ] Manually verify the Update button shows the spinner and is
disabled while a bulk update runs.
|
||
|
|
4bf9b9d1e6 |
feat(site): surface chat lifecycle hook outcomes in the chats UI (#27430)
Surfaces chat lifecycle hook outcomes in the chats UI. Final PR of the lifecycle hooks stack (#27401, #27428, #27429), all now merged. - Show hook notices attached to their user message as timeline notes (`role="note"` so historical notices stay out of the screen reader's assertive live region), and show an info tooltip for notices on queued messages. - Cache the full inserted message batch from send and edit responses so hook-inserted messages survive stream reconnects and queue promotion. - Reconcile the promoted queue head after sending to an errored chat so a missed or delayed queue update neither duplicates nor hides messages, and clear the stale error status so the Thinking indicator appears before the websocket status event. - Ignore an authoritative queue snapshot that still contains a just-promoted message: queued messages are delete-only, so such a snapshot predates the promotion and would both re-show the promoted message and drop messages queued since. Fresh snapshots apply in full and clear the suppression. - Cache the store's reconciled queue on `queue_update` instead of the raw event, so a stale update cannot re-show a promoted message after REST re-hydration. - Refresh chat details when a send or edit fails, because a failed hook dispatch can move the chat to the error state. - Surface tool result error text in the tool rows: the execute failure tooltip shows the actual error instead of a hardcoded "Command failed", and a failed `write_file` renders an error label with the result error text instead of "Wrote <file>" with an args-derived diff of content that was never written. This makes hook tool denials legible in the timeline, and benefits every failed execute or write. - Label a tool call blocked by `pre_tool_use` as failed instead of `Ran <command>`, matching what the write and edit tools already do. The wording derives from the tool-result error flag, so a command that ran and exited non-zero is unaffected. - Render a hook notice below the message it annotates rather than above it, which reads correctly for a "your prompt was rewritten" card. - Give both hook outcomes their own treatment on the create path, where they previously fell through to the generic error alert and an expected policy decision appeared with a stack trace, response data, and a workspaces action. Classification keys on the structured response body rather than the status code, so ordinary permission errors keep their existing rendering. - Unrelated to the hooks work, de-flake `SchedulePage.test.tsx`. Its `fillForm` helper wrapped an already-retrying `findByLabelText` in `waitFor`, so the two 1s budgets raced and a slow first render failed `test-js` with "Timed out in waitFor". This is separable from the rest of the PR if you would rather it land on its own. > This PR was written by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
e3a5a697ab |
fix(site): refresh group member budgets after overrides (#27553)
Group member budget rows remained stale after saving or deleting a user override because the cached member-spend query was not refreshed. Invalidate the affected user's override query and only cached group-member spend queries whose user ID list contains that user. This refreshes relevant rows without invalidating unrelated groups. |
||
|
|
3deecb481e |
chore: remove ai-gateway-cost-control experiment flag (#27579)
## Description Closes [AIGOV-443](https://linear.app/codercom/issue/AIGOV-443/remove-ai-gateway-cost-control-experiment-flag-once-feature-is-stable). The AI Gateway cost control feature is planned for GA on the upcoming release, so this removes the `ExperimentAIGatewayCostControl` experiment and all of its gating. The cost control API endpoints remain gated by the `FeatureAIBridge` license feature (the AI Governance add-on), so this only drops the experiment layer. ## Changes - **`codersdk/deployment.go`**: remove the `ExperimentAIGatewayCostControl` const, its `DisplayName()` case, and its `ExperimentsKnown` entry. - **`enterprise/coderd/coderd.go`**: remove the `httpmw.RequireExperiment(...)` gating from the AI cost control routes. They keep `RequireFeatureMW(codersdk.FeatureAIBridge)`. Affected endpoints: - `GET /organizations/{organization}/groups/ai/spend` - `GET /organizations/{organization}/groups/{groupName}/members/ai/spend` - `GET /organizations/{organization}/ai/spend/export` - `GET /groups/{group}/members/ai/spend` - `GET /groups/{group}/ai/spend` - `GET/PUT/DELETE /users/{user}/ai/budget/override` and `GET /users/{user}/ai/spend` - **`enterprise/coderd/aibridge_test.go`**: drop the experiment from test setup and remove the now-obsolete `RequiresExperiment` negative-path tests. - **Frontend (`site/src/...`)**: remove the `ai-gateway-cost-control` experiment checks from the cost control UI (Groups pages, user dropdown) and their stories/mocks. The feature is now driven solely by the `aibridge` feature visibility. - **Generated**: regenerated `coderd/apidoc/*`, `docs/reference/api/schemas.md`, and `site/src/api/typesGenerated.ts`. ## Out of scope The dogfood `CODER_EXPERIMENTS` config lives in a separate infra repo, not `coder/coder`. Leaving `ai-gateway-cost-control` there is harmless: unknown experiment values are logged as `"ignoring unknown experiment"` at startup and otherwise ignored, so no ordering dependency or breakage. That cleanup can be a follow-up. <details> <summary>Implementation notes</summary> - Verified how unknown experiments are handled in `coderd/coderd.go` `ReadExperiments`: unknown values produce a warning log and are inert, so removing the definition before the dogfood config is updated is safe. - Noticed the group `ai/budget` routes (`/groups/{group}/ai/budget`) were already gated only by `FeatureAIBridge`, never by the experiment. After this change all cost control routes are uniformly feature-gated, resolving that inconsistency. - Removed an obsolete `RequiresExperiment` subtest in `TestUserAISpendStatus` that only asserted a 403 from the experiment gate; with the gate gone it would no longer be blocked pre-RBAC. </details> --- _This PR was created by Coder Agents on behalf of @ssncferreira._ |
||
|
|
d6a5c8e9f8 |
refactor: make user AI budget and spend endpoints consistent (#27611)
## Description
Makes the user AI cost control endpoints consistent.
## Changes
- Replaces the flat `spend_limit_micros` and `limit_source` fields on
`GET /users/{user}/ai/spend` with a nested `effective_budget`, reusing
the type behind `group_budget`. The flat pair made it possible to encode
a limit without a source.
- Renames `AIGroupBudget` to `AIBudgetLimit`, since it also carries
`user_override` limits and is no longer group-specific. The type name is
not part of the wire format.
- Moves `/users/{user}/ai/budget` to `/users/{user}/ai/budget/override`.
The endpoint only ever managed the per-user override, which the type,
the handlers, and the operation IDs all already said; the path was the
only place that didn't.
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
|
||
|
|
e71249a821 |
fix: ai cost control cap configurable AI spend limit (#27640)
## Problem A configured AI spend limit was only validated as `gte=0`, with no upper bound. The group spend query multiplies the per-member limit by the number of attributed members, so a large enough limit overflows `bigint` and fails the whole query, returning an error for every group in the request rather than just the misconfigured one. ## Changes - Add `MaxAISpendLimitMicros`, $1,000,000 per member per budget period. - Reject group budgets and per-user overrides above the maximum with a 400 naming the limit. - Bound both budget forms in the UI so they show the valid range before submitting. Follow-up https://github.com/coder/coder/pull/27589#discussion_r3668956350 Depends on https://github.com/coder/coder/pull/27589 > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira |
||
|
|
4987afada7 |
docs: present AI Governance as included with Premium (#27545)
## Summary AI Governance is now included with Premium licenses instead of being sold as a separate per-user add-on. This updates `docs/` to describe the new packaging, removes "Add-On" from AI Governance references, and refreshes the editions architecture diagram. ## Changes - **`docs/ai-coder/ai-governance.md`**: title is now "AI Governance"; rewrote the licensing statements (previously "a separate, per-user license... not included with a Premium subscription and must be purchased separately") to state it is included with Premium. The usage-pool section now attributes the shared Agent Workspace Build pool to Premium deployments. - **Repeated admonition (28 files under `ai-coder/agent-firewall/` and `ai-coder/ai-gateway/`)**: replaced "requires the AI Governance Add-On / as of Coder v2.32, deployments without the add-on..." with "is part of AI Governance, which is included with a Premium license." The v2.32 add-on gate no longer applies; the gate is now Premium vs. Community. - **`docs/ai-coder/index.md`, `security.md`, `tasks.md`, `usage-data-reporting.md`, `admin/licensing/index.md`, `install/releases/esr-2.29-2.34-upgrade.md`, `ai-gateway/ai-gateway-proxy/setup.md`, `ai-gateway/clients/claude-code.md`**: reworded add-on references to Premium inclusion. - **`docs/manifest.json`**: nav title "AI Governance Add-On" → "AI Governance", updated two descriptions, and swapped the 25 `"state": ["ai governance add-on"]` badges to `["premium"]` so the sidebar badge reads "Premium" instead of "AI Governance Add-On". - **`docs/images/single-region-architecture.png`**: refreshed the diagram in the **Community and Premium editions** tab on [Architecture](https://coder.com/docs/admin/infrastructure/architecture). Also deleted the unreferenced `single-region-architecture.svg` copy. ## Follow-ups outside this PR - The `"ai governance add-on"` doc-state badge is defined in `coder/coder.com` (`src/utils/docs/state.ts`). After this merges, no manifest entry uses that key, so it becomes dead config and can be removed there. - `enterprise/coderd/license/license.go:564-572` still warns admins that "The AI Governance add-on is required to use AI Gateway." That backend string will contradict these docs once shipped. ## Verification - `pnpm run lint-docs`: 0 errors across 504 files - `make lint/emdash`: clean - Vale on the changed Markdown files: 0 errors; remaining warnings are pre-existing gerund headings on untouched lines - `docs/manifest.json` validated as JSON - Confirmed the deleted SVG had no references anywhere in the repo --- PR generated with Coder Agents on behalf of @mattvollmer. |
||
|
|
fb30674806 |
fix(site/src/pages/AgentsPage): order chat transcript by message id (#27620)
## Context Follow-up to #27495 (append-order guarantee for `chat_messages.id`) and #27619 (prompt query ordering), both merged. This PR applies the same id ordering to the transcript the user actually sees. ## Why? `buildOrderedMessageIDs` in `chatStore.ts` sorted by `created_at`, which is `now()` and therefore shared by every row in an insert batch. It builds `orderedMessageIDs`, which is what the transcript renders, so it re-imposed the ordering the backend PRs remove. The failure needs the merge path, not a plain fetch. Initial REST hydration already sorts by numeric id before reaching the store, and `Array.prototype.sort` is stable, so a single correctly ordered response rendered correctly. But `upsertDurableMessages` copies the existing message `Map`, appends new ids, and re-sorts. `Map` iteration is insertion ordered, so when a refetch or reconnect merges earlier ids into a map that already holds later ones, the stable timestamp sort faithfully preserves the wrong order. This also makes the store consistent with `useChatStore.ts` and `api/queries/chatMessageEdits.ts`, which already sort by `id`. ## Changes `buildOrderedMessageIDs` now calls `toSorted` with an `id` comparator inlined at its single call site, and the `byMessageCreatedAt` helper is gone. `ChatMessage.id` is a `number` in `typesGenerated.ts`, backed by a Go `int64`, so numeric subtraction is correct. ## Testing Two vitest cases, both verified red by restoring the timestamp comparator: - `sorts messages by id when created_at disagrees with append order` returned `[2,1]`. - `orders merged messages by id rather than by arrival` returned `[3,4,1,2]`, the exact inversion the merge path produces. `MergedMessagesRenderInIDOrder` in `ChatPageContent.stories.tsx` covers the same merge path through the rendered timeline. All 334 `ChatConversation` unit tests and the 4 `ChatPageContent` storybook interaction tests pass, and `tsc -p .` plus biome are clean. > Opened by Mux on behalf of Mike. |
||
|
|
1c722ff969 |
fix(coderd/database): order the chat prompt query and its boundary by id (#27619)
## Stack context Follows #27495 (merged), which gives `chat_messages.id` an append-order guarantee and moves the history reads onto it. This PR applies the same fix to the query that builds the model prompt. ## Why? `GetChatMessagesForPromptByChatID` mixed two orderings. It selected the compaction boundary with `created_at DESC, id DESC`, then applied that boundary with an `id >` comparison, and returned rows with `created_at ASC, id ASC`. `created_at` is `now()`, so it is the transaction start time. Every row in one insert batch shares it, and concurrent transactions can commit in the opposite order to the one they started in. Two consequences, both reaching the provider: - **Malformed prompts.** A tool result could be ordered ahead of the assistant message that requested it. `chatprompt.injectMissingToolResults` does not repair this: it only handles tool rows already contiguous after an assistant row, and adds missing results. It never moves a tool row that precedes its assistant, and nothing re-sorts the rows in Go. - **Wrong compaction boundary.** The boundary is picked by timestamp but compared by id, so a stale compressed summary could be retained while the actual latest one was dropped. ## Changes Both the boundary CTE and the outer query order by `id`. The `id >` predicate is unchanged, which is the point: the ordering now matches the comparison that was always being made. **The boundary index was dead, so it is rebuilt to match.** `idx_chat_messages_compressed_summary_boundary` was created for exactly this lookup, but its predicate requires `role = 'system'` while compaction writes its summary with the user role (`message_conversion.go:334`, the only writer of `compressed = true`). It matched zero rows, and no other query can use it. Migration `000560` rebuilds it as `(chat_id, id DESC) WHERE compressed AND NOT deleted AND visibility = 'model'`, which also matches the new order key. Measured on PostgreSQL 13 with a 20k-message chat, 11 summaries, and 14 sibling chats so `chat_id` is selective: | boundary lookup | plan | buffers | |---|---|---| | old predicate | Index Scan `idx_chat_messages_chat`, 19,989 rows filtered | 267 | | rebuilt index | Index Only Scan | 2 | Not in scope: the outer `SELECT` still inspects every row of the chat, because its `role = 'system' AND compressed = FALSE` disjunct has no lower `id` bound. That predates this PR and needs a query rewrite rather than an index. ## Testing Two subtests, both verified red by reverting the `ORDER BY` and regenerating: - `OrdersByIDWhenTimestampsDisagree` returned `[4,3,2,1]` instead of `[1,2,3,4]`, placing the tool result before the assistant call. - `CompactionBoundaryUsesID` selected the stale summary and leaked the messages between the two summaries into the prompt. Existing subtests pass unchanged. Migration up/down tests pass, and the rebuilt index was verified red-green: restoring the old predicate returns the plan to a 267-buffer scan, and the old predicate matches 0 rows in the fixture. > Opened by Mux on behalf of Mike. |
||
|
|
b371262e5c |
fix(aibridge): handle sonnet 5 adaptive thinking in bedrock (#27339)
Adds sonnet 5 to the list of models that require adaptive thinking for Bedrock InvokeModel. Smoke-tested locally. > Obligatory disclosure: a Coder agent helped with this. |
||
|
|
c17bed25e0 |
feat: wire chat lifecycle hooks into chatd (#27429)
Wires chat lifecycle hooks into chatd, gated by the `agent-lifecycle-hooks` experiment. Part of the lifecycle hooks stack (#27401, #27428, #27430). See `docs/admin/setup/chat-lifecycle-hooks.md` for the consumer-facing contract. ## Summary When a hook URL is configured, chatd dispatches `session_start`, `user_prompt_submit`, `pre_tool_use`, `post_tool_use`, `pre_compact`, `post_compact`, and `stop` events to the consumer and applies its responses. ## Design - **Stateless**: Coder stores no hook dispatch or decision state. Delivery is at least once; consumers deduplicate on stable payload identifiers (chat ID, event type, tool-use ID) and answer duplicates with the same decision. - **Admission-time prompt effects**: `user_prompt_submit` dispatches exactly once per submission (create, send, queue, edit, subagent spawn) and folds its effects into the stored prompt as typed message parts: original-or-overridden user parts, then model-only `hook-context`, then a user-visible `hook-notice`. Hook context is stripped from every client-facing conversion; hook notices are excluded from model prompts. The server rejects hook parts in client-submitted content. - **Tool gating**: `pre_tool_use` allow can override tool input; deny becomes a synthetic denied tool result, with any returned model context persisted as a model-only transcript row so it never reaches clients. The denial text identifies an external policy (the deployment's lifecycle hook) as the source and marks the decision as persistent, so the model explains the denial instead of retrying it or misreporting it as an infrastructure failure. - **Fail closed**: a dispatch failure rejects the triggering request or moves the chat to the error state in the same transaction as the affected step, so a runnable state is never published with unapproved content. - **Admission before persistence**: `pre_tool_use` is dispatched for the calls the model produced, before the assistant message is stored. See "Staged tool admission" below. - **Fresh dispatch per tool call**: every non-provider-executed tool call is decided by its own `pre_tool_use` dispatch; Coder never reuses an earlier decision on the consumer's behalf. Retries re-dispatch the same logical event. ## Structure All hook dispatch flows through one seam: entry points build a `chathooks.Chat` (chat identity) and a `chathooks.Message` (event details) and call `Trigger.Trigger`, the only component that talks to the dispatcher. The integration lives in the `coderd/x/chatd/chathooks` subpackage, split by responsibility: - `trigger.go`: the trigger seam; builds the wire envelope per event, normalizes deny into a typed error, and holds the package's single enabled-check. - `effects.go`: pure conversion of hook results into transcript rows and prompt parts. - `errors.go`: failure classification (dispatch error messages, denial mapping, tool-result dispatch-failure scanning). - `tooluse.go`: the tool-call gate (`pre_tool_use` preflight, `post_tool_use` payloads, applying admitted input to the step). Server-bound glue stays in `coderd/x/chatd/hook_server.go`: the chat-parking dispatch error handlers, the step-commit row insertion wrappers, and the dynamic post-tool-use state loader, which depends on chatd validation types. This PR adopts the `codersdk/x/agenthooks` and `coderd/x/agenthooks/dispatch` import paths introduced at the tip of #27401; intermediate commits still reference the pre-move paths and are not individually buildable. ## Staged tool admission `pre_tool_use` originally ran at tool execution time, which is after the assistant message carrying the tool call was already committed. An `input_override` therefore had to rewrite stored message content in place. @hugodutka pointed out that chatd treats message content as immutable, and that the rewrite was a shortcut rather than a requirement. It was also a correctness problem in its own right: the rewrite only updated the database, so the transcript could show one input while a different one had executed. The hook now runs before the step is persisted: ```text provider stream ends (tool calls complete, in memory) -> pre_tool_use dispatch per call -> ONE transaction: assistant row with admitted inputs, synthetic denials, hook rows -> execute ``` The step is inserted once, carrying the input the tool runs with. `UpdateChatMessageContentByID` and `Tx.UpdateMessageContent` are deleted from #27428, so message content stays immutable. Two consequences, both intentional: - **Clients converge rather than wait.** Tool-call parts still stream live, so a rewritten call briefly shows the model's proposed input before the committed message replaces it. The chat store already clears stream state when an assistant message arrives, so the stored input wins with no frontend change and no added latency before tool cards appear. - **A call already in history was already admitted.** Execution consumes the stored input instead of dispatching a second decision, which keeps one dispatch and one set of hook effects per call. A consumer policy change between admission and execution applies to later calls, not to calls already admitted. The per-chat debug endpoint still records the provider's original tool input. Its purpose is to report provider behavior, and it requires an explicit per-chat debug flag; the invariant here covers the transcript. ## Configuration Adds `chat-hook-url`, `chat-hook-secret`, `chat-hook-timeout`, and `chat-hook-enabled` deployment options with startup validation. The flags are hidden from `coder server --help` while the feature is experimental; the setup guide documents them. ## Tool input validation Built-in tool arguments reach a consumer as raw JSON with key spelling preserved, but the tools decode those bytes with Go, which matches struct fields case-insensitively and keeps the last match. A policy reading `path` could therefore authorize one value while the tool executed another, and a lone case variant such as `{"PATH":"/secret"}` was invisible to a policy checking for `path`. Coder now rejects a built-in tool call whose input repeats a key or spells a schema property with different capitalization, before the `pre_tool_use` dispatch, so a consumer is never asked to authorize bytes whose meaning depends on the reader. Rejected calls produce an error result the model can retry; unambiguous calls in the same batch still run. A consumer-authored `input_override` is rechecked after the dispatch and fails the turn closed, because the model cannot correct it. Dynamic and MCP inputs are excluded because the client and the workspace agent execute those calls rather than coderd. Two paths needed more than a schema check. Execution resolves a deprecated tool name to its canonical tool, so validation resolves aliases first. The `edit_files` decoder also reads `search` and `replace`, which its schema does not advertise, so those aliases are now matched exactly and their case variants ignored. A hook denial now returns a structured 403 carrying `kind: "hook_denied"`, mirroring the dispatch-failure response that already carries its own kind. Without it a client cannot tell a policy decision apart from a generic failure, and the chat UI titled a denial "Request failed". Adding a kind needs no migration: `ChatErrorKind` is persisted only inside the JSONB `chats.last_error` column, whose decoder accepts unknown kinds. The hook docs also correct the tool-input convergence window. A batch dispatches sequentially before the assistant row commits, so the original input stays visible for a span that scales with the number of tool calls in the step rather than a single hook timeout. > This PR was written by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
91c7232d97 |
feat: add chat suffix messages, idle failure, and content update support (#27428)
Adds generic chat state and query capabilities that the lifecycle hooks integration (#27429) builds on. Part of the lifecycle hooks stack (#27401, #27429, #27430). - `chatstate`: `EditMessage` accepts caller-provided suffix messages inserted after the replacement in the same transaction, transitions can carry a typed error kind, and `FinishError` is also allowed from waiting chats so admission-time failures can park an idle chat in error. - `chatstate`: `ValidateToolResults` holds the submitted-tool-result rules (duplicate, invalid JSON, missing, unexpected) in one place, so `CompleteRequiresAction` and API-level prechecks reject the same payloads with the same typed causes. - `database`: `InsertChat` accepts an optional caller-provided ID. No hook-specific state or behavior is introduced here; these primitives are usable by any caller. An earlier revision added a message-content rewrite primitive so a `pre_tool_use` override could update an already-committed tool call. Message content is immutable by design, and @hugodutka pushed back on changing that. The rewrite is gone: #27429 now dispatches the hook before the assistant message is stored, so the stored input is the one that runs and nothing needs updating. > This PR was written by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
5d2a69d85a |
fix(coderd/x/chatd/chaterror): extract plain-text provider error bodies (#27597)
Follows up #27538. Fixes an issue where `chaterror` would not classify a plain-text aibridge budget error 403 as ChatErrorKindUsageLimit. Root cause: Anthropic adapter drops the body from `ProviderError.Message`, and `providerErrorResponseMessage` extracted only JSON. - Parses the dumped response with `http.ReadResponse` (strips headers, de-chunks, leaves non-dump payloads like Google's raw messages whole). - When JSON extraction yields nothing, falls back to the trimmed first line of the body. - Falls back on `Content-Type: text/plain` only, and never on valid JSON as `Detail` is user-facing. - Adds an end-to-end regression test that Anthropic-shaped 403 budget error → `usage_limit`, not retryable (was `auth`). - Adds table-driven test cases: HTML skipped, whitespace skipped, first-line-only, JSON-without-message, chunked, Google raw message. --- > Generated by Coder Agents on behalf of @johnstcn. |