Compare commits

...

71 Commits

Author SHA1 Message Date
Jake Howell a4a4e80d2d chore: update colors names to tailwind variants 2025-12-01 01:30:15 +00:00
Jake Howell 95aeab3d1f fix: add rem unit to max-width 2025-12-01 00:38:42 +00:00
Jake Howell 6d66b2a8ec fix: update colors and theme in error.html 2025-11-26 04:17:12 +00:00
Jake Howell ca560d36ce fix: remove inflight interceptions from aibridge returned values (#20852)
Addresses [`aibridge#54`](https://github.com/coder/aibridge/issues/54)

When querying against the values in the database for
`/api/experimental/aibridge/interceptions` we found strange behaviour
wherein there was interceptions that lacked prompting and other various
fields we want. Generally this was as a result of the data not actually
existing for these values (as they were inflight).

The simple solution to this was to hide them if they didn't exist. This
PR addresses that.

---------

Co-authored-by: Danny Kopping <danny@coder.com>
2025-11-25 10:23:39 +11:00
Jaayden Halko 6c2900f138 refactor: use a global tooltip provider with a consistent 100 millisecond delay duration (#20869)
Part 1 of 2

- this sets up the TooltipProvider for Storybook in preview.tsx and for
the app in App.tsx along with removing TooltipProvider in some of the
usages of Tooltip
- I tested existing components that haven't had the TooltipProvider
removed and they still function correctly. So should be fine until the
2nd PR to complete the migration.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-24 18:22:54 -05:00
DevCats f08cb2f059 feat: add documentation check workflow for pull requests (#20907)
<!--

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.

-->

Making this PR to test the workflow

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-24 17:05:41 -06:00
leo-wr-ps 3847f3b297 feat(helm): add priorityClassName support (#20678)
**Add priorityClassName support to Coder Helm chart**

Add coder.priorityClassName configuration to the Helm chart that allows
setting the pod's priorityClassName in the deployment

**Usage:**

```
coder:
  priorityClassName: high-priority
```

See: https://github.com/coder/coder/discussions/20676

---------

Co-authored-by: Rowan Smith <rowan@coder.com>
2025-11-25 09:06:33 +11:00
dependabot[bot] 42a24b7334 chore: bump google.golang.org/grpc from 1.76.0 to 1.77.0 (#20892)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from
1.76.0 to 1.77.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/grpc/grpc-go/releases">google.golang.org/grpc's
releases</a>.</em></p>
<blockquote>
<h2>Release 1.77.0</h2>
<h1>API Changes</h1>
<ul>
<li>mem: Replace the <code>Reader</code> interface with a struct for
better performance and maintainability. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8669">#8669</a>)</li>
</ul>
<h1>Behavior Changes</h1>
<ul>
<li>balancer/pickfirst: Remove support for the old
<code>pick_first</code> LB policy via the environment variable
<code>GRPC_EXPERIMENTAL_ENABLE_NEW_PICK_FIRST=false</code>. The new
<code>pick_first</code> has been the default since <code>v1.71.0</code>.
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/8672">#8672</a>)</li>
</ul>
<h1>Bug Fixes</h1>
<ul>
<li>xdsclient: Fix a race condition in the ADS stream implementation
that could result in <code>resource-not-found</code> errors, causing the
gRPC client channel to move to <code>TransientFailure</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8605">#8605</a>)</li>
<li>client: Ignore HTTP status header for gRPC streams. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8548">#8548</a>)</li>
<li>client: Set a read deadline when closing a transport to prevent it
from blocking indefinitely on a broken connection. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8534">#8534</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/jgold2-stripe"><code>@​jgold2-stripe</code></a></li>
</ul>
</li>
<li>client: Fix a bug where default port 443 was not automatically added
to addresses without a specified port when sent to a proxy.
<ul>
<li>Setting environment variable
<code>GRPC_EXPERIMENTAL_ENABLE_DEFAULT_PORT_FOR_PROXY_TARGET=false</code>
disables this change; please file a bug if any problems are encountered
as we will remove this option soon. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8613">#8613</a>)</li>
</ul>
</li>
<li>balancer/pickfirst: Fix a bug where duplicate addresses were not
being ignored as intended. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8611">#8611</a>)</li>
<li>server: Fix a bug that caused overcounting of channelz metrics for
successful and failed streams. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8573">#8573</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/hugehoo"><code>@​hugehoo</code></a></li>
</ul>
</li>
<li>balancer/pickfirst: When configured, shuffle addresses in resolver
updates that lack endpoints. Since gRPC automatically adds endpoints to
resolver updates, this bug only affects custom LB policies that delegate
to <code>pick_first</code> but don't set endpoints. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8610">#8610</a>)</li>
<li>mem: Clear large buffers before re-using. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8670">#8670</a>)</li>
</ul>
<h1>Performance Improvements</h1>
<ul>
<li>transport: Reduce heap allocations to reduce time spent in garbage
collection. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8624">#8624</a>,
<a
href="https://redirect.github.com/grpc/grpc-go/issues/8630">#8630</a>,
<a
href="https://redirect.github.com/grpc/grpc-go/issues/8639">#8639</a>,
<a
href="https://redirect.github.com/grpc/grpc-go/issues/8668">#8668</a>)</li>
<li>transport: Avoid copies when reading and writing Data frames. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8657">#8657</a>,
<a
href="https://redirect.github.com/grpc/grpc-go/issues/8667">#8667</a>)</li>
<li>mem: Avoid clearing newly allocated buffers. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8670">#8670</a>)</li>
</ul>
<h1>New Features</h1>
<ul>
<li>outlierdetection: Add metrics specified in <a
href="https://github.com/grpc/proposal/blob/master/A91-outlier-detection-metrics.md">gRFC
A91</a>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8644">#8644</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/davinci26"><code>@​davinci26</code></a>, <a
href="https://github.com/PardhuKonakanchi"><code>@​PardhuKonakanchi</code></a></li>
</ul>
</li>
<li>stats/opentelemetry: Add support for optional label
<code>grpc.lb.backend_service</code> in per-call metrics (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8637">#8637</a>)</li>
<li>xds: Add support for JWT Call Credentials as specified in <a
href="https://github.com/grpc/proposal/blob/master/A97-xds-jwt-call-creds.md">gRFC
A97</a>. Set environment variable
<code>GRPC_EXPERIMENTAL_XDS_BOOTSTRAP_CALL_CREDS=true</code> to enable
this feature. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8536">#8536</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/dimpavloff"><code>@​dimpavloff</code></a></li>
</ul>
</li>
<li>experimental/stats: Add support for up/down counters. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8581">#8581</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/grpc/grpc-go/commit/805b1f88c5fb9419e3837c72e1deb9c2ec677ffe"><code>805b1f8</code></a>
Change version to 1.77.0 (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8677">#8677</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/ea7b66e1caa21b242b035bc4f598edb82093877f"><code>ea7b66e</code></a>
Cherrypick <a
href="https://redirect.github.com/grpc/grpc-go/issues/8702">#8702</a> to
v1.77.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8709">#8709</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/cadae08d5f37d60083091c103a89d5566b7ae34e"><code>cadae08</code></a>
Cherry-pick <a
href="https://redirect.github.com/grpc/grpc-go/issues/8536">#8536</a> to
v1.77.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8691">#8691</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/4288cfc5aba43fa11ad9b769f58b193b78f76a3b"><code>4288cfc</code></a>
Cherrypick <a
href="https://redirect.github.com/grpc/grpc-go/issues/8657">#8657</a>
and <a
href="https://redirect.github.com/grpc/grpc-go/issues/8667">#8667</a> to
v1.77.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8690">#8690</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/f959da611763ff733f7fb6b4b04c0f796d0fa441"><code>f959da6</code></a>
transport: Reduce heap allocations (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8668">#8668</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/0d49384b60894f29d2da20f7f72987aed4fbb229"><code>0d49384</code></a>
deps: update all dependencies (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8673">#8673</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/e3e142d0e32ff4e500ca140dc5eaed66adac9bfd"><code>e3e142d</code></a>
pickfirst: Remove old pickfirst (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8672">#8672</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/254ab1095e9f4179cebd36517bfb7e61b623e509"><code>254ab10</code></a>
documentation: fix typos in benchmark and auth docs (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8674">#8674</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/2d56bdadb11058f67c48e3c837fcf4a487e15346"><code>2d56bda</code></a>
mem: Remove Reader interface and export the concrete struct (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8669">#8669</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/8ab0c8214a28222821a1a761996b76f9bfa6aca7"><code>8ab0c82</code></a>
mem: Avoid clearing new buffers and clear buffers from simpleBufferPools
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/8670">#8670</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/grpc/grpc-go/compare/v1.76.0...v1.77.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google.golang.org/grpc&package-manager=go_modules&previous-version=1.76.0&new-version=1.77.0)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-24 18:21:45 +00:00
dependabot[bot] ba242b5e77 chore: bump github.com/prometheus/common from 0.65.0 to 0.67.4 (#20890)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps
[github.com/prometheus/common](https://github.com/prometheus/common)
from 0.65.0 to 0.67.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/prometheus/common/releases">github.com/prometheus/common's
releases</a>.</em></p>
<blockquote>
<h2>v0.67.4 / 2025-11-18</h2>
<h2>What's Changed</h2>
<ul>
<li>chore: clean up golangci-lint configuration by <a
href="https://github.com/mmorel-35"><code>@​mmorel-35</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/782">prometheus/common#782</a></li>
<li>chore: 'omitempty' to Oauth2 fields with type Secret to avoid
requiring them by <a
href="https://github.com/JorTurFer"><code>@​JorTurFer</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/864">prometheus/common#864</a></li>
<li>chore: Add omitempty tag to all config fields by <a
href="https://github.com/JorTurFer"><code>@​JorTurFer</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/865">prometheus/common#865</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/prometheus/common/compare/v0.67.3...v0.67.4">https://github.com/prometheus/common/compare/v0.67.3...v0.67.4</a></p>
<h2>v0.67.3 / 2025-11-18</h2>
<h2>What's Changed</h2>
<ul>
<li>Support JWT Profile for Authorization Grant (RFC 7523 3.1) by <a
href="https://github.com/JorTurFer"><code>@​JorTurFer</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/862">prometheus/common#862</a></li>
<li>Config: remove outdated comment about HTTP/2 issues by <a
href="https://github.com/bboreham"><code>@​bboreham</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/863">prometheus/common#863</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/JorTurFer"><code>@​JorTurFer</code></a>
made their first contribution in <a
href="https://redirect.github.com/prometheus/common/pull/862">prometheus/common#862</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/prometheus/common/compare/v0.67.2...v0.67.3">https://github.com/prometheus/common/compare/v0.67.2...v0.67.3</a></p>
<h2>v0.67.2 / 2025-10-28</h2>
<h2>What's Changed</h2>
<ul>
<li>config: Fix panic in <code>tlsRoundTripper</code> when CA file is
absent by <a href="https://github.com/ndk"><code>@​ndk</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/792">prometheus/common#792</a></li>
<li>Cleanup linting issues by <a
href="https://github.com/SuperQ"><code>@​SuperQ</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/860">prometheus/common#860</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/ndk"><code>@​ndk</code></a> made their
first contribution in <a
href="https://redirect.github.com/prometheus/common/pull/792">prometheus/common#792</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/prometheus/common/compare/v0.67.1...v0.67.2">https://github.com/prometheus/common/compare/v0.67.1...v0.67.2</a></p>
<h2>v0.67.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix Go case-insensitive file name collision by <a
href="https://github.com/SuperQ"><code>@​SuperQ</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/853">prometheus/common#853</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/prometheus/common/compare/v0.67.0...v0.67.1">https://github.com/prometheus/common/compare/v0.67.0...v0.67.1</a></p>
<h2>v0.67.0 / 2025-10-07</h2>
<h2>What's Changed</h2>
<ul>
<li>Create CHANGELOG.md for easier communication of library changes,
especially possible breaking changes. by <a
href="https://github.com/ywwg"><code>@​ywwg</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/833">prometheus/common#833</a></li>
<li>model: New test for validation with dots by <a
href="https://github.com/m1k1o"><code>@​m1k1o</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/759">prometheus/common#759</a></li>
<li>expfmt: document NewTextParser as required by <a
href="https://github.com/burgerdev"><code>@​burgerdev</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/842">prometheus/common#842</a></li>
<li>expfmt: Add support for float histograms and gauge histograms by <a
href="https://github.com/beorn7"><code>@​beorn7</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/843">prometheus/common#843</a></li>
<li>Updated minimum Go version to 1.24.0, updated Go dependecies by <a
href="https://github.com/SuperQ"><code>@​SuperQ</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/849">prometheus/common#849</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/m1k1o"><code>@​m1k1o</code></a> made
their first contribution in <a
href="https://redirect.github.com/prometheus/common/pull/759">prometheus/common#759</a></li>
<li><a href="https://github.com/burgerdev"><code>@​burgerdev</code></a>
made their first contribution in <a
href="https://redirect.github.com/prometheus/common/pull/842">prometheus/common#842</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/prometheus/common/compare/v0.66.1...v0.67.0">https://github.com/prometheus/common/compare/v0.66.1...v0.67.0</a></p>
<h2>v0.66.1</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/prometheus/common/blob/main/CHANGELOG.md">github.com/prometheus/common's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>main / unreleased</h2>
<h3>What's Changed</h3>
<h2>v0.67.2 / 2025-10-28</h2>
<h2>What's Changed</h2>
<ul>
<li>config: Fix panic in <code>tlsRoundTripper</code> when CA file is
absent by <a href="https://github.com/ndk"><code>@​ndk</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/792">prometheus/common#792</a></li>
<li>Cleanup linting issues by <a
href="https://github.com/SuperQ"><code>@​SuperQ</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/860">prometheus/common#860</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/ndk"><code>@​ndk</code></a> made their
first contribution in <a
href="https://redirect.github.com/prometheus/common/pull/792">prometheus/common#792</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/prometheus/common/compare/v0.67.1...v0.67.2">https://github.com/prometheus/common/compare/v0.67.1...v0.67.2</a></p>
<h2>v0.67.1 / 2025-10-07</h2>
<h2>What's Changed</h2>
<ul>
<li>Remove VERSION file to avoid Go conflict error in <a
href="https://redirect.github.com/prometheus/common/pull/853">prometheus/common#853</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/prometheus/common/compare/v0.67.0...v0.67.1">https://github.com/prometheus/common/compare/v0.67.0...v0.67.1</a></p>
<h2>v0.67.0 / 2025-10-07</h2>
<h2>What's Changed</h2>
<ul>
<li>Create CHANGELOG.md for easier communication of library changes,
especially possible breaking changes. by <a
href="https://github.com/ywwg"><code>@​ywwg</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/833">prometheus/common#833</a></li>
<li>model: New test for validation with dots by <a
href="https://github.com/m1k1o"><code>@​m1k1o</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/759">prometheus/common#759</a></li>
<li>expfmt: document NewTextParser as required by <a
href="https://github.com/burgerdev"><code>@​burgerdev</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/842">prometheus/common#842</a></li>
<li>expfmt: Add support for float histograms and gauge histograms by <a
href="https://github.com/beorn7"><code>@​beorn7</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/843">prometheus/common#843</a></li>
<li>Updated minimum Go version to 1.24.0, updated Go dependecies by <a
href="https://github.com/SuperQ"><code>@​SuperQ</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/849">prometheus/common#849</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/m1k1o"><code>@​m1k1o</code></a> made
their first contribution in <a
href="https://redirect.github.com/prometheus/common/pull/759">prometheus/common#759</a></li>
<li><a href="https://github.com/burgerdev"><code>@​burgerdev</code></a>
made their first contribution in <a
href="https://redirect.github.com/prometheus/common/pull/842">prometheus/common#842</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/prometheus/common/compare/v0.66.1...v0.67.0">https://github.com/prometheus/common/compare/v0.66.1...v0.67.0</a></p>
<h2>v0.66.1 / 2025-09-05</h2>
<p>This release has no functional changes, it just drops the
dependencies <code>github.com/grafana/regexp</code> and
<code>go.uber.org/atomic</code> and replaces
<code>gopkg.in/yaml.v2</code> with <code>go.yaml.in/yaml/v2</code> (a
drop-in replacement).</p>
<h3>What's Changed</h3>
<ul>
<li>Revert &quot;Use github.com/grafana/regexp instead of regexp&quot;
by <a href="https://github.com/aknuds1"><code>@​aknuds1</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/835">prometheus/common#835</a></li>
<li>Move to supported version of yaml parser by <a
href="https://github.com/dims"><code>@​dims</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/834">prometheus/common#834</a></li>
<li>Revert &quot;Use go.uber.org/atomic instead of sync/atomic (<a
href="https://redirect.github.com/prometheus/common/issues/825">#825</a>)&quot;
by <a href="https://github.com/aknuds1"><code>@​aknuds1</code></a> in <a
href="https://redirect.github.com/prometheus/common/pull/838">prometheus/common#838</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/prometheus/common/compare/v1.20.99...v0.66.1">https://github.com/prometheus/common/compare/v1.20.99...v0.66.1</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/prometheus/common/commit/d80d8544703e59a080a204b6f7429ac6561fb24f"><code>d80d854</code></a>
chore: Add omitempty tag to all config fields (<a
href="https://redirect.github.com/prometheus/common/issues/865">#865</a>)</li>
<li><a
href="https://github.com/prometheus/common/commit/04686b2cfc6804598d99b86070135f9266998c59"><code>04686b2</code></a>
chore: 'omitempty' to Oauth2 fields with type Secret to avoid requiring
them ...</li>
<li><a
href="https://github.com/prometheus/common/commit/0b2fbf31f0e2c21d9e1a4e51e698188fae258cb2"><code>0b2fbf3</code></a>
chore: clean up golangci-lint configuration (<a
href="https://redirect.github.com/prometheus/common/issues/782">#782</a>)</li>
<li><a
href="https://github.com/prometheus/common/commit/b2cdb0785c1498399587cb0bf42aa960d810633a"><code>b2cdb07</code></a>
Merge pull request <a
href="https://redirect.github.com/prometheus/common/issues/863">#863</a>
from prometheus/remove-http2-comment</li>
<li><a
href="https://github.com/prometheus/common/commit/cd1ab56cc1e1d41dbc286d2e501e26515400b9be"><code>cd1ab56</code></a>
Config: remove outdated comment about HTTP/2 issues</li>
<li><a
href="https://github.com/prometheus/common/commit/f4c0aea59fa97a7627730e65cb2e625ec9fc45cf"><code>f4c0aea</code></a>
Support JWT Profile for Authorization Grant (RFC 7523 3.1) (<a
href="https://redirect.github.com/prometheus/common/issues/862">#862</a>)</li>
<li><a
href="https://github.com/prometheus/common/commit/594f4d4a984eb5f1ca8f0983f8b1790e77a5a725"><code>594f4d4</code></a>
Merge pull request <a
href="https://redirect.github.com/prometheus/common/issues/861">#861</a>
from prometheus/beorn7/version</li>
<li><a
href="https://github.com/prometheus/common/commit/440c1a30a0315f2ca0dba99fd7fffb288a3e898b"><code>440c1a3</code></a>
Cut v0.67.2</li>
<li><a
href="https://github.com/prometheus/common/commit/acb18736bed74c218ee4023ed1e0e36cf2dd1612"><code>acb1873</code></a>
Merge pull request <a
href="https://redirect.github.com/prometheus/common/issues/860">#860</a>
from prometheus/superq/linting</li>
<li><a
href="https://github.com/prometheus/common/commit/1e323394d0ceaccda49f263dc81456e33af4263b"><code>1e32339</code></a>
Cleanup linting issues</li>
<li>Additional commits viewable in <a
href="https://github.com/prometheus/common/compare/v0.65.0...v0.67.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/prometheus/common&package-manager=go_modules&previous-version=0.65.0&new-version=0.67.4)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-24 17:10:01 +00:00
Steven Masley 0c43789f3a test: move TestConvertStateGolden to only linux + mac (#20901)
Windows runners are flaky for golden files
closes https://github.com/coder/internal/issues/1141
2025-11-24 10:25:08 -06:00
Steven Masley cefe07d074 feat: purge expired api keys in dbpurge (#20863)
closes https://github.com/coder/coder/issues/19889

This is in response to a migration in v2.27 that takes very long on deployments with large `api_key` tables.
2025-11-24 10:24:32 -06:00
Danny Kopping c6631e1e50 feat: expose aibridged metrics (#20865)
Upgrades `coder/aibridge` to v0.2.0 which includes
https://github.com/coder/aibridge/pull/62.

Creates a `prometheus.Registerer` with a prefix `coder_aibridged_` and
passes that along to coder/aibridge which actually exposes the metrics.

Also includes a side-effect of a change described in
https://github.com/coder/aibridge/pull/62#discussion_r2550017470.

---------

Signed-off-by: Danny Kopping <danny@coder.com>
2025-11-24 18:16:06 +02:00
Atif Ali 6882c43b39 fix: fix API docs manifest generation (#20897) 2025-11-24 18:57:01 +05:00
Kacper Sawicki 6d41bfad81 fix: improve http connection pooling for smtp notifications (#20605)
This change updates how SMTP notifications are polled during scale
tests.

Before, each of the ~2,000 pollers created its own http.Client, which
opened thousands of short-lived TCP connections.
Under heavy load, this ran out of available network ports and caused
errors like `connect: cannot assign requested address`

Now, all pollers share one HTTP connection pool. This prevents port
exhaustion and makes polling faster and more stable.
If a network error happens, the poller will now retry instead of
stopping, so tests keep running until all notifications are received.

The `SMTPRequestTimeout` is now applied per request using a context,
instead of being set on the `http.Client`.
2025-11-24 14:25:18 +01:00
dependabot[bot] bc4838dc88 chore: bump rust from cef0ec9 to 5218a2b in /dogfood/coder (#20895)
Bumps rust from `cef0ec9` to `5218a2b`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rust&package-manager=docker&previous-version=slim&new-version=slim)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-24 13:20:06 +00:00
Atif Ali 636408906f chore(docs): standardize "AIBridge" to "AI Bridge" in documentation (#20831) 2025-11-24 18:09:04 +05:00
Danielle Maywood 8c83ab90cf chore: update github.com/coder/clistat to v1.1.2 (#20894)
Updates github.com/coder/clistat from v1.1.1 to v1.1.2. This release
brings a bug fix for handling more instances where a child cgroup lacks
information, requiring walking up the parent tree.
2025-11-24 13:04:36 +00:00
dependabot[bot] cf11996640 chore: bump github.com/aws/aws-sdk-go-v2/config from 1.31.3 to 1.32.1 (#20889)
Bumps
[github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2)
from 1.31.3 to 1.32.1.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/b737dc9eb14847cd97d3b30ad6a1394efd73245b"><code>b737dc9</code></a>
Release 2024-10-07</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/7279a51bbcd597f4aa7aeeb599c017d3d1679fb6"><code>7279a51</code></a>
Regenerated Clients</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/a1b1f5a17c687371cc53c5dfbb2bf5ff467ff51a"><code>a1b1f5a</code></a>
Update endpoints model</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/4853c41dcd28acb1caca55161aa45015e3765ab7"><code>4853c41</code></a>
Update API model</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/99e2be851c0fc7190099e1fe49e8d3b3c4fe2950"><code>99e2be8</code></a>
Allow empty values on prefix headers (<a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/2816">#2816</a>)</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/18e6b6e66ff440bf1c8b492e6c0bb41d68f7bd83"><code>18e6b6e</code></a>
remove autoscaling smoke tests (<a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/2817">#2817</a>)</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/8200000a3a2d9806617b4b14a800751f4f28773a"><code>8200000</code></a>
remove private metrics collection APIs (<a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/2818">#2818</a>)</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/7a76a2ae73fe6ae04c8dba07570145eba0582555"><code>7a76a2a</code></a>
Release 2024-10-04</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/e35b8bedbb56d7b39d8ccc60cc120a7b61d5fec5"><code>e35b8be</code></a>
Regenerated Clients</li>
<li><a
href="https://github.com/aws/aws-sdk-go-v2/commit/6e9587148dadaebdfeda731a68bb30740aedfcdd"><code>6e95871</code></a>
Update endpoints model</li>
<li>Additional commits viewable in <a
href="https://github.com/aws/aws-sdk-go-v2/compare/config/v1.31.3...v1.32.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/aws/aws-sdk-go-v2/config&package-manager=go_modules&previous-version=1.31.3&new-version=1.32.1)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-24 12:58:31 +00:00
dependabot[bot] 19d11f100b chore: bump github.com/coreos/go-oidc/v3 from 3.16.0 to 3.17.0 (#20888)
Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc)
from 3.16.0 to 3.17.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/coreos/go-oidc/releases">github.com/coreos/go-oidc/v3's
releases</a>.</em></p>
<blockquote>
<h2>v3.17.0</h2>
<h2>What's Changed</h2>
<ul>
<li>oidc: improve error message for mismatched issuer URLs by <a
href="https://github.com/ericchiang"><code>@​ericchiang</code></a> in <a
href="https://redirect.github.com/coreos/go-oidc/pull/469">coreos/go-oidc#469</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/coreos/go-oidc/compare/v3.16.0...v3.17.0">https://github.com/coreos/go-oidc/compare/v3.16.0...v3.17.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/coreos/go-oidc/commit/35b8e031bcac7fed73b96b09d42e6e233a6e6562"><code>35b8e03</code></a>
oidc: improve error message for mismatched issuer URLs</li>
<li>See full diff in <a
href="https://github.com/coreos/go-oidc/compare/v3.16.0...v3.17.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/coreos/go-oidc/v3&package-manager=go_modules&previous-version=3.16.0&new-version=3.17.0)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-24 12:57:52 +00:00
dependabot[bot] 09393f2746 chore(examples/templates/tasks-docker): bump coder/claude-code/coder from 4.0.0 to 4.2.1 (#20882)
[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coder/claude-code/coder&package-manager=terraform&previous-version=4.0.0&new-version=4.2.1)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-24 12:49:37 +00:00
Jakub Domeracki 754ffb243e chore: update monaco-editor to resolve DOMPurify CVEs (#20861)
Closes https://github.com/microsoft/monaco-editor/issues/5078
2025-11-24 13:14:11 +01:00
Danny Kopping 443b0c851d chore: upgrade coder/serpent to allow more readable durations (#20886)
https://github.com/coder/serpent/pull/28 added this capability.

https://github.com/coder/serpent/compare/v0.11.0...v0.12.0

---------

Signed-off-by: Danny Kopping <danny@coder.com>
2025-11-24 09:24:06 +00:00
Rowan Smith a6581c7157 chore: update OIDC scopes to include offline_access (#20876)
This is an update to
https://coder.com/docs/admin/users/oidc-auth/microsoft#enable-refresh-tokens-recommended.
We recommend users enable refresh tokens but don't actually give them
the env var value to add.

https://coder.com/docs/admin/users/oidc-auth/refresh-tokens does a good
job of including `offline_access` in the list, so the first page should
align with this.
2025-11-24 07:04:21 +05:00
dependabot[bot] 855fb8704c chore: bump coder/code-server/coder from 1.3.1 to 1.4.0 in /dogfood/coder-envbuilder (#20881)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)



[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coder/code-server/coder&package-manager=terraform&previous-version=1.3.1&new-version=1.4.0)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-24 00:44:10 +00:00
dependabot[bot] d0e4432fca chore: bump coder/code-server/coder from 1.3.1 to 1.4.0 in /dogfood/coder (#20879)
[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coder/code-server/coder&package-manager=terraform&previous-version=1.3.1&new-version=1.4.0)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-24 00:44:03 +00:00
dependabot[bot] d8e30c0982 chore: bump coder/claude-code/coder from 4.1.0 to 4.2.1 in /dogfood/coder (#20880)
[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coder/claude-code/coder&package-manager=terraform&previous-version=4.1.0&new-version=4.2.1)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-24 00:43:58 +00:00
dependabot[bot] ee07687cae chore: bump coder/jetbrains/coder from 1.1.1 to 1.2.0 in /dogfood/coder (#20877)
[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coder/jetbrains/coder&package-manager=terraform&previous-version=1.1.1&new-version=1.2.0)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-24 00:43:49 +00:00
dependabot[bot] 28a3e4c2c5 chore: bump coder/mux/coder from 1.0.0 to 1.0.1 in /dogfood/coder (#20878)
[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coder/mux/coder&package-manager=terraform&previous-version=1.0.0&new-version=1.0.1)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-24 00:43:45 +00:00
Zach b4cc982cc2 fix: ensure embedded-postgres state is wiped between retries (#20809)
Retries were previously added when starting embedded postgres to
mitigate port allocation conflicts (we can't use an ephemeral port for
tests). Retries alone seemingly did not fix the test flakes. A new
failure mode appeared on the retries: timing out connecting to the
database.

When a port discovery error occurrs, embedded-postgres does not create
the database. If the data directory exists on the next attempt,
embedded-postgres will assume the database has already been created.
This seems to cause the timeout error. Wipe all state between retries to
ensure attempts execute the same logic that creates the database.

[#658](https://github.com/coder/internal/issues/658)
2025-11-21 08:55:01 -07:00
Steven Masley a61b8bc5ce test: add golden file test for ConvertState (#20832)
Refactoring `ConvertState` is something we should eventually do. This PR
adds a golden file unit test for the output of `ConvertState` (even
errors).

That way if a refactor occurs, we can verify the output is unchanged for
our test cases.
2025-11-21 07:27:20 -06:00
Mathias Fredriksson e0a32e04e8 fix(site): do not render invalid task status URI, fix GitHub new links (#20858)
Fixes #20429
2025-11-21 14:24:50 +02:00
Susana Ferreira 2a9afc77de feat: associate task icon with workspaces (#20834)
## Problem

Workspaces associated with tasks were not visually distinguishable in
the workspaces list view. Additionally, the list workspaces endpoint was
not returning the `task_id` field.

<img width="2784" height="864" alt="Screenshot 2025-11-20 at 10 32 22"
src="https://github.com/user-attachments/assets/60704f16-3c66-4553-9215-f10654998a38"
/>

## Changes

- Fix `ConvertWorkspaceRows` to include `task_id` in the list workspaces
endpoint response
- Add "Task" icon to the workspace list view for workspaces associated
with tasks
- Add test to verify `task_id` is correctly returned by the list
workspaces endpoint
- Add Storybook story to showcase the Task icon in the workspace list

Closes https://github.com/coder/coder/issues/20802
2025-11-21 11:47:10 +00:00
Sas Swart 2840fdcb54 feat(agent): add agent socket API (#20717)
relates to: https://github.com/coder/internal/issues/1094

This is number 2 of 5 pull requests in an effort to add agent script
ordering. It adds a drpc API that is exposed via a local socket. This
API serves access to a lightweight DAG based dependency manager that was
inspired by systemd.

In follow-up PRs:

* This unit manager will be plumbed into the workspace agent struct.
* CLI commands will use this agentsocket api to express dependencies
between coder scripts

I used an LLM to produce some of these changes, but I have conducted
thorough self review and consider this contribution to be ready for an
external reviewer.
2025-11-21 13:09:27 +02:00
Danny Kopping 5a7d4f69f6 feat: add configurable retention for aibridge (#20828)
Closes https://github.com/coder/internal/issues/1134

---------

Signed-off-by: Danny Kopping <danny@coder.com>
2025-11-21 11:35:36 +02:00
Danielle Maywood 83966e346a fix(site): hide empty tasks list when templates are empty (#20845)
When there are no task templates, only the empty templates
prompt is displayed, and the tasks section (including controls and
table) is hidden.
2025-11-21 09:29:11 +00:00
Atif Ali 3fe29ecf89 docs: fix ANTHROPIC_BASE_URL example in AI Bridge client docs (#20853) 2025-11-21 10:38:06 +02:00
Jaayden Halko ddcc841bdc fix: set a default for presets to match the app default (#20848) 2025-11-20 10:55:07 -08:00
Marcin Tojek d004710a74 feat: add prebuild invalidation via last_invalidated_at timestamp (#20582)
Updates #17917
2025-11-20 17:12:25 +01:00
Jaayden Halko c2319e5b4e fix: prevent tooltip appearing on dropdown open (#20765)
The tooltip inside CopyButton inside Userdropdown is appearing
automatically when the dropdown is opened. This feels a bit janky and
the goal of this fix is to only show the tooltip content when the user
hovers the copy button.



https://github.com/user-attachments/assets/2e41da8d-08c5-476b-b0fc-a40d4f8e3d6c
2025-11-20 08:38:07 -05:00
Phorcys 0cd33d1abb chore(docs/admin/users): fix typo in headless auth page (#20841) 2025-11-20 11:25:54 +01:00
Phorcys 426cc98f7c fix(site/src/modules/apps): distinguish JB Toolbox from Gateway (#20830)
Edits the "To use <APP>, you need to have Jetbrains Toolbox installed"
error message to vary based on JetBrains Toolbox vs. Gateway.

---------

Co-authored-by: Jake Howell <jake@hwll.me>
2025-11-20 09:54:42 +00:00
Spike Curtis 007f2df079 fix: use API, not request context to insert audit/connection logs (#20829)
Fixes: #20744

Upsert audit and connection log entries with a context derived from the API context, rather than the individual request so that we don't error out if the request is canceled or the client hangs up (e.g. if we return an error).
2025-11-20 13:01:50 +04:00
blinkagent[bot] 48b8e22502 fix: add Windows stub for CacheTFProviders (#20840)
Fixes https://github.com/coder/internal/issues/1119

## Description

The `CacheTFProviders` function in `testutil/terraform_cache.go` was
only available on Linux and macOS due to the `//go:build linux ||
darwin` build tag. This caused a compile error on Windows when
`enterprise/coderd/workspaces_test.go` tried to call it:

```
enterprise\coderd\workspaces_test.go:3403:28: undefined: testutil.CacheTFProviders
```

## Changes

1. Added `testutil/terraform_cache_windows.go` with a Windows-specific
stub implementation that returns an empty string
2. Updated `downloadProviders` helper in
`enterprise/coderd/workspaces_test.go` to handle empty paths gracefully

## Behavior

- On Linux/macOS: Terraform providers are cached as before
- On Windows: Provider caching is skipped, tests download providers
normally during `terraform init`

## Testing

This should fix the Windows nightly gauntlet failure. The test will
still run on Windows, just without provider caching optimization.

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2025-11-20 07:52:07 +00:00
dependabot[bot] 18bef5ea2f chore: bump golang.org/x/crypto from 0.44.0 to 0.45.0 (#20838)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from
0.44.0 to 0.45.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/crypto/commit/4e0068c0098be10d7025c99ab7c50ce454c1f0f9"><code>4e0068c</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/crypto/commit/e79546e28b85ea53dd37afe1c4102746ef553b9c"><code>e79546e</code></a>
ssh: curb GSSAPI DoS risk by limiting number of specified OIDs</li>
<li><a
href="https://github.com/golang/crypto/commit/f91f7a7c31bf90b39c1de895ad116a2bacc88748"><code>f91f7a7</code></a>
ssh/agent: prevent panic on malformed constraint</li>
<li><a
href="https://github.com/golang/crypto/commit/2df4153a0311bdfea44376e0eb6ef2faefb0275b"><code>2df4153</code></a>
acme/autocert: let automatic renewal work with short lifetime certs</li>
<li><a
href="https://github.com/golang/crypto/commit/bcf6a849efcf4702fa5172cb0998b46c3da1e989"><code>bcf6a84</code></a>
acme: pass context to request</li>
<li><a
href="https://github.com/golang/crypto/commit/b4f2b62076abeee4e43fb59544dac565715fbf1e"><code>b4f2b62</code></a>
ssh: fix error message on unsupported cipher</li>
<li><a
href="https://github.com/golang/crypto/commit/79ec3a51fcc7fbd2691d56155d578225ccc542e2"><code>79ec3a5</code></a>
ssh: allow to bind to a hostname in remote forwarding</li>
<li>See full diff in <a
href="https://github.com/golang/crypto/compare/v0.44.0...v0.45.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/crypto&package-manager=go_modules&previous-version=0.44.0&new-version=0.45.0)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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)
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/coder/coder/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-20 00:41:43 +00:00
Asher b4cb490c72 chore: enable debug logs over playwright (#20784) 2025-11-19 15:27:40 -09:00
DevCats 7615c2792b chore: add positron icon (#20780)
Co-authored-by: ケイラ <mckayla@hey.com>
2025-11-19 16:19:54 -06:00
Susana Ferreira 753e125758 chore(site): add storybook stories for task initialization states (#20760)
Adds a Storybook story to visualize the task initialization states
(workspace pending/starting, agent connecting/starting) that were
recently added.

<img width="2310" height="1600" alt="Screenshot 2025-11-17 at 18 58 55"
src="https://github.com/user-attachments/assets/16471dcf-2f7d-41d2-beba-2cf0c84c9bf0"
/>

Follow-up from PR: https://github.com/coder/coder/pull/20692
2025-11-19 18:30:14 +00:00
Susana Ferreira 17edeeaf04 fix(site): fix flaky Chromatic tests (#20808)
## Problem

The `OrgsSortedAlphabetically` test from
`OrganizationSidebarView.stories.tsx` was failing on Chromatic. Test
logic was attempting to verify organization sorting order
programmatically. This was identified in the Chromatic build of PR:
https://www.chromatic.com/build?appId=624de63c6aacee003aa84340&number=26015

After fixing this test, two additional tests started failing:
* `VanillaJavascriptError ` from `GlobalErrorBoundary.stories.tsx`: Test
was making incorrect assertions about stack trace content
* `MarkAllNotificationsAsReadError` from
`NotificationsInbox.stories.tsx`: Test was flaky due to competing
WebSocket error messages

## Solution

These are Chromatic snapshot tests, so implementation details (like
sorting order or exact error message content) are already validated by
the visual snapshots. Programmatic assertions were causing flakiness and
are redundant.
2025-11-19 18:17:12 +00:00
Sas Swart 500c17e257 feat(agent): add agent unit manager (#20715)
relates to: https://github.com/coder/internal/issues/1094

This is number 1 of 5 pull requests in an effort to add agent script
ordering. It adds a unit manager, which uses an underlying DAG and a
list of subscribers to inform units when their dependencies have changed
in status.

In follow-up PRs:
* This unit manager will be plumbed into the workspace agent struct. 
* It will then be exposed to users via a new socket based drpc API 
* The agentsocket API will then become accessible via CLI commands that
allow coder scripts to express their dependencies on one another.

This is an experimental feature. There may be ways to improve the
efficiency of the manager struct, but it is more important to validate
this feature with customers before we invest in such optimizations.

See the tests for examples of how units may communicate with one
another. Actual CLI usage will be analogous.

I used an LLM to produce some of these changes, but I have conducted
thorough self review and consider this contribution to be ready for an
external reviewer.
2025-11-19 19:03:37 +02:00
Cian Johnston 35b9df86b3 chore(docs): document preset description and icon fields (#20705)
Closes https://github.com/coder/coder/issues/20599

Generated by Claude Code, reviewed by me.
2025-11-19 10:02:43 -06:00
Steven Masley aff208048e feat: fix build timeline to include entire stage timings (#20805)
Measure entire stage durations for each terraform cmd execution
2025-11-19 09:35:33 -06:00
Steven Masley a10c5ff381 chore: protect build timings insert for invalid enums (#20821)
Database insert errors will fail the transaction. So this error is
fatal. Properly return it for a better error call stack, and not just
hiding the error in the logs.
2025-11-19 09:34:19 -06:00
Mathias Fredriksson f6556fce9f test(coderd/workspaceapps/apptest): fix lastusedat assertion for all test (#20827)
The test flake can be verified by setting `ReportInterval` to a really
low value, like `100 * time.Millisecond`.

We now set it to a really high value to avoid triggering flush without 
manually calling the function in test. This can easily happen because 
the default value is 30s and we run tests in parallel. The assertion
typically happens such that:

	[use workspace] -> [fetch previous last used] -> [flush]
	-> [fetch new last used]

When this edge case is triggered:

	[use workspace] -> [report interval flush]
	-> [fetch previous last used] -> [flush] -> [fetch new last used]

In this case, both the previous and new last used will be the same,
breaking the test assertion.

Fixes coder/internal#960
Fixes coder/internal#975
2025-11-19 15:10:59 +02:00
Danielle Maywood 8e22cd707a feat(site): add startup script error alerts to Task Page (#20820)
Refactors Task page UI to show startup script errors as compact warning
buttons in the topbar.

Closes https://github.com/coder/coder/issues/20418
2025-11-19 11:44:22 +00:00
Spike Curtis 8ee6e9457e fix: wait for build in task status load generator (#20800)
Wait for the External workspace build job to complete before attempting to pull its credentials from Coder. This resolves a race in the load generator.
2025-11-19 10:35:31 +04:00
Spike Curtis 0bbb7dd0a3 feat: add cleanup to task-status load test runner (#20799)
Implement Cleanup in the task status Runner, to delete the external workspaces created.
2025-11-19 10:24:30 +04:00
Spike Curtis 5ea1353d46 feat: add exp scaletest task-status command (#20761)
Adds `coder exp scaletest task-status` subcommand to generate task status update load on the Coder server.
2025-11-19 10:13:32 +04:00
Jake Howell 52f8143ad3 fix: rename AI Governance to AI Bridge (#20790)
This pull-request simply renames our `AI Governance` feature to `AI
Bridge` whilst we evaluate the future of how we want to render the
governance of AI related features.
2025-11-19 16:47:06 +11:00
AlexanderSarson 085370ec6d chore: add "positron:" to allowed external app protocols (#20803)
<!--

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.

-->
2025-11-19 14:11:56 +11:00
Ethan c12bba40ad fix(enterprise/cli): preserve actual error when getting provisioner key details (#20813)
Fixes #20781

Previously, when `GetProvisionerKey()` failed, the actual error was
swallowed:
```
error: unable to get provisioner key details
```

Now the actual error is preserved using error wrapping, so users can see
the real cause (e.g., 404 Not Found, connection refused, invalid key,
etc.):
```
error: unable to get provisioner key details: GET https://...: 404 Not Found
```

This makes it much easier to diagnose configuration issues.

---
*Generated by [mux](https://cmux.io)*
2025-11-19 02:09:16 +11:00
Paweł Banaszewski 158243d146 fix: add cache for terraform installer files (#20776)
Replaces not working mocks by simple proxy that caches terraform files using test
cache
https://github.com/coder/coder/blob/16b8e6072fd84f45404e3f84bb2b6fea2424b090/testutil/cache.go#L13
Fixes: https://github.com/coder/internal/issues/1126
2025-11-18 09:52:44 +01:00
Yevhenii Shcherbina eb644732d7 feat: enable boundary on dogfood (#20766)
Enable boundary on dogfood.
Allowed domains are specified in config.yaml file.
2025-11-17 16:59:24 -05:00
Yevhenii Shcherbina a83328c1f0 docs: improve boundary docs (#20806)
Update docs for boundary v0.2.0 release.
2025-11-17 15:41:59 -05:00
Steven Masley a2728439ff docs: add API key scopes documentation (#20742)
## Description

Adds a brief section to the API & Session Tokens documentation
explaining API key scopes.

## Changes

- Added "API Key Scopes" section to
`docs/admin/users/sessions-tokens.md`
- Includes overview of scope functionality and security benefits
- Documents scope format (`resource:action`) and wildcard usage
- Provides CLI examples for creating scoped tokens
- Lists common scope examples with descriptions

## Motivation

Users need documentation on how to create and use scoped API tokens for
improved security by limiting token permissions to only necessary
operations.

## Testing

- Reviewed documentation formatting
- Verified markdown structure
- Confirmed examples are accurate
2025-11-17 11:34:01 -06:00
Susana Ferreira 16b8e6072f fix: set codersdk.Task current_state during task initialization (#20692)
## Problem

With the new tasks data model, a task starts with an `initializing`
status. However, the API returns `current_state: null` to represent the
agent state, causing the frontend to display "No message available".
This PR updates `codersdk.Task` to return a `current_state` when the
task is initializing with meaningful messages about what's happening
during task initialization.

**Previous message**

<img width="2764" height="288" alt="Screenshot 2025-11-07 at 09 06 13"
src="https://github.com/user-attachments/assets/feec9f15-91ca-4378-8565-5f9de062d11a"
/>

**New message**

<img width="2726" height="226" alt="Screenshot 2025-11-12 at 11 00 15"
src="https://github.com/user-attachments/assets/2f9bee3e-7ac4-4382-b1c3-1d06bbc2906e"
/>

## Changes

- Populate `current_state` with descriptive initialization messages when
task status is `initializing` and no valid app status exists for the
current build
- **dbfake**: Fix `WorkspaceBuild` builder to properly handle
pending/running jobs by linking tasks without requiring agent/app
resources

**Note:** UI Storybook changes to reflect these new messages will be
addressed in a follow-up PR.

Closes: https://github.com/coder/internal/issues/1063
2025-11-17 13:24:12 +00:00
dependabot[bot] 355150072b chore: bump ubuntu from 0950623 to 104ae83 in /dogfood/coder (#20797)
Bumps ubuntu from `0950623` to `104ae83`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ubuntu&package-manager=docker&previous-version=jammy&new-version=jammy)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-17 12:23:50 +00:00
dependabot[bot] ad3e8885e4 chore: bump rust from d9ba801 to cef0ec9 in /dogfood/coder (#20798)
Bumps rust from `d9ba801` to `cef0ec9`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rust&package-manager=docker&previous-version=slim&new-version=slim)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-17 12:23:46 +00:00
dependabot[bot] 0b0813e30c ci: bump the github-actions group with 3 updates (#20796)
Bumps the github-actions group with 3 updates:
[crate-ci/typos](https://github.com/crate-ci/typos),
[peter-evans/repository-dispatch](https://github.com/peter-evans/repository-dispatch)
and [github/codeql-action](https://github.com/github/codeql-action).

Updates `crate-ci/typos` from 1.39.0 to 1.39.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/crate-ci/typos/releases">crate-ci/typos's
releases</a>.</em></p>
<blockquote>
<h2>v1.39.2</h2>
<h2>[1.39.2] - 2025-11-13</h2>
<h3>Fixes</h3>
<ul>
<li>Don't offer <code>entry</code> as a correction for
<code>entrys</code></li>
</ul>
<h2>v1.39.1</h2>
<h2>[1.39.1] - 2025-11-12</h2>
<h3>Features</h3>
<ul>
<li>Make <code>--help</code> more vibrant</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/crate-ci/typos/blob/master/CHANGELOG.md">crate-ci/typos's
changelog</a>.</em></p>
<blockquote>
<h1>Change Log</h1>
<p>All notable changes to this project will be documented in this
file.</p>
<p>The format is based on <a href="https://keepachangelog.com/">Keep a
Changelog</a>
and this project adheres to <a href="https://semver.org/">Semantic
Versioning</a>.</p>
<!-- raw HTML omitted -->
<h2>[Unreleased] - ReleaseDate</h2>
<h2>[1.39.2] - 2025-11-13</h2>
<h3>Fixes</h3>
<ul>
<li>Don't offer <code>entry</code> as a correction for
<code>entrys</code></li>
</ul>
<h2>[1.39.1] - 2025-11-12</h2>
<h3>Features</h3>
<ul>
<li>Make <code>--help</code> more vibrant</li>
</ul>
<h2>[1.39.0] - 2025-10-31</h2>
<h3>Features</h3>
<ul>
<li>Updated the dictionary with the <a
href="https://redirect.github.com/crate-ci/typos/issues/1383">October
2025</a> changes</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>When a typo is pluralized, prefer pluralized corrections</li>
</ul>
<h2>[1.38.1] - 2025-10-07</h2>
<h3>Fixes</h3>
<ul>
<li>Ignore common golang identifiers</li>
</ul>
<h2>[1.38.0] - 2025-10-06</h2>
<h3>Features</h3>
<ul>
<li>Update type list</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Don't correct <code>typ</code></li>
<li>Consistently error on unused config fields</li>
</ul>
<h2>[1.37.3] - 2025-10-06</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/crate-ci/typos/commit/626c4bedb751ce0b7f03262ca97ddda9a076ae1c"><code>626c4be</code></a>
chore: Release</li>
<li><a
href="https://github.com/crate-ci/typos/commit/c6b458db05d00c3037bc9a1102b84febc9fff2f4"><code>c6b458d</code></a>
docs: Update changelog</li>
<li><a
href="https://github.com/crate-ci/typos/commit/eed04198a67af7f32b16141261aa8f911cba1f5f"><code>eed0419</code></a>
Merge pull request <a
href="https://redirect.github.com/crate-ci/typos/issues/1423">#1423</a>
from epage/entrys</li>
<li><a
href="https://github.com/crate-ci/typos/commit/40383f41a2f90743ef28b8c8b1c2d5a42b7651a0"><code>40383f4</code></a>
fix(dict): Don't offer 'entry' as a correction for 'entrys'</li>
<li><a
href="https://github.com/crate-ci/typos/commit/1af53e3774f068183ffd0c7193eb061a2b65a531"><code>1af53e3</code></a>
chore: Release</li>
<li><a
href="https://github.com/crate-ci/typos/commit/e5d291b81a9bd645c218da268c94df78b6e5e605"><code>e5d291b</code></a>
docs: Update changelog</li>
<li><a
href="https://github.com/crate-ci/typos/commit/55474f5ff9f9a923f4e1a218c9b39271517d847e"><code>55474f5</code></a>
Merge pull request <a
href="https://redirect.github.com/crate-ci/typos/issues/1417">#1417</a>
from starsep/colorful_help</li>
<li><a
href="https://github.com/crate-ci/typos/commit/78b93759507833a789853c44a1d9a767fcd15024"><code>78b9375</code></a>
feat: Enable colors for typos --help</li>
<li><a
href="https://github.com/crate-ci/typos/commit/308f8f5788c91e7d099279f046cdbc3662956dcd"><code>308f8f5</code></a>
Merge pull request <a
href="https://redirect.github.com/crate-ci/typos/issues/1409">#1409</a>
from crate-ci/renovate/actions-download-artifact-6.x</li>
<li><a
href="https://github.com/crate-ci/typos/commit/cf03418f507191943e33099ce6ba74c4c79b0257"><code>cf03418</code></a>
Merge pull request <a
href="https://redirect.github.com/crate-ci/typos/issues/1410">#1410</a>
from crate-ci/renovate/actions-setup-python-6.x</li>
<li>Additional commits viewable in <a
href="https://github.com/crate-ci/typos/compare/07d900b8fa1097806b8adb6391b0d3e0ac2fdea7...626c4bedb751ce0b7f03262ca97ddda9a076ae1c">compare
view</a></li>
</ul>
</details>
<br />

Updates `peter-evans/repository-dispatch` from 4.0.0 to 4.0.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/peter-evans/repository-dispatch/releases">peter-evans/repository-dispatch's
releases</a>.</em></p>
<blockquote>
<h2>v4.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>build(deps): bump peter-evans/repository-dispatch from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/repository-dispatch/pull/428">peter-evans/repository-dispatch#428</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.127 to
18.19.129 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/repository-dispatch/pull/429">peter-evans/repository-dispatch#429</a></li>
<li>build(deps): bump the github-actions group with 3 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/repository-dispatch/pull/431">peter-evans/repository-dispatch#431</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.129 to
18.19.130 in the npm group by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/repository-dispatch/pull/432">peter-evans/repository-dispatch#432</a></li>
<li>Fix node version in actions.yml by <a
href="https://github.com/peter-evans"><code>@​peter-evans</code></a> in
<a
href="https://redirect.github.com/peter-evans/repository-dispatch/pull/433">peter-evans/repository-dispatch#433</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/peter-evans/repository-dispatch/compare/v4.0.0...v4.0.1">https://github.com/peter-evans/repository-dispatch/compare/v4.0.0...v4.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/peter-evans/repository-dispatch/commit/28959ce8df70de7be546dd1250a005dd32156697"><code>28959ce</code></a>
Fix node version in actions.yml (<a
href="https://redirect.github.com/peter-evans/repository-dispatch/issues/433">#433</a>)</li>
<li><a
href="https://github.com/peter-evans/repository-dispatch/commit/25d29c2bbf9b1fd1e3e924e485d754498fdc84fd"><code>25d29c2</code></a>
build(deps-dev): bump <code>@​types/node</code> in the npm group (<a
href="https://redirect.github.com/peter-evans/repository-dispatch/issues/432">#432</a>)</li>
<li><a
href="https://github.com/peter-evans/repository-dispatch/commit/830136c66487b256600fec7788578990092647bc"><code>830136c</code></a>
build(deps): bump the github-actions group with 3 updates (<a
href="https://redirect.github.com/peter-evans/repository-dispatch/issues/431">#431</a>)</li>
<li><a
href="https://github.com/peter-evans/repository-dispatch/commit/2c856c63feddee6147cab2f38801935b6a59a765"><code>2c856c6</code></a>
ci: update dependabot config</li>
<li><a
href="https://github.com/peter-evans/repository-dispatch/commit/66739071c2122a05106fc2a2c306fdaf33bb9cda"><code>6673907</code></a>
build(deps-dev): bump <code>@​types/node</code> from 18.19.127 to
18.19.129 (<a
href="https://redirect.github.com/peter-evans/repository-dispatch/issues/429">#429</a>)</li>
<li><a
href="https://github.com/peter-evans/repository-dispatch/commit/952a211c1efd1abc91cf289f80655b4692ba8cff"><code>952a211</code></a>
build(deps): bump peter-evans/repository-dispatch from 3 to 4 (<a
href="https://redirect.github.com/peter-evans/repository-dispatch/issues/428">#428</a>)</li>
<li>See full diff in <a
href="https://github.com/peter-evans/repository-dispatch/compare/5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f...28959ce8df70de7be546dd1250a005dd32156697">compare
view</a></li>
</ul>
</details>
<br />

Updates `github/codeql-action` from 4.31.2 to 4.31.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/releases">github/codeql-action's
releases</a>.</em></p>
<blockquote>
<h2>v4.31.3</h2>
<h1>CodeQL Action Changelog</h1>
<p>See the <a
href="https://github.com/github/codeql-action/releases">releases
page</a> for the relevant changes to the CodeQL CLI and language
packs.</p>
<h2>4.31.3 - 13 Nov 2025</h2>
<ul>
<li>CodeQL Action v3 will be deprecated in December 2026. The Action now
logs a warning for customers who are running v3 but could be running v4.
For more information, see <a
href="https://github.blog/changelog/2025-10-28-upcoming-deprecation-of-codeql-action-v3/">Upcoming
deprecation of CodeQL Action v3</a>.</li>
<li>Update default CodeQL bundle version to 2.23.5. <a
href="https://redirect.github.com/github/codeql-action/pull/3288">#3288</a></li>
</ul>
<p>See the full <a
href="https://github.com/github/codeql-action/blob/v4.31.3/CHANGELOG.md">CHANGELOG.md</a>
for more information.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/blob/main/CHANGELOG.md">github/codeql-action's
changelog</a>.</em></p>
<blockquote>
<h1>CodeQL Action Changelog</h1>
<p>See the <a
href="https://github.com/github/codeql-action/releases">releases
page</a> for the relevant changes to the CodeQL CLI and language
packs.</p>
<h2>[UNRELEASED]</h2>
<p>No user facing changes.</p>
<h2>4.31.3 - 13 Nov 2025</h2>
<ul>
<li>CodeQL Action v3 will be deprecated in December 2026. The Action now
logs a warning for customers who are running v3 but could be running v4.
For more information, see <a
href="https://github.blog/changelog/2025-10-28-upcoming-deprecation-of-codeql-action-v3/">Upcoming
deprecation of CodeQL Action v3</a>.</li>
<li>Update default CodeQL bundle version to 2.23.5. <a
href="https://redirect.github.com/github/codeql-action/pull/3288">#3288</a></li>
</ul>
<h2>4.31.2 - 30 Oct 2025</h2>
<p>No user facing changes.</p>
<h2>4.31.1 - 30 Oct 2025</h2>
<ul>
<li>The <code>add-snippets</code> input has been removed from the
<code>analyze</code> action. This input has been deprecated since CodeQL
Action 3.26.4 in August 2024 when this removal was announced.</li>
</ul>
<h2>4.31.0 - 24 Oct 2025</h2>
<ul>
<li>Bump minimum CodeQL bundle version to 2.17.6. <a
href="https://redirect.github.com/github/codeql-action/pull/3223">#3223</a></li>
<li>When SARIF files are uploaded by the <code>analyze</code> or
<code>upload-sarif</code> actions, the CodeQL Action automatically
performs post-processing steps to prepare the data for the upload.
Previously, these post-processing steps were only performed before an
upload took place. We are now changing this so that the post-processing
steps will always be performed, even when the SARIF files are not
uploaded. This does not change anything for the
<code>upload-sarif</code> action. For <code>analyze</code>, this may
affect Advanced Setup for CodeQL users who specify a value other than
<code>always</code> for the <code>upload</code> input. <a
href="https://redirect.github.com/github/codeql-action/pull/3222">#3222</a></li>
</ul>
<h2>4.30.9 - 17 Oct 2025</h2>
<ul>
<li>Update default CodeQL bundle version to 2.23.3. <a
href="https://redirect.github.com/github/codeql-action/pull/3205">#3205</a></li>
<li>Experimental: A new <code>setup-codeql</code> action has been added
which is similar to <code>init</code>, except it only installs the
CodeQL CLI and does not initialize a database. Do not use this in
production as it is part of an internal experiment and subject to change
at any time. <a
href="https://redirect.github.com/github/codeql-action/pull/3204">#3204</a></li>
</ul>
<h2>4.30.8 - 10 Oct 2025</h2>
<p>No user facing changes.</p>
<h2>4.30.7 - 06 Oct 2025</h2>
<ul>
<li>[v4+ only] The CodeQL Action now runs on Node.js v24. <a
href="https://redirect.github.com/github/codeql-action/pull/3169">#3169</a></li>
</ul>
<h2>3.30.6 - 02 Oct 2025</h2>
<ul>
<li>Update default CodeQL bundle version to 2.23.2. <a
href="https://redirect.github.com/github/codeql-action/pull/3168">#3168</a></li>
</ul>
<h2>3.30.5 - 26 Sep 2025</h2>
<ul>
<li>We fixed a bug that was introduced in <code>3.30.4</code> with
<code>upload-sarif</code> which resulted in files without a
<code>.sarif</code> extension not getting uploaded. <a
href="https://redirect.github.com/github/codeql-action/pull/3160">#3160</a></li>
</ul>
<h2>3.30.4 - 25 Sep 2025</h2>
<ul>
<li>We have improved the CodeQL Action's ability to validate that the
workflow it is used in does not use different versions of the CodeQL
Action for different workflow steps. Mixing different versions of the
CodeQL Action in the same workflow is unsupported and can lead to
unpredictable results. A warning will now be emitted from the
<code>codeql-action/init</code> step if different versions of the CodeQL
Action are detected in the workflow file. Additionally, an error will
now be thrown by the other CodeQL Action steps if they load a
configuration file that was generated by a different version of the
<code>codeql-action/init</code> step. <a
href="https://redirect.github.com/github/codeql-action/pull/3099">#3099</a>
and <a
href="https://redirect.github.com/github/codeql-action/pull/3100">#3100</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/github/codeql-action/commit/014f16e7ab1402f30e7c3329d33797e7948572db"><code>014f16e</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3293">#3293</a>
from github/update-v4.31.3-8c10e89c7</li>
<li><a
href="https://github.com/github/codeql-action/commit/14d898ef09787f3258d7542ed62ad3da38295b68"><code>14d898e</code></a>
Update changelog for v4.31.3</li>
<li><a
href="https://github.com/github/codeql-action/commit/8c10e89c78c754f211c440ce6999ed8668811c3b"><code>8c10e89</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3288">#3288</a>
from github/update-bundle/codeql-bundle-v2.23.5</li>
<li><a
href="https://github.com/github/codeql-action/commit/9777b01a4952c6266ad795197c2dd702065a6d19"><code>9777b01</code></a>
Merge branch 'main' into update-bundle/codeql-bundle-v2.23.5</li>
<li><a
href="https://github.com/github/codeql-action/commit/456a74a6faf33b08baa780e0bf31b41fabd1ca35"><code>456a74a</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3289">#3289</a>
from github/mbg/ci/setup-dotnet</li>
<li><a
href="https://github.com/github/codeql-action/commit/3fac49c14012959fab197865a1813bffe8a04dd1"><code>3fac49c</code></a>
Update remaining workflows</li>
<li><a
href="https://github.com/github/codeql-action/commit/38a3a7258f252b705a070bd91df7a72a50d61318"><code>38a3a72</code></a>
Enable <code>installDotNet</code> in all workflows that analyse C#</li>
<li><a
href="https://github.com/github/codeql-action/commit/58c9eb6c034b7054387301aa21926d94da049b69"><code>58c9eb6</code></a>
Add <code>global.json</code></li>
<li><a
href="https://github.com/github/codeql-action/commit/f20e02164a8bd2f32913932752d67ee2bbf22246"><code>f20e021</code></a>
Add support for adding <code>setup-dotnet</code> steps to
<code>sync.sh</code></li>
<li><a
href="https://github.com/github/codeql-action/commit/8d3d4001e38901ebbee39c134163b67198c956f7"><code>8d3d400</code></a>
Add changelog note</li>
<li>Additional commits viewable in <a
href="https://github.com/github/codeql-action/compare/0499de31b99561a6d14a36a5f662c2a54f91beee...014f16e7ab1402f30e7c3329d33797e7948572db">compare
view</a></li>
</ul>
</details>
<br />

<details>
<summary>Most Recent Ignore Conditions Applied to This Pull
Request</summary>

| Dependency Name | Ignore Conditions |
| --- | --- |
| crate-ci/typos | [>= 1.30.a, < 1.31] |
</details>


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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 12:19:23 +00:00
dependabot[bot] 430c8c2dd2 chore: bump github.com/anthropics/anthropic-sdk-go from 1.17.0 to 1.18.0 (#20795)
Bumps
[github.com/anthropics/anthropic-sdk-go](https://github.com/anthropics/anthropic-sdk-go)
from 1.17.0 to 1.18.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/anthropics/anthropic-sdk-go/releases">github.com/anthropics/anthropic-sdk-go's
releases</a>.</em></p>
<blockquote>
<h2>v1.18.0</h2>
<h2>1.18.0 (2025-11-14)</h2>
<p>Full Changelog: <a
href="https://github.com/anthropics/anthropic-sdk-go/compare/v1.17.0...v1.18.0">v1.17.0...v1.18.0</a></p>
<h3>Features</h3>
<ul>
<li><strong>api:</strong> add support for structured outputs beta (<a
href="https://github.com/anthropics/anthropic-sdk-go/commit/fb9cfb4e4b571d5fec7da9874610aa8820aee80c">fb9cfb4</a>)</li>
</ul>
<h3>Chores</h3>
<ul>
<li>bump gjson version (<a
href="https://github.com/anthropics/anthropic-sdk-go/commit/69b5e0e40757884bece66397fb6ca769f4e00118">69b5e0e</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/anthropics/anthropic-sdk-go/blob/main/CHANGELOG.md">github.com/anthropics/anthropic-sdk-go's
changelog</a>.</em></p>
<blockquote>
<h2>1.18.0 (2025-11-14)</h2>
<p>Full Changelog: <a
href="https://github.com/anthropics/anthropic-sdk-go/compare/v1.17.0...v1.18.0">v1.17.0...v1.18.0</a></p>
<h3>Features</h3>
<ul>
<li><strong>api:</strong> add support for structured outputs beta (<a
href="https://github.com/anthropics/anthropic-sdk-go/commit/fb9cfb4e4b571d5fec7da9874610aa8820aee80c">fb9cfb4</a>)</li>
</ul>
<h3>Chores</h3>
<ul>
<li>bump gjson version (<a
href="https://github.com/anthropics/anthropic-sdk-go/commit/69b5e0e40757884bece66397fb6ca769f4e00118">69b5e0e</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/anthropics/anthropic-sdk-go/commit/f4266c4d6cb52f0efd239777f322db2387168334"><code>f4266c4</code></a>
release: 1.18.0</li>
<li><a
href="https://github.com/anthropics/anthropic-sdk-go/commit/e3776089b6095d27726c31d82a8e2c9f12bd0cf1"><code>e377608</code></a>
feat(api): add support for structured outputs beta</li>
<li><a
href="https://github.com/anthropics/anthropic-sdk-go/commit/fc01187b262243230c5f5a89c08d835d65b17c6d"><code>fc01187</code></a>
codegen metadata</li>
<li><a
href="https://github.com/anthropics/anthropic-sdk-go/commit/4e9c5a8a928a43e81a133a18ef0396907d833c6a"><code>4e9c5a8</code></a>
codegen metadata</li>
<li>See full diff in <a
href="https://github.com/anthropics/anthropic-sdk-go/compare/v1.17.0...v1.18.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/anthropics/anthropic-sdk-go&package-manager=go_modules&previous-version=1.17.0&new-version=1.18.0)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-17 12:15:11 +00:00
dependabot[bot] 1c15534c98 chore: bump the x group with 6 updates (#20792)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps the x group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.43.0` |
`0.44.0` |
| [golang.org/x/mod](https://github.com/golang/mod) | `0.29.0` |
`0.30.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.46.0` |
`0.47.0` |
| [golang.org/x/term](https://github.com/golang/term) | `0.36.0` |
`0.37.0` |
| [golang.org/x/text](https://github.com/golang/text) | `0.30.0` |
`0.31.0` |
| [golang.org/x/tools](https://github.com/golang/tools) | `0.38.0` |
`0.39.0` |

Updates `golang.org/x/crypto` from 0.43.0 to 0.44.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/crypto/commit/122a78f140d9d3303ed3261bc374bbbca149140f"><code>122a78f</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/crypto/commit/c0531f9c34514ad5c5551e2d6ce569ca673a8afd"><code>c0531f9</code></a>
all: eliminate vet diagnostics</li>
<li><a
href="https://github.com/golang/crypto/commit/0997000b45e3a40598272081bcad03ffd21b8adb"><code>0997000</code></a>
all: fix some comments</li>
<li><a
href="https://github.com/golang/crypto/commit/017a1aaa2d993492ef6f74ebe7c87f33d82d3717"><code>017a1aa</code></a>
chacha20poly1305: panic on dst and additionalData overlap</li>
<li><a
href="https://github.com/golang/crypto/commit/cf29fa96f8b66328e59829f064539321159bfa5b"><code>cf29fa9</code></a>
sha3: make it mostly a wrapper around crypto/sha3</li>
<li><a
href="https://github.com/golang/crypto/commit/0b7aa0cfb07b6b13ead990b67cb3cb8639871f90"><code>0b7aa0c</code></a>
ssh: use reflect.TypeFor instead of reflect.TypeOf</li>
<li><a
href="https://github.com/golang/crypto/commit/1faea2975ced2153e5086c1ee135f983db10150a"><code>1faea29</code></a>
all: fix some typos in comment</li>
<li>See full diff in <a
href="https://github.com/golang/crypto/compare/v0.43.0...v0.44.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `golang.org/x/mod` from 0.29.0 to 0.30.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/mod/commit/7416265ac6eca137286fb2adcc87f9feec458b4c"><code>7416265</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/mod/commit/5517a715a62aaf2d2ab02e64ce67586c60767e8f"><code>5517a71</code></a>
all: fix some comments</li>
<li><a
href="https://github.com/golang/mod/commit/b6cdd1a97c6a688a6a842cdc667ec2e68174ba9d"><code>b6cdd1a</code></a>
modfile: use reflect.TypeFor instead of reflect.TypeOf</li>
<li>See full diff in <a
href="https://github.com/golang/mod/compare/v0.29.0...v0.30.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `golang.org/x/net` from 0.46.0 to 0.47.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/net/commit/9a296438e54dff851a45667aa645a97003b44db5"><code>9a29643</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/net/commit/07cefd8a6bb170785052142a96034f2b2f7115bc"><code>07cefd8</code></a>
context: deprecate</li>
<li><a
href="https://github.com/golang/net/commit/5ac9daca088ab4f378d7df849f6c7d28bea86071"><code>5ac9dac</code></a>
publicsuffix: don't treat ip addresses as domain names</li>
<li><a
href="https://github.com/golang/net/commit/d1f64cc67036f272ea900194674afcf71eb3e816"><code>d1f64cc</code></a>
quic: use testing/synctest</li>
<li><a
href="https://github.com/golang/net/commit/fff0469cf57f71bea0cbc4cc8cbfb8f92be7a417"><code>fff0469</code></a>
http2: document that RFC 7540 prioritization does not work with small
payloads</li>
<li><a
href="https://github.com/golang/net/commit/f35e3a4dd2a34d1d47fac5b66210a40e2d374a79"><code>f35e3a4</code></a>
http2: fix weight overflow in RFC 7540 write scheduler</li>
<li><a
href="https://github.com/golang/net/commit/89adc90ac46a688eeda3a4a95b71acf073bd059c"><code>89adc90</code></a>
http2: fix typo referring to RFC 9218 as RFC 9128 instead</li>
<li><a
href="https://github.com/golang/net/commit/8d76a2c082f6f995c2ef1ce8a0d56f2e33b48092"><code>8d76a2c</code></a>
quic: don't defer MAX_STREAMS frames indefinitely</li>
<li><a
href="https://github.com/golang/net/commit/027f8b700eb72af5c0175a945535a42670628351"><code>027f8b7</code></a>
quic: fix expected ACK Delay in client's ACK after HANDSHAKE_DONE</li>
<li><a
href="https://github.com/golang/net/commit/dec9fe711ec1385a2aa169a3f8145d295abcca16"><code>dec9fe7</code></a>
dns/dnsmessage: update SVCB packing to prohibit name compression</li>
<li>Additional commits viewable in <a
href="https://github.com/golang/net/compare/v0.46.0...v0.47.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `golang.org/x/term` from 0.36.0 to 0.37.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/term/commit/1231d5465be98a7c5f01140358c142d365d4fbb6"><code>1231d54</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/term/commit/3475bc8ef1a53c8b2f1fa69d5f804135ca97f035"><code>3475bc8</code></a>
term: fix some comments</li>
<li>See full diff in <a
href="https://github.com/golang/term/compare/v0.36.0...v0.37.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `golang.org/x/text` from 0.30.0 to 0.31.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/text/commit/e7ff6b3572e1a83c072ef150c985f86603986e1b"><code>e7ff6b3</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/text/commit/fbf012b8c1140cde0210e241356553d0000332e8"><code>fbf012b</code></a>
all: use reflect.TypeFor instead of reflect.TypeOf</li>
<li>See full diff in <a
href="https://github.com/golang/text/compare/v0.30.0...v0.31.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `golang.org/x/tools` from 0.38.0 to 0.39.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/tools/commit/034e59c473362f8f2be47694d98fd3f12a1ad497"><code>034e59c</code></a>
internal/analysis/analyzerutil: fix FileUsesGoVersion</li>
<li><a
href="https://github.com/golang/tools/commit/076bd8052bceea0f0fdb90b2ec9205c8cbf5b00d"><code>076bd80</code></a>
gopls/internal/filewatcher: retry directory reading upon failure</li>
<li><a
href="https://github.com/golang/tools/commit/605803fb4494b25a23ab310776fea0fa1250f5c8"><code>605803f</code></a>
go/analysis/passes/loopclosure: simplify using IsMethodNamed</li>
<li><a
href="https://github.com/golang/tools/commit/2c6e03f2da11279a147c887c2babfa5daa8f8f02"><code>2c6e03f</code></a>
internal/testenv: allow Apple diff too</li>
<li><a
href="https://github.com/golang/tools/commit/82112c070f1367acf2fd846c2db5eb4208995893"><code>82112c0</code></a>
gopls/internal/settings: correct git issue for fieldalignment
warning</li>
<li><a
href="https://github.com/golang/tools/commit/1f978562b5d8e9396610d2b52f5e6c7584eb6eef"><code>1f97856</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/tools/commit/c24121c97d900d2a5ba384cc97dedf0de3e18f34"><code>c24121c</code></a>
go/analysis/passes/modernize: stditerators: even better name
heuristic</li>
<li><a
href="https://github.com/golang/tools/commit/edb958762845d4f1feb4ec79582b2ea8b02533be"><code>edb9587</code></a>
go/analysis/passes/modernize: stditerators: better name heuristic</li>
<li><a
href="https://github.com/golang/tools/commit/2f6a4f9b3cb8896e8e9cd7c18d610299d0e92b32"><code>2f6a4f9</code></a>
go/analysis/passes/modernize: forvar: handle &quot;if v := v; cond
{&quot;</li>
<li><a
href="https://github.com/golang/tools/commit/efd8c43777a5aace999c8162ee81f4b8c9c0695c"><code>efd8c43</code></a>
go/analysis: don't apply fixes to generated files</li>
<li>Additional commits viewable in <a
href="https://github.com/golang/tools/compare/v0.38.0...v0.39.0">compare
view</a></li>
</ul>
</details>
<br />


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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 12:14:53 +00:00
dependabot[bot] 04cf5f8690 chore: bump google.golang.org/api from 0.255.0 to 0.256.0 (#20794)
Bumps
[google.golang.org/api](https://github.com/googleapis/google-api-go-client)
from 0.255.0 to 0.256.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/googleapis/google-api-go-client/releases">google.golang.org/api's
releases</a>.</em></p>
<blockquote>
<h2>v0.256.0</h2>
<h2><a
href="https://github.com/googleapis/google-api-go-client/compare/v0.255.0...v0.256.0">0.256.0</a>
(2025-11-10)</h2>
<h3>Features</h3>
<ul>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3366">#3366</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/997c613e70ddbf1483e4212641cb09db86f7716b">997c613</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3368">#3368</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/57e958d22096bca64db83dc7888f248f0d8a6bc4">57e958d</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3369">#3369</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/5d436f7172766d9dab04cfcf3dd0e5054228a084">5d436f7</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3370">#3370</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/140a610ace04e635f919acf8f7a5441d1d4546b9">140a610</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3371">#3371</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/39a2bc07bfc4693cf0a99de0fe70807783471e99">39a2bc0</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md">google.golang.org/api's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/googleapis/google-api-go-client/compare/v0.255.0...v0.256.0">0.256.0</a>
(2025-11-10)</h2>
<h3>Features</h3>
<ul>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3366">#3366</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/997c613e70ddbf1483e4212641cb09db86f7716b">997c613</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3368">#3368</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/57e958d22096bca64db83dc7888f248f0d8a6bc4">57e958d</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3369">#3369</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/5d436f7172766d9dab04cfcf3dd0e5054228a084">5d436f7</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3370">#3370</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/140a610ace04e635f919acf8f7a5441d1d4546b9">140a610</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3371">#3371</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/39a2bc07bfc4693cf0a99de0fe70807783471e99">39a2bc0</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/811ba14d5fc1b87bd0058d49b80293164db986fa"><code>811ba14</code></a>
chore(main): release 0.256.0 (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3367">#3367</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/11afd811cc00001a2691e5086db1824d94c141cb"><code>11afd81</code></a>
chore(all): update all (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3372">#3372</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/39a2bc07bfc4693cf0a99de0fe70807783471e99"><code>39a2bc0</code></a>
feat(all): auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3371">#3371</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/140a610ace04e635f919acf8f7a5441d1d4546b9"><code>140a610</code></a>
feat(all): auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3370">#3370</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/5d436f7172766d9dab04cfcf3dd0e5054228a084"><code>5d436f7</code></a>
feat(all): auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3369">#3369</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/57e958d22096bca64db83dc7888f248f0d8a6bc4"><code>57e958d</code></a>
feat(all): auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3368">#3368</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/997c613e70ddbf1483e4212641cb09db86f7716b"><code>997c613</code></a>
feat(all): auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3366">#3366</a>)</li>
<li>See full diff in <a
href="https://github.com/googleapis/google-api-go-client/compare/v0.255.0...v0.256.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google.golang.org/api&package-manager=go_modules&previous-version=0.255.0&new-version=0.256.0)](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 merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@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>
2025-11-17 12:14:37 +00:00
271 changed files with 11989 additions and 1672 deletions
+9 -1
View File
@@ -191,7 +191,7 @@ jobs:
# Check for any typos
- name: Check for typos
uses: crate-ci/typos@07d900b8fa1097806b8adb6391b0d3e0ac2fdea7 # v1.39.0
uses: crate-ci/typos@626c4bedb751ce0b7f03262ca97ddda9a076ae1c # v1.39.2
with:
config: .github/workflows/typos.toml
@@ -756,6 +756,14 @@ jobs:
path: ./site/test-results/**/*.webm
retention-days: 7
- name: Upload debug log
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: coderd-debug-logs${{ matrix.variant.premium && '-premium' || '' }}
path: ./site/e2e/test-results/debug.log
retention-days: 7
- name: Upload pprof dumps
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
+205
View File
@@ -0,0 +1,205 @@
# This workflow checks if a PR requires documentation updates.
# It creates a Coder Task that uses AI to analyze the PR changes,
# search existing docs, and comment with recommendations.
#
# Triggered by: Adding the "doc-check" label to a PR, or manual dispatch.
name: AI Documentation Check
on:
pull_request:
types:
- labeled
workflow_dispatch:
inputs:
pr_url:
description: "Pull Request URL to check"
required: true
type: string
template_preset:
description: "Template preset to use"
required: false
default: ""
type: string
jobs:
doc-check:
name: Analyze PR for Documentation Updates Needed
runs-on: ubuntu-latest
if: |
(github.event.label.name == 'doc-check' || github.event_name == 'workflow_dispatch') &&
(github.event.pull_request.draft == false || github.event_name == 'workflow_dispatch')
timeout-minutes: 30
env:
CODER_URL: ${{ secrets.DOC_CHECK_CODER_URL }}
CODER_SESSION_TOKEN: ${{ secrets.DOC_CHECK_CODER_SESSION_TOKEN }}
permissions:
contents: read
pull-requests: write
actions: write
steps:
- name: Determine PR Context
id: determine-context
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_EVENT_PR_HTML_URL: ${{ github.event.pull_request.html_url }}
GITHUB_EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
GITHUB_EVENT_SENDER_ID: ${{ github.event.sender.id }}
GITHUB_EVENT_SENDER_LOGIN: ${{ github.event.sender.login }}
INPUTS_PR_URL: ${{ inputs.pr_url }}
INPUTS_TEMPLATE_PRESET: ${{ inputs.template_preset || '' }}
GH_TOKEN: ${{ github.token }}
run: |
echo "Using template preset: ${INPUTS_TEMPLATE_PRESET}"
echo "template_preset=${INPUTS_TEMPLATE_PRESET}" >> "${GITHUB_OUTPUT}"
# For workflow_dispatch, use the provided PR URL
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
if ! GITHUB_USER_ID=$(gh api "users/${GITHUB_ACTOR}" --jq '.id'); then
echo "::error::Failed to get GitHub user ID for actor ${GITHUB_ACTOR}"
exit 1
fi
echo "Using workflow_dispatch actor: ${GITHUB_ACTOR} (ID: ${GITHUB_USER_ID})"
echo "github_user_id=${GITHUB_USER_ID}" >> "${GITHUB_OUTPUT}"
echo "github_username=${GITHUB_ACTOR}" >> "${GITHUB_OUTPUT}"
echo "Using PR URL: ${INPUTS_PR_URL}"
# Convert /pull/ to /issues/ for create-task-action compatibility
ISSUE_URL="${INPUTS_PR_URL/\/pull\//\/issues\/}"
echo "pr_url=${ISSUE_URL}" >> "${GITHUB_OUTPUT}"
# Extract PR number from URL for later use
PR_NUMBER=$(echo "${INPUTS_PR_URL}" | grep -oP '(?<=pull/)\d+')
echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
elif [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
GITHUB_USER_ID=${GITHUB_EVENT_SENDER_ID}
echo "Using label adder: ${GITHUB_EVENT_SENDER_LOGIN} (ID: ${GITHUB_USER_ID})"
echo "github_user_id=${GITHUB_USER_ID}" >> "${GITHUB_OUTPUT}"
echo "github_username=${GITHUB_EVENT_SENDER_LOGIN}" >> "${GITHUB_OUTPUT}"
echo "Using PR URL: ${GITHUB_EVENT_PR_HTML_URL}"
# Convert /pull/ to /issues/ for create-task-action compatibility
ISSUE_URL="${GITHUB_EVENT_PR_HTML_URL/\/pull\//\/issues\/}"
echo "pr_url=${ISSUE_URL}" >> "${GITHUB_OUTPUT}"
echo "pr_number=${GITHUB_EVENT_PR_NUMBER}" >> "${GITHUB_OUTPUT}"
else
echo "::error::Unsupported event type: ${GITHUB_EVENT_NAME}"
exit 1
fi
- name: Extract changed files and build prompt
id: extract-context
env:
PR_URL: ${{ steps.determine-context.outputs.pr_url }}
PR_NUMBER: ${{ steps.determine-context.outputs.pr_number }}
GH_TOKEN: ${{ github.token }}
run: |
echo "Analyzing PR #${PR_NUMBER}"
# Build task prompt - using unquoted heredoc so variables expand
TASK_PROMPT=$(cat <<EOF
Review PR #${PR_NUMBER} and determine if documentation needs updating or creating.
PR URL: ${PR_URL}
WORKFLOW:
1. Setup (repo is pre-cloned at ~/coder)
cd ~/coder
git fetch origin pull/${PR_NUMBER}/head:pr-${PR_NUMBER}
git checkout pr-${PR_NUMBER}
2. Get PR info
Use GitHub MCP tools to get PR title, body, and diff
Or use: git diff main...pr-${PR_NUMBER}
3. Understand Changes
Read the diff and identify what changed
Ask: Is this user-facing? Does it change behavior? Is it a new feature?
4. Search for Related Docs
cat ~/coder/docs/manifest.json | jq '.routes[] | {title, path}' | head -50
grep -ri "relevant_term" ~/coder/docs/ --include="*.md"
5. Decide
NEEDS DOCS if: New feature, API change, CLI change, behavior change, user-visible
NO DOCS if: Internal refactor, test-only, already documented, non-user-facing, dependency updates
FIRST check: Did this PR already update docs? If yes and complete, say "No Changes Needed"
6. Comment on the PR using this format
COMMENT FORMAT:
## 📚 Documentation Check
### ✅ Updates Needed
- **[docs/path/file.md](github_link)** - Brief what needs changing
### 📝 New Docs Needed
- **docs/suggested/location.md** - What should be documented
### ✨ No Changes Needed
[Reason: Documents already updated in PR | Internal changes only | Test-only | No user-facing impact]
---
*This comment was generated by an AI Agent through [Coder Tasks](https://coder.com/docs/ai-coder/tasks)*
DOCS STRUCTURE:
Read ~/coder/docs/manifest.json for the complete documentation structure.
Common areas include: reference/, admin/, user-guides/, ai-coder/, install/, tutorials/
But check manifest.json - it has everything.
EOF
)
# Output the prompt
{
echo "task_prompt<<EOFOUTPUT"
echo "${TASK_PROMPT}"
echo "EOFOUTPUT"
} >> "${GITHUB_OUTPUT}"
- name: Checkout create-task-action
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 1
path: ./.github/actions/create-task-action
persist-credentials: false
ref: main
repository: coder/create-task-action
- name: Create Coder Task for Documentation Check
id: create_task
uses: ./.github/actions/create-task-action
with:
coder-url: ${{ secrets.DOC_CHECK_CODER_URL }}
coder-token: ${{ secrets.DOC_CHECK_CODER_SESSION_TOKEN }}
coder-organization: "default"
coder-template-name: coder
coder-template-preset: ${{ steps.determine-context.outputs.template_preset }}
coder-task-name-prefix: doc-check
coder-task-prompt: ${{ steps.extract-context.outputs.task_prompt }}
github-user-id: ${{ steps.determine-context.outputs.github_user_id }}
github-token: ${{ github.token }}
github-issue-url: ${{ steps.determine-context.outputs.pr_url }}
comment-on-issue: true
- name: Write outputs
env:
TASK_CREATED: ${{ steps.create_task.outputs.task-created }}
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
TASK_URL: ${{ steps.create_task.outputs.task-url }}
PR_URL: ${{ steps.determine-context.outputs.pr_url }}
run: |
{
echo "## Documentation Check Task"
echo ""
echo "**PR:** ${PR_URL}"
echo "**Task created:** ${TASK_CREATED}"
echo "**Task name:** ${TASK_NAME}"
echo "**Task URL:** ${TASK_URL}"
echo ""
echo "The Coder task is analyzing the PR changes and will comment with documentation recommendations."
} >> "${GITHUB_STEP_SUMMARY}"
+1 -1
View File
@@ -785,7 +785,7 @@ jobs:
- name: Send repository-dispatch event
if: ${{ !inputs.dry_run }}
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ secrets.CDRCI_GITHUB_TOKEN }}
repository: coder/packages
+1 -1
View File
@@ -47,6 +47,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5
uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5
with:
sarif_file: results.sarif
+3 -3
View File
@@ -40,7 +40,7 @@ jobs:
uses: ./.github/actions/setup-go
- name: Initialize CodeQL
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5
with:
languages: go, javascript
@@ -50,7 +50,7 @@ jobs:
rm Makefile
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5
- name: Send Slack notification on failure
if: ${{ failure() }}
@@ -154,7 +154,7 @@ jobs:
severity: "CRITICAL,HIGH"
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5
uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5
with:
sarif_file: trivy-results.sarif
category: "Trivy"
+1
View File
@@ -9,6 +9,7 @@ IST = "IST"
MacOS = "macOS"
AKS = "AKS"
O_WRONLY = "O_WRONLY"
AIBridge = "AI Bridge"
[default.extend-words]
AKS = "AKS"
+10
View File
@@ -642,6 +642,7 @@ AIBRIDGED_MOCKS := \
GEN_FILES := \
tailnet/proto/tailnet.pb.go \
agent/proto/agent.pb.go \
agent/agentsocket/proto/agentsocket.pb.go \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
vpn/vpn.pb.go \
@@ -696,6 +697,7 @@ gen/mark-fresh:
agent/proto/agent.pb.go \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
agent/agentsocket/proto/agentsocket.pb.go \
vpn/vpn.pb.go \
enterprise/aibridged/proto/aibridged.pb.go \
coderd/database/dump.sql \
@@ -800,6 +802,14 @@ agent/proto/agent.pb.go: agent/proto/agent.proto
--go-drpc_opt=paths=source_relative \
./agent/proto/agent.proto
agent/agentsocket/proto/agentsocket.pb.go: agent/agentsocket/proto/agentsocket.proto
protoc \
--go_out=. \
--go_opt=paths=source_relative \
--go-drpc_out=. \
--go-drpc_opt=paths=source_relative \
./agent/agentsocket/proto/agentsocket.proto
provisionersdk/proto/provisioner.pb.go: provisionersdk/proto/provisioner.proto
protoc \
--go_out=. \
+968
View File
@@ -0,0 +1,968 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.30.0
// protoc v4.23.4
// source: agent/agentsocket/proto/agentsocket.proto
package proto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type PingRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *PingRequest) Reset() {
*x = PingRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *PingRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PingRequest) ProtoMessage() {}
func (x *PingRequest) ProtoReflect() protoreflect.Message {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead.
func (*PingRequest) Descriptor() ([]byte, []int) {
return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{0}
}
type PingResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *PingResponse) Reset() {
*x = PingResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *PingResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PingResponse) ProtoMessage() {}
func (x *PingResponse) ProtoReflect() protoreflect.Message {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PingResponse.ProtoReflect.Descriptor instead.
func (*PingResponse) Descriptor() ([]byte, []int) {
return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{1}
}
type SyncStartRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Unit string `protobuf:"bytes,1,opt,name=unit,proto3" json:"unit,omitempty"`
}
func (x *SyncStartRequest) Reset() {
*x = SyncStartRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SyncStartRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SyncStartRequest) ProtoMessage() {}
func (x *SyncStartRequest) ProtoReflect() protoreflect.Message {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SyncStartRequest.ProtoReflect.Descriptor instead.
func (*SyncStartRequest) Descriptor() ([]byte, []int) {
return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{2}
}
func (x *SyncStartRequest) GetUnit() string {
if x != nil {
return x.Unit
}
return ""
}
type SyncStartResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *SyncStartResponse) Reset() {
*x = SyncStartResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SyncStartResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SyncStartResponse) ProtoMessage() {}
func (x *SyncStartResponse) ProtoReflect() protoreflect.Message {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SyncStartResponse.ProtoReflect.Descriptor instead.
func (*SyncStartResponse) Descriptor() ([]byte, []int) {
return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{3}
}
type SyncWantRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Unit string `protobuf:"bytes,1,opt,name=unit,proto3" json:"unit,omitempty"`
DependsOn string `protobuf:"bytes,2,opt,name=depends_on,json=dependsOn,proto3" json:"depends_on,omitempty"`
}
func (x *SyncWantRequest) Reset() {
*x = SyncWantRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SyncWantRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SyncWantRequest) ProtoMessage() {}
func (x *SyncWantRequest) ProtoReflect() protoreflect.Message {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SyncWantRequest.ProtoReflect.Descriptor instead.
func (*SyncWantRequest) Descriptor() ([]byte, []int) {
return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{4}
}
func (x *SyncWantRequest) GetUnit() string {
if x != nil {
return x.Unit
}
return ""
}
func (x *SyncWantRequest) GetDependsOn() string {
if x != nil {
return x.DependsOn
}
return ""
}
type SyncWantResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *SyncWantResponse) Reset() {
*x = SyncWantResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SyncWantResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SyncWantResponse) ProtoMessage() {}
func (x *SyncWantResponse) ProtoReflect() protoreflect.Message {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SyncWantResponse.ProtoReflect.Descriptor instead.
func (*SyncWantResponse) Descriptor() ([]byte, []int) {
return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{5}
}
type SyncCompleteRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Unit string `protobuf:"bytes,1,opt,name=unit,proto3" json:"unit,omitempty"`
}
func (x *SyncCompleteRequest) Reset() {
*x = SyncCompleteRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SyncCompleteRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SyncCompleteRequest) ProtoMessage() {}
func (x *SyncCompleteRequest) ProtoReflect() protoreflect.Message {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SyncCompleteRequest.ProtoReflect.Descriptor instead.
func (*SyncCompleteRequest) Descriptor() ([]byte, []int) {
return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{6}
}
func (x *SyncCompleteRequest) GetUnit() string {
if x != nil {
return x.Unit
}
return ""
}
type SyncCompleteResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *SyncCompleteResponse) Reset() {
*x = SyncCompleteResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SyncCompleteResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SyncCompleteResponse) ProtoMessage() {}
func (x *SyncCompleteResponse) ProtoReflect() protoreflect.Message {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SyncCompleteResponse.ProtoReflect.Descriptor instead.
func (*SyncCompleteResponse) Descriptor() ([]byte, []int) {
return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{7}
}
type SyncReadyRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Unit string `protobuf:"bytes,1,opt,name=unit,proto3" json:"unit,omitempty"`
}
func (x *SyncReadyRequest) Reset() {
*x = SyncReadyRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SyncReadyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SyncReadyRequest) ProtoMessage() {}
func (x *SyncReadyRequest) ProtoReflect() protoreflect.Message {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SyncReadyRequest.ProtoReflect.Descriptor instead.
func (*SyncReadyRequest) Descriptor() ([]byte, []int) {
return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{8}
}
func (x *SyncReadyRequest) GetUnit() string {
if x != nil {
return x.Unit
}
return ""
}
type SyncReadyResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Ready bool `protobuf:"varint,1,opt,name=ready,proto3" json:"ready,omitempty"`
}
func (x *SyncReadyResponse) Reset() {
*x = SyncReadyResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SyncReadyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SyncReadyResponse) ProtoMessage() {}
func (x *SyncReadyResponse) ProtoReflect() protoreflect.Message {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SyncReadyResponse.ProtoReflect.Descriptor instead.
func (*SyncReadyResponse) Descriptor() ([]byte, []int) {
return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{9}
}
func (x *SyncReadyResponse) GetReady() bool {
if x != nil {
return x.Ready
}
return false
}
type SyncStatusRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Unit string `protobuf:"bytes,1,opt,name=unit,proto3" json:"unit,omitempty"`
}
func (x *SyncStatusRequest) Reset() {
*x = SyncStatusRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SyncStatusRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SyncStatusRequest) ProtoMessage() {}
func (x *SyncStatusRequest) ProtoReflect() protoreflect.Message {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SyncStatusRequest.ProtoReflect.Descriptor instead.
func (*SyncStatusRequest) Descriptor() ([]byte, []int) {
return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{10}
}
func (x *SyncStatusRequest) GetUnit() string {
if x != nil {
return x.Unit
}
return ""
}
type DependencyInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Unit string `protobuf:"bytes,1,opt,name=unit,proto3" json:"unit,omitempty"`
DependsOn string `protobuf:"bytes,2,opt,name=depends_on,json=dependsOn,proto3" json:"depends_on,omitempty"`
RequiredStatus string `protobuf:"bytes,3,opt,name=required_status,json=requiredStatus,proto3" json:"required_status,omitempty"`
CurrentStatus string `protobuf:"bytes,4,opt,name=current_status,json=currentStatus,proto3" json:"current_status,omitempty"`
IsSatisfied bool `protobuf:"varint,5,opt,name=is_satisfied,json=isSatisfied,proto3" json:"is_satisfied,omitempty"`
}
func (x *DependencyInfo) Reset() {
*x = DependencyInfo{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DependencyInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DependencyInfo) ProtoMessage() {}
func (x *DependencyInfo) ProtoReflect() protoreflect.Message {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[11]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DependencyInfo.ProtoReflect.Descriptor instead.
func (*DependencyInfo) Descriptor() ([]byte, []int) {
return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{11}
}
func (x *DependencyInfo) GetUnit() string {
if x != nil {
return x.Unit
}
return ""
}
func (x *DependencyInfo) GetDependsOn() string {
if x != nil {
return x.DependsOn
}
return ""
}
func (x *DependencyInfo) GetRequiredStatus() string {
if x != nil {
return x.RequiredStatus
}
return ""
}
func (x *DependencyInfo) GetCurrentStatus() string {
if x != nil {
return x.CurrentStatus
}
return ""
}
func (x *DependencyInfo) GetIsSatisfied() bool {
if x != nil {
return x.IsSatisfied
}
return false
}
type SyncStatusResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
IsReady bool `protobuf:"varint,2,opt,name=is_ready,json=isReady,proto3" json:"is_ready,omitempty"`
Dependencies []*DependencyInfo `protobuf:"bytes,3,rep,name=dependencies,proto3" json:"dependencies,omitempty"`
}
func (x *SyncStatusResponse) Reset() {
*x = SyncStatusResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SyncStatusResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SyncStatusResponse) ProtoMessage() {}
func (x *SyncStatusResponse) ProtoReflect() protoreflect.Message {
mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[12]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SyncStatusResponse.ProtoReflect.Descriptor instead.
func (*SyncStatusResponse) Descriptor() ([]byte, []int) {
return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{12}
}
func (x *SyncStatusResponse) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
func (x *SyncStatusResponse) GetIsReady() bool {
if x != nil {
return x.IsReady
}
return false
}
func (x *SyncStatusResponse) GetDependencies() []*DependencyInfo {
if x != nil {
return x.Dependencies
}
return nil
}
var File_agent_agentsocket_proto_agentsocket_proto protoreflect.FileDescriptor
var file_agent_agentsocket_proto_agentsocket_proto_rawDesc = []byte{
0x0a, 0x29, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63,
0x6b, 0x65, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73,
0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x14, 0x63, 0x6f, 0x64,
0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x76,
0x31, 0x22, 0x0d, 0x0a, 0x0b, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x22, 0x0e, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x26, 0x0a, 0x10, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x04, 0x75, 0x6e, 0x69, 0x74, 0x22, 0x13, 0x0a, 0x11, 0x53, 0x79, 0x6e, 0x63,
0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x44, 0x0a,
0x0f, 0x53, 0x79, 0x6e, 0x63, 0x57, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x12, 0x12, 0x0a, 0x04, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x75, 0x6e, 0x69, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x73, 0x5f,
0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64,
0x73, 0x4f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x79, 0x6e, 0x63, 0x57, 0x61, 0x6e, 0x74, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0x0a, 0x13, 0x53, 0x79, 0x6e, 0x63, 0x43,
0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12,
0x0a, 0x04, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x6e,
0x69, 0x74, 0x22, 0x16, 0x0a, 0x14, 0x53, 0x79, 0x6e, 0x63, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65,
0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x26, 0x0a, 0x10, 0x53, 0x79,
0x6e, 0x63, 0x52, 0x65, 0x61, 0x64, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12,
0x0a, 0x04, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x6e,
0x69, 0x74, 0x22, 0x29, 0x0a, 0x11, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x61, 0x64, 0x79, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x61, 0x64, 0x79,
0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x72, 0x65, 0x61, 0x64, 0x79, 0x22, 0x27, 0x0a,
0x11, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x75, 0x6e, 0x69, 0x74, 0x22, 0xb6, 0x01, 0x0a, 0x0e, 0x44, 0x65, 0x70, 0x65, 0x6e,
0x64, 0x65, 0x6e, 0x63, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x6e, 0x69,
0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x6e, 0x69, 0x74, 0x12, 0x1d, 0x0a,
0x0a, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x73, 0x5f, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x73, 0x4f, 0x6e, 0x12, 0x27, 0x0a, 0x0f,
0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74,
0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63,
0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x0c,
0x69, 0x73, 0x5f, 0x73, 0x61, 0x74, 0x69, 0x73, 0x66, 0x69, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01,
0x28, 0x08, 0x52, 0x0b, 0x69, 0x73, 0x53, 0x61, 0x74, 0x69, 0x73, 0x66, 0x69, 0x65, 0x64, 0x22,
0x91, 0x01, 0x0a, 0x12, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x19,
0x0a, 0x08, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08,
0x52, 0x07, 0x69, 0x73, 0x52, 0x65, 0x61, 0x64, 0x79, 0x12, 0x48, 0x0a, 0x0c, 0x64, 0x65, 0x70,
0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63,
0x6b, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63,
0x79, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0c, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63,
0x69, 0x65, 0x73, 0x32, 0xbb, 0x04, 0x0a, 0x0b, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x6f, 0x63,
0x6b, 0x65, 0x74, 0x12, 0x4d, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x21, 0x2e, 0x63, 0x6f,
0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e,
0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22,
0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b,
0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x5c, 0x0a, 0x09, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12,
0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63,
0x6b, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x72, 0x74,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e,
0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53,
0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x59, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x57, 0x61, 0x6e, 0x74, 0x12, 0x25, 0x2e, 0x63,
0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74,
0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x57, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e,
0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x57,
0x61, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x65, 0x0a, 0x0c, 0x53,
0x79, 0x6e, 0x63, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x29, 0x2e, 0x63, 0x6f,
0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e,
0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61,
0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79,
0x6e, 0x63, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x5c, 0x0a, 0x09, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x61, 0x64, 0x79, 0x12,
0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63,
0x6b, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x61, 0x64, 0x79,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e,
0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53,
0x79, 0x6e, 0x63, 0x52, 0x65, 0x61, 0x64, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x5f, 0x0a, 0x0a, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x27,
0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b,
0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e,
0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53,
0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61,
0x67, 0x65, 0x6e, 0x74, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_agent_agentsocket_proto_agentsocket_proto_rawDescOnce sync.Once
file_agent_agentsocket_proto_agentsocket_proto_rawDescData = file_agent_agentsocket_proto_agentsocket_proto_rawDesc
)
func file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP() []byte {
file_agent_agentsocket_proto_agentsocket_proto_rawDescOnce.Do(func() {
file_agent_agentsocket_proto_agentsocket_proto_rawDescData = protoimpl.X.CompressGZIP(file_agent_agentsocket_proto_agentsocket_proto_rawDescData)
})
return file_agent_agentsocket_proto_agentsocket_proto_rawDescData
}
var file_agent_agentsocket_proto_agentsocket_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
var file_agent_agentsocket_proto_agentsocket_proto_goTypes = []interface{}{
(*PingRequest)(nil), // 0: coder.agentsocket.v1.PingRequest
(*PingResponse)(nil), // 1: coder.agentsocket.v1.PingResponse
(*SyncStartRequest)(nil), // 2: coder.agentsocket.v1.SyncStartRequest
(*SyncStartResponse)(nil), // 3: coder.agentsocket.v1.SyncStartResponse
(*SyncWantRequest)(nil), // 4: coder.agentsocket.v1.SyncWantRequest
(*SyncWantResponse)(nil), // 5: coder.agentsocket.v1.SyncWantResponse
(*SyncCompleteRequest)(nil), // 6: coder.agentsocket.v1.SyncCompleteRequest
(*SyncCompleteResponse)(nil), // 7: coder.agentsocket.v1.SyncCompleteResponse
(*SyncReadyRequest)(nil), // 8: coder.agentsocket.v1.SyncReadyRequest
(*SyncReadyResponse)(nil), // 9: coder.agentsocket.v1.SyncReadyResponse
(*SyncStatusRequest)(nil), // 10: coder.agentsocket.v1.SyncStatusRequest
(*DependencyInfo)(nil), // 11: coder.agentsocket.v1.DependencyInfo
(*SyncStatusResponse)(nil), // 12: coder.agentsocket.v1.SyncStatusResponse
}
var file_agent_agentsocket_proto_agentsocket_proto_depIdxs = []int32{
11, // 0: coder.agentsocket.v1.SyncStatusResponse.dependencies:type_name -> coder.agentsocket.v1.DependencyInfo
0, // 1: coder.agentsocket.v1.AgentSocket.Ping:input_type -> coder.agentsocket.v1.PingRequest
2, // 2: coder.agentsocket.v1.AgentSocket.SyncStart:input_type -> coder.agentsocket.v1.SyncStartRequest
4, // 3: coder.agentsocket.v1.AgentSocket.SyncWant:input_type -> coder.agentsocket.v1.SyncWantRequest
6, // 4: coder.agentsocket.v1.AgentSocket.SyncComplete:input_type -> coder.agentsocket.v1.SyncCompleteRequest
8, // 5: coder.agentsocket.v1.AgentSocket.SyncReady:input_type -> coder.agentsocket.v1.SyncReadyRequest
10, // 6: coder.agentsocket.v1.AgentSocket.SyncStatus:input_type -> coder.agentsocket.v1.SyncStatusRequest
1, // 7: coder.agentsocket.v1.AgentSocket.Ping:output_type -> coder.agentsocket.v1.PingResponse
3, // 8: coder.agentsocket.v1.AgentSocket.SyncStart:output_type -> coder.agentsocket.v1.SyncStartResponse
5, // 9: coder.agentsocket.v1.AgentSocket.SyncWant:output_type -> coder.agentsocket.v1.SyncWantResponse
7, // 10: coder.agentsocket.v1.AgentSocket.SyncComplete:output_type -> coder.agentsocket.v1.SyncCompleteResponse
9, // 11: coder.agentsocket.v1.AgentSocket.SyncReady:output_type -> coder.agentsocket.v1.SyncReadyResponse
12, // 12: coder.agentsocket.v1.AgentSocket.SyncStatus:output_type -> coder.agentsocket.v1.SyncStatusResponse
7, // [7:13] is the sub-list for method output_type
1, // [1:7] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_agent_agentsocket_proto_agentsocket_proto_init() }
func file_agent_agentsocket_proto_agentsocket_proto_init() {
if File_agent_agentsocket_proto_agentsocket_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_agent_agentsocket_proto_agentsocket_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PingRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_agentsocket_proto_agentsocket_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PingResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_agentsocket_proto_agentsocket_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SyncStartRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_agentsocket_proto_agentsocket_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SyncStartResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_agentsocket_proto_agentsocket_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SyncWantRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_agentsocket_proto_agentsocket_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SyncWantResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_agentsocket_proto_agentsocket_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SyncCompleteRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_agentsocket_proto_agentsocket_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SyncCompleteResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_agentsocket_proto_agentsocket_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SyncReadyRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_agentsocket_proto_agentsocket_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SyncReadyResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_agentsocket_proto_agentsocket_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SyncStatusRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_agentsocket_proto_agentsocket_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DependencyInfo); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_agentsocket_proto_agentsocket_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SyncStatusResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_agent_agentsocket_proto_agentsocket_proto_rawDesc,
NumEnums: 0,
NumMessages: 13,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_agent_agentsocket_proto_agentsocket_proto_goTypes,
DependencyIndexes: file_agent_agentsocket_proto_agentsocket_proto_depIdxs,
MessageInfos: file_agent_agentsocket_proto_agentsocket_proto_msgTypes,
}.Build()
File_agent_agentsocket_proto_agentsocket_proto = out.File
file_agent_agentsocket_proto_agentsocket_proto_rawDesc = nil
file_agent_agentsocket_proto_agentsocket_proto_goTypes = nil
file_agent_agentsocket_proto_agentsocket_proto_depIdxs = nil
}
+69
View File
@@ -0,0 +1,69 @@
syntax = "proto3";
option go_package = "github.com/coder/coder/v2/agent/agentsocket/proto";
package coder.agentsocket.v1;
message PingRequest {}
message PingResponse {}
message SyncStartRequest {
string unit = 1;
}
message SyncStartResponse {}
message SyncWantRequest {
string unit = 1;
string depends_on = 2;
}
message SyncWantResponse {}
message SyncCompleteRequest {
string unit = 1;
}
message SyncCompleteResponse {}
message SyncReadyRequest {
string unit = 1;
}
message SyncReadyResponse {
bool ready = 1;
}
message SyncStatusRequest {
string unit = 1;
}
message DependencyInfo {
string unit = 1;
string depends_on = 2;
string required_status = 3;
string current_status = 4;
bool is_satisfied = 5;
}
message SyncStatusResponse {
string status = 1;
bool is_ready = 2;
repeated DependencyInfo dependencies = 3;
}
// AgentSocket provides direct access to the agent over local IPC.
service AgentSocket {
// Ping the agent to check if it is alive.
rpc Ping(PingRequest) returns (PingResponse);
// Report the start of a unit.
rpc SyncStart(SyncStartRequest) returns (SyncStartResponse);
// Declare a dependency between units.
rpc SyncWant(SyncWantRequest) returns (SyncWantResponse);
// Report the completion of a unit.
rpc SyncComplete(SyncCompleteRequest) returns (SyncCompleteResponse);
// Request whether a unit is ready to be started. That is, all dependencies are satisfied.
rpc SyncReady(SyncReadyRequest) returns (SyncReadyResponse);
// Get the status of a unit and list its dependencies.
rpc SyncStatus(SyncStatusRequest) returns (SyncStatusResponse);
}
@@ -0,0 +1,311 @@
// Code generated by protoc-gen-go-drpc. DO NOT EDIT.
// protoc-gen-go-drpc version: v0.0.34
// source: agent/agentsocket/proto/agentsocket.proto
package proto
import (
context "context"
errors "errors"
protojson "google.golang.org/protobuf/encoding/protojson"
proto "google.golang.org/protobuf/proto"
drpc "storj.io/drpc"
drpcerr "storj.io/drpc/drpcerr"
)
type drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto struct{}
func (drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto) Marshal(msg drpc.Message) ([]byte, error) {
return proto.Marshal(msg.(proto.Message))
}
func (drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto) MarshalAppend(buf []byte, msg drpc.Message) ([]byte, error) {
return proto.MarshalOptions{}.MarshalAppend(buf, msg.(proto.Message))
}
func (drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto) Unmarshal(buf []byte, msg drpc.Message) error {
return proto.Unmarshal(buf, msg.(proto.Message))
}
func (drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto) JSONMarshal(msg drpc.Message) ([]byte, error) {
return protojson.Marshal(msg.(proto.Message))
}
func (drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto) JSONUnmarshal(buf []byte, msg drpc.Message) error {
return protojson.Unmarshal(buf, msg.(proto.Message))
}
type DRPCAgentSocketClient interface {
DRPCConn() drpc.Conn
Ping(ctx context.Context, in *PingRequest) (*PingResponse, error)
SyncStart(ctx context.Context, in *SyncStartRequest) (*SyncStartResponse, error)
SyncWant(ctx context.Context, in *SyncWantRequest) (*SyncWantResponse, error)
SyncComplete(ctx context.Context, in *SyncCompleteRequest) (*SyncCompleteResponse, error)
SyncReady(ctx context.Context, in *SyncReadyRequest) (*SyncReadyResponse, error)
SyncStatus(ctx context.Context, in *SyncStatusRequest) (*SyncStatusResponse, error)
}
type drpcAgentSocketClient struct {
cc drpc.Conn
}
func NewDRPCAgentSocketClient(cc drpc.Conn) DRPCAgentSocketClient {
return &drpcAgentSocketClient{cc}
}
func (c *drpcAgentSocketClient) DRPCConn() drpc.Conn { return c.cc }
func (c *drpcAgentSocketClient) Ping(ctx context.Context, in *PingRequest) (*PingResponse, error) {
out := new(PingResponse)
err := c.cc.Invoke(ctx, "/coder.agentsocket.v1.AgentSocket/Ping", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *drpcAgentSocketClient) SyncStart(ctx context.Context, in *SyncStartRequest) (*SyncStartResponse, error) {
out := new(SyncStartResponse)
err := c.cc.Invoke(ctx, "/coder.agentsocket.v1.AgentSocket/SyncStart", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *drpcAgentSocketClient) SyncWant(ctx context.Context, in *SyncWantRequest) (*SyncWantResponse, error) {
out := new(SyncWantResponse)
err := c.cc.Invoke(ctx, "/coder.agentsocket.v1.AgentSocket/SyncWant", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *drpcAgentSocketClient) SyncComplete(ctx context.Context, in *SyncCompleteRequest) (*SyncCompleteResponse, error) {
out := new(SyncCompleteResponse)
err := c.cc.Invoke(ctx, "/coder.agentsocket.v1.AgentSocket/SyncComplete", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *drpcAgentSocketClient) SyncReady(ctx context.Context, in *SyncReadyRequest) (*SyncReadyResponse, error) {
out := new(SyncReadyResponse)
err := c.cc.Invoke(ctx, "/coder.agentsocket.v1.AgentSocket/SyncReady", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *drpcAgentSocketClient) SyncStatus(ctx context.Context, in *SyncStatusRequest) (*SyncStatusResponse, error) {
out := new(SyncStatusResponse)
err := c.cc.Invoke(ctx, "/coder.agentsocket.v1.AgentSocket/SyncStatus", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
type DRPCAgentSocketServer interface {
Ping(context.Context, *PingRequest) (*PingResponse, error)
SyncStart(context.Context, *SyncStartRequest) (*SyncStartResponse, error)
SyncWant(context.Context, *SyncWantRequest) (*SyncWantResponse, error)
SyncComplete(context.Context, *SyncCompleteRequest) (*SyncCompleteResponse, error)
SyncReady(context.Context, *SyncReadyRequest) (*SyncReadyResponse, error)
SyncStatus(context.Context, *SyncStatusRequest) (*SyncStatusResponse, error)
}
type DRPCAgentSocketUnimplementedServer struct{}
func (s *DRPCAgentSocketUnimplementedServer) Ping(context.Context, *PingRequest) (*PingResponse, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentSocketUnimplementedServer) SyncStart(context.Context, *SyncStartRequest) (*SyncStartResponse, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentSocketUnimplementedServer) SyncWant(context.Context, *SyncWantRequest) (*SyncWantResponse, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentSocketUnimplementedServer) SyncComplete(context.Context, *SyncCompleteRequest) (*SyncCompleteResponse, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentSocketUnimplementedServer) SyncReady(context.Context, *SyncReadyRequest) (*SyncReadyResponse, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentSocketUnimplementedServer) SyncStatus(context.Context, *SyncStatusRequest) (*SyncStatusResponse, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
type DRPCAgentSocketDescription struct{}
func (DRPCAgentSocketDescription) NumMethods() int { return 6 }
func (DRPCAgentSocketDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) {
switch n {
case 0:
return "/coder.agentsocket.v1.AgentSocket/Ping", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentSocketServer).
Ping(
ctx,
in1.(*PingRequest),
)
}, DRPCAgentSocketServer.Ping, true
case 1:
return "/coder.agentsocket.v1.AgentSocket/SyncStart", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentSocketServer).
SyncStart(
ctx,
in1.(*SyncStartRequest),
)
}, DRPCAgentSocketServer.SyncStart, true
case 2:
return "/coder.agentsocket.v1.AgentSocket/SyncWant", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentSocketServer).
SyncWant(
ctx,
in1.(*SyncWantRequest),
)
}, DRPCAgentSocketServer.SyncWant, true
case 3:
return "/coder.agentsocket.v1.AgentSocket/SyncComplete", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentSocketServer).
SyncComplete(
ctx,
in1.(*SyncCompleteRequest),
)
}, DRPCAgentSocketServer.SyncComplete, true
case 4:
return "/coder.agentsocket.v1.AgentSocket/SyncReady", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentSocketServer).
SyncReady(
ctx,
in1.(*SyncReadyRequest),
)
}, DRPCAgentSocketServer.SyncReady, true
case 5:
return "/coder.agentsocket.v1.AgentSocket/SyncStatus", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentSocketServer).
SyncStatus(
ctx,
in1.(*SyncStatusRequest),
)
}, DRPCAgentSocketServer.SyncStatus, true
default:
return "", nil, nil, nil, false
}
}
func DRPCRegisterAgentSocket(mux drpc.Mux, impl DRPCAgentSocketServer) error {
return mux.Register(impl, DRPCAgentSocketDescription{})
}
type DRPCAgentSocket_PingStream interface {
drpc.Stream
SendAndClose(*PingResponse) error
}
type drpcAgentSocket_PingStream struct {
drpc.Stream
}
func (x *drpcAgentSocket_PingStream) SendAndClose(m *PingResponse) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}); err != nil {
return err
}
return x.CloseSend()
}
type DRPCAgentSocket_SyncStartStream interface {
drpc.Stream
SendAndClose(*SyncStartResponse) error
}
type drpcAgentSocket_SyncStartStream struct {
drpc.Stream
}
func (x *drpcAgentSocket_SyncStartStream) SendAndClose(m *SyncStartResponse) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}); err != nil {
return err
}
return x.CloseSend()
}
type DRPCAgentSocket_SyncWantStream interface {
drpc.Stream
SendAndClose(*SyncWantResponse) error
}
type drpcAgentSocket_SyncWantStream struct {
drpc.Stream
}
func (x *drpcAgentSocket_SyncWantStream) SendAndClose(m *SyncWantResponse) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}); err != nil {
return err
}
return x.CloseSend()
}
type DRPCAgentSocket_SyncCompleteStream interface {
drpc.Stream
SendAndClose(*SyncCompleteResponse) error
}
type drpcAgentSocket_SyncCompleteStream struct {
drpc.Stream
}
func (x *drpcAgentSocket_SyncCompleteStream) SendAndClose(m *SyncCompleteResponse) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}); err != nil {
return err
}
return x.CloseSend()
}
type DRPCAgentSocket_SyncReadyStream interface {
drpc.Stream
SendAndClose(*SyncReadyResponse) error
}
type drpcAgentSocket_SyncReadyStream struct {
drpc.Stream
}
func (x *drpcAgentSocket_SyncReadyStream) SendAndClose(m *SyncReadyResponse) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}); err != nil {
return err
}
return x.CloseSend()
}
type DRPCAgentSocket_SyncStatusStream interface {
drpc.Stream
SendAndClose(*SyncStatusResponse) error
}
type drpcAgentSocket_SyncStatusStream struct {
drpc.Stream
}
func (x *drpcAgentSocket_SyncStatusStream) SendAndClose(m *SyncStatusResponse) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}); err != nil {
return err
}
return x.CloseSend()
}
+17
View File
@@ -0,0 +1,17 @@
package proto
import "github.com/coder/coder/v2/apiversion"
// Version history:
//
// API v1.0:
// - Initial release
// - Ping
// - Sync operations: SyncStart, SyncWant, SyncComplete, SyncWait, SyncStatus
const (
CurrentMajor = 1
CurrentMinor = 0
)
var CurrentVersion = apiversion.New(CurrentMajor, CurrentMinor)
+185
View File
@@ -0,0 +1,185 @@
package agentsocket
import (
"context"
"errors"
"net"
"sync"
"golang.org/x/xerrors"
"github.com/hashicorp/yamux"
"storj.io/drpc/drpcmux"
"storj.io/drpc/drpcserver"
"cdr.dev/slog"
"github.com/coder/coder/v2/agent/agentsocket/proto"
"github.com/coder/coder/v2/agent/unit"
"github.com/coder/coder/v2/codersdk/drpcsdk"
)
// Server provides access to the DRPCAgentSocketService via a Unix domain socket.
// Do not invoke Server{} directly. Use NewServer() instead.
type Server struct {
logger slog.Logger
path string
drpcServer *drpcserver.Server
service *DRPCAgentSocketService
mu sync.Mutex
listener net.Listener
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
}
func NewServer(path string, logger slog.Logger) (*Server, error) {
logger = logger.Named("agentsocket-server")
server := &Server{
logger: logger,
path: path,
service: &DRPCAgentSocketService{
logger: logger,
unitManager: unit.NewManager(),
},
}
mux := drpcmux.New()
err := proto.DRPCRegisterAgentSocket(mux, server.service)
if err != nil {
return nil, xerrors.Errorf("failed to register drpc service: %w", err)
}
server.drpcServer = drpcserver.NewWithOptions(mux, drpcserver.Options{
Manager: drpcsdk.DefaultDRPCOptions(nil),
Log: func(err error) {
if errors.Is(err, context.Canceled) ||
errors.Is(err, context.DeadlineExceeded) {
return
}
logger.Debug(context.Background(), "drpc server error", slog.Error(err))
},
})
if server.path == "" {
var err error
server.path, err = getDefaultSocketPath()
if err != nil {
return nil, xerrors.Errorf("get default socket path: %w", err)
}
}
listener, err := createSocket(server.path)
if err != nil {
return nil, xerrors.Errorf("create socket: %w", err)
}
server.listener = listener
// This context is canceled by server.Close().
// canceling it will close all connections.
server.ctx, server.cancel = context.WithCancel(context.Background())
server.logger.Info(server.ctx, "agent socket server started", slog.F("path", server.path))
server.wg.Add(1)
go func() {
defer server.wg.Done()
server.acceptConnections()
}()
return server, nil
}
func (s *Server) Close() error {
s.mu.Lock()
if s.listener == nil {
s.mu.Unlock()
return nil
}
s.logger.Info(s.ctx, "stopping agent socket server")
s.cancel()
if err := s.listener.Close(); err != nil {
s.logger.Warn(s.ctx, "error closing socket listener", slog.Error(err))
}
s.listener = nil
s.mu.Unlock()
// Wait for all connections to finish
s.wg.Wait()
if err := cleanupSocket(s.path); err != nil {
s.logger.Warn(s.ctx, "error cleaning up socket file", slog.Error(err))
}
s.logger.Info(s.ctx, "agent socket server stopped")
return nil
}
func (s *Server) acceptConnections() {
// In an edge case, Close() might race with acceptConnections() and set s.listener to nil.
// Therefore, we grab a copy of the listener under a lock. We might still get a nil listener,
// but then we know close has already run and we can return early.
s.mu.Lock()
listener := s.listener
s.mu.Unlock()
if listener == nil {
return
}
for {
select {
case <-s.ctx.Done():
return
default:
}
conn, err := listener.Accept()
if err != nil {
s.logger.Warn(s.ctx, "error accepting connection", slog.Error(err))
continue
}
s.mu.Lock()
if s.listener == nil {
s.mu.Unlock()
_ = conn.Close()
return
}
s.wg.Add(1)
s.mu.Unlock()
go func() {
defer s.wg.Done()
s.handleConnection(conn)
}()
}
}
func (s *Server) handleConnection(conn net.Conn) {
defer conn.Close()
s.logger.Debug(s.ctx, "new connection accepted", slog.F("remote_addr", conn.RemoteAddr()))
config := yamux.DefaultConfig()
config.LogOutput = nil
config.Logger = slog.Stdlib(s.ctx, s.logger.Named("agentsocket-yamux"), slog.LevelInfo)
session, err := yamux.Server(conn, config)
if err != nil {
s.logger.Warn(s.ctx, "failed to create yamux session", slog.Error(err))
return
}
defer session.Close()
err = s.drpcServer.Serve(s.ctx, session)
if err != nil {
s.logger.Debug(s.ctx, "drpc server finished", slog.Error(err))
}
}
+52
View File
@@ -0,0 +1,52 @@
package agentsocket_test
import (
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/require"
"cdr.dev/slog"
"github.com/coder/coder/v2/agent/agentsocket"
)
func TestServer(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("agentsocket is not supported on Windows")
}
t.Run("StartStop", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(t.TempDir(), "test.sock")
logger := slog.Make().Leveled(slog.LevelDebug)
server, err := agentsocket.NewServer(socketPath, logger)
require.NoError(t, err)
require.NoError(t, server.Close())
})
t.Run("AlreadyStarted", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(t.TempDir(), "test.sock")
logger := slog.Make().Leveled(slog.LevelDebug)
server1, err := agentsocket.NewServer(socketPath, logger)
require.NoError(t, err)
defer server1.Close()
_, err = agentsocket.NewServer(socketPath, logger)
require.ErrorContains(t, err, "create socket")
})
t.Run("AutoSocketPath", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(t.TempDir(), "test.sock")
logger := slog.Make().Leveled(slog.LevelDebug)
server, err := agentsocket.NewServer(socketPath, logger)
require.NoError(t, err)
require.NoError(t, server.Close())
})
}
+142
View File
@@ -0,0 +1,142 @@
package agentsocket
import (
"context"
"errors"
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/v2/agent/agentsocket/proto"
"github.com/coder/coder/v2/agent/unit"
)
var _ proto.DRPCAgentSocketServer = (*DRPCAgentSocketService)(nil)
var ErrUnitManagerNotAvailable = xerrors.New("unit manager not available")
type DRPCAgentSocketService struct {
unitManager *unit.Manager
logger slog.Logger
}
func (*DRPCAgentSocketService) Ping(_ context.Context, _ *proto.PingRequest) (*proto.PingResponse, error) {
return &proto.PingResponse{}, nil
}
func (s *DRPCAgentSocketService) SyncStart(_ context.Context, req *proto.SyncStartRequest) (*proto.SyncStartResponse, error) {
if s.unitManager == nil {
return nil, xerrors.Errorf("SyncStart: %w", ErrUnitManagerNotAvailable)
}
unitID := unit.ID(req.Unit)
if err := s.unitManager.Register(unitID); err != nil {
if !errors.Is(err, unit.ErrUnitAlreadyRegistered) {
return nil, xerrors.Errorf("SyncStart: %w", err)
}
}
isReady, err := s.unitManager.IsReady(unitID)
if err != nil {
return nil, xerrors.Errorf("cannot check readiness: %w", err)
}
if !isReady {
return nil, xerrors.Errorf("cannot start unit %q: unit not ready", req.Unit)
}
err = s.unitManager.UpdateStatus(unitID, unit.StatusStarted)
if err != nil {
return nil, xerrors.Errorf("cannot start unit %q: %w", req.Unit, err)
}
return &proto.SyncStartResponse{}, nil
}
func (s *DRPCAgentSocketService) SyncWant(_ context.Context, req *proto.SyncWantRequest) (*proto.SyncWantResponse, error) {
if s.unitManager == nil {
return nil, xerrors.Errorf("cannot add dependency: %w", ErrUnitManagerNotAvailable)
}
unitID := unit.ID(req.Unit)
dependsOnID := unit.ID(req.DependsOn)
if err := s.unitManager.Register(unitID); err != nil && !errors.Is(err, unit.ErrUnitAlreadyRegistered) {
return nil, xerrors.Errorf("cannot add dependency: %w", err)
}
if err := s.unitManager.AddDependency(unitID, dependsOnID, unit.StatusComplete); err != nil {
return nil, xerrors.Errorf("cannot add dependency: %w", err)
}
return &proto.SyncWantResponse{}, nil
}
func (s *DRPCAgentSocketService) SyncComplete(_ context.Context, req *proto.SyncCompleteRequest) (*proto.SyncCompleteResponse, error) {
if s.unitManager == nil {
return nil, xerrors.Errorf("cannot complete unit: %w", ErrUnitManagerNotAvailable)
}
unitID := unit.ID(req.Unit)
if err := s.unitManager.UpdateStatus(unitID, unit.StatusComplete); err != nil {
return nil, xerrors.Errorf("cannot complete unit %q: %w", req.Unit, err)
}
return &proto.SyncCompleteResponse{}, nil
}
func (s *DRPCAgentSocketService) SyncReady(_ context.Context, req *proto.SyncReadyRequest) (*proto.SyncReadyResponse, error) {
if s.unitManager == nil {
return nil, xerrors.Errorf("cannot check readiness: %w", ErrUnitManagerNotAvailable)
}
unitID := unit.ID(req.Unit)
isReady, err := s.unitManager.IsReady(unitID)
if err != nil {
return nil, xerrors.Errorf("cannot check readiness: %w", err)
}
return &proto.SyncReadyResponse{
Ready: isReady,
}, nil
}
func (s *DRPCAgentSocketService) SyncStatus(_ context.Context, req *proto.SyncStatusRequest) (*proto.SyncStatusResponse, error) {
if s.unitManager == nil {
return nil, xerrors.Errorf("cannot get status for unit %q: %w", req.Unit, ErrUnitManagerNotAvailable)
}
unitID := unit.ID(req.Unit)
isReady, err := s.unitManager.IsReady(unitID)
if err != nil {
return nil, xerrors.Errorf("cannot check readiness: %w", err)
}
dependencies, err := s.unitManager.GetAllDependencies(unitID)
if err != nil {
return nil, xerrors.Errorf("failed to get dependencies: %w", err)
}
var depInfos []*proto.DependencyInfo
for _, dep := range dependencies {
depInfos = append(depInfos, &proto.DependencyInfo{
Unit: string(dep.Unit),
DependsOn: string(dep.DependsOn),
RequiredStatus: string(dep.RequiredStatus),
CurrentStatus: string(dep.CurrentStatus),
IsSatisfied: dep.IsSatisfied,
})
}
u, err := s.unitManager.Unit(unitID)
if err != nil {
return nil, xerrors.Errorf("cannot get status for unit %q: %w", req.Unit, err)
}
return &proto.SyncStatusResponse{
Status: string(u.Status()),
IsReady: isReady,
Dependencies: depInfos,
}, nil
}
+470
View File
@@ -0,0 +1,470 @@
package agentsocket_test
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/hashicorp/yamux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cdr.dev/slog"
"github.com/coder/coder/v2/agent/agentsocket"
"github.com/coder/coder/v2/agent/agentsocket/proto"
"github.com/coder/coder/v2/agent/unit"
"github.com/coder/coder/v2/codersdk/drpcsdk"
)
// tempDirUnixSocket returns a temporary directory that can safely hold unix
// sockets (probably).
//
// During tests on darwin we hit the max path length limit for unix sockets
// pretty easily in the default location, so this function uses /tmp instead to
// get shorter paths. To keep paths short, we use a hash of the test name
// instead of the full test name.
func tempDirUnixSocket(t *testing.T) string {
t.Helper()
if runtime.GOOS == "darwin" {
// Use a short hash of the test name to keep the path under 104 chars
hash := sha256.Sum256([]byte(t.Name()))
hashStr := hex.EncodeToString(hash[:])[:8] // Use first 8 chars of hash
dir, err := os.MkdirTemp("/tmp", fmt.Sprintf("c-%s-", hashStr))
require.NoError(t, err, "create temp dir for unix socket test")
t.Cleanup(func() {
err := os.RemoveAll(dir)
assert.NoError(t, err, "remove temp dir", dir)
})
return dir
}
return t.TempDir()
}
// newSocketClient creates a DRPC client connected to the Unix socket at the given path.
func newSocketClient(t *testing.T, socketPath string) proto.DRPCAgentSocketClient {
t.Helper()
conn, err := net.Dial("unix", socketPath)
require.NoError(t, err)
config := yamux.DefaultConfig()
config.Logger = nil
session, err := yamux.Client(conn, config)
require.NoError(t, err)
client := proto.NewDRPCAgentSocketClient(drpcsdk.MultiplexedConn(session))
t.Cleanup(func() {
_ = session.Close()
_ = conn.Close()
})
return client
}
func TestDRPCAgentSocketService(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("agentsocket is not supported on Windows")
}
t.Run("Ping", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(tempDirUnixSocket(t), "test.sock")
server, err := agentsocket.NewServer(
socketPath,
slog.Make().Leveled(slog.LevelDebug),
)
require.NoError(t, err)
defer server.Close()
client := newSocketClient(t, socketPath)
_, err = client.Ping(context.Background(), &proto.PingRequest{})
require.NoError(t, err)
})
t.Run("SyncStart", func(t *testing.T) {
t.Parallel()
t.Run("NewUnit", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(tempDirUnixSocket(t), "test.sock")
server, err := agentsocket.NewServer(
socketPath,
slog.Make().Leveled(slog.LevelDebug),
)
require.NoError(t, err)
defer server.Close()
client := newSocketClient(t, socketPath)
_, err = client.SyncStart(context.Background(), &proto.SyncStartRequest{
Unit: "test-unit",
})
require.NoError(t, err)
status, err := client.SyncStatus(context.Background(), &proto.SyncStatusRequest{
Unit: "test-unit",
})
require.NoError(t, err)
require.Equal(t, "started", status.Status)
})
t.Run("UnitAlreadyStarted", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(tempDirUnixSocket(t), "test.sock")
server, err := agentsocket.NewServer(
socketPath,
slog.Make().Leveled(slog.LevelDebug),
)
require.NoError(t, err)
defer server.Close()
client := newSocketClient(t, socketPath)
// First Start
_, err = client.SyncStart(context.Background(), &proto.SyncStartRequest{
Unit: "test-unit",
})
require.NoError(t, err)
status, err := client.SyncStatus(context.Background(), &proto.SyncStatusRequest{
Unit: "test-unit",
})
require.NoError(t, err)
require.Equal(t, "started", status.Status)
// Second Start
_, err = client.SyncStart(context.Background(), &proto.SyncStartRequest{
Unit: "test-unit",
})
require.ErrorContains(t, err, unit.ErrSameStatusAlreadySet.Error())
status, err = client.SyncStatus(context.Background(), &proto.SyncStatusRequest{
Unit: "test-unit",
})
require.NoError(t, err)
require.Equal(t, "started", status.Status)
})
t.Run("UnitAlreadyCompleted", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(tempDirUnixSocket(t), "test.sock")
server, err := agentsocket.NewServer(
socketPath,
slog.Make().Leveled(slog.LevelDebug),
)
require.NoError(t, err)
defer server.Close()
client := newSocketClient(t, socketPath)
// First start
_, err = client.SyncStart(context.Background(), &proto.SyncStartRequest{
Unit: "test-unit",
})
require.NoError(t, err)
status, err := client.SyncStatus(context.Background(), &proto.SyncStatusRequest{
Unit: "test-unit",
})
require.NoError(t, err)
require.Equal(t, "started", status.Status)
// Complete the unit
_, err = client.SyncComplete(context.Background(), &proto.SyncCompleteRequest{
Unit: "test-unit",
})
require.NoError(t, err)
status, err = client.SyncStatus(context.Background(), &proto.SyncStatusRequest{
Unit: "test-unit",
})
require.NoError(t, err)
require.Equal(t, "completed", status.Status)
// Second start
_, err = client.SyncStart(context.Background(), &proto.SyncStartRequest{
Unit: "test-unit",
})
require.NoError(t, err)
status, err = client.SyncStatus(context.Background(), &proto.SyncStatusRequest{
Unit: "test-unit",
})
require.NoError(t, err)
require.Equal(t, "started", status.Status)
})
t.Run("UnitNotReady", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(tempDirUnixSocket(t), "test.sock")
server, err := agentsocket.NewServer(
socketPath,
slog.Make().Leveled(slog.LevelDebug),
)
require.NoError(t, err)
defer server.Close()
client := newSocketClient(t, socketPath)
_, err = client.SyncWant(context.Background(), &proto.SyncWantRequest{
Unit: "test-unit",
DependsOn: "dependency-unit",
})
require.NoError(t, err)
_, err = client.SyncStart(context.Background(), &proto.SyncStartRequest{
Unit: "test-unit",
})
require.ErrorContains(t, err, "unit not ready")
status, err := client.SyncStatus(context.Background(), &proto.SyncStatusRequest{
Unit: "test-unit",
})
require.NoError(t, err)
require.Equal(t, string(unit.StatusPending), status.Status)
require.False(t, status.IsReady)
})
})
t.Run("SyncWant", func(t *testing.T) {
t.Parallel()
t.Run("NewUnits", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(tempDirUnixSocket(t), "test.sock")
server, err := agentsocket.NewServer(
socketPath,
slog.Make().Leveled(slog.LevelDebug),
)
require.NoError(t, err)
defer server.Close()
client := newSocketClient(t, socketPath)
// If dependency units are not registered, they are registered automatically
_, err = client.SyncWant(context.Background(), &proto.SyncWantRequest{
Unit: "test-unit",
DependsOn: "dependency-unit",
})
require.NoError(t, err)
status, err := client.SyncStatus(context.Background(), &proto.SyncStatusRequest{
Unit: "test-unit",
})
require.NoError(t, err)
require.Len(t, status.Dependencies, 1)
require.Equal(t, "dependency-unit", status.Dependencies[0].DependsOn)
require.Equal(t, "completed", status.Dependencies[0].RequiredStatus)
})
t.Run("DependencyAlreadyRegistered", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(tempDirUnixSocket(t), "test.sock")
server, err := agentsocket.NewServer(
socketPath,
slog.Make().Leveled(slog.LevelDebug),
)
require.NoError(t, err)
defer server.Close()
client := newSocketClient(t, socketPath)
// Start the dependency unit
_, err = client.SyncStart(context.Background(), &proto.SyncStartRequest{
Unit: "dependency-unit",
})
require.NoError(t, err)
status, err := client.SyncStatus(context.Background(), &proto.SyncStatusRequest{
Unit: "dependency-unit",
})
require.NoError(t, err)
require.Equal(t, "started", status.Status)
// Add the dependency after the dependency unit has already started
_, err = client.SyncWant(context.Background(), &proto.SyncWantRequest{
Unit: "test-unit",
DependsOn: "dependency-unit",
})
// Dependencies can be added even if the dependency unit has already started
require.NoError(t, err)
// The dependency is now reflected in the test unit's status
status, err = client.SyncStatus(context.Background(), &proto.SyncStatusRequest{
Unit: "test-unit",
})
require.NoError(t, err)
require.Equal(t, "dependency-unit", status.Dependencies[0].DependsOn)
require.Equal(t, "completed", status.Dependencies[0].RequiredStatus)
})
t.Run("DependencyAddedAfterDependentStarted", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(tempDirUnixSocket(t), "test.sock")
server, err := agentsocket.NewServer(
socketPath,
slog.Make().Leveled(slog.LevelDebug),
)
require.NoError(t, err)
defer server.Close()
client := newSocketClient(t, socketPath)
// Start the dependent unit
_, err = client.SyncStart(context.Background(), &proto.SyncStartRequest{
Unit: "test-unit",
})
require.NoError(t, err)
status, err := client.SyncStatus(context.Background(), &proto.SyncStatusRequest{
Unit: "test-unit",
})
require.NoError(t, err)
require.Equal(t, "started", status.Status)
// Add the dependency after the dependency unit has already started
_, err = client.SyncWant(context.Background(), &proto.SyncWantRequest{
Unit: "test-unit",
DependsOn: "dependency-unit",
})
// Dependencies can be added even if the dependent unit has already started.
// The dependency applies the next time a unit is started. The current status is not updated.
// This is to allow flexible dependency management. It does mean that users of this API should
// take care to add dependencies before they start their dependent units.
require.NoError(t, err)
// The dependency is now reflected in the test unit's status
status, err = client.SyncStatus(context.Background(), &proto.SyncStatusRequest{
Unit: "test-unit",
})
require.NoError(t, err)
require.Equal(t, "dependency-unit", status.Dependencies[0].DependsOn)
require.Equal(t, "completed", status.Dependencies[0].RequiredStatus)
})
})
t.Run("SyncReady", func(t *testing.T) {
t.Parallel()
t.Run("UnregisteredUnit", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(tempDirUnixSocket(t), "test.sock")
server, err := agentsocket.NewServer(
socketPath,
slog.Make().Leveled(slog.LevelDebug),
)
require.NoError(t, err)
defer server.Close()
client := newSocketClient(t, socketPath)
response, err := client.SyncReady(context.Background(), &proto.SyncReadyRequest{
Unit: "unregistered-unit",
})
require.NoError(t, err)
require.False(t, response.Ready)
})
t.Run("UnitNotReady", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(tempDirUnixSocket(t), "test.sock")
server, err := agentsocket.NewServer(
socketPath,
slog.Make().Leveled(slog.LevelDebug),
)
require.NoError(t, err)
defer server.Close()
client := newSocketClient(t, socketPath)
// Register a unit with an unsatisfied dependency
_, err = client.SyncWant(context.Background(), &proto.SyncWantRequest{
Unit: "test-unit",
DependsOn: "dependency-unit",
})
require.NoError(t, err)
// Check readiness - should be false because dependency is not satisfied
response, err := client.SyncReady(context.Background(), &proto.SyncReadyRequest{
Unit: "test-unit",
})
require.NoError(t, err)
require.False(t, response.Ready)
})
t.Run("UnitReady", func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(tempDirUnixSocket(t), "test.sock")
server, err := agentsocket.NewServer(
socketPath,
slog.Make().Leveled(slog.LevelDebug),
)
require.NoError(t, err)
defer server.Close()
client := newSocketClient(t, socketPath)
// Register a unit with no dependencies - should be ready immediately
_, err = client.SyncStart(context.Background(), &proto.SyncStartRequest{
Unit: "test-unit",
})
require.NoError(t, err)
// Check readiness - should be true
_, err = client.SyncReady(context.Background(), &proto.SyncReadyRequest{
Unit: "test-unit",
})
require.NoError(t, err)
// Also test a unit with satisfied dependencies
_, err = client.SyncWant(context.Background(), &proto.SyncWantRequest{
Unit: "dependent-unit",
DependsOn: "test-unit",
})
require.NoError(t, err)
// Complete the dependency
_, err = client.SyncComplete(context.Background(), &proto.SyncCompleteRequest{
Unit: "test-unit",
})
require.NoError(t, err)
// Now dependent-unit should be ready
_, err = client.SyncReady(context.Background(), &proto.SyncReadyRequest{
Unit: "dependent-unit",
})
require.NoError(t, err)
})
})
}
+83
View File
@@ -0,0 +1,83 @@
//go:build !windows
package agentsocket
import (
"crypto/rand"
"encoding/hex"
"net"
"os"
"path/filepath"
"time"
"golang.org/x/xerrors"
)
// createSocket creates a Unix domain socket listener
func createSocket(path string) (net.Listener, error) {
if !isSocketAvailable(path) {
return nil, xerrors.Errorf("socket path %s is not available", path)
}
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return nil, xerrors.Errorf("remove existing socket: %w", err)
}
// Create parent directory if it doesn't exist
parentDir := filepath.Dir(path)
if err := os.MkdirAll(parentDir, 0o700); err != nil {
return nil, xerrors.Errorf("create socket directory: %w", err)
}
listener, err := net.Listen("unix", path)
if err != nil {
return nil, xerrors.Errorf("listen on unix socket: %w", err)
}
if err := os.Chmod(path, 0o600); err != nil {
_ = listener.Close()
return nil, xerrors.Errorf("set socket permissions: %w", err)
}
return listener, nil
}
// getDefaultSocketPath returns the default socket path for Unix-like systems
func getDefaultSocketPath() (string, error) {
randomBytes := make([]byte, 4)
if _, err := rand.Read(randomBytes); err != nil {
return "", xerrors.Errorf("generate random socket name: %w", err)
}
randomSuffix := hex.EncodeToString(randomBytes)
// Try XDG_RUNTIME_DIR first
if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" {
return filepath.Join(runtimeDir, "coder-agent-"+randomSuffix+".sock"), nil
}
return filepath.Join("/tmp", "coder-agent-"+randomSuffix+".sock"), nil
}
// CleanupSocket removes the socket file
func cleanupSocket(path string) error {
return os.Remove(path)
}
// isSocketAvailable checks if a socket path is available for use
func isSocketAvailable(path string) bool {
// Check if file exists
if _, err := os.Stat(path); os.IsNotExist(err) {
return true
}
// Try to connect to see if it's actually listening
dialer := net.Dialer{Timeout: 10 * time.Second}
conn, err := dialer.Dial("unix", path)
if err != nil {
// If we can't connect, the socket is not in use
// Socket is available for use
return true
}
_ = conn.Close()
// Socket is in use
return false
}
+27
View File
@@ -0,0 +1,27 @@
//go:build windows
package agentsocket
import (
"net"
"golang.org/x/xerrors"
)
// createSocket returns an error indicating that agentsocket is not supported on Windows.
// This feature is unix-only in its current experimental state.
func createSocket(_ string) (net.Listener, error) {
return nil, xerrors.New("agentsocket is not supported on Windows")
}
// getDefaultSocketPath returns an error indicating that agentsocket is not supported on Windows.
// This feature is unix-only in its current experimental state.
func getDefaultSocketPath() (string, error) {
return "", xerrors.New("agentsocket is not supported on Windows")
}
// cleanupSocket is a no-op on Windows since agentsocket is not supported.
func cleanupSocket(_ string) error {
// No-op since agentsocket is not supported on Windows
return nil
}
+1 -1
View File
@@ -58,7 +58,7 @@ func (g *Graph[EdgeType, VertexType]) AddEdge(from, to VertexType, edge EdgeType
toID := g.getOrCreateVertexID(to)
if g.canReach(to, from) {
return xerrors.Errorf("adding edge (%v -> %v) would create a cycle", from, to)
return xerrors.Errorf("adding edge (%v -> %v): %w", from, to, ErrCycleDetected)
}
g.gonumGraph.SetEdge(simple.Edge{F: simple.Node(fromID), T: simple.Node(toID)})
+3 -5
View File
@@ -148,8 +148,7 @@ func TestGraph(t *testing.T) {
graph := &testGraph{}
unit1 := &testGraphVertex{Name: "unit1"}
err := graph.AddEdge(unit1, unit1, testEdgeCompleted)
require.Error(t, err)
require.ErrorContains(t, err, fmt.Sprintf("adding edge (%v -> %v) would create a cycle", unit1, unit1))
require.ErrorIs(t, err, unit.ErrCycleDetected)
return graph
},
@@ -160,8 +159,7 @@ func TestGraph(t *testing.T) {
err := graph.AddEdge(unit1, unit2, testEdgeCompleted)
require.NoError(t, err)
err = graph.AddEdge(unit2, unit1, testEdgeStarted)
require.Error(t, err)
require.ErrorContains(t, err, fmt.Sprintf("adding edge (%v -> %v) would create a cycle", unit2, unit1))
require.ErrorIs(t, err, unit.ErrCycleDetected)
return graph
},
@@ -341,7 +339,7 @@ func TestGraphThreadSafety(t *testing.T) {
// Verify all attempts correctly returned cycle error
for i, err := range cycleErrors {
require.Error(t, err, "goroutine %d should have detected cycle", i)
require.Contains(t, err.Error(), "would create a cycle")
require.ErrorIs(t, err, unit.ErrCycleDetected)
}
// Verify graph remains valid (original chain intact)
+280
View File
@@ -0,0 +1,280 @@
package unit
import (
"errors"
"sync"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/util/slice"
)
var (
ErrUnitIDRequired = xerrors.New("unit name is required")
ErrUnitNotFound = xerrors.New("unit not found")
ErrUnitAlreadyRegistered = xerrors.New("unit already registered")
ErrCannotUpdateOtherUnit = xerrors.New("cannot update other unit's status")
ErrDependenciesNotSatisfied = xerrors.New("unit dependencies not satisfied")
ErrSameStatusAlreadySet = xerrors.New("same status already set")
ErrCycleDetected = xerrors.New("cycle detected")
ErrFailedToAddDependency = xerrors.New("failed to add dependency")
)
// Status represents the status of a unit.
type Status string
// Status constants for dependency tracking.
const (
StatusNotRegistered Status = ""
StatusPending Status = "pending"
StatusStarted Status = "started"
StatusComplete Status = "completed"
)
// ID provides a type narrowed representation of the unique identifier of a unit.
type ID string
// Unit represents a point-in-time snapshot of a vertex in the dependency graph.
// Units may depend on other units, or be depended on by other units. The unit struct
// is not aware of updates made to the dependency graph after it is initialized and should
// not be cached.
type Unit struct {
id ID
status Status
// ready is true if all dependencies are satisfied.
// It does not have an accessor method on Unit, because a unit cannot know whether it is ready.
// Only the Manager can calculate whether a unit is ready based on knowledge of the dependency graph.
// To discourage use of an outdated readiness value, only the Manager should set and return this field.
ready bool
}
func (u Unit) ID() ID {
return u.id
}
func (u Unit) Status() Status {
return u.status
}
// Dependency represents a dependency relationship between units.
type Dependency struct {
Unit ID
DependsOn ID
RequiredStatus Status
CurrentStatus Status
IsSatisfied bool
}
// Manager provides reactive dependency tracking over a Graph.
// It manages Unit registration, dependency relationships, and status updates
// with automatic recalculation of readiness when dependencies are satisfied.
type Manager struct {
mu sync.RWMutex
// The underlying graph that stores dependency relationships
graph *Graph[Status, ID]
// Store vertex instances for each unit to ensure consistent references
units map[ID]Unit
}
// NewManager creates a new Manager instance.
func NewManager() *Manager {
return &Manager{
graph: &Graph[Status, ID]{},
units: make(map[ID]Unit),
}
}
// Register adds a unit to the manager if it is not already registered.
// If a Unit is already registered (per the ID field), it is not updated.
func (m *Manager) Register(id ID) error {
m.mu.Lock()
defer m.mu.Unlock()
if id == "" {
return xerrors.Errorf("registering unit %q: %w", id, ErrUnitIDRequired)
}
if m.registered(id) {
return xerrors.Errorf("registering unit %q: %w", id, ErrUnitAlreadyRegistered)
}
m.units[id] = Unit{
id: id,
status: StatusPending,
ready: true,
}
return nil
}
// registered checks if a unit is registered in the manager.
func (m *Manager) registered(id ID) bool {
return m.units[id].status != StatusNotRegistered
}
// Unit fetches a unit from the manager. If the unit does not exist,
// it returns the Unit zero-value as a placeholder unit, because
// units may depend on other units that have not yet been created.
func (m *Manager) Unit(id ID) (Unit, error) {
if id == "" {
return Unit{}, xerrors.Errorf("unit ID cannot be empty: %w", ErrUnitIDRequired)
}
m.mu.RLock()
defer m.mu.RUnlock()
return m.units[id], nil
}
func (m *Manager) IsReady(id ID) (bool, error) {
if id == "" {
return false, xerrors.Errorf("unit ID cannot be empty: %w", ErrUnitIDRequired)
}
m.mu.RLock()
defer m.mu.RUnlock()
if !m.registered(id) {
return false, nil
}
return m.units[id].ready, nil
}
// AddDependency adds a dependency relationship between units.
// The unit depends on the dependsOn unit reaching the requiredStatus.
func (m *Manager) AddDependency(unit ID, dependsOn ID, requiredStatus Status) error {
m.mu.Lock()
defer m.mu.Unlock()
switch {
case unit == "":
return xerrors.Errorf("dependent name cannot be empty: %w", ErrUnitIDRequired)
case dependsOn == "":
return xerrors.Errorf("dependency name cannot be empty: %w", ErrUnitIDRequired)
case !m.registered(unit):
return xerrors.Errorf("dependent unit %q must be registered first: %w", unit, ErrUnitNotFound)
}
// Add the dependency edge to the graph
// The edge goes from unit to dependsOn, representing the dependency
err := m.graph.AddEdge(unit, dependsOn, requiredStatus)
if err != nil {
return xerrors.Errorf("adding edge for unit %q: %w", unit, errors.Join(ErrFailedToAddDependency, err))
}
// Recalculate readiness for the unit since it now has a new dependency
m.recalculateReadinessUnsafe(unit)
return nil
}
// UpdateStatus updates a unit's status and recalculates readiness for affected dependents.
func (m *Manager) UpdateStatus(unit ID, newStatus Status) error {
m.mu.Lock()
defer m.mu.Unlock()
switch {
case unit == "":
return xerrors.Errorf("updating status for unit %q: %w", unit, ErrUnitIDRequired)
case !m.registered(unit):
return xerrors.Errorf("unit %q must be registered first: %w", unit, ErrUnitNotFound)
}
u := m.units[unit]
if u.status == newStatus {
return xerrors.Errorf("checking status for unit %q: %w", unit, ErrSameStatusAlreadySet)
}
u.status = newStatus
m.units[unit] = u
// Get all units that depend on this one (reverse adjacent vertices)
dependents := m.graph.GetReverseAdjacentVertices(unit)
// Recalculate readiness for all dependents
for _, dependent := range dependents {
m.recalculateReadinessUnsafe(dependent.From)
}
return nil
}
// recalculateReadinessUnsafe recalculates the readiness state for a unit.
// This method assumes the caller holds the write lock.
func (m *Manager) recalculateReadinessUnsafe(unit ID) {
u := m.units[unit]
dependencies := m.graph.GetForwardAdjacentVertices(unit)
allSatisfied := true
for _, dependency := range dependencies {
requiredStatus := dependency.Edge
dependsOnUnit := m.units[dependency.To]
if dependsOnUnit.status != requiredStatus {
allSatisfied = false
break
}
}
u.ready = allSatisfied
m.units[unit] = u
}
// GetGraph returns the underlying graph for visualization and debugging.
// This should be used carefully as it exposes the internal graph structure.
func (m *Manager) GetGraph() *Graph[Status, ID] {
return m.graph
}
// GetAllDependencies returns all dependencies for a unit, both satisfied and unsatisfied.
func (m *Manager) GetAllDependencies(unit ID) ([]Dependency, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if unit == "" {
return nil, xerrors.Errorf("unit ID cannot be empty: %w", ErrUnitIDRequired)
}
if !m.registered(unit) {
return nil, xerrors.Errorf("checking registration for unit %q: %w", unit, ErrUnitNotFound)
}
dependencies := m.graph.GetForwardAdjacentVertices(unit)
var allDependencies []Dependency
for _, dependency := range dependencies {
dependsOnUnit := m.units[dependency.To]
requiredStatus := dependency.Edge
allDependencies = append(allDependencies, Dependency{
Unit: unit,
DependsOn: dependency.To,
RequiredStatus: requiredStatus,
CurrentStatus: dependsOnUnit.status,
IsSatisfied: dependsOnUnit.status == requiredStatus,
})
}
return allDependencies, nil
}
// GetUnmetDependencies returns a list of unsatisfied dependencies for a unit.
func (m *Manager) GetUnmetDependencies(unit ID) ([]Dependency, error) {
allDependencies, err := m.GetAllDependencies(unit)
if err != nil {
return nil, err
}
var unmetDependencies []Dependency = slice.Filter(allDependencies, func(dependency Dependency) bool {
return !dependency.IsSatisfied
})
return unmetDependencies, nil
}
// ExportDOT exports the dependency graph to DOT format for visualization.
func (m *Manager) ExportDOT(name string) (string, error) {
return m.graph.ToDOT(name)
}
+743
View File
@@ -0,0 +1,743 @@
package unit_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/agent/unit"
)
const (
unitA unit.ID = "serviceA"
unitB unit.ID = "serviceB"
unitC unit.ID = "serviceC"
unitD unit.ID = "serviceD"
)
func TestManager_UnitValidation(t *testing.T) {
t.Parallel()
t.Run("Empty Unit Name", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
err := manager.Register("")
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
err = manager.AddDependency("", unitA, unit.StatusStarted)
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
err = manager.AddDependency(unitA, "", unit.StatusStarted)
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
dependencies, err := manager.GetAllDependencies("")
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
require.Len(t, dependencies, 0)
unmetDependencies, err := manager.GetUnmetDependencies("")
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
require.Len(t, unmetDependencies, 0)
err = manager.UpdateStatus("", unit.StatusStarted)
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
isReady, err := manager.IsReady("")
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
require.False(t, isReady)
u, err := manager.Unit("")
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
assert.Equal(t, unit.Unit{}, u)
})
}
func TestManager_Register(t *testing.T) {
t.Parallel()
t.Run("RegisterNewUnit", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Given: a unit is registered
err := manager.Register(unitA)
require.NoError(t, err)
// Then: the unit should be ready (no dependencies)
u, err := manager.Unit(unitA)
require.NoError(t, err)
assert.Equal(t, unitA, u.ID())
assert.Equal(t, unit.StatusPending, u.Status())
isReady, err := manager.IsReady(unitA)
require.NoError(t, err)
assert.True(t, isReady)
})
t.Run("RegisterDuplicateUnit", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Given: a unit is registered
err := manager.Register(unitA)
require.NoError(t, err)
// Newly registered units have StatusPending. We update the unit status to StatusStarted,
// so we can later assert that it is not overwritten back to StatusPending by the second
// register call
manager.UpdateStatus(unitA, unit.StatusStarted)
// When: the unit is registered again
err = manager.Register(unitA)
// Then: a descriptive error should be returned
require.ErrorIs(t, err, unit.ErrUnitAlreadyRegistered)
// Then: the unit status should not be overwritten
u, err := manager.Unit(unitA)
require.NoError(t, err)
assert.Equal(t, unit.StatusStarted, u.Status())
isReady, err := manager.IsReady(unitA)
require.NoError(t, err)
assert.True(t, isReady)
})
t.Run("RegisterMultipleUnits", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Given: multiple units are registered
unitIDs := []unit.ID{unitA, unitB, unitC}
for _, unit := range unitIDs {
err := manager.Register(unit)
require.NoError(t, err)
}
// Then: all units should be ready initially
for _, unitID := range unitIDs {
u, err := manager.Unit(unitID)
require.NoError(t, err)
assert.Equal(t, unit.StatusPending, u.Status())
isReady, err := manager.IsReady(unitID)
require.NoError(t, err)
assert.True(t, isReady)
}
})
}
func TestManager_AddDependency(t *testing.T) {
t.Parallel()
t.Run("AddDependencyBetweenRegisteredUnits", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Given: units A and B are registered
err := manager.Register(unitA)
require.NoError(t, err)
err = manager.Register(unitB)
require.NoError(t, err)
// Given: Unit A depends on Unit B being unit.StatusStarted
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
require.NoError(t, err)
// Then: Unit A should not be ready (depends on B)
u, err := manager.Unit(unitA)
require.NoError(t, err)
assert.Equal(t, unit.StatusPending, u.Status())
isReady, err := manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
// Then: Unit B should still be ready (no dependencies)
u, err = manager.Unit(unitB)
require.NoError(t, err)
assert.Equal(t, unit.StatusPending, u.Status())
isReady, err = manager.IsReady(unitB)
require.NoError(t, err)
assert.True(t, isReady)
// When: Unit B is started
err = manager.UpdateStatus(unitB, unit.StatusStarted)
require.NoError(t, err)
// Then: Unit A should be ready, because its dependency is now in the desired state.
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.True(t, isReady)
// When: Unit B is stopped
err = manager.UpdateStatus(unitB, unit.StatusPending)
require.NoError(t, err)
// Then: Unit A should no longer be ready, because its dependency is not in the desired state.
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
})
t.Run("AddDependencyByAnUnregisteredDependentUnit", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Given Unit B is registered
err := manager.Register(unitB)
require.NoError(t, err)
// Given Unit A depends on Unit B being started
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
// Then: a descriptive error communicates that the dependency cannot be added
// because the dependent unit must be registered first.
require.ErrorIs(t, err, unit.ErrUnitNotFound)
})
t.Run("AddDependencyOnAnUnregisteredUnit", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Given unit A is registered
err := manager.Register(unitA)
require.NoError(t, err)
// Given Unit B is not yet registered
// And Unit A depends on Unit B being started
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
require.NoError(t, err)
// Then: The dependency should be visible in Unit A's status
dependencies, err := manager.GetAllDependencies(unitA)
require.NoError(t, err)
require.Len(t, dependencies, 1)
assert.Equal(t, unitB, dependencies[0].DependsOn)
assert.Equal(t, unit.StatusStarted, dependencies[0].RequiredStatus)
assert.False(t, dependencies[0].IsSatisfied)
u, err := manager.Unit(unitB)
require.NoError(t, err)
assert.Equal(t, unit.StatusNotRegistered, u.Status())
// Then: Unit A should not be ready, because it depends on Unit B
isReady, err := manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
// When: Unit B is registered
err = manager.Register(unitB)
require.NoError(t, err)
// Then: Unit A should still not be ready.
// Unit B is not registered, but it has not been started as required by the dependency.
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
// When: Unit B is started
err = manager.UpdateStatus(unitB, unit.StatusStarted)
require.NoError(t, err)
// Then: Unit A should be ready, because its dependency is now in the desired state.
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.True(t, isReady)
})
t.Run("AddDependencyCreatesACyclicDependency", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Register units
err := manager.Register(unitA)
require.NoError(t, err)
err = manager.Register(unitB)
require.NoError(t, err)
err = manager.Register(unitC)
require.NoError(t, err)
err = manager.Register(unitD)
require.NoError(t, err)
// A depends on B
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
require.NoError(t, err)
// B depends on C
err = manager.AddDependency(unitB, unitC, unit.StatusStarted)
require.NoError(t, err)
// C depends on D
err = manager.AddDependency(unitC, unitD, unit.StatusStarted)
require.NoError(t, err)
// Try to make D depend on A (creates indirect cycle)
err = manager.AddDependency(unitD, unitA, unit.StatusStarted)
require.ErrorIs(t, err, unit.ErrCycleDetected)
})
t.Run("UpdatingADependency", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Given units A and B are registered
err := manager.Register(unitA)
require.NoError(t, err)
err = manager.Register(unitB)
require.NoError(t, err)
// Given Unit A depends on Unit B being unit.StatusStarted
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
require.NoError(t, err)
// When: The dependency is updated to unit.StatusComplete
err = manager.AddDependency(unitA, unitB, unit.StatusComplete)
require.NoError(t, err)
// Then: Unit A should only have one dependency, and it should be unit.StatusComplete
dependencies, err := manager.GetAllDependencies(unitA)
require.NoError(t, err)
require.Len(t, dependencies, 1)
assert.Equal(t, unit.StatusComplete, dependencies[0].RequiredStatus)
})
}
func TestManager_UpdateStatus(t *testing.T) {
t.Parallel()
t.Run("UpdateStatusTriggersReadinessRecalculation", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Given units A and B are registered
err := manager.Register(unitA)
require.NoError(t, err)
err = manager.Register(unitB)
require.NoError(t, err)
// Given Unit A depends on Unit B being unit.StatusStarted
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
require.NoError(t, err)
// Then: Unit A should not be ready (depends on B)
u, err := manager.Unit(unitA)
require.NoError(t, err)
assert.Equal(t, unit.StatusPending, u.Status())
isReady, err := manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
// When: Unit B is started
err = manager.UpdateStatus(unitB, unit.StatusStarted)
require.NoError(t, err)
// Then: Unit A should be ready, because its dependency is now in the desired state.
u, err = manager.Unit(unitA)
require.NoError(t, err)
assert.Equal(t, unit.StatusPending, u.Status())
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.True(t, isReady)
})
t.Run("UpdateStatusWithUnregisteredUnit", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Given Unit A is not registered
// When: Unit A is updated to unit.StatusStarted
err := manager.UpdateStatus(unitA, unit.StatusStarted)
// Then: a descriptive error communicates that the unit must be registered first.
require.ErrorIs(t, err, unit.ErrUnitNotFound)
})
t.Run("LinearChainDependencies", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Given units A, B, and C are registered
err := manager.Register(unitA)
require.NoError(t, err)
err = manager.Register(unitB)
require.NoError(t, err)
err = manager.Register(unitC)
require.NoError(t, err)
// Create chain: A depends on B being "started", B depends on C being "completed"
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
require.NoError(t, err)
err = manager.AddDependency(unitB, unitC, unit.StatusComplete)
require.NoError(t, err)
// Then: only Unit C should be ready (no dependencies)
u, err := manager.Unit(unitC)
require.NoError(t, err)
assert.Equal(t, unit.StatusPending, u.Status())
isReady, err := manager.IsReady(unitC)
require.NoError(t, err)
assert.True(t, isReady)
u, err = manager.Unit(unitB)
require.NoError(t, err)
assert.Equal(t, unit.StatusPending, u.Status())
isReady, err = manager.IsReady(unitB)
require.NoError(t, err)
assert.False(t, isReady)
u, err = manager.Unit(unitA)
require.NoError(t, err)
assert.Equal(t, unit.StatusPending, u.Status())
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
// When: Unit C is completed
err = manager.UpdateStatus(unitC, unit.StatusComplete)
require.NoError(t, err)
// Then: Unit B should be ready, because its dependency is now in the desired state.
u, err = manager.Unit(unitB)
require.NoError(t, err)
assert.Equal(t, unit.StatusPending, u.Status())
isReady, err = manager.IsReady(unitB)
require.NoError(t, err)
assert.True(t, isReady)
u, err = manager.Unit(unitA)
require.NoError(t, err)
assert.Equal(t, unit.StatusPending, u.Status())
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
u, err = manager.Unit(unitB)
require.NoError(t, err)
assert.Equal(t, unit.StatusPending, u.Status())
isReady, err = manager.IsReady(unitB)
require.NoError(t, err)
assert.True(t, isReady)
// When: Unit B is started
err = manager.UpdateStatus(unitB, unit.StatusStarted)
require.NoError(t, err)
// Then: Unit A should be ready, because its dependency is now in the desired state.
u, err = manager.Unit(unitA)
require.NoError(t, err)
assert.Equal(t, unit.StatusPending, u.Status())
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.True(t, isReady)
})
}
func TestManager_GetUnmetDependencies(t *testing.T) {
t.Parallel()
t.Run("GetUnmetDependenciesForUnitWithNoDependencies", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Given: Unit A is registered
err := manager.Register(unitA)
require.NoError(t, err)
// Given: Unit A has no dependencies
// Then: Unit A should have no unmet dependencies
unmet, err := manager.GetUnmetDependencies(unitA)
require.NoError(t, err)
assert.Empty(t, unmet)
})
t.Run("GetUnmetDependenciesForUnitWithUnsatisfiedDependencies", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
err := manager.Register(unitA)
require.NoError(t, err)
err = manager.Register(unitB)
require.NoError(t, err)
// Given: Unit A depends on Unit B being unit.StatusStarted
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
require.NoError(t, err)
unmet, err := manager.GetUnmetDependencies(unitA)
require.NoError(t, err)
require.Len(t, unmet, 1)
assert.Equal(t, unitA, unmet[0].Unit)
assert.Equal(t, unitB, unmet[0].DependsOn)
assert.Equal(t, unit.StatusStarted, unmet[0].RequiredStatus)
assert.False(t, unmet[0].IsSatisfied)
})
t.Run("GetUnmetDependenciesForUnitWithSatisfiedDependencies", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Given: Unit A and Unit B are registered
err := manager.Register(unitA)
require.NoError(t, err)
err = manager.Register(unitB)
require.NoError(t, err)
// Given: Unit A depends on Unit B being unit.StatusStarted
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
require.NoError(t, err)
// When: Unit B is started
err = manager.UpdateStatus(unitB, unit.StatusStarted)
require.NoError(t, err)
// Then: Unit A should have no unmet dependencies
unmet, err := manager.GetUnmetDependencies(unitA)
require.NoError(t, err)
assert.Empty(t, unmet)
})
t.Run("GetUnmetDependenciesForUnregisteredUnit", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// When: Unit A is requested
unmet, err := manager.GetUnmetDependencies(unitA)
// Then: a descriptive error communicates that the unit must be registered first.
require.ErrorIs(t, err, unit.ErrUnitNotFound)
assert.Nil(t, unmet)
})
}
func TestManager_MultipleDependencies(t *testing.T) {
t.Parallel()
t.Run("UnitWithMultipleDependencies", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Register all units
units := []unit.ID{unitA, unitB, unitC, unitD}
for _, unit := range units {
err := manager.Register(unit)
require.NoError(t, err)
}
// A depends on B being unit.StatusStarted AND C being "started"
err := manager.AddDependency(unitA, unitB, unit.StatusStarted)
require.NoError(t, err)
err = manager.AddDependency(unitA, unitC, unit.StatusStarted)
require.NoError(t, err)
// A should not be ready (depends on both B and C)
isReady, err := manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
// Update B to unit.StatusStarted - A should still not be ready (needs C too)
err = manager.UpdateStatus(unitB, unit.StatusStarted)
require.NoError(t, err)
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
// Update C to "started" - A should now be ready
err = manager.UpdateStatus(unitC, unit.StatusStarted)
require.NoError(t, err)
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.True(t, isReady)
})
t.Run("ComplexDependencyChain", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Register all units
units := []unit.ID{unitA, unitB, unitC, unitD}
for _, unit := range units {
err := manager.Register(unit)
require.NoError(t, err)
}
// Create complex dependency graph:
// A depends on B being unit.StatusStarted AND C being "started"
err := manager.AddDependency(unitA, unitB, unit.StatusStarted)
require.NoError(t, err)
err = manager.AddDependency(unitA, unitC, unit.StatusStarted)
require.NoError(t, err)
// B depends on D being "completed"
err = manager.AddDependency(unitB, unitD, unit.StatusComplete)
require.NoError(t, err)
// C depends on D being "completed"
err = manager.AddDependency(unitC, unitD, unit.StatusComplete)
require.NoError(t, err)
// Initially only D is ready
isReady, err := manager.IsReady(unitD)
require.NoError(t, err)
assert.True(t, isReady)
isReady, err = manager.IsReady(unitB)
require.NoError(t, err)
assert.False(t, isReady)
isReady, err = manager.IsReady(unitC)
require.NoError(t, err)
assert.False(t, isReady)
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
// Update D to "completed" - B and C should become ready
err = manager.UpdateStatus(unitD, unit.StatusComplete)
require.NoError(t, err)
isReady, err = manager.IsReady(unitB)
require.NoError(t, err)
assert.True(t, isReady)
isReady, err = manager.IsReady(unitC)
require.NoError(t, err)
assert.True(t, isReady)
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
// Update B to unit.StatusStarted - A should still not be ready (needs C)
err = manager.UpdateStatus(unitB, unit.StatusStarted)
require.NoError(t, err)
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
// Update C to "started" - A should now be ready
err = manager.UpdateStatus(unitC, unit.StatusStarted)
require.NoError(t, err)
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.True(t, isReady)
})
t.Run("DifferentStatusTypes", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Register units
err := manager.Register(unitA)
require.NoError(t, err)
err = manager.Register(unitB)
require.NoError(t, err)
err = manager.Register(unitC)
require.NoError(t, err)
// Given: Unit A depends on Unit B being unit.StatusStarted
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
require.NoError(t, err)
// Given: Unit A depends on Unit C being "completed"
err = manager.AddDependency(unitA, unitC, unit.StatusComplete)
require.NoError(t, err)
// When: Unit B is started
err = manager.UpdateStatus(unitB, unit.StatusStarted)
require.NoError(t, err)
// Then: Unit A should not be ready, because only one of its dependencies is in the desired state.
// It still requires Unit C to be completed.
isReady, err := manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
// When: Unit C is completed
err = manager.UpdateStatus(unitC, unit.StatusComplete)
require.NoError(t, err)
// Then: Unit A should be ready, because both of its dependencies are in the desired state.
isReady, err = manager.IsReady(unitA)
require.NoError(t, err)
assert.True(t, isReady)
})
}
func TestManager_IsReady(t *testing.T) {
t.Parallel()
t.Run("IsReadyWithUnregisteredUnit", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Given: a unit is not registered
u, err := manager.Unit(unitA)
require.NoError(t, err)
assert.Equal(t, unit.StatusNotRegistered, u.Status())
// Then: the unit is not ready
isReady, err := manager.IsReady(unitA)
require.NoError(t, err)
assert.False(t, isReady)
})
}
func TestManager_ToDOT(t *testing.T) {
t.Parallel()
t.Run("ExportSimpleGraph", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Register units
err := manager.Register(unitA)
require.NoError(t, err)
err = manager.Register(unitB)
require.NoError(t, err)
// Add dependency
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
require.NoError(t, err)
dot, err := manager.ExportDOT("test")
require.NoError(t, err)
assert.NotEmpty(t, dot)
assert.Contains(t, dot, "digraph")
})
t.Run("ExportComplexGraph", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
// Register all units
units := []unit.ID{unitA, unitB, unitC, unitD}
for _, unit := range units {
err := manager.Register(unit)
require.NoError(t, err)
}
// Create complex dependency graph
// A depends on B and C, B depends on D, C depends on D
err := manager.AddDependency(unitA, unitB, unit.StatusStarted)
require.NoError(t, err)
err = manager.AddDependency(unitA, unitC, unit.StatusStarted)
require.NoError(t, err)
err = manager.AddDependency(unitB, unitD, unit.StatusComplete)
require.NoError(t, err)
err = manager.AddDependency(unitC, unitD, unit.StatusComplete)
require.NoError(t, err)
dot, err := manager.ExportDOT("complex")
require.NoError(t, err)
assert.NotEmpty(t, dot)
assert.Contains(t, dot, "digraph")
})
}
+1
View File
@@ -64,6 +64,7 @@ func (r *RootCmd) scaletestCmd() *serpent.Command {
r.scaletestWorkspaceTraffic(),
r.scaletestAutostart(),
r.scaletestNotifications(),
r.scaletestTaskStatus(),
r.scaletestSMTP(),
r.scaletestPrebuilds(),
},
+10
View File
@@ -142,6 +142,15 @@ func (r *RootCmd) scaletestNotifications() *serpent.Command {
triggerTimes[id] = make(chan time.Time, 1)
}
smtpHTTPTransport := &http.Transport{
MaxConnsPerHost: 512,
MaxIdleConnsPerHost: 512,
IdleConnTimeout: 60 * time.Second,
}
smtpHTTPClient := &http.Client{
Transport: smtpHTTPTransport,
}
configs := make([]notifications.Config, 0, userCount)
for range templateAdminCount {
config := notifications.Config{
@@ -157,6 +166,7 @@ func (r *RootCmd) scaletestNotifications() *serpent.Command {
Metrics: metrics,
SMTPApiURL: smtpAPIURL,
SMTPRequestTimeout: smtpRequestTimeout,
SMTPHttpClient: smtpHTTPClient,
}
if err := config.Validate(); err != nil {
return xerrors.Errorf("validate config: %w", err)
+275
View File
@@ -0,0 +1,275 @@
//go:build !slim
package cli
import (
"context"
"fmt"
"net/http"
"sync"
"time"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/xerrors"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/serpent"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/scaletest/harness"
"github.com/coder/coder/v2/scaletest/taskstatus"
)
const (
taskStatusTestName = "task-status"
)
func (r *RootCmd) scaletestTaskStatus() *serpent.Command {
var (
count int64
template string
workspaceNamePrefix string
appSlug string
reportStatusPeriod time.Duration
reportStatusDuration time.Duration
baselineDuration time.Duration
tracingFlags = &scaletestTracingFlags{}
prometheusFlags = &scaletestPrometheusFlags{}
timeoutStrategy = &timeoutFlags{}
cleanupStrategy = newScaletestCleanupStrategy()
output = &scaletestOutputFlags{}
)
orgContext := NewOrganizationContext()
cmd := &serpent.Command{
Use: "task-status",
Short: "Generates load on the Coder server by simulating task status reporting",
Long: `This test creates external workspaces and simulates AI agents reporting task status.
After all runners connect, it waits for the baseline duration before triggering status reporting.`,
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
outputs, err := output.parse()
if err != nil {
return xerrors.Errorf("could not parse --output flags: %w", err)
}
client, err := r.InitClient(inv)
if err != nil {
return err
}
org, err := orgContext.Selected(inv, client)
if err != nil {
return err
}
_, err = requireAdmin(ctx, client)
if err != nil {
return err
}
// Disable rate limits for this test
client.HTTPClient = &http.Client{
Transport: &codersdk.HeaderTransport{
Transport: http.DefaultTransport,
Header: map[string][]string{
codersdk.BypassRatelimitHeader: {"true"},
},
},
}
// Find the template
tpl, err := parseTemplate(ctx, client, []uuid.UUID{org.ID}, template)
if err != nil {
return xerrors.Errorf("parse template %q: %w", template, err)
}
templateID := tpl.ID
reg := prometheus.NewRegistry()
metrics := taskstatus.NewMetrics(reg)
logger := slog.Make(sloghuman.Sink(inv.Stdout)).Leveled(slog.LevelDebug)
prometheusSrvClose := ServeHandler(ctx, logger, promhttp.HandlerFor(reg, promhttp.HandlerOpts{}), prometheusFlags.Address, "prometheus")
defer prometheusSrvClose()
tracerProvider, closeTracing, tracingEnabled, err := tracingFlags.provider(ctx)
if err != nil {
return xerrors.Errorf("create tracer provider: %w", err)
}
defer func() {
// Allow time for traces to flush even if command context is
// canceled. This is a no-op if tracing is not enabled.
_, _ = fmt.Fprintln(inv.Stderr, "\nUploading traces...")
if err := closeTracing(ctx); err != nil {
_, _ = fmt.Fprintf(inv.Stderr, "\nError uploading traces: %+v\n", err)
}
// Wait for prometheus metrics to be scraped
_, _ = fmt.Fprintf(inv.Stderr, "Waiting %s for prometheus metrics to be scraped\n", prometheusFlags.Wait)
<-time.After(prometheusFlags.Wait)
}()
tracer := tracerProvider.Tracer(scaletestTracerName)
// Setup shared resources for coordination
connectedWaitGroup := &sync.WaitGroup{}
connectedWaitGroup.Add(int(count))
startReporting := make(chan struct{})
// Create the test harness
th := harness.NewTestHarness(
timeoutStrategy.wrapStrategy(harness.ConcurrentExecutionStrategy{}),
cleanupStrategy.toStrategy(),
)
// Create runners
for i := range count {
workspaceName := fmt.Sprintf("%s-%d", workspaceNamePrefix, i)
cfg := taskstatus.Config{
TemplateID: templateID,
WorkspaceName: workspaceName,
AppSlug: appSlug,
ConnectedWaitGroup: connectedWaitGroup,
StartReporting: startReporting,
ReportStatusPeriod: reportStatusPeriod,
ReportStatusDuration: reportStatusDuration,
Metrics: metrics,
MetricLabelValues: []string{},
}
if err := cfg.Validate(); err != nil {
return xerrors.Errorf("validate config for runner %d: %w", i, err)
}
var runner harness.Runnable = taskstatus.NewRunner(client, cfg)
if tracingEnabled {
runner = &runnableTraceWrapper{
tracer: tracer,
spanName: fmt.Sprintf("%s/%d", taskStatusTestName, i),
runner: runner,
}
}
th.AddRun(taskStatusTestName, workspaceName, runner)
}
// Start the test in a separate goroutine so we can coordinate timing
testCtx, testCancel := timeoutStrategy.toContext(ctx)
defer testCancel()
testDone := make(chan error)
go func() {
testDone <- th.Run(testCtx)
}()
// Wait for all runners to connect
logger.Info(ctx, "waiting for all runners to connect")
waitCtx, waitCancel := context.WithTimeout(ctx, 5*time.Minute)
defer waitCancel()
connectDone := make(chan struct{})
go func() {
connectedWaitGroup.Wait()
close(connectDone)
}()
select {
case <-waitCtx.Done():
return xerrors.Errorf("timeout waiting for runners to connect")
case <-connectDone:
logger.Info(ctx, "all runners connected")
}
// Wait for baseline duration
logger.Info(ctx, "waiting for baseline duration", slog.F("duration", baselineDuration))
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(baselineDuration):
}
// Trigger all runners to start reporting
logger.Info(ctx, "triggering runners to start reporting task status")
close(startReporting)
// Wait for the test to complete
err = <-testDone
if err != nil {
return xerrors.Errorf("run test harness: %w", err)
}
res := th.Results()
for _, o := range outputs {
err = o.write(res, inv.Stdout)
if err != nil {
return xerrors.Errorf("write output %q to %q: %w", o.format, o.path, err)
}
}
cleanupCtx, cleanupCancel := cleanupStrategy.toContext(ctx)
defer cleanupCancel()
err = th.Cleanup(cleanupCtx)
if err != nil {
return xerrors.Errorf("cleanup tests: %w", err)
}
if res.TotalFail > 0 {
return xerrors.New("load test failed, see above for more details")
}
return nil
},
}
cmd.Options = serpent.OptionSet{
{
Flag: "count",
Description: "Number of concurrent runners to create.",
Default: "10",
Value: serpent.Int64Of(&count),
},
{
Flag: "template",
Description: "Name or UUID of the template to use for the scale test. The template MUST include a coder_external_agent and a coder_app.",
Default: "scaletest-task-status",
Value: serpent.StringOf(&template),
},
{
Flag: "workspace-name-prefix",
Description: "Prefix for workspace names (will be suffixed with index).",
Default: "scaletest-task-status",
Value: serpent.StringOf(&workspaceNamePrefix),
},
{
Flag: "app-slug",
Description: "Slug of the app designated as the AI Agent.",
Default: "ai-agent",
Value: serpent.StringOf(&appSlug),
},
{
Flag: "report-status-period",
Description: "Time between reporting task statuses.",
Default: "10s",
Value: serpent.DurationOf(&reportStatusPeriod),
},
{
Flag: "report-status-duration",
Description: "Total time to report task statuses after baseline.",
Default: "15m",
Value: serpent.DurationOf(&reportStatusDuration),
},
{
Flag: "baseline-duration",
Description: "Duration to wait after all runners connect before starting to report status.",
Default: "10m",
Value: serpent.DurationOf(&baselineDuration),
},
}
orgContext.AttachOptions(cmd)
output.attach(&cmd.Options)
tracingFlags.attach(&cmd.Options)
prometheusFlags.attach(&cmd.Options)
timeoutStrategy.attach(&cmd.Options)
cleanupStrategy.attach(&cmd.Options)
return cmd
}
+19 -12
View File
@@ -1029,7 +1029,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
defer shutdownConns()
// Ensures that old database entries are cleaned up over time!
purger := dbpurge.New(ctx, logger.Named("dbpurge"), options.Database, quartz.NewReal())
purger := dbpurge.New(ctx, logger.Named("dbpurge"), options.Database, options.DeploymentValues, quartz.NewReal())
defer purger.Close()
// Updates workspace usage
@@ -2143,21 +2143,33 @@ func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logg
}
stdlibLogger := slog.Stdlib(ctx, logger.Named("postgres"), slog.LevelDebug)
// If the port is not defined, an available port will be found dynamically.
// If the port is not defined, an available port will be found dynamically. This has
// implications in CI because here is no way to tell Postgres to use an ephemeral
// port, so to avoid flaky tests in CI we need to retry EmbeddedPostgres.Start in
// case of a race condition where the port we quickly listen on and close in
// embeddedPostgresURL() is not free by the time the embedded postgres starts up.
// The maximum retry attempts _should_ cover most cases where port conflicts occur
// in CI and cause flaky tests.
maxAttempts := 1
_, err = cfg.PostgresPort().Read()
// Important: if retryPortDiscovery is changed to not include testing.Testing(),
// the retry logic below also needs to be updated to ensure we don't delete an
// existing database
retryPortDiscovery := errors.Is(err, os.ErrNotExist) && testing.Testing()
if retryPortDiscovery {
// There is no way to tell Postgres to use an ephemeral port, so in order to avoid
// flaky tests in CI we need to retry EmbeddedPostgres.Start in case of a race
// condition where the port we quickly listen on and close in embeddedPostgresURL()
// is not free by the time the embedded postgres starts up. This maximum_should
// cover most cases where port conflicts occur in CI and cause flaky tests.
maxAttempts = 3
}
var startErr error
for attempt := 0; attempt < maxAttempts; attempt++ {
if retryPortDiscovery && attempt > 0 {
// Clean up the data and runtime directories and the port file from the
// previous failed attempt to ensure a clean slate for the next attempt.
_ = os.RemoveAll(filepath.Join(cfg.PostgresPath(), "data"))
_ = os.RemoveAll(filepath.Join(cfg.PostgresPath(), "runtime"))
_ = cfg.PostgresPort().Delete()
}
// Ensure a password and port have been generated.
connectionURL, err := embeddedPostgresURL(cfg)
if err != nil {
@@ -2204,11 +2216,6 @@ func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logg
slog.F("port", pgPort),
slog.Error(startErr),
)
if retryPortDiscovery {
// Since a retry is needed, we wipe the port stored here at the beginning of the loop.
_ = cfg.PostgresPort().Delete()
}
}
return "", nil, xerrors.Errorf("failed to start built-in PostgreSQL after %d attempts. "+
+10 -6
View File
@@ -80,12 +80,7 @@ OPTIONS:
Periodically check for new releases of Coder and inform the owner. The
check is performed once per day.
AIBRIDGE OPTIONS:
--aibridge-inject-coder-mcp-tools bool, $CODER_AIBRIDGE_INJECT_CODER_MCP_TOOLS (default: false)
Whether to inject Coder's MCP tools into intercepted AI Bridge
requests (requires the "oauth2" and "mcp-server-http" experiments to
be enabled).
AI BRIDGE OPTIONS:
--aibridge-anthropic-base-url string, $CODER_AIBRIDGE_ANTHROPIC_BASE_URL (default: https://api.anthropic.com/)
The base URL of the Anthropic API.
@@ -111,9 +106,18 @@ AIBRIDGE OPTIONS:
See
https://docs.claude.com/en/docs/claude-code/settings#environment-variables.
--aibridge-retention duration, $CODER_AIBRIDGE_RETENTION (default: 60d)
Length of time to retain data such as interceptions and all related
records (token, prompt, tool use).
--aibridge-enabled bool, $CODER_AIBRIDGE_ENABLED (default: false)
Whether to start an in-memory aibridged instance.
--aibridge-inject-coder-mcp-tools bool, $CODER_AIBRIDGE_INJECT_CODER_MCP_TOOLS (default: false)
Whether to inject Coder's MCP tools into intercepted AI Bridge
requests (requires the "oauth2" and "mcp-server-http" experiments to
be enabled).
--aibridge-openai-base-url string, $CODER_AIBRIDGE_OPENAI_BASE_URL (default: https://api.openai.com/v1/)
The base URL of the OpenAI API.
+4
View File
@@ -751,3 +751,7 @@ aibridge:
# (requires the "oauth2" and "mcp-server-http" experiments to be enabled).
# (default: false, type: bool)
inject_coder_mcp_tools: false
# Length of time to retain data such as interceptions and all related records
# (token, prompt, tool use).
# (default: 60d, type: duration)
retention: 1440h0m0s
+79 -18
View File
@@ -13,6 +13,7 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtime"
@@ -23,6 +24,7 @@ import (
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/searchquery"
"github.com/coder/coder/v2/coderd/taskname"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
@@ -270,15 +272,21 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
func taskFromDBTaskAndWorkspace(dbTask database.Task, ws codersdk.Workspace) codersdk.Task {
var taskAgentLifecycle *codersdk.WorkspaceAgentLifecycle
var taskAgentHealth *codersdk.WorkspaceAgentHealth
var taskAppHealth *codersdk.WorkspaceAppHealth
// If we have an agent ID from the task, find the agent details in the
// workspace.
if dbTask.WorkspaceAgentLifecycleState.Valid {
taskAgentLifecycle = ptr.Ref(codersdk.WorkspaceAgentLifecycle(dbTask.WorkspaceAgentLifecycleState.WorkspaceAgentLifecycleState))
}
if dbTask.WorkspaceAppHealth.Valid {
taskAppHealth = ptr.Ref(codersdk.WorkspaceAppHealth(dbTask.WorkspaceAppHealth.WorkspaceAppHealth))
}
// If we have an agent ID from the task, find the agent health info
if dbTask.WorkspaceAgentID.Valid {
findTaskAgentLoop:
for _, resource := range ws.LatestBuild.Resources {
for _, agent := range resource.Agents {
if agent.ID == dbTask.WorkspaceAgentID.UUID {
taskAgentLifecycle = &agent.LifecycleState
taskAgentHealth = &agent.Health
break findTaskAgentLoop
}
@@ -286,21 +294,7 @@ func taskFromDBTaskAndWorkspace(dbTask database.Task, ws codersdk.Workspace) cod
}
}
// Ignore 'latest app status' if it is older than the latest build and the
// latest build is a 'start' transition. This ensures that you don't show a
// stale app status from a previous build. For stop transitions, there is
// still value in showing the latest app status.
var currentState *codersdk.TaskStateEntry
if ws.LatestAppStatus != nil {
if ws.LatestBuild.Transition != codersdk.WorkspaceTransitionStart || ws.LatestAppStatus.CreatedAt.After(ws.LatestBuild.CreatedAt) {
currentState = &codersdk.TaskStateEntry{
Timestamp: ws.LatestAppStatus.CreatedAt,
State: codersdk.TaskState(ws.LatestAppStatus.State),
Message: ws.LatestAppStatus.Message,
URI: ws.LatestAppStatus.URI,
}
}
}
currentState := deriveTaskCurrentState(dbTask, ws, taskAgentLifecycle, taskAppHealth)
return codersdk.Task{
ID: dbTask.ID,
@@ -330,6 +324,73 @@ func taskFromDBTaskAndWorkspace(dbTask database.Task, ws codersdk.Workspace) cod
}
}
// deriveTaskCurrentState determines the current state of a task based on the
// workspace's latest app status and initialization phase.
// Returns nil if no valid state can be determined.
func deriveTaskCurrentState(
dbTask database.Task,
ws codersdk.Workspace,
taskAgentLifecycle *codersdk.WorkspaceAgentLifecycle,
taskAppHealth *codersdk.WorkspaceAppHealth,
) *codersdk.TaskStateEntry {
var currentState *codersdk.TaskStateEntry
// Ignore 'latest app status' if it is older than the latest build and the
// latest build is a 'start' transition. This ensures that you don't show a
// stale app status from a previous build. For stop transitions, there is
// still value in showing the latest app status.
if ws.LatestAppStatus != nil {
if ws.LatestBuild.Transition != codersdk.WorkspaceTransitionStart || ws.LatestAppStatus.CreatedAt.After(ws.LatestBuild.CreatedAt) {
currentState = &codersdk.TaskStateEntry{
Timestamp: ws.LatestAppStatus.CreatedAt,
State: codersdk.TaskState(ws.LatestAppStatus.State),
Message: ws.LatestAppStatus.Message,
URI: ws.LatestAppStatus.URI,
}
}
}
// If no valid agent state was found for the current build and the task is initializing,
// provide a descriptive initialization message.
if currentState == nil && dbTask.Status == database.TaskStatusInitializing {
message := "Initializing workspace"
switch {
case ws.LatestBuild.Status == codersdk.WorkspaceStatusPending ||
ws.LatestBuild.Status == codersdk.WorkspaceStatusStarting:
message = fmt.Sprintf("Workspace is %s", ws.LatestBuild.Status)
case taskAgentLifecycle != nil:
switch {
case *taskAgentLifecycle == codersdk.WorkspaceAgentLifecycleCreated:
message = "Agent is connecting"
case *taskAgentLifecycle == codersdk.WorkspaceAgentLifecycleStarting:
message = "Agent is starting"
case *taskAgentLifecycle == codersdk.WorkspaceAgentLifecycleReady:
if taskAppHealth != nil && *taskAppHealth == codersdk.WorkspaceAppHealthInitializing {
message = "App is initializing"
} else {
// In case the workspace app is not initializing,
// the overall task status should be updated accordingly
message = "Initializing workspace applications"
}
default:
// In case the workspace agent is not initializing,
// the overall task status should be updated accordingly
message = "Initializing workspace agent"
}
}
currentState = &codersdk.TaskStateEntry{
Timestamp: ws.LatestBuild.CreatedAt,
State: codersdk.TaskStateWorking,
Message: message,
URI: "",
}
}
return currentState
}
// @Summary List AI tasks
// @Description: EXPERIMENTAL: this endpoint is experimental and not guaranteed to be stable.
// @ID list-tasks
+223
View File
@@ -0,0 +1,223 @@
package coderd
import (
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
)
func TestDeriveTaskCurrentState_Unit(t *testing.T) {
t.Parallel()
now := time.Now()
tests := []struct {
name string
task database.Task
agentLifecycle *codersdk.WorkspaceAgentLifecycle
appHealth *codersdk.WorkspaceAppHealth
latestAppStatus *codersdk.WorkspaceAppStatus
latestBuild codersdk.WorkspaceBuild
expectCurrentState bool
expectedTimestamp time.Time
expectedState codersdk.TaskState
expectedMessage string
}{
{
name: "NoAppStatus",
task: database.Task{
ID: uuid.New(),
Status: database.TaskStatusActive,
},
agentLifecycle: nil,
appHealth: nil,
latestAppStatus: nil,
latestBuild: codersdk.WorkspaceBuild{
Transition: codersdk.WorkspaceTransitionStart,
CreatedAt: now,
},
expectCurrentState: false,
},
{
name: "BuildStartTransition_AppStatus_NewerThanBuild",
task: database.Task{
ID: uuid.New(),
Status: database.TaskStatusActive,
},
agentLifecycle: nil,
appHealth: nil,
latestAppStatus: &codersdk.WorkspaceAppStatus{
State: codersdk.WorkspaceAppStatusStateWorking,
Message: "Task is working",
CreatedAt: now.Add(1 * time.Minute),
},
latestBuild: codersdk.WorkspaceBuild{
Transition: codersdk.WorkspaceTransitionStart,
CreatedAt: now,
},
expectCurrentState: true,
expectedTimestamp: now.Add(1 * time.Minute),
expectedState: codersdk.TaskState(codersdk.WorkspaceAppStatusStateWorking),
expectedMessage: "Task is working",
},
{
name: "BuildStartTransition_StaleAppStatus_OlderThanBuild",
task: database.Task{
ID: uuid.New(),
Status: database.TaskStatusActive,
},
agentLifecycle: nil,
appHealth: nil,
latestAppStatus: &codersdk.WorkspaceAppStatus{
State: codersdk.WorkspaceAppStatusStateComplete,
Message: "Previous task completed",
CreatedAt: now.Add(-1 * time.Minute),
},
latestBuild: codersdk.WorkspaceBuild{
Transition: codersdk.WorkspaceTransitionStart,
CreatedAt: now,
},
expectCurrentState: false,
},
{
name: "BuildStopTransition",
task: database.Task{
ID: uuid.New(),
Status: database.TaskStatusActive,
},
agentLifecycle: nil,
appHealth: nil,
latestAppStatus: &codersdk.WorkspaceAppStatus{
State: codersdk.WorkspaceAppStatusStateComplete,
Message: "Task completed before stop",
CreatedAt: now.Add(-1 * time.Minute),
},
latestBuild: codersdk.WorkspaceBuild{
Transition: codersdk.WorkspaceTransitionStop,
CreatedAt: now,
},
expectCurrentState: true,
expectedTimestamp: now.Add(-1 * time.Minute),
expectedState: codersdk.TaskState(codersdk.WorkspaceAppStatusStateComplete),
expectedMessage: "Task completed before stop",
},
{
name: "TaskInitializing_WorkspacePending",
task: database.Task{
ID: uuid.New(),
Status: database.TaskStatusInitializing,
},
agentLifecycle: nil,
appHealth: nil,
latestAppStatus: nil,
latestBuild: codersdk.WorkspaceBuild{
Status: codersdk.WorkspaceStatusPending,
CreatedAt: now,
},
expectCurrentState: true,
expectedTimestamp: now,
expectedState: codersdk.TaskStateWorking,
expectedMessage: "Workspace is pending",
},
{
name: "TaskInitializing_WorkspaceStarting",
task: database.Task{
ID: uuid.New(),
Status: database.TaskStatusInitializing,
},
agentLifecycle: nil,
appHealth: nil,
latestAppStatus: nil,
latestBuild: codersdk.WorkspaceBuild{
Status: codersdk.WorkspaceStatusStarting,
CreatedAt: now,
},
expectCurrentState: true,
expectedTimestamp: now,
expectedState: codersdk.TaskStateWorking,
expectedMessage: "Workspace is starting",
},
{
name: "TaskInitializing_AgentConnecting",
task: database.Task{
ID: uuid.New(),
Status: database.TaskStatusInitializing,
},
agentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleCreated),
appHealth: nil,
latestAppStatus: nil,
latestBuild: codersdk.WorkspaceBuild{
Status: codersdk.WorkspaceStatusRunning,
CreatedAt: now,
},
expectCurrentState: true,
expectedTimestamp: now,
expectedState: codersdk.TaskStateWorking,
expectedMessage: "Agent is connecting",
},
{
name: "TaskInitializing_AgentStarting",
task: database.Task{
ID: uuid.New(),
Status: database.TaskStatusInitializing,
},
agentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleStarting),
appHealth: nil,
latestAppStatus: nil,
latestBuild: codersdk.WorkspaceBuild{
Status: codersdk.WorkspaceStatusRunning,
CreatedAt: now,
},
expectCurrentState: true,
expectedTimestamp: now,
expectedState: codersdk.TaskStateWorking,
expectedMessage: "Agent is starting",
},
{
name: "TaskInitializing_AppInitializing",
task: database.Task{
ID: uuid.New(),
Status: database.TaskStatusInitializing,
},
agentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady),
appHealth: ptr.Ref(codersdk.WorkspaceAppHealthInitializing),
latestAppStatus: nil,
latestBuild: codersdk.WorkspaceBuild{
Status: codersdk.WorkspaceStatusRunning,
CreatedAt: now,
},
expectCurrentState: true,
expectedTimestamp: now,
expectedState: codersdk.TaskStateWorking,
expectedMessage: "App is initializing",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ws := codersdk.Workspace{
LatestBuild: tt.latestBuild,
LatestAppStatus: tt.latestAppStatus,
}
currentState := deriveTaskCurrentState(tt.task, ws, tt.agentLifecycle, tt.appHealth)
if tt.expectCurrentState {
require.NotNil(t, currentState)
assert.Equal(t, tt.expectedTimestamp.UTC(), currentState.Timestamp.UTC())
assert.Equal(t, tt.expectedState, currentState.State)
assert.Equal(t, tt.expectedMessage, currentState.Message)
} else {
assert.Nil(t, currentState)
}
})
}
}
+6 -2
View File
@@ -240,14 +240,18 @@ func TestTasks(t *testing.T) {
assert.NotNil(t, updated.CurrentState, "current state should not be nil")
assert.Equal(t, "all done", updated.CurrentState.Message)
assert.Equal(t, codersdk.TaskStateComplete, updated.CurrentState.State)
previousCurrentState := updated.CurrentState
// Start the workspace again
coderdtest.MustTransitionWorkspace(t, client, task.WorkspaceID.UUID, codersdk.WorkspaceTransitionStop, codersdk.WorkspaceTransitionStart)
// Verify that the status from the previous build is no longer present
// Verify that the status from the previous build has been cleared
// and replaced by the agent initialization status.
updated, err = exp.TaskByID(ctx, task.ID)
require.NoError(t, err)
assert.Nil(t, updated.CurrentState, "current state should be nil")
assert.NotEqual(t, previousCurrentState, updated.CurrentState)
assert.Equal(t, codersdk.TaskStateWorking, updated.CurrentState.State)
assert.NotEqual(t, "all done", updated.CurrentState.Message)
})
t.Run("Delete", func(t *testing.T) {
+66 -3
View File
@@ -96,10 +96,10 @@ const docTemplate = `{
"application/json"
],
"tags": [
"AIBridge"
"AI Bridge"
],
"summary": "List AIBridge interceptions",
"operationId": "list-aibridge-interceptions",
"summary": "List AI Bridge interceptions",
"operationId": "list-ai-bridge-interceptions",
"parameters": [
{
"type": "string",
@@ -6002,6 +6002,41 @@ const docTemplate = `{
}
}
},
"/templates/{template}/prebuilds/invalidate": {
"post": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Enterprise"
],
"summary": "Invalidate presets for template",
"operationId": "invalidate-presets-for-template",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Template ID",
"name": "template",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.InvalidatePresetsResponse"
}
}
}
}
},
"/templates/{template}/versions": {
"get": {
"security": [
@@ -11705,6 +11740,9 @@ const docTemplate = `{
},
"openai": {
"$ref": "#/definitions/codersdk.AIBridgeOpenAIConfig"
},
"retention": {
"type": "integer"
}
}
},
@@ -14889,6 +14927,31 @@ const docTemplate = `{
"InsightsReportIntervalWeek"
]
},
"codersdk.InvalidatePresetsResponse": {
"type": "object",
"properties": {
"invalidated": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.InvalidatedPreset"
}
}
}
},
"codersdk.InvalidatedPreset": {
"type": "object",
"properties": {
"preset_name": {
"type": "string"
},
"template_name": {
"type": "string"
},
"template_version_name": {
"type": "string"
}
}
},
"codersdk.IssueReconnectingPTYSignedTokenRequest": {
"type": "object",
"required": [
+62 -3
View File
@@ -73,9 +73,9 @@
}
],
"produces": ["application/json"],
"tags": ["AIBridge"],
"summary": "List AIBridge interceptions",
"operationId": "list-aibridge-interceptions",
"tags": ["AI Bridge"],
"summary": "List AI Bridge interceptions",
"operationId": "list-ai-bridge-interceptions",
"parameters": [
{
"type": "string",
@@ -5309,6 +5309,37 @@
}
}
},
"/templates/{template}/prebuilds/invalidate": {
"post": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Enterprise"],
"summary": "Invalidate presets for template",
"operationId": "invalidate-presets-for-template",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Template ID",
"name": "template",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.InvalidatePresetsResponse"
}
}
}
}
},
"/templates/{template}/versions": {
"get": {
"security": [
@@ -10401,6 +10432,9 @@
},
"openai": {
"$ref": "#/definitions/codersdk.AIBridgeOpenAIConfig"
},
"retention": {
"type": "integer"
}
}
},
@@ -13487,6 +13521,31 @@
"InsightsReportIntervalWeek"
]
},
"codersdk.InvalidatePresetsResponse": {
"type": "object",
"properties": {
"invalidated": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.InvalidatedPreset"
}
}
}
},
"codersdk.InvalidatedPreset": {
"type": "object",
"properties": {
"preset_name": {
"type": "string"
},
"template_name": {
"type": "string"
},
"template_version_name": {
"type": "string"
}
}
},
"codersdk.IssueReconnectingPTYSignedTokenRequest": {
"type": "object",
"required": ["agentID", "url"],
+1
View File
@@ -610,6 +610,7 @@ func New(options *Options) *API {
dbRolluper: options.DatabaseRolluper,
}
api.WorkspaceAppsProvider = workspaceapps.NewDBTokenProvider(
ctx,
options.Logger.Named("workspaceapps"),
options.AccessURL,
options.Authorizer,
+12
View File
@@ -1021,6 +1021,18 @@ func AIBridgeToolUsage(usage database.AIBridgeToolUsage) codersdk.AIBridgeToolUs
}
}
func InvalidatedPresets(invalidatedPresets []database.UpdatePresetsLastInvalidatedAtRow) []codersdk.InvalidatedPreset {
var presets []codersdk.InvalidatedPreset
for _, p := range invalidatedPresets {
presets = append(presets, codersdk.InvalidatedPreset{
TemplateName: p.TemplateName,
TemplateVersionName: p.TemplateVersionName,
PresetName: p.TemplateVersionPresetName,
})
}
return presets
}
func jsonOrEmptyMap(rawMessage pqtype.NullRawMessage) map[string]any {
var m map[string]any
if !rawMessage.Valid {
+33 -3
View File
@@ -596,19 +596,19 @@ var (
// See aibridged package.
subjectAibridged = rbac.Subject{
Type: rbac.SubjectAibridged,
FriendlyName: "AIBridge Daemon",
FriendlyName: "AI Bridge Daemon",
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "aibridged"},
DisplayName: "AIBridge Daemon",
DisplayName: "AI Bridge Daemon",
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceUser.Type: {
policy.ActionRead, // Required to validate API key owner is active.
policy.ActionReadPersonal, // Required to read users' external auth links. // TODO: this is too broad; reduce scope to just external_auth_links by creating separate resource.
},
rbac.ResourceApiKey.Type: {policy.ActionRead}, // Validate API keys.
rbac.ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
rbac.ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
}),
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{},
@@ -1641,6 +1641,15 @@ func (q *querier) DeleteCustomRole(ctx context.Context, arg database.DeleteCusto
return q.db.DeleteCustomRole(ctx, arg)
}
func (q *querier) DeleteExpiredAPIKeys(ctx context.Context, arg database.DeleteExpiredAPIKeysParams) (int64, error) {
// Requires DELETE across all API keys.
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceApiKey); err != nil {
return 0, err
}
return q.db.DeleteExpiredAPIKeys(ctx, arg)
}
func (q *querier) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error {
return fetchAndExec(q.log, q.auth, policy.ActionUpdatePersonal, func(ctx context.Context, arg database.DeleteExternalAuthLinkParams) (database.ExternalAuthLink, error) {
//nolint:gosimple
@@ -1723,6 +1732,13 @@ func (q *querier) DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Contex
return q.db.DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx, arg)
}
func (q *querier) DeleteOldAIBridgeRecords(ctx context.Context, beforeTime time.Time) (int32, error) {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAibridgeInterception); err != nil {
return -1, err
}
return q.db.DeleteOldAIBridgeRecords(ctx, beforeTime)
}
func (q *querier) DeleteOldAuditLogConnectionEvents(ctx context.Context, threshold database.DeleteOldAuditLogConnectionEventsParams) error {
// `ResourceSystem` is deprecated, but it doesn't make sense to add
// `policy.ActionDelete` to `ResourceAuditLog`, since this is the one and
@@ -4972,6 +4988,20 @@ func (q *querier) UpdatePresetPrebuildStatus(ctx context.Context, arg database.U
return q.db.UpdatePresetPrebuildStatus(ctx, arg)
}
func (q *querier) UpdatePresetsLastInvalidatedAt(ctx context.Context, arg database.UpdatePresetsLastInvalidatedAtParams) ([]database.UpdatePresetsLastInvalidatedAtRow, error) {
// Fetch template to check authorization
template, err := q.db.GetTemplateByID(ctx, arg.TemplateID)
if err != nil {
return nil, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil {
return nil, err
}
return q.db.UpdatePresetsLastInvalidatedAt(ctx, arg)
}
func (q *querier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerDaemon); err != nil {
return err
+21
View File
@@ -216,6 +216,14 @@ func (s *MethodTestSuite) TestAPIKey() {
dbm.EXPECT().DeleteAPIKeyByID(gomock.Any(), key.ID).Return(nil).AnyTimes()
check.Args(key.ID).Asserts(key, policy.ActionDelete).Returns()
}))
s.Run("DeleteExpiredAPIKeys", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
args := database.DeleteExpiredAPIKeysParams{
Before: time.Date(2025, 11, 21, 0, 0, 0, 0, time.UTC),
LimitCount: 1000,
}
dbm.EXPECT().DeleteExpiredAPIKeys(gomock.Any(), args).Return(int64(0), nil).AnyTimes()
check.Args(args).Asserts(rbac.ResourceApiKey, policy.ActionDelete).Returns(int64(0))
}))
s.Run("GetAPIKeyByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
key := testutil.Fake(s.T(), faker, database.APIKey{})
dbm.EXPECT().GetAPIKeyByID(gomock.Any(), key.ID).Return(key, nil).AnyTimes()
@@ -1315,6 +1323,13 @@ func (s *MethodTestSuite) TestTemplate() {
dbm.EXPECT().UpsertTemplateUsageStats(gomock.Any()).Return(nil).AnyTimes()
check.Asserts(rbac.ResourceSystem, policy.ActionUpdate)
}))
s.Run("UpdatePresetsLastInvalidatedAt", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
t1 := testutil.Fake(s.T(), faker, database.Template{})
arg := database.UpdatePresetsLastInvalidatedAtParams{LastInvalidatedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, TemplateID: t1.ID}
dbm.EXPECT().GetTemplateByID(gomock.Any(), t1.ID).Return(t1, nil).AnyTimes()
dbm.EXPECT().UpdatePresetsLastInvalidatedAt(gomock.Any(), arg).Return([]database.UpdatePresetsLastInvalidatedAtRow{}, nil).AnyTimes()
check.Args(arg).Asserts(t1, policy.ActionUpdate)
}))
}
func (s *MethodTestSuite) TestUser() {
@@ -4647,6 +4662,12 @@ func (s *MethodTestSuite) TestAIBridge() {
db.EXPECT().UpdateAIBridgeInterceptionEnded(gomock.Any(), params).Return(intc, nil).AnyTimes()
check.Args(params).Asserts(intc, policy.ActionUpdate).Returns(intc)
}))
s.Run("DeleteOldAIBridgeRecords", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
t := dbtime.Now()
db.EXPECT().DeleteOldAIBridgeRecords(gomock.Any(), t).Return(int32(0), nil).AnyTimes()
check.Args(t).Asserts(rbac.ResourceAibridgeInterception, policy.ActionDelete)
}))
}
func (s *MethodTestSuite) TestTelemetry() {
+12 -3
View File
@@ -361,12 +361,20 @@ func (b WorkspaceBuildBuilder) doInTX() WorkspaceResponse {
require.Fail(b.t, "task app not configured but workspace is a task workspace")
}
app := mustWorkspaceAppByWorkspaceAndBuildAndAppID(ownerCtx, b.t, b.db, resp.Workspace.ID, resp.Build.BuildNumber, b.taskAppID)
workspaceAgentID := uuid.NullUUID{}
workspaceAppID := uuid.NullUUID{}
// Workspace agent and app are only properly set upon job completion
if b.jobStatus != database.ProvisionerJobStatusPending && b.jobStatus != database.ProvisionerJobStatusRunning {
app := mustWorkspaceAppByWorkspaceAndBuildAndAppID(ownerCtx, b.t, b.db, resp.Workspace.ID, resp.Build.BuildNumber, b.taskAppID)
workspaceAgentID = uuid.NullUUID{UUID: app.AgentID, Valid: true}
workspaceAppID = uuid.NullUUID{UUID: app.ID, Valid: true}
}
_, err = b.db.UpsertTaskWorkspaceApp(ownerCtx, database.UpsertTaskWorkspaceAppParams{
TaskID: task.ID,
WorkspaceBuildNumber: resp.Build.BuildNumber,
WorkspaceAgentID: uuid.NullUUID{UUID: app.AgentID, Valid: true},
WorkspaceAppID: uuid.NullUUID{UUID: app.ID, Valid: true},
WorkspaceAgentID: workspaceAgentID,
WorkspaceAppID: workspaceAppID,
})
require.NoError(b.t, err, "upsert task workspace app")
b.logger.Debug(context.Background(), "linked task to workspace build",
@@ -605,6 +613,7 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse {
IsDefault: false,
Description: preset.Description,
Icon: preset.Icon,
LastInvalidatedAt: preset.LastInvalidatedAt,
})
t.logger.Debug(context.Background(), "added preset",
slog.F("preset_id", prst.ID),
+9 -1
View File
@@ -175,6 +175,13 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey, munge ...func
}
}
// It does not make sense for the created_at to be after the expires_at.
// So if expires is set, change the default created_at to be 24 hours before.
var createdAt time.Time
if !seed.ExpiresAt.IsZero() && seed.CreatedAt.IsZero() {
createdAt = seed.ExpiresAt.Add(-24 * time.Hour)
}
params := database.InsertAPIKeyParams{
ID: takeFirst(seed.ID, id),
// 0 defaults to 86400 at the db layer
@@ -184,7 +191,7 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey, munge ...func
UserID: takeFirst(seed.UserID, uuid.New()),
LastUsed: takeFirst(seed.LastUsed, dbtime.Now()),
ExpiresAt: takeFirst(seed.ExpiresAt, dbtime.Now().Add(time.Hour)),
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
CreatedAt: takeFirst(seed.CreatedAt, createdAt, dbtime.Now()),
UpdatedAt: takeFirst(seed.UpdatedAt, dbtime.Now()),
LoginType: takeFirst(seed.LoginType, database.LoginTypePassword),
Scopes: takeFirstSlice([]database.APIKeyScope(seed.Scopes), []database.APIKeyScope{database.ApiKeyScopeCoderAll}),
@@ -1428,6 +1435,7 @@ func Preset(t testing.TB, db database.Store, seed database.InsertPresetParams) d
IsDefault: seed.IsDefault,
Description: seed.Description,
Icon: seed.Icon,
LastInvalidatedAt: seed.LastInvalidatedAt,
})
require.NoError(t, err, "insert preset")
return preset
+21
View File
@@ -312,6 +312,13 @@ func (m queryMetricsStore) DeleteCustomRole(ctx context.Context, arg database.De
return r0
}
func (m queryMetricsStore) DeleteExpiredAPIKeys(ctx context.Context, arg database.DeleteExpiredAPIKeysParams) (int64, error) {
start := time.Now()
r0, r1 := m.s.DeleteExpiredAPIKeys(ctx, arg)
m.queryLatencies.WithLabelValues("DeleteExpiredAPIKeys").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error {
start := time.Now()
r0 := m.s.DeleteExternalAuthLink(ctx, arg)
@@ -389,6 +396,13 @@ func (m queryMetricsStore) DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx conte
return r0
}
func (m queryMetricsStore) DeleteOldAIBridgeRecords(ctx context.Context, beforeTime time.Time) (int32, error) {
start := time.Now()
r0, r1 := m.s.DeleteOldAIBridgeRecords(ctx, beforeTime)
m.queryLatencies.WithLabelValues("DeleteOldAIBridgeRecords").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) DeleteOldAuditLogConnectionEvents(ctx context.Context, threshold database.DeleteOldAuditLogConnectionEventsParams) error {
start := time.Now()
r0 := m.s.DeleteOldAuditLogConnectionEvents(ctx, threshold)
@@ -3070,6 +3084,13 @@ func (m queryMetricsStore) UpdatePresetPrebuildStatus(ctx context.Context, arg d
return r0
}
func (m queryMetricsStore) UpdatePresetsLastInvalidatedAt(ctx context.Context, arg database.UpdatePresetsLastInvalidatedAtParams) ([]database.UpdatePresetsLastInvalidatedAtRow, error) {
start := time.Now()
r0, r1 := m.s.UpdatePresetsLastInvalidatedAt(ctx, arg)
m.queryLatencies.WithLabelValues("UpdatePresetsLastInvalidatedAt").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error {
start := time.Now()
r0 := m.s.UpdateProvisionerDaemonLastSeenAt(ctx, arg)
+45
View File
@@ -554,6 +554,21 @@ func (mr *MockStoreMockRecorder) DeleteCustomRole(ctx, arg any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCustomRole", reflect.TypeOf((*MockStore)(nil).DeleteCustomRole), ctx, arg)
}
// DeleteExpiredAPIKeys mocks base method.
func (m *MockStore) DeleteExpiredAPIKeys(ctx context.Context, arg database.DeleteExpiredAPIKeysParams) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteExpiredAPIKeys", ctx, arg)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteExpiredAPIKeys indicates an expected call of DeleteExpiredAPIKeys.
func (mr *MockStoreMockRecorder) DeleteExpiredAPIKeys(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExpiredAPIKeys", reflect.TypeOf((*MockStore)(nil).DeleteExpiredAPIKeys), ctx, arg)
}
// DeleteExternalAuthLink mocks base method.
func (m *MockStore) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error {
m.ctrl.T.Helper()
@@ -709,6 +724,21 @@ func (mr *MockStoreMockRecorder) DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOAuth2ProviderAppTokensByAppAndUserID", reflect.TypeOf((*MockStore)(nil).DeleteOAuth2ProviderAppTokensByAppAndUserID), ctx, arg)
}
// DeleteOldAIBridgeRecords mocks base method.
func (m *MockStore) DeleteOldAIBridgeRecords(ctx context.Context, beforeTime time.Time) (int32, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteOldAIBridgeRecords", ctx, beforeTime)
ret0, _ := ret[0].(int32)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteOldAIBridgeRecords indicates an expected call of DeleteOldAIBridgeRecords.
func (mr *MockStoreMockRecorder) DeleteOldAIBridgeRecords(ctx, beforeTime any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldAIBridgeRecords", reflect.TypeOf((*MockStore)(nil).DeleteOldAIBridgeRecords), ctx, beforeTime)
}
// DeleteOldAuditLogConnectionEvents mocks base method.
func (m *MockStore) DeleteOldAuditLogConnectionEvents(ctx context.Context, arg database.DeleteOldAuditLogConnectionEventsParams) error {
m.ctrl.T.Helper()
@@ -6598,6 +6628,21 @@ func (mr *MockStoreMockRecorder) UpdatePresetPrebuildStatus(ctx, arg any) *gomoc
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePresetPrebuildStatus", reflect.TypeOf((*MockStore)(nil).UpdatePresetPrebuildStatus), ctx, arg)
}
// UpdatePresetsLastInvalidatedAt mocks base method.
func (m *MockStore) UpdatePresetsLastInvalidatedAt(ctx context.Context, arg database.UpdatePresetsLastInvalidatedAtParams) ([]database.UpdatePresetsLastInvalidatedAtRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdatePresetsLastInvalidatedAt", ctx, arg)
ret0, _ := ret[0].([]database.UpdatePresetsLastInvalidatedAtRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdatePresetsLastInvalidatedAt indicates an expected call of UpdatePresetsLastInvalidatedAt.
func (mr *MockStoreMockRecorder) UpdatePresetsLastInvalidatedAt(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePresetsLastInvalidatedAt", reflect.TypeOf((*MockStore)(nil).UpdatePresetsLastInvalidatedAt), ctx, arg)
}
// UpdateProvisionerDaemonLastSeenAt mocks base method.
func (m *MockStore) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error {
m.ctrl.T.Helper()
+27 -2
View File
@@ -13,6 +13,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/pproflabel"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/quartz"
)
@@ -36,7 +37,7 @@ const (
// It is the caller's responsibility to call Close on the returned instance.
//
// This is for cleaning up old, unused resources from the database that take up space.
func New(ctx context.Context, logger slog.Logger, db database.Store, clk quartz.Clock) io.Closer {
func New(ctx context.Context, logger slog.Logger, db database.Store, vals *codersdk.DeploymentValues, clk quartz.Clock) io.Closer {
closed := make(chan struct{})
ctx, cancelFunc := context.WithCancel(ctx)
@@ -77,6 +78,19 @@ func New(ctx context.Context, logger slog.Logger, db database.Store, clk quartz.
if err := tx.ExpirePrebuildsAPIKeys(ctx, dbtime.Time(start)); err != nil {
return xerrors.Errorf("failed to expire prebuilds user api keys: %w", err)
}
expiredAPIKeys, err := tx.DeleteExpiredAPIKeys(ctx, database.DeleteExpiredAPIKeysParams{
// Leave expired keys for a week to allow the backend to know the difference
// between a 404 and an expired key. This purge code is just to bound the size of
// the table to something more reasonable.
Before: dbtime.Time(start.Add(time.Hour * 24 * 7 * -1)),
// There could be a lot of expired keys here, so set a limit to prevent this
// taking too long.
// This runs every 10 minutes, so it deletes ~1.5m keys per day at most.
LimitCount: 10000,
})
if err != nil {
return xerrors.Errorf("failed to delete expired api keys: %w", err)
}
deleteOldTelemetryLocksBefore := start.Add(-maxTelemetryHeartbeatAge)
if err := tx.DeleteOldTelemetryLocks(ctx, deleteOldTelemetryLocksBefore); err != nil {
return xerrors.Errorf("failed to delete old telemetry locks: %w", err)
@@ -90,7 +104,18 @@ func New(ctx context.Context, logger slog.Logger, db database.Store, clk quartz.
return xerrors.Errorf("failed to delete old audit log connection events: %w", err)
}
logger.Debug(ctx, "purged old database entries", slog.F("duration", clk.Since(start)))
deleteAIBridgeRecordsBefore := start.Add(-vals.AI.BridgeConfig.Retention.Value())
// nolint:gocritic // Needs to run as aibridge context.
purgedAIBridgeRecords, err := tx.DeleteOldAIBridgeRecords(dbauthz.AsAIBridged(ctx), deleteAIBridgeRecordsBefore)
if err != nil {
return xerrors.Errorf("failed to delete old aibridge records: %w", err)
}
logger.Debug(ctx, "purged old database entries",
slog.F("expired_api_keys", expiredAPIKeys),
slog.F("aibridge_records", purgedAIBridgeRecords),
slog.F("duration", clk.Since(start)),
)
return nil
}, database.DefaultTXOptions().WithID("db_purge")); err != nil {
+177 -7
View File
@@ -33,6 +33,7 @@ import (
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
"github.com/coder/serpent"
)
func TestMain(m *testing.M) {
@@ -51,7 +52,7 @@ func TestPurge(t *testing.T) {
done := awaitDoTick(ctx, t, clk)
mDB := dbmock.NewMockStore(gomock.NewController(t))
mDB.EXPECT().InTx(gomock.Any(), database.DefaultTXOptions().WithID("db_purge")).Return(nil).Times(2)
purger := dbpurge.New(context.Background(), testutil.Logger(t), mDB, clk)
purger := dbpurge.New(context.Background(), testutil.Logger(t), mDB, &codersdk.DeploymentValues{}, clk)
<-done // wait for doTick() to run.
require.NoError(t, purger.Close())
}
@@ -129,7 +130,7 @@ func TestDeleteOldWorkspaceAgentStats(t *testing.T) {
})
// when
closer := dbpurge.New(ctx, logger, db, clk)
closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{}, clk)
defer closer.Close()
// then
@@ -154,7 +155,7 @@ func TestDeleteOldWorkspaceAgentStats(t *testing.T) {
// Start a new purger to immediately trigger delete after rollup.
_ = closer.Close()
closer = dbpurge.New(ctx, logger, db, clk)
closer = dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{}, clk)
defer closer.Close()
// then
@@ -245,7 +246,7 @@ func TestDeleteOldWorkspaceAgentLogs(t *testing.T) {
// After dbpurge completes, the ticker is reset. Trap this call.
done := awaitDoTick(ctx, t, clk)
closer := dbpurge.New(ctx, logger, db, clk)
closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{}, clk)
defer closer.Close()
<-done // doTick() has now run.
@@ -466,7 +467,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) {
require.NoError(t, err)
// when
closer := dbpurge.New(ctx, logger, db, clk)
closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{}, clk)
defer closer.Close()
// then
@@ -570,7 +571,7 @@ func TestDeleteOldAuditLogConnectionEvents(t *testing.T) {
// Run the purge
done := awaitDoTick(ctx, t, clk)
closer := dbpurge.New(ctx, logger, db, clk)
closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{}, clk)
defer closer.Close()
// Wait for tick
testutil.TryReceive(ctx, t, done)
@@ -733,7 +734,7 @@ func TestDeleteOldTelemetryHeartbeats(t *testing.T) {
require.NoError(t, err)
done := awaitDoTick(ctx, t, clk)
closer := dbpurge.New(ctx, logger, db, clk)
closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{}, clk)
defer closer.Close()
<-done // doTick() has now run.
@@ -757,3 +758,172 @@ func TestDeleteOldTelemetryHeartbeats(t *testing.T) {
return totalCount == 2 && oldCount == 0
}, testutil.WaitShort, testutil.IntervalFast, "it should delete old telemetry heartbeats")
}
func TestDeleteOldAIBridgeRecords(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
clk := quartz.NewMock(t)
now := time.Date(2025, 1, 15, 7, 30, 0, 0, time.UTC)
retentionPeriod := 30 * 24 * time.Hour // 30 days
afterThreshold := now.Add(-retentionPeriod).Add(-24 * time.Hour) // 31 days ago (older than threshold)
beforeThreshold := now.Add(-15 * 24 * time.Hour) // 15 days ago (newer than threshold)
closeBeforeThreshold := now.Add(-retentionPeriod).Add(24 * time.Hour) // 29 days ago
clk.Set(now).MustWait(ctx)
db, _ := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure())
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
user := dbgen.User(t, db, database.User{})
// Create old AI Bridge interception (should be deleted)
oldInterception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
ID: uuid.New(),
APIKeyID: sql.NullString{},
InitiatorID: user.ID,
Provider: "anthropic",
Model: "claude-3-5-sonnet",
StartedAt: afterThreshold,
}, &afterThreshold)
// Create old interception with related records (should all be deleted)
oldInterceptionWithRelated := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
ID: uuid.New(),
APIKeyID: sql.NullString{},
InitiatorID: user.ID,
Provider: "openai",
Model: "gpt-4",
StartedAt: afterThreshold,
}, &afterThreshold)
_ = dbgen.AIBridgeTokenUsage(t, db, database.InsertAIBridgeTokenUsageParams{
ID: uuid.New(),
InterceptionID: oldInterceptionWithRelated.ID,
ProviderResponseID: "resp-1",
InputTokens: 100,
OutputTokens: 50,
CreatedAt: afterThreshold,
})
_ = dbgen.AIBridgeUserPrompt(t, db, database.InsertAIBridgeUserPromptParams{
ID: uuid.New(),
InterceptionID: oldInterceptionWithRelated.ID,
ProviderResponseID: "resp-1",
Prompt: "test prompt",
CreatedAt: afterThreshold,
})
_ = dbgen.AIBridgeToolUsage(t, db, database.InsertAIBridgeToolUsageParams{
ID: uuid.New(),
InterceptionID: oldInterceptionWithRelated.ID,
ProviderResponseID: "resp-1",
Tool: "test-tool",
ServerUrl: sql.NullString{String: "http://test", Valid: true},
Input: "{}",
Injected: true,
CreatedAt: afterThreshold,
})
// Create recent AI Bridge interception (should be kept)
recentInterception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
ID: uuid.New(),
APIKeyID: sql.NullString{},
InitiatorID: user.ID,
Provider: "anthropic",
Model: "claude-3-5-sonnet",
StartedAt: beforeThreshold,
}, &beforeThreshold)
// Create interception close to threshold (should be kept)
nearThresholdInterception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
ID: uuid.New(),
APIKeyID: sql.NullString{},
InitiatorID: user.ID,
Provider: "anthropic",
Model: "claude-3-5-sonnet",
StartedAt: closeBeforeThreshold,
}, &closeBeforeThreshold)
_ = dbgen.AIBridgeTokenUsage(t, db, database.InsertAIBridgeTokenUsageParams{
ID: uuid.New(),
InterceptionID: nearThresholdInterception.ID,
ProviderResponseID: "resp-1",
InputTokens: 100,
OutputTokens: 50,
CreatedAt: closeBeforeThreshold,
})
_ = dbgen.AIBridgeUserPrompt(t, db, database.InsertAIBridgeUserPromptParams{
ID: uuid.New(),
InterceptionID: nearThresholdInterception.ID,
ProviderResponseID: "resp-1",
Prompt: "test prompt",
CreatedAt: closeBeforeThreshold,
})
_ = dbgen.AIBridgeToolUsage(t, db, database.InsertAIBridgeToolUsageParams{
ID: uuid.New(),
InterceptionID: nearThresholdInterception.ID,
ProviderResponseID: "resp-1",
Tool: "test-tool",
ServerUrl: sql.NullString{String: "http://test", Valid: true},
Input: "{}",
Injected: true,
CreatedAt: closeBeforeThreshold,
})
// Run the purge with configured retention period
done := awaitDoTick(ctx, t, clk)
closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{
AI: codersdk.AIConfig{
BridgeConfig: codersdk.AIBridgeConfig{
Retention: serpent.Duration(retentionPeriod),
},
},
}, clk)
defer closer.Close()
// Wait for tick
testutil.TryReceive(ctx, t, done)
// Verify results by querying all AI Bridge records
interceptions, err := db.GetAIBridgeInterceptions(ctx)
require.NoError(t, err)
// Extract interception IDs for comparison
interceptionIDs := make([]uuid.UUID, len(interceptions))
for i, interception := range interceptions {
interceptionIDs[i] = interception.ID
}
require.NotContains(t, interceptionIDs, oldInterception.ID, "old interception should be deleted")
require.NotContains(t, interceptionIDs, oldInterceptionWithRelated.ID, "old interception with related records should be deleted")
// Verify related records were also deleted
oldTokenUsages, err := db.GetAIBridgeTokenUsagesByInterceptionID(ctx, oldInterceptionWithRelated.ID)
require.NoError(t, err)
require.Empty(t, oldTokenUsages, "old token usages should be deleted")
oldUserPrompts, err := db.GetAIBridgeUserPromptsByInterceptionID(ctx, oldInterceptionWithRelated.ID)
require.NoError(t, err)
require.Empty(t, oldUserPrompts, "old user prompts should be deleted")
oldToolUsages, err := db.GetAIBridgeToolUsagesByInterceptionID(ctx, oldInterceptionWithRelated.ID)
require.NoError(t, err)
require.Empty(t, oldToolUsages, "old tool usages should be deleted")
require.Contains(t, interceptionIDs, recentInterception.ID, "recent interception should be kept")
require.Contains(t, interceptionIDs, nearThresholdInterception.ID, "near threshold interception should be kept")
// Verify related records were NOT deleted
newTokenUsages, err := db.GetAIBridgeTokenUsagesByInterceptionID(ctx, nearThresholdInterception.ID)
require.NoError(t, err)
require.Len(t, newTokenUsages, 1, "near threshold token usages should not be deleted")
newUserPrompts, err := db.GetAIBridgeUserPromptsByInterceptionID(ctx, nearThresholdInterception.ID)
require.NoError(t, err)
require.Len(t, newUserPrompts, 1, "near threshold user prompts should not be deleted")
newToolUsages, err := db.GetAIBridgeToolUsagesByInterceptionID(ctx, nearThresholdInterception.ID)
require.NoError(t, err)
require.Len(t, newToolUsages, 1, "near threshold tool usages should not be deleted")
}
+2 -1
View File
@@ -2170,7 +2170,8 @@ CREATE TABLE template_version_presets (
scheduling_timezone text DEFAULT ''::text NOT NULL,
is_default boolean DEFAULT false NOT NULL,
description character varying(128) DEFAULT ''::character varying NOT NULL,
icon character varying(256) DEFAULT ''::character varying NOT NULL
icon character varying(256) DEFAULT ''::character varying NOT NULL,
last_invalidated_at timestamp with time zone
);
COMMENT ON COLUMN template_version_presets.description IS 'Short text describing the preset (max 128 characters).';
@@ -0,0 +1 @@
ALTER TABLE template_version_presets DROP COLUMN last_invalidated_at;
@@ -0,0 +1 @@
ALTER TABLE template_version_presets ADD COLUMN last_invalidated_at TIMESTAMPTZ;
+1
View File
@@ -662,6 +662,7 @@ func ConvertWorkspaceRows(rows []GetWorkspacesRow) []Workspace {
TemplateIcon: r.TemplateIcon,
TemplateDescription: r.TemplateDescription,
NextStartAt: r.NextStartAt,
TaskID: r.TaskID,
}
}
+2 -1
View File
@@ -4452,7 +4452,8 @@ type TemplateVersionPreset struct {
// Short text describing the preset (max 128 characters).
Description string `db:"description" json:"description"`
// URL or path to an icon representing the preset (max 256 characters).
Icon string `db:"icon" json:"icon"`
Icon string `db:"icon" json:"icon"`
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
}
type TemplateVersionPresetParameter struct {
+5 -1
View File
@@ -91,6 +91,7 @@ type sqlcQuerier interface {
DeleteCoordinator(ctx context.Context, id uuid.UUID) error
DeleteCryptoKey(ctx context.Context, arg DeleteCryptoKeyParams) (CryptoKey, error)
DeleteCustomRole(ctx context.Context, arg DeleteCustomRoleParams) error
DeleteExpiredAPIKeys(ctx context.Context, arg DeleteExpiredAPIKeysParams) (int64, error)
DeleteExternalAuthLink(ctx context.Context, arg DeleteExternalAuthLinkParams) error
DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error
DeleteGroupByID(ctx context.Context, id uuid.UUID) error
@@ -102,6 +103,8 @@ type sqlcQuerier interface {
DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppCodesByAppAndUserIDParams) error
DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) error
DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppTokensByAppAndUserIDParams) error
// Cumulative count.
DeleteOldAIBridgeRecords(ctx context.Context, beforeTime time.Time) (int32, error)
DeleteOldAuditLogConnectionEvents(ctx context.Context, arg DeleteOldAuditLogConnectionEventsParams) error
// Delete all notification messages which have not been updated for over a week.
DeleteOldNotificationMessages(ctx context.Context) error
@@ -610,7 +613,7 @@ type sqlcQuerier interface {
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error)
ListAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams) ([]ListAIBridgeInterceptionsRow, error)
// Finds all unique AIBridge interception telemetry summaries combinations
// Finds all unique AI Bridge interception telemetry summaries combinations
// (provider, model, client) in the given timeframe for telemetry reporting.
ListAIBridgeInterceptionsTelemetrySummaries(ctx context.Context, arg ListAIBridgeInterceptionsTelemetrySummariesParams) ([]ListAIBridgeInterceptionsTelemetrySummariesRow, error)
ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeTokenUsage, error)
@@ -673,6 +676,7 @@ type sqlcQuerier interface {
// This is an optimization to clean up stale pending jobs.
UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg UpdatePrebuildProvisionerJobWithCancelParams) ([]UpdatePrebuildProvisionerJobWithCancelRow, error)
UpdatePresetPrebuildStatus(ctx context.Context, arg UpdatePresetPrebuildStatusParams) error
UpdatePresetsLastInvalidatedAt(ctx context.Context, arg UpdatePresetsLastInvalidatedAtParams) ([]UpdatePresetsLastInvalidatedAtRow, error)
UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error
UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error
UpdateProvisionerJobLogsLength(ctx context.Context, arg UpdateProvisionerJobLogsLengthParams) error
+78
View File
@@ -7835,3 +7835,81 @@ func TestUpdateAIBridgeInterceptionEnded(t *testing.T) {
}
})
}
func TestDeleteExpiredAPIKeys(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
// Constant time for testing
now := time.Date(2025, 11, 20, 12, 0, 0, 0, time.UTC)
expiredBefore := now.Add(-time.Hour) // Anything before this is expired
ctx := testutil.Context(t, testutil.WaitLong)
user := dbgen.User(t, db, database.User{})
expiredTimes := []time.Time{
expiredBefore.Add(-time.Hour * 24 * 365),
expiredBefore.Add(-time.Hour * 24),
expiredBefore.Add(-time.Hour),
expiredBefore.Add(-time.Minute),
expiredBefore.Add(-time.Second),
}
for _, exp := range expiredTimes {
// Expired api keys
dbgen.APIKey(t, db, database.APIKey{UserID: user.ID, ExpiresAt: exp})
}
unexpiredTimes := []time.Time{
expiredBefore.Add(time.Hour * 24 * 365),
expiredBefore.Add(time.Hour * 24),
expiredBefore.Add(time.Hour),
expiredBefore.Add(time.Minute),
expiredBefore.Add(time.Second),
}
for _, unexp := range unexpiredTimes {
// Unexpired api keys
dbgen.APIKey(t, db, database.APIKey{UserID: user.ID, ExpiresAt: unexp})
}
// All keys are present before deletion
keys, err := db.GetAPIKeysByUserID(ctx, database.GetAPIKeysByUserIDParams{
LoginType: user.LoginType,
UserID: user.ID,
})
require.NoError(t, err)
require.Len(t, keys, len(expiredTimes)+len(unexpiredTimes))
// Delete expired keys
// First verify the limit works by deleting one at a time
deletedCount, err := db.DeleteExpiredAPIKeys(ctx, database.DeleteExpiredAPIKeysParams{
Before: expiredBefore,
LimitCount: 1,
})
require.NoError(t, err)
require.Equal(t, int64(1), deletedCount)
// Ensure it was deleted
remaining, err := db.GetAPIKeysByUserID(ctx, database.GetAPIKeysByUserIDParams{
LoginType: user.LoginType,
UserID: user.ID,
})
require.NoError(t, err)
require.Len(t, remaining, len(expiredTimes)+len(unexpiredTimes)-1)
// Delete the rest of the expired keys
deletedCount, err = db.DeleteExpiredAPIKeys(ctx, database.DeleteExpiredAPIKeysParams{
Before: expiredBefore,
LimitCount: 100,
})
require.NoError(t, err)
require.Equal(t, int64(len(expiredTimes)-1), deletedCount)
// Ensure only unexpired keys remain
remaining, err = db.GetAPIKeysByUserID(ctx, database.GetAPIKeysByUserIDParams{
LoginType: user.LoginType,
UserID: user.ID,
})
require.NoError(t, err)
require.Len(t, remaining, len(unexpiredTimes))
}
+151 -9
View File
@@ -275,8 +275,10 @@ SELECT
FROM
aibridge_interceptions
WHERE
-- Remove inflight interceptions (ones which lack an ended_at value).
aibridge_interceptions.ended_at IS NOT NULL
-- Filter by time frame
CASE
AND CASE
WHEN $1::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at >= $1::timestamptz
ELSE true
END
@@ -324,6 +326,49 @@ func (q *sqlQuerier) CountAIBridgeInterceptions(ctx context.Context, arg CountAI
return count, err
}
const deleteOldAIBridgeRecords = `-- name: DeleteOldAIBridgeRecords :one
WITH
-- We don't have FK relationships between the dependent tables and aibridge_interceptions, so we can't rely on DELETE CASCADE.
to_delete AS (
SELECT id FROM aibridge_interceptions
WHERE started_at < $1::timestamp with time zone
),
-- CTEs are executed in order.
tool_usages AS (
DELETE FROM aibridge_tool_usages
WHERE interception_id IN (SELECT id FROM to_delete)
RETURNING 1
),
token_usages AS (
DELETE FROM aibridge_token_usages
WHERE interception_id IN (SELECT id FROM to_delete)
RETURNING 1
),
user_prompts AS (
DELETE FROM aibridge_user_prompts
WHERE interception_id IN (SELECT id FROM to_delete)
RETURNING 1
),
interceptions AS (
DELETE FROM aibridge_interceptions
WHERE id IN (SELECT id FROM to_delete)
RETURNING 1
)
SELECT
(SELECT COUNT(*) FROM tool_usages) +
(SELECT COUNT(*) FROM token_usages) +
(SELECT COUNT(*) FROM user_prompts) +
(SELECT COUNT(*) FROM interceptions) as total_deleted
`
// Cumulative count.
func (q *sqlQuerier) DeleteOldAIBridgeRecords(ctx context.Context, beforeTime time.Time) (int32, error) {
row := q.db.QueryRowContext(ctx, deleteOldAIBridgeRecords, beforeTime)
var total_deleted int32
err := row.Scan(&total_deleted)
return total_deleted, err
}
const getAIBridgeInterceptionByID = `-- name: GetAIBridgeInterceptionByID :one
SELECT
id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id
@@ -701,8 +746,10 @@ FROM
JOIN
visible_users ON visible_users.id = aibridge_interceptions.initiator_id
WHERE
-- Remove inflight interceptions (ones which lack an ended_at value).
aibridge_interceptions.ended_at IS NOT NULL
-- Filter by time frame
CASE
AND CASE
WHEN $1::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at >= $1::timestamptz
ELSE true
END
@@ -837,7 +884,7 @@ type ListAIBridgeInterceptionsTelemetrySummariesRow struct {
Client string `db:"client" json:"client"`
}
// Finds all unique AIBridge interception telemetry summaries combinations
// Finds all unique AI Bridge interception telemetry summaries combinations
// (provider, model, client) in the given timeframe for telemetry reporting.
func (q *sqlQuerier) ListAIBridgeInterceptionsTelemetrySummaries(ctx context.Context, arg ListAIBridgeInterceptionsTelemetrySummariesParams) ([]ListAIBridgeInterceptionsTelemetrySummariesRow, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeInterceptionsTelemetrySummaries, arg.EndedAtAfter, arg.EndedAtBefore)
@@ -1060,6 +1107,38 @@ func (q *sqlQuerier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context
return err
}
const deleteExpiredAPIKeys = `-- name: DeleteExpiredAPIKeys :one
WITH expired_keys AS (
SELECT id
FROM api_keys
-- expired keys only
WHERE expires_at < $1::timestamptz
LIMIT $2
),
deleted_rows AS (
DELETE FROM
api_keys
USING
expired_keys
WHERE
api_keys.id = expired_keys.id
RETURNING api_keys.id
)
SELECT COUNT(deleted_rows.id) AS deleted_count FROM deleted_rows
`
type DeleteExpiredAPIKeysParams struct {
Before time.Time `db:"before" json:"before"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
func (q *sqlQuerier) DeleteExpiredAPIKeys(ctx context.Context, arg DeleteExpiredAPIKeysParams) (int64, error) {
row := q.db.QueryRowContext(ctx, deleteExpiredAPIKeys, arg.Before, arg.LimitCount)
var deleted_count int64
err := row.Scan(&deleted_count)
return deleted_count, err
}
const expirePrebuildsAPIKeys = `-- name: ExpirePrebuildsAPIKeys :exec
WITH unexpired_prebuilds_workspace_session_tokens AS (
SELECT id, SUBSTRING(token_name FROM 38 FOR 36)::uuid AS workspace_id
@@ -8709,6 +8788,7 @@ SELECT
tvp.scheduling_timezone,
tvp.invalidate_after_secs AS ttl,
tvp.prebuild_status,
tvp.last_invalidated_at,
t.deleted,
t.deprecated != '' AS deprecated
FROM templates t
@@ -8734,6 +8814,7 @@ type GetTemplatePresetsWithPrebuildsRow struct {
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
Ttl sql.NullInt32 `db:"ttl" json:"ttl"`
PrebuildStatus PrebuildStatus `db:"prebuild_status" json:"prebuild_status"`
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
Deleted bool `db:"deleted" json:"deleted"`
Deprecated bool `db:"deprecated" json:"deprecated"`
}
@@ -8764,6 +8845,7 @@ func (q *sqlQuerier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templa
&i.SchedulingTimezone,
&i.Ttl,
&i.PrebuildStatus,
&i.LastInvalidatedAt,
&i.Deleted,
&i.Deprecated,
); err != nil {
@@ -8897,7 +8979,7 @@ func (q *sqlQuerier) GetActivePresetPrebuildSchedules(ctx context.Context) ([]Te
}
const getPresetByID = `-- name: GetPresetByID :one
SELECT tvp.id, tvp.template_version_id, tvp.name, tvp.created_at, tvp.desired_instances, tvp.invalidate_after_secs, tvp.prebuild_status, tvp.scheduling_timezone, tvp.is_default, tvp.description, tvp.icon, tv.template_id, tv.organization_id FROM
SELECT tvp.id, tvp.template_version_id, tvp.name, tvp.created_at, tvp.desired_instances, tvp.invalidate_after_secs, tvp.prebuild_status, tvp.scheduling_timezone, tvp.is_default, tvp.description, tvp.icon, tvp.last_invalidated_at, tv.template_id, tv.organization_id FROM
template_version_presets tvp
INNER JOIN template_versions tv ON tvp.template_version_id = tv.id
WHERE tvp.id = $1
@@ -8915,6 +8997,7 @@ type GetPresetByIDRow struct {
IsDefault bool `db:"is_default" json:"is_default"`
Description string `db:"description" json:"description"`
Icon string `db:"icon" json:"icon"`
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}
@@ -8934,6 +9017,7 @@ func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (Get
&i.IsDefault,
&i.Description,
&i.Icon,
&i.LastInvalidatedAt,
&i.TemplateID,
&i.OrganizationID,
)
@@ -8942,7 +9026,7 @@ func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (Get
const getPresetByWorkspaceBuildID = `-- name: GetPresetByWorkspaceBuildID :one
SELECT
template_version_presets.id, template_version_presets.template_version_id, template_version_presets.name, template_version_presets.created_at, template_version_presets.desired_instances, template_version_presets.invalidate_after_secs, template_version_presets.prebuild_status, template_version_presets.scheduling_timezone, template_version_presets.is_default, template_version_presets.description, template_version_presets.icon
template_version_presets.id, template_version_presets.template_version_id, template_version_presets.name, template_version_presets.created_at, template_version_presets.desired_instances, template_version_presets.invalidate_after_secs, template_version_presets.prebuild_status, template_version_presets.scheduling_timezone, template_version_presets.is_default, template_version_presets.description, template_version_presets.icon, template_version_presets.last_invalidated_at
FROM
template_version_presets
INNER JOIN workspace_builds ON workspace_builds.template_version_preset_id = template_version_presets.id
@@ -8965,6 +9049,7 @@ func (q *sqlQuerier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceB
&i.IsDefault,
&i.Description,
&i.Icon,
&i.LastInvalidatedAt,
)
return i, err
}
@@ -9046,7 +9131,7 @@ func (q *sqlQuerier) GetPresetParametersByTemplateVersionID(ctx context.Context,
const getPresetsByTemplateVersionID = `-- name: GetPresetsByTemplateVersionID :many
SELECT
id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default, description, icon
id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default, description, icon, last_invalidated_at
FROM
template_version_presets
WHERE
@@ -9074,6 +9159,7 @@ func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, template
&i.IsDefault,
&i.Description,
&i.Icon,
&i.LastInvalidatedAt,
); err != nil {
return nil, err
}
@@ -9099,7 +9185,8 @@ INSERT INTO template_version_presets (
scheduling_timezone,
is_default,
description,
icon
icon,
last_invalidated_at
)
VALUES (
$1,
@@ -9111,8 +9198,9 @@ VALUES (
$7,
$8,
$9,
$10
) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default, description, icon
$10,
$11
) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default, description, icon, last_invalidated_at
`
type InsertPresetParams struct {
@@ -9126,6 +9214,7 @@ type InsertPresetParams struct {
IsDefault bool `db:"is_default" json:"is_default"`
Description string `db:"description" json:"description"`
Icon string `db:"icon" json:"icon"`
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
}
func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error) {
@@ -9140,6 +9229,7 @@ func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (
arg.IsDefault,
arg.Description,
arg.Icon,
arg.LastInvalidatedAt,
)
var i TemplateVersionPreset
err := row.Scan(
@@ -9154,6 +9244,7 @@ func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (
&i.IsDefault,
&i.Description,
&i.Icon,
&i.LastInvalidatedAt,
)
return i, err
}
@@ -9249,6 +9340,57 @@ func (q *sqlQuerier) UpdatePresetPrebuildStatus(ctx context.Context, arg UpdateP
return err
}
const updatePresetsLastInvalidatedAt = `-- name: UpdatePresetsLastInvalidatedAt :many
UPDATE
template_version_presets tvp
SET
last_invalidated_at = $1
FROM
templates t
JOIN template_versions tv ON tv.id = t.active_version_id
WHERE
t.id = $2
AND tvp.template_version_id = tv.id
RETURNING
t.name AS template_name,
tv.name AS template_version_name,
tvp.name AS template_version_preset_name
`
type UpdatePresetsLastInvalidatedAtParams struct {
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
}
type UpdatePresetsLastInvalidatedAtRow struct {
TemplateName string `db:"template_name" json:"template_name"`
TemplateVersionName string `db:"template_version_name" json:"template_version_name"`
TemplateVersionPresetName string `db:"template_version_preset_name" json:"template_version_preset_name"`
}
func (q *sqlQuerier) UpdatePresetsLastInvalidatedAt(ctx context.Context, arg UpdatePresetsLastInvalidatedAtParams) ([]UpdatePresetsLastInvalidatedAtRow, error) {
rows, err := q.db.QueryContext(ctx, updatePresetsLastInvalidatedAt, arg.LastInvalidatedAt, arg.TemplateID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []UpdatePresetsLastInvalidatedAtRow
for rows.Next() {
var i UpdatePresetsLastInvalidatedAtRow
if err := rows.Scan(&i.TemplateName, &i.TemplateVersionName, &i.TemplateVersionPresetName); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteOldProvisionerDaemons = `-- name: DeleteOldProvisionerDaemons :exec
DELETE FROM provisioner_daemons WHERE (
(created_at < (NOW() - INTERVAL '7 days') AND last_seen_at IS NULL) OR
+42 -3
View File
@@ -89,8 +89,10 @@ SELECT
FROM
aibridge_interceptions
WHERE
-- Remove inflight interceptions (ones which lack an ended_at value).
aibridge_interceptions.ended_at IS NOT NULL
-- Filter by time frame
CASE
AND CASE
WHEN @started_after::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at >= @started_after::timestamptz
ELSE true
END
@@ -126,8 +128,10 @@ FROM
JOIN
visible_users ON visible_users.id = aibridge_interceptions.initiator_id
WHERE
-- Remove inflight interceptions (ones which lack an ended_at value).
aibridge_interceptions.ended_at IS NOT NULL
-- Filter by time frame
CASE
AND CASE
WHEN @started_after::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at >= @started_after::timestamptz
ELSE true
END
@@ -209,7 +213,7 @@ ORDER BY
id ASC;
-- name: ListAIBridgeInterceptionsTelemetrySummaries :many
-- Finds all unique AIBridge interception telemetry summaries combinations
-- Finds all unique AI Bridge interception telemetry summaries combinations
-- (provider, model, client) in the given timeframe for telemetry reporting.
SELECT
DISTINCT ON (provider, model, client)
@@ -326,3 +330,38 @@ FROM
prompt_aggregates pa,
tool_aggregates tool_agg
;
-- name: DeleteOldAIBridgeRecords :one
WITH
-- We don't have FK relationships between the dependent tables and aibridge_interceptions, so we can't rely on DELETE CASCADE.
to_delete AS (
SELECT id FROM aibridge_interceptions
WHERE started_at < @before_time::timestamp with time zone
),
-- CTEs are executed in order.
tool_usages AS (
DELETE FROM aibridge_tool_usages
WHERE interception_id IN (SELECT id FROM to_delete)
RETURNING 1
),
token_usages AS (
DELETE FROM aibridge_token_usages
WHERE interception_id IN (SELECT id FROM to_delete)
RETURNING 1
),
user_prompts AS (
DELETE FROM aibridge_user_prompts
WHERE interception_id IN (SELECT id FROM to_delete)
RETURNING 1
),
interceptions AS (
DELETE FROM aibridge_interceptions
WHERE id IN (SELECT id FROM to_delete)
RETURNING 1
)
-- Cumulative count.
SELECT
(SELECT COUNT(*) FROM tool_usages) +
(SELECT COUNT(*) FROM token_usages) +
(SELECT COUNT(*) FROM user_prompts) +
(SELECT COUNT(*) FROM interceptions) as total_deleted;
+20
View File
@@ -85,6 +85,26 @@ DELETE FROM
WHERE
user_id = $1;
-- name: DeleteExpiredAPIKeys :one
WITH expired_keys AS (
SELECT id
FROM api_keys
-- expired keys only
WHERE expires_at < @before::timestamptz
LIMIT @limit_count
),
deleted_rows AS (
DELETE FROM
api_keys
USING
expired_keys
WHERE
api_keys.id = expired_keys.id
RETURNING api_keys.id
)
SELECT COUNT(deleted_rows.id) AS deleted_count FROM deleted_rows;
;
-- name: ExpirePrebuildsAPIKeys :exec
-- Firstly, collect api_keys owned by the prebuilds user that correlate
-- to workspaces no longer owned by the prebuilds user.
+1
View File
@@ -51,6 +51,7 @@ SELECT
tvp.scheduling_timezone,
tvp.invalidate_after_secs AS ttl,
tvp.prebuild_status,
tvp.last_invalidated_at,
t.deleted,
t.deprecated != '' AS deprecated
FROM templates t
+20 -2
View File
@@ -9,7 +9,8 @@ INSERT INTO template_version_presets (
scheduling_timezone,
is_default,
description,
icon
icon,
last_invalidated_at
)
VALUES (
@id,
@@ -21,7 +22,8 @@ VALUES (
@scheduling_timezone,
@is_default,
@description,
@icon
@icon,
@last_invalidated_at
) RETURNING *;
-- name: InsertPresetParameters :many
@@ -103,3 +105,19 @@ WHERE
tv.id = t.active_version_id
AND NOT t.deleted
AND t.deprecated = '';
-- name: UpdatePresetsLastInvalidatedAt :many
UPDATE
template_version_presets tvp
SET
last_invalidated_at = @last_invalidated_at
FROM
templates t
JOIN template_versions tv ON tv.id = t.active_version_id
WHERE
t.id = @template_id
AND tvp.template_version_id = tv.id
RETURNING
t.name AS template_name,
tv.name AS template_version_name,
tvp.name AS template_version_preset_name;
+21 -12
View File
@@ -125,20 +125,29 @@ func (s GlobalSnapshot) IsHardLimited(presetID uuid.UUID) bool {
}
// filterExpiredWorkspaces splits running workspaces into expired and non-expired
// based on the preset's TTL.
// If TTL is missing or zero, all workspaces are considered non-expired.
// based on the preset's TTL and last_invalidated_at timestamp.
// A prebuild is considered expired if:
// 1. The preset has been invalidated (last_invalidated_at is set), OR
// 2. It exceeds the preset's TTL (if TTL is set)
// If TTL is missing or zero, only last_invalidated_at is checked.
func filterExpiredWorkspaces(preset database.GetTemplatePresetsWithPrebuildsRow, runningWorkspaces []database.GetRunningPrebuiltWorkspacesRow) (nonExpired []database.GetRunningPrebuiltWorkspacesRow, expired []database.GetRunningPrebuiltWorkspacesRow) {
if !preset.Ttl.Valid {
return runningWorkspaces, expired
}
ttl := time.Duration(preset.Ttl.Int32) * time.Second
if ttl <= 0 {
return runningWorkspaces, expired
}
for _, prebuild := range runningWorkspaces {
if time.Since(prebuild.CreatedAt) > ttl {
isExpired := false
// Check if prebuild was created before last invalidation
if preset.LastInvalidatedAt.Valid && prebuild.CreatedAt.Before(preset.LastInvalidatedAt.Time) {
isExpired = true
}
// Check TTL expiration if set
if !isExpired && preset.Ttl.Valid {
ttl := time.Duration(preset.Ttl.Int32) * time.Second
if ttl > 0 && time.Since(prebuild.CreatedAt) > ttl {
isExpired = true
}
}
if isExpired {
expired = append(expired, prebuild)
} else {
nonExpired = append(nonExpired, prebuild)
+71 -1
View File
@@ -600,6 +600,9 @@ func TestExpiredPrebuilds(t *testing.T) {
running int32
desired int32
expired int32
invalidated int32
checkFn func(runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions)
}{
// With 2 running prebuilds, none of which are expired, and the desired count is met,
@@ -708,6 +711,52 @@ func TestExpiredPrebuilds(t *testing.T) {
},
}
validateState(t, expectedState, state)
validateActions(t, expectedActions, actions)
},
},
{
name: "preset has been invalidated - both instances expired",
running: 2,
desired: 2,
expired: 0,
invalidated: 2,
checkFn: func(runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
expectedState := prebuilds.ReconciliationState{Actual: 2, Desired: 2, Expired: 2}
expectedActions := []*prebuilds.ReconciliationActions{
{
ActionType: prebuilds.ActionTypeDelete,
DeleteIDs: []uuid.UUID{runningPrebuilds[0].ID, runningPrebuilds[1].ID},
},
{
ActionType: prebuilds.ActionTypeCreate,
Create: 2,
},
}
validateState(t, expectedState, state)
validateActions(t, expectedActions, actions)
},
},
{
name: "preset has been invalidated, but one prebuild instance is newer",
running: 2,
desired: 2,
expired: 0,
invalidated: 1,
checkFn: func(runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
expectedState := prebuilds.ReconciliationState{Actual: 2, Desired: 2, Expired: 1}
expectedActions := []*prebuilds.ReconciliationActions{
{
ActionType: prebuilds.ActionTypeDelete,
DeleteIDs: []uuid.UUID{runningPrebuilds[0].ID},
},
{
ActionType: prebuilds.ActionTypeCreate,
Create: 1,
},
}
validateState(t, expectedState, state)
validateActions(t, expectedActions, actions)
},
@@ -719,7 +768,17 @@ func TestExpiredPrebuilds(t *testing.T) {
t.Parallel()
// GIVEN: a preset.
defaultPreset := preset(true, tc.desired, current)
now := time.Now()
invalidatedAt := now.Add(1 * time.Minute)
var muts []func(row database.GetTemplatePresetsWithPrebuildsRow) database.GetTemplatePresetsWithPrebuildsRow
if tc.invalidated > 0 {
muts = append(muts, func(row database.GetTemplatePresetsWithPrebuildsRow) database.GetTemplatePresetsWithPrebuildsRow {
row.LastInvalidatedAt = sql.NullTime{Valid: true, Time: invalidatedAt}
return row
})
}
defaultPreset := preset(true, tc.desired, current, muts...)
presets := []database.GetTemplatePresetsWithPrebuildsRow{
defaultPreset,
}
@@ -727,11 +786,22 @@ func TestExpiredPrebuilds(t *testing.T) {
// GIVEN: running prebuilt workspaces for the preset.
running := make([]database.GetRunningPrebuiltWorkspacesRow, 0, tc.running)
expiredCount := 0
invalidatedCount := 0
ttlDuration := time.Duration(defaultPreset.Ttl.Int32)
for range tc.running {
name, err := prebuilds.GenerateName()
require.NoError(t, err)
prebuildCreateAt := time.Now()
if int(tc.invalidated) > invalidatedCount {
prebuildCreateAt = prebuildCreateAt.Add(-ttlDuration - 10*time.Second)
invalidatedCount++
} else if invalidatedCount > 0 {
// Only `tc.invalidated` instances have been invalidated,
// so the next instance is assumed to be created after `invalidatedAt`.
prebuildCreateAt = invalidatedAt.Add(1 * time.Minute)
}
if int(tc.expired) > expiredCount {
// Update the prebuild workspace createdAt to exceed its TTL (5 seconds)
prebuildCreateAt = prebuildCreateAt.Add(-ttlDuration - 10*time.Second)
@@ -2175,6 +2175,12 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
continue
}
// Scan does not guarantee validity
if !stg.Valid() {
s.Logger.Warn(ctx, "invalid stage, will fail insert based one enum", slog.F("value", t.Stage))
continue
}
params.Stage = append(params.Stage, stg)
params.Source = append(params.Source, t.Source)
params.Resource = append(params.Resource, t.Resource)
@@ -2184,8 +2190,11 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
}
_, err = db.InsertProvisionerJobTimings(ctx, params)
if err != nil {
// Log error but don't fail the whole transaction for non-critical data
// A database error here will "fail" this transaction. Making this error fatal.
// If this error is seen, add checks above to validate the insert parameters. In
// production, timings should not be a fatal error.
s.Logger.Warn(ctx, "failed to update provisioner job timings", slog.F("job_id", jobID), slog.Error(err))
return xerrors.Errorf("update provisioner job timings: %w", err)
}
// On start, we want to ensure that workspace agents timeout statuses
@@ -2572,6 +2581,7 @@ func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store,
IsDefault: protoPreset.GetDefault(),
Description: protoPreset.Description,
Icon: protoPreset.Icon,
LastInvalidatedAt: sql.NullTime{},
})
if err != nil {
return xerrors.Errorf("insert preset: %w", err)
+1 -1
View File
@@ -81,7 +81,7 @@ func ConnectionLogConverter() *sqltypes.VariableConverter {
func AIBridgeInterceptionConverter() *sqltypes.VariableConverter {
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
resourceIDMatcher(),
// AIBridge interceptions are not tied to any organization.
// AI Bridge interceptions are not tied to any organization.
sqltypes.StringVarMatcher("''", []string{"input", "object", "org_owner"}),
sqltypes.StringVarMatcher("initiator_id :: text", []string{"input", "object", "owner"}),
)
+4 -4
View File
@@ -751,7 +751,7 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
eg.Go(func() error {
summaries, err := r.generateAIBridgeInterceptionsSummaries(ctx)
if err != nil {
return xerrors.Errorf("generate AIBridge interceptions telemetry summaries: %w", err)
return xerrors.Errorf("generate AI Bridge interceptions telemetry summaries: %w", err)
}
snapshot.AIBridgeInterceptionsSummaries = summaries
return nil
@@ -785,7 +785,7 @@ func (r *remoteReporter) generateAIBridgeInterceptionsSummaries(ctx context.Cont
return nil, nil
}
if err != nil {
return nil, xerrors.Errorf("insert AIBridge interceptions telemetry lock (period_ending_at=%q): %w", endedAtBefore, err)
return nil, xerrors.Errorf("insert AI Bridge interceptions telemetry lock (period_ending_at=%q): %w", endedAtBefore, err)
}
// List the summary categories that need to be calculated.
@@ -794,7 +794,7 @@ func (r *remoteReporter) generateAIBridgeInterceptionsSummaries(ctx context.Cont
EndedAtBefore: endedAtBefore, // exclusive
})
if err != nil {
return nil, xerrors.Errorf("list AIBridge interceptions telemetry summaries (startedAtAfter=%q, endedAtBefore=%q): %w", endedAtAfter, endedAtBefore, err)
return nil, xerrors.Errorf("list AI Bridge interceptions telemetry summaries (startedAtAfter=%q, endedAtBefore=%q): %w", endedAtAfter, endedAtBefore, err)
}
// Calculate and convert the summaries for all categories.
@@ -813,7 +813,7 @@ func (r *remoteReporter) generateAIBridgeInterceptionsSummaries(ctx context.Cont
EndedAtBefore: endedAtBefore,
})
if err != nil {
return xerrors.Errorf("calculate AIBridge interceptions telemetry summary (provider=%q, model=%q, client=%q, startedAtAfter=%q, endedAtBefore=%q): %w", category.Provider, category.Model, category.Client, endedAtAfter, endedAtBefore, err)
return xerrors.Errorf("calculate AI Bridge interceptions telemetry summary (provider=%q, model=%q, client=%q, startedAtAfter=%q, endedAtBefore=%q): %w", category.Provider, category.Model, category.Client, endedAtAfter, endedAtBefore, err)
}
// Double check that at least one interception was found in the
+16
View File
@@ -195,6 +195,22 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De
if opts.DisableSubdomainApps {
opts.AppHost = ""
}
if opts.StatsCollectorOptions.ReportInterval == 0 {
// Set to a really high value to avoid triggering flush without manually
// calling the function in test. This can easily happen because the
// default value is 30s and we run tests in parallel. The assertion
// typically happens such that:
//
// [use workspace] -> [fetch previous last used] -> [flush] -> [fetch new last used]
//
// When this edge case is triggered:
//
// [use workspace] -> [report interval flush] -> [fetch previous last used] -> [flush] -> [fetch new last used]
//
// In this case, both the previous and new last used will be the same,
// breaking the test assertion.
opts.StatsCollectorOptions.ReportInterval = 9001 * time.Hour
}
deployment := factory(t, opts)
+9 -3
View File
@@ -35,6 +35,7 @@ import (
// by querying the database if the request is missing a valid token.
type DBTokenProvider struct {
Logger slog.Logger
ctx context.Context
// DashboardURL is the main dashboard access URL for error pages.
DashboardURL *url.URL
@@ -50,7 +51,8 @@ type DBTokenProvider struct {
var _ SignedTokenProvider = &DBTokenProvider{}
func NewDBTokenProvider(log slog.Logger,
func NewDBTokenProvider(ctx context.Context,
log slog.Logger,
accessURL *url.URL,
authz rbac.Authorizer,
connectionLogger *atomic.Pointer[connectionlog.ConnectionLogger],
@@ -70,6 +72,7 @@ func NewDBTokenProvider(log slog.Logger,
return &DBTokenProvider{
Logger: log,
ctx: ctx,
DashboardURL: accessURL,
Authorizer: authz,
ConnectionLogger: connectionLogger,
@@ -94,7 +97,7 @@ func (p *DBTokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r *
// // permissions.
dangerousSystemCtx := dbauthz.AsSystemRestricted(ctx)
aReq, commitAudit := p.connLogInitRequest(ctx, rw, r)
aReq, commitAudit := p.connLogInitRequest(rw, r)
defer commitAudit()
appReq := issueReq.AppRequest.Normalize()
@@ -406,7 +409,7 @@ type connLogRequest struct {
//
// A session is unique to the agent, app, user and users IP. If any of these
// values change, a new session and connect log is created.
func (p *DBTokenProvider) connLogInitRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) (aReq *connLogRequest, commit func()) {
func (p *DBTokenProvider) connLogInitRequest(w http.ResponseWriter, r *http.Request) (aReq *connLogRequest, commit func()) {
// Get the status writer from the request context so we can figure
// out the HTTP status and autocommit the audit log.
sw, ok := w.(*tracing.StatusWriter)
@@ -422,6 +425,9 @@ func (p *DBTokenProvider) connLogInitRequest(ctx context.Context, w http.Respons
// this ensures that the status and response body are available.
var committed bool
return aReq, func() {
// We want to log/audit the connection attempt even if the request context has expired.
ctx, cancel := context.WithCancel(p.ctx)
defer cancel()
if committed {
return
}
+58
View File
@@ -4794,6 +4794,64 @@ func TestWorkspaceFilterHasAITask(t *testing.T) {
require.Len(t, res.Workspaces, 4)
}
func TestWorkspaceListTasks(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
expClient := codersdk.NewExperimentalClient(client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
HasAiTasks: true,
}}},
},
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
// Given: a regular user workspace
workspaceWithoutTask, err := client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: "user-workspace",
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceWithoutTask.LatestBuild.ID)
// Given: a workspace associated with a task
task, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
TemplateVersionID: template.ActiveVersionID,
Input: "Some task prompt",
})
require.NoError(t, err)
assert.True(t, task.WorkspaceID.Valid)
workspaceWithTask, err := client.Workspace(ctx, task.WorkspaceID.UUID)
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceWithTask.LatestBuild.ID)
assert.NotEmpty(t, task.Name)
assert.Equal(t, template.ID, task.TemplateID)
// When: listing the workspaces
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
require.NoError(t, err)
assert.Equal(t, workspaces.Count, 2)
// Then: verify TaskID is only set for task workspaces
for _, workspace := range workspaces.Workspaces {
if workspace.ID == workspaceWithoutTask.ID {
assert.False(t, workspace.TaskID.Valid)
} else if workspace.ID == workspaceWithTask.ID {
assert.True(t, workspace.TaskID.Valid)
assert.Equal(t, task.ID, workspace.TaskID.UUID)
}
}
}
func TestWorkspaceAppUpsertRestart(t *testing.T) {
t.Parallel()
+2 -2
View File
@@ -65,7 +65,7 @@ type AIBridgeListInterceptionsResponse struct {
// @typescript-ignore AIBridgeListInterceptionsFilter
type AIBridgeListInterceptionsFilter struct {
// Limit defaults to 100, max is 1000.
// Offset based pagination is not supported for AIBridge interceptions. Use
// Offset based pagination is not supported for AI Bridge interceptions. Use
// cursor pagination instead with after_id.
Pagination Pagination `json:"pagination,omitempty"`
@@ -112,7 +112,7 @@ func (f AIBridgeListInterceptionsFilter) asRequestOption() RequestOption {
}
}
// AIBridgeListInterceptions returns AIBridge interceptions with the given
// AIBridgeListInterceptions returns AI Bridge interceptions with the given
// filter.
func (c *Client) AIBridgeListInterceptions(ctx context.Context, filter AIBridgeListInterceptionsFilter) (AIBridgeListInterceptionsResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/aibridge/interceptions", nil, filter.asRequestOption(), filter.Pagination.asRequestOption(), filter.Pagination.asRequestOption())
+24 -12
View File
@@ -1174,7 +1174,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
YAML: "inbox",
}
deploymentGroupAIBridge = serpent.Group{
Name: "AIBridge",
Name: "AI Bridge",
YAML: "aibridge",
}
)
@@ -3238,9 +3238,9 @@ Write out the current server config as YAML to stdout.`,
YAML: "hideAITasks",
},
// AIBridge Options
// AI Bridge Options
{
Name: "AIBridge Enabled",
Name: "AI Bridge Enabled",
Description: "Whether to start an in-memory aibridged instance.",
Flag: "aibridge-enabled",
Env: "CODER_AIBRIDGE_ENABLED",
@@ -3250,7 +3250,7 @@ Write out the current server config as YAML to stdout.`,
YAML: "enabled",
},
{
Name: "AIBridge OpenAI Base URL",
Name: "AI Bridge OpenAI Base URL",
Description: "The base URL of the OpenAI API.",
Flag: "aibridge-openai-base-url",
Env: "CODER_AIBRIDGE_OPENAI_BASE_URL",
@@ -3260,7 +3260,7 @@ Write out the current server config as YAML to stdout.`,
YAML: "openai_base_url",
},
{
Name: "AIBridge OpenAI Key",
Name: "AI Bridge OpenAI Key",
Description: "The key to authenticate against the OpenAI API.",
Flag: "aibridge-openai-key",
Env: "CODER_AIBRIDGE_OPENAI_KEY",
@@ -3270,7 +3270,7 @@ Write out the current server config as YAML to stdout.`,
YAML: "openai_key",
},
{
Name: "AIBridge Anthropic Base URL",
Name: "AI Bridge Anthropic Base URL",
Description: "The base URL of the Anthropic API.",
Flag: "aibridge-anthropic-base-url",
Env: "CODER_AIBRIDGE_ANTHROPIC_BASE_URL",
@@ -3280,7 +3280,7 @@ Write out the current server config as YAML to stdout.`,
YAML: "anthropic_base_url",
},
{
Name: "AIBridge Anthropic Key",
Name: "AI Bridge Anthropic Key",
Description: "The key to authenticate against the Anthropic API.",
Flag: "aibridge-anthropic-key",
Env: "CODER_AIBRIDGE_ANTHROPIC_KEY",
@@ -3290,7 +3290,7 @@ Write out the current server config as YAML to stdout.`,
YAML: "anthropic_key",
},
{
Name: "AIBridge Bedrock Region",
Name: "AI Bridge Bedrock Region",
Description: "The AWS Bedrock API region.",
Flag: "aibridge-bedrock-region",
Env: "CODER_AIBRIDGE_BEDROCK_REGION",
@@ -3300,7 +3300,7 @@ Write out the current server config as YAML to stdout.`,
YAML: "bedrock_region",
},
{
Name: "AIBridge Bedrock Access Key",
Name: "AI Bridge Bedrock Access Key",
Description: "The access key to authenticate against the AWS Bedrock API.",
Flag: "aibridge-bedrock-access-key",
Env: "CODER_AIBRIDGE_BEDROCK_ACCESS_KEY",
@@ -3310,7 +3310,7 @@ Write out the current server config as YAML to stdout.`,
YAML: "bedrock_access_key",
},
{
Name: "AIBridge Bedrock Access Key Secret",
Name: "AI Bridge Bedrock Access Key Secret",
Description: "The access key secret to use with the access key to authenticate against the AWS Bedrock API.",
Flag: "aibridge-bedrock-access-key-secret",
Env: "CODER_AIBRIDGE_BEDROCK_ACCESS_KEY_SECRET",
@@ -3320,7 +3320,7 @@ Write out the current server config as YAML to stdout.`,
YAML: "bedrock_access_key_secret",
},
{
Name: "AIBridge Bedrock Model",
Name: "AI Bridge Bedrock Model",
Description: "The model to use when making requests to the AWS Bedrock API.",
Flag: "aibridge-bedrock-model",
Env: "CODER_AIBRIDGE_BEDROCK_MODEL",
@@ -3330,7 +3330,7 @@ Write out the current server config as YAML to stdout.`,
YAML: "bedrock_model",
},
{
Name: "AIBridge Bedrock Small Fast Model",
Name: "AI Bridge Bedrock Small Fast Model",
Description: "The small fast model to use when making requests to the AWS Bedrock API. Claude Code uses Haiku-class models to perform background tasks. See https://docs.claude.com/en/docs/claude-code/settings#environment-variables.",
Flag: "aibridge-bedrock-small-fastmodel",
Env: "CODER_AIBRIDGE_BEDROCK_SMALL_FAST_MODEL",
@@ -3349,6 +3349,17 @@ Write out the current server config as YAML to stdout.`,
Group: &deploymentGroupAIBridge,
YAML: "inject_coder_mcp_tools",
},
{
Name: "AI Bridge Data Retention Duration",
Description: "Length of time to retain data such as interceptions and all related records (token, prompt, tool use).",
Flag: "aibridge-retention",
Env: "CODER_AIBRIDGE_RETENTION",
Value: &c.AI.BridgeConfig.Retention,
Default: "60d",
Group: &deploymentGroupAIBridge,
YAML: "retention",
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
},
{
Name: "Enable Authorization Recordings",
Description: "All api requests will have a header including all authorization calls made during the request. " +
@@ -3373,6 +3384,7 @@ type AIBridgeConfig struct {
Anthropic AIBridgeAnthropicConfig `json:"anthropic" typescript:",notnull"`
Bedrock AIBridgeBedrockConfig `json:"bedrock" typescript:",notnull"`
InjectCoderMCPTools serpent.Bool `json:"inject_coder_mcp_tools" typescript:",notnull"`
Retention serpent.Duration `json:"retention" typescript:",notnull"`
}
type AIBridgeOpenAIConfig struct {
+31
View File
@@ -513,3 +513,34 @@ func (c *Client) StarterTemplates(ctx context.Context) ([]TemplateExample, error
var templateExamples []TemplateExample
return templateExamples, json.NewDecoder(res.Body).Decode(&templateExamples)
}
type InvalidatePresetsResponse struct {
Invalidated []InvalidatedPreset `json:"invalidated"`
}
type InvalidatedPreset struct {
TemplateName string `json:"template_name"`
TemplateVersionName string `json:"template_version_name"`
PresetName string `json:"preset_name"`
}
// InvalidateTemplatePresets invalidates all presets for the
// template's active version by setting last_invalidated_at timestamp.
// The reconciler will then mark these prebuilds as expired and create new ones.
func (c *Client) InvalidateTemplatePresets(ctx context.Context, template uuid.UUID) (InvalidatePresetsResponse, error) {
res, err := c.Request(ctx, http.MethodPost,
fmt.Sprintf("/api/v2/templates/%s/prebuilds/invalidate", template),
nil,
)
if err != nil {
return InvalidatePresetsResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return InvalidatePresetsResponse{}, ReadBodyAsError(res)
}
var response InvalidatePresetsResponse
return response, json.NewDecoder(res.Body).Decode(&response)
}
+91 -84
View File
@@ -104,90 +104,97 @@ deployment. They will always be available from the agent.
<!-- Code generated by 'make docs/admin/integrations/prometheus.md'. DO NOT EDIT -->
| Name | Type | Description | Labels |
|---------------------------------------------------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| `agent_scripts_executed_total` | counter | Total number of scripts executed by the Coder agent. Includes cron scheduled scripts. | `agent_name` `success` `template_name` `username` `workspace_name` |
| `coderd_agents_apps` | gauge | Agent applications with statuses. | `agent_name` `app_name` `health` `username` `workspace_name` |
| `coderd_agents_connection_latencies_seconds` | gauge | Agent connection latencies in seconds. | `agent_name` `derp_region` `preferred` `username` `workspace_name` |
| `coderd_agents_connections` | gauge | Agent connections with statuses. | `agent_name` `lifecycle_state` `status` `tailnet_node` `username` `workspace_name` |
| `coderd_agents_up` | gauge | The number of active agents per workspace. | `template_name` `username` `workspace_name` |
| `coderd_agentstats_connection_count` | gauge | The number of established connections by agent | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_connection_median_latency_seconds` | gauge | The median agent connection latency | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_currently_reachable_peers` | gauge | The number of peers (e.g. clients) that are currently reachable over the encrypted network. | `agent_name` `connection_type` `template_name` `username` `workspace_name` |
| `coderd_agentstats_rx_bytes` | gauge | Agent Rx bytes | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_jetbrains` | gauge | The number of session established by JetBrains | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_reconnecting_pty` | gauge | The number of session established by reconnecting PTY | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_ssh` | gauge | The number of session established by SSH | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_vscode` | gauge | The number of session established by VSCode | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_startup_script_seconds` | gauge | The number of seconds the startup script took to execute. | `agent_name` `success` `template_name` `username` `workspace_name` |
| `coderd_agentstats_tx_bytes` | gauge | Agent Tx bytes | `agent_name` `username` `workspace_name` |
| `coderd_api_active_users_duration_hour` | gauge | The number of users that have been active within the last hour. | |
| `coderd_api_concurrent_requests` | gauge | The number of concurrent API requests. | |
| `coderd_api_concurrent_websockets` | gauge | The total number of concurrent API websockets. | |
| `coderd_api_request_latencies_seconds` | histogram | Latency distribution of requests in seconds. | `method` `path` |
| `coderd_api_requests_processed_total` | counter | The total number of processed API requests | `code` `method` `path` |
| `coderd_api_websocket_durations_seconds` | histogram | Websocket duration distribution of requests in seconds. | `path` |
| `coderd_api_workspace_latest_build` | gauge | The latest workspace builds with a status. | `status` |
| `coderd_api_workspace_latest_build_total` | gauge | DEPRECATED: use coderd_api_workspace_latest_build instead | `status` |
| `coderd_insights_applications_usage_seconds` | gauge | The application usage per template. | `application_name` `slug` `template_name` |
| `coderd_insights_parameters` | gauge | The parameter usage per template. | `parameter_name` `parameter_type` `parameter_value` `template_name` |
| `coderd_insights_templates_active_users` | gauge | The number of active users of the template. | `template_name` |
| `coderd_license_active_users` | gauge | The number of active users. | |
| `coderd_license_limit_users` | gauge | The user seats limit based on the active Coder license. | |
| `coderd_license_user_limit_enabled` | gauge | Returns 1 if the current license enforces the user limit. | |
| `coderd_metrics_collector_agents_execution_seconds` | histogram | Histogram for duration of agents metrics collection in seconds. | |
| `coderd_oauth2_external_requests_rate_limit` | gauge | The total number of allowed requests per interval. | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_next_reset_unix` | gauge | Unix timestamp of the next interval | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_remaining` | gauge | The remaining number of allowed requests in this interval. | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_reset_in_seconds` | gauge | Seconds until the next interval | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_total` | gauge | DEPRECATED: use coderd_oauth2_external_requests_rate_limit instead | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_used` | gauge | The number of requests made in this interval. | `name` `resource` |
| `coderd_oauth2_external_requests_total` | counter | The total number of api calls made to external oauth2 providers. 'status_code' will be 0 if the request failed with no response. | `name` `source` `status_code` |
| `coderd_prebuilt_workspace_claim_duration_seconds` | histogram | Time to claim a prebuilt workspace by organization, template, and preset. | `organization_name` `preset_name` `template_name` |
| `coderd_provisionerd_job_timings_seconds` | histogram | The provisioner job time duration in seconds. | `provisioner` `status` |
| `coderd_provisionerd_jobs_current` | gauge | The number of currently running provisioner jobs. | `provisioner` |
| `coderd_provisionerd_num_daemons` | gauge | The number of provisioner daemons. | |
| `coderd_provisionerd_workspace_build_timings_seconds` | histogram | The time taken for a workspace to build. | `status` `template_name` `template_version` `workspace_transition` |
| `coderd_workspace_builds_total` | counter | The number of workspaces started, updated, or deleted. | `action` `owner_email` `status` `template_name` `template_version` `workspace_name` |
| `coderd_workspace_creation_duration_seconds` | histogram | Time to create a workspace by organization, template, preset, and type (regular or prebuild). | `organization_name` `preset_name` `template_name` `type` |
| `coderd_workspace_creation_total` | counter | Total regular (non-prebuilt) workspace creations by organization, template, and preset. | `organization_name` `preset_name` `template_name` |
| `coderd_workspace_latest_build_status` | gauge | The current workspace statuses by template, transition, and owner. | `status` `template_name` `template_version` `workspace_owner` `workspace_transition` |
| `go_gc_duration_seconds` | summary | A summary of the pause duration of garbage collection cycles. | |
| `go_goroutines` | gauge | Number of goroutines that currently exist. | |
| `go_info` | gauge | Information about the Go environment. | `version` |
| `go_memstats_alloc_bytes` | gauge | Number of bytes allocated and still in use. | |
| `go_memstats_alloc_bytes_total` | counter | Total number of bytes allocated, even if freed. | |
| `go_memstats_buck_hash_sys_bytes` | gauge | Number of bytes used by the profiling bucket hash table. | |
| `go_memstats_frees_total` | counter | Total number of frees. | |
| `go_memstats_gc_sys_bytes` | gauge | Number of bytes used for garbage collection system metadata. | |
| `go_memstats_heap_alloc_bytes` | gauge | Number of heap bytes allocated and still in use. | |
| `go_memstats_heap_idle_bytes` | gauge | Number of heap bytes waiting to be used. | |
| `go_memstats_heap_inuse_bytes` | gauge | Number of heap bytes that are in use. | |
| `go_memstats_heap_objects` | gauge | Number of allocated objects. | |
| `go_memstats_heap_released_bytes` | gauge | Number of heap bytes released to OS. | |
| `go_memstats_heap_sys_bytes` | gauge | Number of heap bytes obtained from system. | |
| `go_memstats_last_gc_time_seconds` | gauge | Number of seconds since 1970 of last garbage collection. | |
| `go_memstats_lookups_total` | counter | Total number of pointer lookups. | |
| `go_memstats_mallocs_total` | counter | Total number of mallocs. | |
| `go_memstats_mcache_inuse_bytes` | gauge | Number of bytes in use by mcache structures. | |
| `go_memstats_mcache_sys_bytes` | gauge | Number of bytes used for mcache structures obtained from system. | |
| `go_memstats_mspan_inuse_bytes` | gauge | Number of bytes in use by mspan structures. | |
| `go_memstats_mspan_sys_bytes` | gauge | Number of bytes used for mspan structures obtained from system. | |
| `go_memstats_next_gc_bytes` | gauge | Number of heap bytes when next garbage collection will take place. | |
| `go_memstats_other_sys_bytes` | gauge | Number of bytes used for other system allocations. | |
| `go_memstats_stack_inuse_bytes` | gauge | Number of bytes in use by the stack allocator. | |
| `go_memstats_stack_sys_bytes` | gauge | Number of bytes obtained from system for stack allocator. | |
| `go_memstats_sys_bytes` | gauge | Number of bytes obtained from system. | |
| `go_threads` | gauge | Number of OS threads created. | |
| `process_cpu_seconds_total` | counter | Total user and system CPU time spent in seconds. | |
| `process_max_fds` | gauge | Maximum number of open file descriptors. | |
| `process_open_fds` | gauge | Number of open file descriptors. | |
| `process_resident_memory_bytes` | gauge | Resident memory size in bytes. | |
| `process_start_time_seconds` | gauge | Start time of the process since unix epoch in seconds. | |
| `process_virtual_memory_bytes` | gauge | Virtual memory size in bytes. | |
| `process_virtual_memory_max_bytes` | gauge | Maximum amount of virtual memory available in bytes. | |
| `promhttp_metric_handler_requests_in_flight` | gauge | Current number of scrapes being served. | |
| `promhttp_metric_handler_requests_total` | counter | Total number of scrapes by HTTP status code. | `code` |
| Name | Type | Description | Labels |
|---------------------------------------------------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| `agent_scripts_executed_total` | counter | Total number of scripts executed by the Coder agent. Includes cron scheduled scripts. | `agent_name` `success` `template_name` `username` `workspace_name` |
| `coder_aibridged_injected_tool_invocations_total` | counter | The number of times an injected MCP tool was invoked by aibridge. | `model` `name` `provider` `server` |
| `coder_aibridged_interceptions_duration_seconds` | histogram | The total duration of intercepted requests, in seconds. The majority of this time will be the upstream processing of the request. aibridge has no control over upstream processing time, so it's just an illustrative metric. | `model` `provider` |
| `coder_aibridged_interceptions_inflight` | gauge | The number of intercepted requests which are being processed. | `model` `provider` `route` |
| `coder_aibridged_interceptions_total` | counter | The count of intercepted requests. | `initiator_id` `method` `model` `provider` `route` `status` |
| `coder_aibridged_non_injected_tool_selections_total` | counter | The number of times an AI model selected a tool to be invoked by the client. | `model` `name` `provider` |
| `coder_aibridged_prompts_total` | counter | The number of prompts issued by users (initiators). | `initiator_id` `model` `provider` |
| `coder_aibridged_tokens_total` | counter | The number of tokens used by intercepted requests. | `initiator_id` `model` `provider` `type` |
| `coderd_agents_apps` | gauge | Agent applications with statuses. | `agent_name` `app_name` `health` `username` `workspace_name` |
| `coderd_agents_connection_latencies_seconds` | gauge | Agent connection latencies in seconds. | `agent_name` `derp_region` `preferred` `username` `workspace_name` |
| `coderd_agents_connections` | gauge | Agent connections with statuses. | `agent_name` `lifecycle_state` `status` `tailnet_node` `username` `workspace_name` |
| `coderd_agents_up` | gauge | The number of active agents per workspace. | `template_name` `username` `workspace_name` |
| `coderd_agentstats_connection_count` | gauge | The number of established connections by agent | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_connection_median_latency_seconds` | gauge | The median agent connection latency | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_currently_reachable_peers` | gauge | The number of peers (e.g. clients) that are currently reachable over the encrypted network. | `agent_name` `connection_type` `template_name` `username` `workspace_name` |
| `coderd_agentstats_rx_bytes` | gauge | Agent Rx bytes | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_jetbrains` | gauge | The number of session established by JetBrains | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_reconnecting_pty` | gauge | The number of session established by reconnecting PTY | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_ssh` | gauge | The number of session established by SSH | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_vscode` | gauge | The number of session established by VSCode | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_startup_script_seconds` | gauge | The number of seconds the startup script took to execute. | `agent_name` `success` `template_name` `username` `workspace_name` |
| `coderd_agentstats_tx_bytes` | gauge | Agent Tx bytes | `agent_name` `username` `workspace_name` |
| `coderd_api_active_users_duration_hour` | gauge | The number of users that have been active within the last hour. | |
| `coderd_api_concurrent_requests` | gauge | The number of concurrent API requests. | |
| `coderd_api_concurrent_websockets` | gauge | The total number of concurrent API websockets. | |
| `coderd_api_request_latencies_seconds` | histogram | Latency distribution of requests in seconds. | `method` `path` |
| `coderd_api_requests_processed_total` | counter | The total number of processed API requests | `code` `method` `path` |
| `coderd_api_websocket_durations_seconds` | histogram | Websocket duration distribution of requests in seconds. | `path` |
| `coderd_api_workspace_latest_build` | gauge | The latest workspace builds with a status. | `status` |
| `coderd_api_workspace_latest_build_total` | gauge | DEPRECATED: use coderd_api_workspace_latest_build instead | `status` |
| `coderd_insights_applications_usage_seconds` | gauge | The application usage per template. | `application_name` `slug` `template_name` |
| `coderd_insights_parameters` | gauge | The parameter usage per template. | `parameter_name` `parameter_type` `parameter_value` `template_name` |
| `coderd_insights_templates_active_users` | gauge | The number of active users of the template. | `template_name` |
| `coderd_license_active_users` | gauge | The number of active users. | |
| `coderd_license_limit_users` | gauge | The user seats limit based on the active Coder license. | |
| `coderd_license_user_limit_enabled` | gauge | Returns 1 if the current license enforces the user limit. | |
| `coderd_metrics_collector_agents_execution_seconds` | histogram | Histogram for duration of agents metrics collection in seconds. | |
| `coderd_oauth2_external_requests_rate_limit` | gauge | The total number of allowed requests per interval. | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_next_reset_unix` | gauge | Unix timestamp of the next interval | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_remaining` | gauge | The remaining number of allowed requests in this interval. | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_reset_in_seconds` | gauge | Seconds until the next interval | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_total` | gauge | DEPRECATED: use coderd_oauth2_external_requests_rate_limit instead | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_used` | gauge | The number of requests made in this interval. | `name` `resource` |
| `coderd_oauth2_external_requests_total` | counter | The total number of api calls made to external oauth2 providers. 'status_code' will be 0 if the request failed with no response. | `name` `source` `status_code` |
| `coderd_prebuilt_workspace_claim_duration_seconds` | histogram | Time to claim a prebuilt workspace by organization, template, and preset. | `organization_name` `preset_name` `template_name` |
| `coderd_provisionerd_job_timings_seconds` | histogram | The provisioner job time duration in seconds. | `provisioner` `status` |
| `coderd_provisionerd_jobs_current` | gauge | The number of currently running provisioner jobs. | `provisioner` |
| `coderd_provisionerd_num_daemons` | gauge | The number of provisioner daemons. | |
| `coderd_provisionerd_workspace_build_timings_seconds` | histogram | The time taken for a workspace to build. | `status` `template_name` `template_version` `workspace_transition` |
| `coderd_workspace_builds_total` | counter | The number of workspaces started, updated, or deleted. | `action` `owner_email` `status` `template_name` `template_version` `workspace_name` |
| `coderd_workspace_creation_duration_seconds` | histogram | Time to create a workspace by organization, template, preset, and type (regular or prebuild). | `organization_name` `preset_name` `template_name` `type` |
| `coderd_workspace_creation_total` | counter | Total regular (non-prebuilt) workspace creations by organization, template, and preset. | `organization_name` `preset_name` `template_name` |
| `coderd_workspace_latest_build_status` | gauge | The current workspace statuses by template, transition, and owner. | `status` `template_name` `template_version` `workspace_owner` `workspace_transition` |
| `go_gc_duration_seconds` | summary | A summary of the pause duration of garbage collection cycles. | |
| `go_goroutines` | gauge | Number of goroutines that currently exist. | |
| `go_info` | gauge | Information about the Go environment. | `version` |
| `go_memstats_alloc_bytes` | gauge | Number of bytes allocated and still in use. | |
| `go_memstats_alloc_bytes_total` | counter | Total number of bytes allocated, even if freed. | |
| `go_memstats_buck_hash_sys_bytes` | gauge | Number of bytes used by the profiling bucket hash table. | |
| `go_memstats_frees_total` | counter | Total number of frees. | |
| `go_memstats_gc_sys_bytes` | gauge | Number of bytes used for garbage collection system metadata. | |
| `go_memstats_heap_alloc_bytes` | gauge | Number of heap bytes allocated and still in use. | |
| `go_memstats_heap_idle_bytes` | gauge | Number of heap bytes waiting to be used. | |
| `go_memstats_heap_inuse_bytes` | gauge | Number of heap bytes that are in use. | |
| `go_memstats_heap_objects` | gauge | Number of allocated objects. | |
| `go_memstats_heap_released_bytes` | gauge | Number of heap bytes released to OS. | |
| `go_memstats_heap_sys_bytes` | gauge | Number of heap bytes obtained from system. | |
| `go_memstats_last_gc_time_seconds` | gauge | Number of seconds since 1970 of last garbage collection. | |
| `go_memstats_lookups_total` | counter | Total number of pointer lookups. | |
| `go_memstats_mallocs_total` | counter | Total number of mallocs. | |
| `go_memstats_mcache_inuse_bytes` | gauge | Number of bytes in use by mcache structures. | |
| `go_memstats_mcache_sys_bytes` | gauge | Number of bytes used for mcache structures obtained from system. | |
| `go_memstats_mspan_inuse_bytes` | gauge | Number of bytes in use by mspan structures. | |
| `go_memstats_mspan_sys_bytes` | gauge | Number of bytes used for mspan structures obtained from system. | |
| `go_memstats_next_gc_bytes` | gauge | Number of heap bytes when next garbage collection will take place. | |
| `go_memstats_other_sys_bytes` | gauge | Number of bytes used for other system allocations. | |
| `go_memstats_stack_inuse_bytes` | gauge | Number of bytes in use by the stack allocator. | |
| `go_memstats_stack_sys_bytes` | gauge | Number of bytes obtained from system for stack allocator. | |
| `go_memstats_sys_bytes` | gauge | Number of bytes obtained from system. | |
| `go_threads` | gauge | Number of OS threads created. | |
| `process_cpu_seconds_total` | counter | Total user and system CPU time spent in seconds. | |
| `process_max_fds` | gauge | Maximum number of open file descriptors. | |
| `process_open_fds` | gauge | Number of open file descriptors. | |
| `process_resident_memory_bytes` | gauge | Resident memory size in bytes. | |
| `process_start_time_seconds` | gauge | Start time of the process since unix epoch in seconds. | |
| `process_virtual_memory_bytes` | gauge | Virtual memory size in bytes. | |
| `process_virtual_memory_max_bytes` | gauge | Maximum amount of virtual memory available in bytes. | |
| `promhttp_metric_handler_requests_in_flight` | gauge | Current number of scrapes being served. | |
| `promhttp_metric_handler_requests_total` | counter | Total number of scrapes by HTTP status code. | `code` |
<!-- End generated by 'make docs/admin/integrations/prometheus.md'. -->
@@ -322,15 +322,33 @@ their needs.
![Template with options in the preset dropdown](../../../images/admin/templates/extend-templates/template-preset-dropdown.png)
Use `coder_workspace_preset` to define the preset parameters.
After you save the template file, the presets will be available for all new
workspace deployments.
Use the
[`coder_workspace_preset`](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/workspace_preset)
data source to define the preset parameters. After you save the template file,
the presets will be available for all new workspace deployments.
### Optional preset fields
In addition to the required `name` and `parameters` fields, you can enhance your
workspace presets with optional `description` and `icon` fields:
- **description**: A helpful text description that provides additional context
about the preset. This helps users understand what the preset is for and when
to use it.
- **icon**: A visual icon displayed alongside the preset name in the UI. Use
emoji icons with the format `/emojis/{code}.png` (e.g.,
`/emojis/1f1fa-1f1f8.png` for the US flag emoji 🇺🇸).
For a complete list of all available fields, see the
[Terraform provider documentation](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/workspace_preset#schema).
<details><summary>Expand for an example</summary>
```tf
data "coder_workspace_preset" "goland-gpu" {
name = "GoLand with GPU"
description = "Development workspace with GPU acceleration for GoLand IDE"
icon = "/emojis/1f680.png"
parameters = {
"machine_type" = "n1-standard-1"
"attach_gpu" = "true"
@@ -339,6 +357,16 @@ data "coder_workspace_preset" "goland-gpu" {
}
}
data "coder_workspace_preset" "pittsburgh" {
name = "Pittsburgh"
description = "Development workspace hosted in United States"
icon = "/emojis/1f1fa-1f1f8.png"
parameters = {
"region" = "us-pittsburgh"
"machine_type" = "n1-standard-2"
}
}
data "coder_parameter" "machine_type" {
name = "machine_type"
display_name = "Machine Type"
@@ -355,16 +383,23 @@ data "coder_parameter" "attach_gpu" {
data "coder_parameter" "gcp_region" {
name = "gcp_region"
display_name = "Machine Type"
display_name = "GCP Region"
type = "string"
default = "n1-standard-2"
default = "us-central1-a"
}
data "coder_parameter" "jetbrains_ide" {
name = "jetbrains_ide"
display_name = "Machine Type"
display_name = "JetBrains IDE"
type = "string"
default = "n1-standard-2"
default = "IU"
}
data "coder_parameter" "region" {
name = "region"
display_name = "Region"
type = "string"
default = "us-east-1"
}
```
+1 -1
View File
@@ -16,7 +16,7 @@ You must have the User Admin role or above to create headless users.
coder users create \
--email="coder-bot@coder.com" \
--username="coder-bot" \
--login-type="none \
--login-type="none" \
```
## UI
+1 -1
View File
@@ -44,7 +44,7 @@ CODER_OIDC_ICON_URL=/icon/microsoft.svg
```env
# Keep standard scopes
CODER_OIDC_SCOPES=openid,profile,email
CODER_OIDC_SCOPES=openid,profile,email,offline_access
```
After changing settings, users must log out and back in once to obtain refresh tokens
+51
View File
@@ -80,3 +80,54 @@ You can use the
[`CODER_MAX_TOKEN_LIFETIME`](https://coder.com/docs/reference/cli/server#--max-token-lifetime)
server flag to set the maximum duration for long-lived tokens in your
deployment.
## API Key Scopes
API key scopes allow you to limit the permissions of a token to specific operations. By default, tokens are created with the `all` scope, granting full access to all actions the user can perform. For improved security, you can create tokens with limited scopes that restrict access to only the operations needed.
Scopes follow the format `resource:action`, where `resource` is the type of object (like `workspace`, `template`, or `user`) and `action` is the operation (like `read`, `create`, `update`, or `delete`). You can also use wildcards like `workspace:*` to grant all permissions for a specific resource type.
### Creating tokens with scopes
You can specify scopes when creating a token using the `--scope` flag:
```sh
# Create a token that can only read workspaces
coder tokens create --name "readonly-token" --scope "workspace:read"
# Create a token with multiple scopes
coder tokens create --name "limited-token" --scope "workspace:read" --scope "template:read"
```
Common scope examples include:
- `workspace:read` - View workspace information
- `workspace:*` - Full workspace access (create, read, update, delete)
- `template:read` - View template information
- `api_key:read` - View API keys (useful for automation)
- `application_connect` - Connect to workspace applications
For a complete list of available scopes, see the API reference documentation.
### Allow lists (advanced)
For additional security, you can combine scopes with allow lists to restrict tokens to specific resources. Allow lists let you limit a token to only interact with particular workspaces, templates, or other resources by their UUID:
```sh
# Create a token limited to a specific workspace
coder tokens create --name "workspace-token" \
--scope "workspace:read" \
--allow "workspace:a1b2c3d4-5678-90ab-cdef-1234567890ab"
```
**Important:** Allow lists are exclusive - the token can **only** perform actions on resources explicitly listed. In the example above, the token can only read the specified workspace and cannot access any other resources (templates, organizations, other workspaces, etc.). To maintain access to other resources, you must explicitly add them to the allow list:
```sh
# Token that can read one workspace AND access templates and user info
coder tokens create --name "limited-token" \
--scope "workspace:read" --scope "template:*" --scope "user:read" \
--allow "workspace:a1b2c3d4-5678-90ab-cdef-1234567890ab" \
--allow "template:*" \
--allow "user:*" \
... etc
```
+65 -11
View File
@@ -16,32 +16,86 @@ Agent Boundaries offer network policy enforcement, which blocks domains and HTTP
The easiest way to use Agent Boundaries is through existing Coder modules, such as the [Claude Code module](https://registry.coder.com/modules/coder/claude-code). It can also be ran directly in the terminal by installing the [CLI](https://github.com/coder/boundary).
Below is an example of how to configure Agent Boundaries for usage in your workspace.
There are two supported ways to configure Boundary today:
1. **Inline module configuration** fastest for quick testing.
2. **External `config.yaml`** best when you need a large allow list or want everyone who launches Boundary manually to share the same config.
### Option 1: Inline module configuration (quick start)
Put every setting directly in the Terraform module when you just want to experiment:
```tf
module "claude-code" {
source = "dev.registry.coder.com/coder/claude-code/coder"
version = "4.1.0"
enable_boundary = true
boundary_version = "main"
boundary_log_dir = "/tmp/boundary_logs"
boundary_version = "v0.2.0"
boundary_log_dir = "/tmp/boundary_logs"
boundary_log_level = "WARN"
boundary_additional_allowed_urls = ["GET *google.com"]
boundary_additional_allowed_urls = ["domain=google.com"]
boundary_proxy_port = "8087"
version = "3.2.1"
}
```
- `boundary_version` defines what version of Boundary is being applied. This is set to `main`, which points to the main branch of `coder/boundary`.
All Boundary knobs live in Terraform, so you can iterate quickly without creating extra files.
### Option 2: Keep policy in `config.yaml` (extensive allow lists)
When you need to maintain a long allow list or share a detailed policy with teammates, keep Terraform minimal and move the rest into `config.yaml`:
```tf
module "claude-code" {
source = "dev.registry.coder.com/coder/claude-code/coder"
version = "4.1.0"
enable_boundary = true
boundary_version = "v0.2.0"
}
```
Then create a `config.yaml` file in your template directory with your policy:
```yaml
allowlist:
- "domain=google.com"
- "method=GET,HEAD domain=api.github.com"
- "method=POST domain=api.example.com path=/users,/posts"
log_dir: /tmp/boundary_logs
proxy_port: 8087
log_level: warn
```
Add a `coder_script` resource to mount the configuration file into the workspace filesystem:
```tf
resource "coder_script" "boundary_config_setup" {
agent_id = coder_agent.dev.id
display_name = "Boundary Setup Configuration"
run_on_start = true
script = <<-EOF
#!/bin/sh
mkdir -p ~/.config/coder_boundary
echo '${base64encode(file("${path.module}/config.yaml"))}' | base64 -d > ~/.config/coder_boundary/config.yaml
chmod 600 ~/.config/coder_boundary/config.yaml
EOF
}
```
Boundary automatically reads `config.yaml` from `~/.config/coder_boundary/` when it starts, so everyone who launches Boundary manually inside the workspace picks up the same configuration without extra flags. This is especially convenient for managing extensive allow lists in version control.
- `boundary_version` defines what version of Boundary is being applied. This is set to `v0.2.0`, which points to the v0.2.0 release tag of `coder/boundary`.
- `boundary_log_dir` is the directory where log files are written to when the workspace spins up.
- `boundary_log_level` defines the verbosity at which requests are logged. Boundary uses the following verbosity levels:
- `WARN`: logs only requests that have been blocked by Boundary
- `INFO`: logs all requests at a high level
- `DEBUG`: logs all requests in detail
- `boundary_additional_allowed_urls`: defines the URLs that the agent can access, in additional to the default URLs required for the agent to work
- `github.com` means only the specific domain is allowed
- `*.github.com` means only the subdomains are allowed - the specific domain is excluded
- `*github.com` means both the specific domain and all subdomains are allowed
- You can also also filter on methods, hostnames, and paths - for example, `GET,HEAD *github.com/coder`.
- `boundary_additional_allowed_urls`: defines the URLs that the agent can access, in addition to the default URLs required for the agent to work. Rules use the format `"key=value [key=value ...]"`:
- `domain=github.com` - allows the domain and all its subdomains
- `domain=*.github.com` - allows only subdomains (the specific domain is excluded)
- `method=GET,HEAD domain=api.github.com` - allows specific HTTP methods for a domain
- `method=POST domain=api.example.com path=/users,/posts` - allows specific methods, domain, and paths
- `path=/api/v1/*,/api/v2/*` - allows specific URL paths
You can also run Agent Boundaries directly in your workspace and configure it per template. You can do so by installing the [binary](https://github.com/coder/boundary) into the workspace image or at start-up. You can do so with the following command:
+8 -8
View File
@@ -42,7 +42,7 @@ resource "coder_agent" "dev" {
os = "linux"
dir = local.repo_dir
env = {
ANTHROPIC_BASE_URL : "${data.coder_workspace.me.url}/api/v2/aibridge/anthropic",
ANTHROPIC_BASE_URL : "${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic",
ANTHROPIC_AUTH_TOKEN : data.coder_workspace_owner.me.session_token
}
... # other agent configuration
@@ -63,7 +63,7 @@ resource "coder_agent" "dev" {
os = "linux"
dir = local.repo_dir
env = {
ANTHROPIC_BASE_URL : "${data.coder_workspace.me.url}/api/v2/aibridge/anthropic",
ANTHROPIC_BASE_URL : "${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic",
ANTHROPIC_AUTH_TOKEN : data.coder_workspace_owner.me.session_token
}
... # other agent configuration
@@ -96,17 +96,17 @@ The table below shows tested AI clients and their compatibility with AI Bridge.
| Client | OpenAI support | Anthropic support | Notes |
|-------------------------------------------------------------------------------------------------------------------------------------|----------------|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [Claude Code](https://docs.claude.com/en/docs/claude-code/settings#environment-variables) | N/A | ✅ | Works out of the box and can be preconfigured in templates. |
| Claude Code (VS Code) | N/A | ✅ | May require signing in once; afterwards respects workspace environment variables. |
| [Claude Code](https://docs.claude.com/en/docs/claude-code/settings#environment-variables) | - | ✅ | Works out of the box and can be preconfigured in templates. |
| Claude Code (VS Code) | - | ✅ | May require signing in once; afterwards respects workspace environment variables. |
| [Cursor](https://cursor.com/docs/settings/api-keys) | ⚠️ | ❌ | Only non-reasoning models like `gpt-4.1` are available when using a custom endpoint. Requests still transit Cursor's cloud. There is no central admin setting to configure this. |
| [Roo Code](https://docs.roocode.com/features/api-configuration-profiles#creating-and-managing-profiles) | ✅ | ✅ | Use the **OpenAI Compatible** provider with the legacy format to avoid `/v1/responses`. |
| [Codex CLI](https://github.com/openai/codex/blob/main/docs/config.md#model_providers) | ✅ | N/A | `gpt-5-codex` support is [in progress](https://github.com/coder/aibridge/issues/16). |
| [GitHub Copilot (VS Code)](https://code.visualstudio.com/docs/copilot/customization/language-models#_use-an-openaicompatible-model) | ✅ | ❌ | Requires the pre-release extension. Anthropic endpoints are not supported. |
| [GitHub Copilot (VS Code)](https://code.visualstudio.com/docs/copilot/customization/language-models#_add-an-openaicompatible-model) | ✅ | ❌ | Requires the pre-release extension. Anthropic endpoints are not supported. |
| [Goose](https://block.github.io/goose/docs/getting-started/providers/#available-providers) | ❓ | ❓ | |
| [Goose Desktop](https://block.github.io/goose/docs/getting-started/providers/#available-providers) | ❓ | ✅ | |
| WindSurf | ❌ | | No option to override the base URL. |
| Sourcegraph Amp | ❌ | | No option to override the base URL. |
| Kiro | ❌ | | No option to override the base URL. |
| WindSurf | ❌ | | No option to override the base URL. |
| Sourcegraph Amp | ❌ | | No option to override the base URL. |
| Kiro | ❌ | | No option to override the base URL. |
| [Copilot CLI](https://github.com/github/copilot-cli/issues/104) | ❌ | ❌ | No support for custom base URLs and uses a `GITHUB_TOKEN` for authentication. |
| [Kilo Code](https://kilocode.ai/docs/features/api-configuration-profiles#creating-and-managing-profiles) | ✅ | ✅ | Similar to Roo Code. |
| Gemini CLI | ❌ | ❌ | Not supported yet. |
+27 -3
View File
@@ -1130,6 +1130,10 @@
"title": "General",
"path": "./reference/api/general.md"
},
{
"title": "AI Bridge",
"path": "./reference/api/aibridge.md"
},
{
"title": "Agents",
"path": "./reference/api/agents.md"
@@ -1162,6 +1166,10 @@
"title": "Enterprise",
"path": "./reference/api/enterprise.md"
},
{
"title": "Experimental",
"path": "./reference/api/experimental.md"
},
{
"title": "Files",
"path": "./reference/api/files.md"
@@ -1170,6 +1178,10 @@
"title": "Git",
"path": "./reference/api/git.md"
},
{
"title": "InitScript",
"path": "./reference/api/initscript.md"
},
{
"title": "Insights",
"path": "./reference/api/insights.md"
@@ -1178,6 +1190,10 @@
"title": "Members",
"path": "./reference/api/members.md"
},
{
"title": "Notifications",
"path": "./reference/api/notifications.md"
},
{
"title": "Organizations",
"path": "./reference/api/organizations.md"
@@ -1186,6 +1202,14 @@
"title": "PortSharing",
"path": "./reference/api/portsharing.md"
},
{
"title": "Prebuilds",
"path": "./reference/api/prebuilds.md"
},
{
"title": "Provisioning",
"path": "./reference/api/provisioning.md"
},
{
"title": "Schemas",
"path": "./reference/api/schemas.md"
@@ -1216,17 +1240,17 @@
"children": [
{
"title": "aibridge",
"description": "Manage AIBridge.",
"description": "Manage AI Bridge.",
"path": "reference/cli/aibridge.md"
},
{
"title": "aibridge interceptions",
"description": "Manage AIBridge interceptions.",
"description": "Manage AI Bridge interceptions.",
"path": "reference/cli/aibridge_interceptions.md"
},
{
"title": "aibridge interceptions list",
"description": "List AIBridge interceptions as JSON.",
"description": "List AI Bridge interceptions as JSON.",
"path": "reference/cli/aibridge_interceptions_list.md"
},
{
+2 -2
View File
@@ -1,6 +1,6 @@
# AIBridge
# AI Bridge
## List AIBridge interceptions
## List AI Bridge interceptions
### Code samples
+43
View File
@@ -3788,6 +3788,49 @@ Status Code **200**
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Invalidate presets for template
### Code samples
```shell
# Example request using curl
curl -X POST http://coder-server:8080/api/v2/templates/{template}/prebuilds/invalidate \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`POST /templates/{template}/prebuilds/invalidate`
### Parameters
| Name | In | Type | Required | Description |
|------------|------|--------------|----------|-------------|
| `template` | path | string(uuid) | true | Template ID |
### Example responses
> 200 Response
```json
{
"invalidated": [
{
"preset_name": "string",
"template_name": "string",
"template_version_name": "string"
}
]
}
```
### Responses
| Status | Meaning | Description | Schema |
|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------------------------|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.InvalidatePresetsResponse](schemas.md#codersdkinvalidatepresetsresponse) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get user quiet hours schedule
### Code samples
+2 -1
View File
@@ -179,7 +179,8 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"openai": {
"base_url": "string",
"key": "string"
}
},
"retention": 0
}
},
"allow_workspace_renames": true,
+47 -4
View File
@@ -393,7 +393,8 @@
"openai": {
"base_url": "string",
"key": "string"
}
},
"retention": 0
}
```
@@ -406,6 +407,7 @@
| `enabled` | boolean | false | | |
| `inject_coder_mcp_tools` | boolean | false | | |
| `openai` | [codersdk.AIBridgeOpenAIConfig](#codersdkaibridgeopenaiconfig) | false | | |
| `retention` | integer | false | | |
## codersdk.AIBridgeInterception
@@ -701,7 +703,8 @@
"openai": {
"base_url": "string",
"key": "string"
}
},
"retention": 0
}
}
```
@@ -2858,7 +2861,8 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
"openai": {
"base_url": "string",
"key": "string"
}
},
"retention": 0
}
},
"allow_workspace_renames": true,
@@ -3373,7 +3377,8 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
"openai": {
"base_url": "string",
"key": "string"
}
},
"retention": 0
}
},
"allow_workspace_renames": true,
@@ -4715,6 +4720,44 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
| `day` |
| `week` |
## codersdk.InvalidatePresetsResponse
```json
{
"invalidated": [
{
"preset_name": "string",
"template_name": "string",
"template_version_name": "string"
}
]
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|---------------|-------------------------------------------------------------------|----------|--------------|-------------|
| `invalidated` | array of [codersdk.InvalidatedPreset](#codersdkinvalidatedpreset) | false | | |
## codersdk.InvalidatedPreset
```json
{
"preset_name": "string",
"template_name": "string",
"template_version_name": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|-------------------------|--------|----------|--------------|-------------|
| `preset_name` | string | false | | |
| `template_name` | string | false | | |
| `template_version_name` | string | false | | |
## codersdk.IssueReconnectingPTYSignedTokenRequest
```json
+4 -4
View File
@@ -1,7 +1,7 @@
<!-- DO NOT EDIT | GENERATED CONTENT -->
# aibridge
Manage AIBridge.
Manage AI Bridge.
## Usage
@@ -11,6 +11,6 @@ coder aibridge
## Subcommands
| Name | Purpose |
|-----------------------------------------------------------|--------------------------------|
| [<code>interceptions</code>](./aibridge_interceptions.md) | Manage AIBridge interceptions. |
| Name | Purpose |
|-----------------------------------------------------------|---------------------------------|
| [<code>interceptions</code>](./aibridge_interceptions.md) | Manage AI Bridge interceptions. |
+4 -4
View File
@@ -1,7 +1,7 @@
<!-- DO NOT EDIT | GENERATED CONTENT -->
# aibridge interceptions
Manage AIBridge interceptions.
Manage AI Bridge interceptions.
## Usage
@@ -11,6 +11,6 @@ coder aibridge interceptions
## Subcommands
| Name | Purpose |
|-------------------------------------------------------|--------------------------------------|
| [<code>list</code>](./aibridge_interceptions_list.md) | List AIBridge interceptions as JSON. |
| Name | Purpose |
|-------------------------------------------------------|---------------------------------------|
| [<code>list</code>](./aibridge_interceptions_list.md) | List AI Bridge interceptions as JSON. |
+1 -1
View File
@@ -1,7 +1,7 @@
<!-- DO NOT EDIT | GENERATED CONTENT -->
# aibridge interceptions list
List AIBridge interceptions as JSON.
List AI Bridge interceptions as JSON.
## Usage
+1 -1
View File
@@ -68,7 +68,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr
| [<code>groups</code>](./groups.md) | Manage groups |
| [<code>prebuilds</code>](./prebuilds.md) | Manage Coder prebuilds |
| [<code>external-workspaces</code>](./external-workspaces.md) | Create or manage external workspaces |
| [<code>aibridge</code>](./aibridge.md) | Manage AIBridge. |
| [<code>aibridge</code>](./aibridge.md) | Manage AI Bridge. |
## Options
+11
View File
@@ -1763,3 +1763,14 @@ The small fast model to use when making requests to the AWS Bedrock API. Claude
| Default | <code>false</code> |
Whether to inject Coder's MCP tools into intercepted AI Bridge requests (requires the "oauth2" and "mcp-server-http" experiments to be enabled).
### --aibridge-retention
| | |
|-------------|----------------------------------------|
| Type | <code>duration</code> |
| Environment | <code>$CODER_AIBRIDGE_RETENTION</code> |
| YAML | <code>aibridge.retention</code> |
| Default | <code>60d</code> |
Length of time to retain data such as interceptions and all related records (token, prompt, tool use).
+1 -1
View File
@@ -123,7 +123,7 @@ module "personalize" {
module "code-server" {
source = "dev.registry.coder.com/coder/code-server/coder"
version = "1.3.1"
version = "1.4.0"
agent_id = coder_agent.dev.id
folder = local.repo_dir
auto_install_extensions = true
+3 -3
View File
@@ -1,5 +1,5 @@
# 1.86.0
FROM rust:slim@sha256:d9ba8014603166915f7e0fcaa9af09df2a1fc30547e75a72c1d34165139f036a AS rust-utils
FROM rust:slim@sha256:5218a2b4b4cb172f26503ac2b2de8e5ffd629ae1c0d885aff2cbe97fd4d1a409 AS rust-utils
# Install rust helper programs
ENV CARGO_INSTALL_ROOT=/tmp/
# Use more reliable mirrors for Debian packages
@@ -8,7 +8,7 @@ RUN sed -i 's|http://deb.debian.org/debian|http://mirrors.edge.kernel.org/debian
RUN apt-get update && apt-get install -y libssl-dev openssl pkg-config build-essential
RUN cargo install jj-cli typos-cli watchexec-cli
FROM ubuntu:jammy@sha256:09506232a8004baa32c47d68f1e5c307d648fdd59f5e7eaa42aaf87914100db3 AS go
FROM ubuntu:jammy@sha256:104ae83764a5119017b8e8d6218fa0832b09df65aae7d5a6de29a85d813da2fb AS go
# Install Go manually, so that we can control the version
ARG GO_VERSION=1.24.10
@@ -102,7 +102,7 @@ RUN curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/d
unzip protoc.zip && \
rm protoc.zip
FROM ubuntu:jammy@sha256:09506232a8004baa32c47d68f1e5c307d648fdd59f5e7eaa42aaf87914100db3
FROM ubuntu:jammy@sha256:104ae83764a5119017b8e8d6218fa0832b09df65aae7d5a6de29a85d813da2fb
SHELL ["/bin/bash", "-c"]
+222
View File
@@ -0,0 +1,222 @@
allowlist:
# specified in claude-code module as well (effectively a duplicate); needed for basic functionality of claude-code agent
- domain=anthropic.com
- domain=registry.npmjs.org
- domain=sentry.io
- domain=claude.ai
- domain=dev.coder.com
# test domains
- method=GET domain=google.com
- method=GET domain=typicode.com
# domain used in coder task workspaces
- method=POST domain=http-intake.logs.datadoghq.com
# Default allowed domains from Claude Code on the web
# Source: https://code.claude.com/docs/en/claude-code-on-the-web#default-allowed-domains
# Anthropic Services
- domain=api.anthropic.com
- domain=statsig.anthropic.com
- domain=claude.ai
# Version Control
- domain=github.com
- domain=www.github.com
- domain=api.github.com
- domain=raw.githubusercontent.com
- domain=objects.githubusercontent.com
- domain=codeload.github.com
- domain=avatars.githubusercontent.com
- domain=camo.githubusercontent.com
- domain=gist.github.com
- domain=gitlab.com
- domain=www.gitlab.com
- domain=registry.gitlab.com
- domain=bitbucket.org
- domain=www.bitbucket.org
- domain=api.bitbucket.org
# Container Registries
- domain=registry-1.docker.io
- domain=auth.docker.io
- domain=index.docker.io
- domain=hub.docker.com
- domain=www.docker.com
- domain=production.cloudflare.docker.com
- domain=download.docker.com
- domain=*.gcr.io
- domain=ghcr.io
- domain=mcr.microsoft.com
- domain=*.data.mcr.microsoft.com
# Cloud Platforms
- domain=cloud.google.com
- domain=accounts.google.com
- domain=gcloud.google.com
- domain=*.googleapis.com
- domain=storage.googleapis.com
- domain=compute.googleapis.com
- domain=container.googleapis.com
- domain=azure.com
- domain=portal.azure.com
- domain=microsoft.com
- domain=www.microsoft.com
- domain=*.microsoftonline.com
- domain=packages.microsoft.com
- domain=dotnet.microsoft.com
- domain=dot.net
- domain=visualstudio.com
- domain=dev.azure.com
- domain=oracle.com
- domain=www.oracle.com
- domain=java.com
- domain=www.java.com
- domain=java.net
- domain=www.java.net
- domain=download.oracle.com
- domain=yum.oracle.com
# Package Managers - JavaScript/Node
- domain=registry.npmjs.org
- domain=www.npmjs.com
- domain=www.npmjs.org
- domain=npmjs.com
- domain=npmjs.org
- domain=yarnpkg.com
- domain=registry.yarnpkg.com
# Package Managers - Python
- domain=pypi.org
- domain=www.pypi.org
- domain=files.pythonhosted.org
- domain=pythonhosted.org
- domain=test.pypi.org
- domain=pypi.python.org
- domain=pypa.io
- domain=www.pypa.io
# Package Managers - Ruby
- domain=rubygems.org
- domain=www.rubygems.org
- domain=api.rubygems.org
- domain=index.rubygems.org
- domain=ruby-lang.org
- domain=www.ruby-lang.org
- domain=rubyforge.org
- domain=www.rubyforge.org
- domain=rubyonrails.org
- domain=www.rubyonrails.org
- domain=rvm.io
- domain=get.rvm.io
# Package Managers - Rust
- domain=crates.io
- domain=www.crates.io
- domain=static.crates.io
- domain=rustup.rs
- domain=static.rust-lang.org
- domain=www.rust-lang.org
# Package Managers - Go
- domain=proxy.golang.org
- domain=sum.golang.org
- domain=index.golang.org
- domain=golang.org
- domain=www.golang.org
- domain=goproxy.io
- domain=pkg.go.dev
# Package Managers - JVM
- domain=maven.org
- domain=repo.maven.org
- domain=central.maven.org
- domain=repo1.maven.org
- domain=jcenter.bintray.com
- domain=gradle.org
- domain=www.gradle.org
- domain=services.gradle.org
- domain=spring.io
- domain=repo.spring.io
# Package Managers - Other Languages
- domain=packagist.org
- domain=www.packagist.org
- domain=repo.packagist.org
- domain=nuget.org
- domain=www.nuget.org
- domain=api.nuget.org
- domain=pub.dev
- domain=api.pub.dev
- domain=hex.pm
- domain=www.hex.pm
- domain=cpan.org
- domain=www.cpan.org
- domain=metacpan.org
- domain=www.metacpan.org
- domain=api.metacpan.org
- domain=cocoapods.org
- domain=www.cocoapods.org
- domain=cdn.cocoapods.org
- domain=haskell.org
- domain=www.haskell.org
- domain=hackage.haskell.org
- domain=swift.org
- domain=www.swift.org
# Linux Distributions
- domain=archive.ubuntu.com
- domain=security.ubuntu.com
- domain=ubuntu.com
- domain=www.ubuntu.com
- domain=*.ubuntu.com
- domain=ppa.launchpad.net
- domain=launchpad.net
- domain=www.launchpad.net
# Development Tools & Platforms
- domain=dl.k8s.io
- domain=pkgs.k8s.io
- domain=k8s.io
- domain=www.k8s.io
- domain=releases.hashicorp.com
- domain=apt.releases.hashicorp.com
- domain=rpm.releases.hashicorp.com
- domain=archive.releases.hashicorp.com
- domain=hashicorp.com
- domain=www.hashicorp.com
- domain=repo.anaconda.com
- domain=conda.anaconda.org
- domain=anaconda.org
- domain=www.anaconda.com
- domain=anaconda.com
- domain=continuum.io
- domain=apache.org
- domain=www.apache.org
- domain=archive.apache.org
- domain=downloads.apache.org
- domain=eclipse.org
- domain=www.eclipse.org
- domain=download.eclipse.org
- domain=nodejs.org
- domain=www.nodejs.org
# Cloud Services & Monitoring
- domain=statsig.com
- domain=www.statsig.com
- domain=api.statsig.com
- domain=*.sentry.io
# Content Delivery & Mirrors
- domain=*.sourceforge.net
- domain=packagecloud.io
- domain=*.packagecloud.io
# Schema & Configuration
- domain=json-schema.org
- domain=www.json-schema.org
- domain=json.schemastore.org
- domain=www.schemastore.org
log_dir: /tmp/boundary_logs
log_level: warn
proxy_port: 8087
+19 -4
View File
@@ -378,7 +378,7 @@ module "personalize" {
module "mux" {
count = contains(jsondecode(data.coder_parameter.ide_choices.value), "mux") ? data.coder_workspace.me.start_count : 0
source = "registry.coder.com/coder/mux/coder"
version = "1.0.0"
version = "1.0.1"
agent_id = coder_agent.dev.id
subdomain = true
}
@@ -386,7 +386,7 @@ module "mux" {
module "code-server" {
count = contains(jsondecode(data.coder_parameter.ide_choices.value), "code-server") ? data.coder_workspace.me.start_count : 0
source = "dev.registry.coder.com/coder/code-server/coder"
version = "1.3.1"
version = "1.4.0"
agent_id = coder_agent.dev.id
folder = local.repo_dir
auto_install_extensions = true
@@ -408,7 +408,7 @@ module "vscode-web" {
module "jetbrains" {
count = contains(jsondecode(data.coder_parameter.ide_choices.value), "jetbrains") ? data.coder_workspace.me.start_count : 0
source = "dev.registry.coder.com/coder/jetbrains/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.dev.id
agent_name = "dev"
folder = local.repo_dir
@@ -844,10 +844,25 @@ locals {
EOT
}
resource "coder_script" "boundary_config_setup" {
agent_id = coder_agent.dev.id
display_name = "Boundary Setup Configuration"
run_on_start = true
script = <<-EOF
#!/bin/sh
mkdir -p ~/.config/coder_boundary
echo '${base64encode(file("${path.module}/boundary-config.yaml"))}' | base64 -d > ~/.config/coder_boundary/config.yaml
chmod 600 ~/.config/coder_boundary/config.yaml
EOF
}
module "claude-code" {
count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0
source = "dev.registry.coder.com/coder/claude-code/coder"
version = "4.0.0"
version = "4.2.1"
enable_boundary = true
boundary_version = "v0.2.0"
agent_id = coder_agent.dev.id
workdir = local.repo_dir
claude_code_version = "latest"
+7 -6
View File
@@ -55,13 +55,14 @@ func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger)
ctx, cancel := context.WithCancel(ctx)
daemon := &Server{
logger: logger,
clientDialer: rpcDialer,
logger: logger,
clientDialer: rpcDialer,
clientCh: make(chan DRPCClient),
lifecycleCtx: ctx,
cancelFn: cancel,
initConnectionCh: make(chan struct{}),
requestBridgePool: pool,
clientCh: make(chan DRPCClient),
lifecycleCtx: ctx,
cancelFn: cancel,
initConnectionCh: make(chan struct{}),
}
daemon.wg.Add(1)
@@ -9,6 +9,8 @@ import (
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
promtest "github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/require"
"github.com/coder/aibridge"
@@ -166,7 +168,7 @@ func TestIntegration(t *testing.T) {
logger := testutil.Logger(t)
providers := []aibridge.Provider{aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{BaseURL: mockOpenAI.URL})}
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger)
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, nil, logger)
require.NoError(t, err)
// Given: aibridged is started.
@@ -253,3 +255,109 @@ func TestIntegration(t *testing.T) {
// Then: the MCP server was initialized.
require.Contains(t, mcpTokenReceived, authLink.OAuthAccessToken, "mock MCP server not requested")
}
// TestIntegrationWithMetrics validates that Prometheus metrics are correctly incremented
// when requests are processed through aibridged.
func TestIntegrationWithMetrics(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
// Create prometheus registry and metrics.
registry := prometheus.NewRegistry()
metrics := aibridge.NewMetrics(registry)
// Set up mock OpenAI server.
mockOpenAI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{
"id": "chatcmpl-test",
"object": "chat.completion",
"created": 1753343279,
"model": "gpt-4.1",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "test response"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15
}
}`))
}))
t.Cleanup(mockOpenAI.Close)
// Database and coderd setup.
db, ps := dbtestutil.NewDB(t)
client, _, api, firstUser := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
Options: &coderdtest.Options{
Database: db,
Pubsub: ps,
},
})
userClient, _ := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
// Create an API token for the user.
apiKey, err := userClient.CreateToken(ctx, "me", codersdk.CreateTokenRequest{
TokenName: fmt.Sprintf("test-key-%d", time.Now().UnixNano()),
Lifetime: time.Hour,
Scope: codersdk.APIKeyScopeCoderAll,
})
require.NoError(t, err)
// Create aibridge client.
aiBridgeClient, err := api.CreateInMemoryAIBridgeServer(ctx)
require.NoError(t, err)
logger := testutil.Logger(t)
providers := []aibridge.Provider{aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{BaseURL: mockOpenAI.URL})}
// Create pool with metrics.
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, metrics, logger)
require.NoError(t, err)
// Given: aibridged is started.
srv, err := aibridged.New(ctx, pool, func(ctx context.Context) (aibridged.DRPCClient, error) {
return aiBridgeClient, nil
}, logger)
require.NoError(t, err, "create new aibridged")
t.Cleanup(func() {
_ = srv.Shutdown(ctx)
})
// When: a request is made to aibridged.
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/openai/v1/chat/completions", bytes.NewBufferString(`{
"messages": [
{
"role": "user",
"content": "test message"
}
],
"model": "gpt-4.1"
}`))
require.NoError(t, err, "make request to test server")
req.Header.Add("Authorization", "Bearer "+apiKey.Key)
req.Header.Add("Accept", "application/json")
// When: aibridged handles the request.
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
// Then: the interceptions metric should increase to 1.
// This is not exhaustively checking the available metrics; just an indicative one to prove
// the plumbing is working.
require.Eventually(t, func() bool {
count := promtest.ToFloat64(metrics.InterceptionCount)
return count == 1
}, testutil.WaitShort, testutil.IntervalFast, "interceptions_total metric should be 1")
}
+2 -3
View File
@@ -41,8 +41,7 @@ func newTestServer(t *testing.T) (*aibridged.Server, *mock.MockDRPCClient, *mock
pool,
func(ctx context.Context) (aibridged.DRPCClient, error) {
return client, nil
},
logger)
}, logger)
require.NoError(t, err, "create new aibridged")
t.Cleanup(func() {
srv.Shutdown(context.Background())
@@ -291,7 +290,7 @@ func TestRouting(t *testing.T) {
aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{BaseURL: openaiSrv.URL}),
aibridge.NewAnthropicProvider(aibridge.AnthropicConfig{BaseURL: antSrv.URL}, nil),
}
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger)
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, nil, logger)
require.NoError(t, err)
conn := &mockDRPCConn{}
client.EXPECT().DRPCConn().AnyTimes().Return(conn)
+7 -3
View File
@@ -51,11 +51,13 @@ type CachedBridgePool struct {
singleflight *singleflight.Group[string, *aibridge.RequestBridge]
metrics *aibridge.Metrics
shutDownOnce sync.Once
shuttingDownCh chan struct{}
}
func NewCachedBridgePool(options PoolOptions, providers []aibridge.Provider, logger slog.Logger) (*CachedBridgePool, error) {
func NewCachedBridgePool(options PoolOptions, providers []aibridge.Provider, metrics *aibridge.Metrics, logger slog.Logger) (*CachedBridgePool, error) {
cache, err := ristretto.NewCache(&ristretto.Config[string, *aibridge.RequestBridge]{
NumCounters: options.MaxItems * 10, // Docs suggest setting this 10x number of keys.
MaxCost: options.MaxItems * cacheCost, // Up to n instances.
@@ -88,6 +90,8 @@ func NewCachedBridgePool(options PoolOptions, providers []aibridge.Provider, log
singleflight: &singleflight.Group[string, *aibridge.RequestBridge]{},
metrics: metrics,
shuttingDownCh: make(chan struct{}),
}, nil
}
@@ -154,7 +158,7 @@ func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn Cl
}
}
bridge, err := aibridge.NewRequestBridge(ctx, p.providers, p.logger, recorder, mcpServers)
bridge, err := aibridge.NewRequestBridge(ctx, p.providers, recorder, mcpServers, p.metrics, p.logger)
if err != nil {
return nil, xerrors.Errorf("create new request bridge: %w", err)
}
@@ -167,7 +171,7 @@ func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn Cl
return instance, err
}
func (p *CachedBridgePool) Metrics() PoolMetrics {
func (p *CachedBridgePool) CacheMetrics() PoolMetrics {
if p.cache == nil {
return nil
}
+16 -16
View File
@@ -30,7 +30,7 @@ func TestPool(t *testing.T) {
mcpProxy := mcpmock.NewMockServerProxier(ctrl)
opts := aibridged.PoolOptions{MaxItems: 1, TTL: time.Second}
pool, err := aibridged.NewCachedBridgePool(opts, nil, logger)
pool, err := aibridged.NewCachedBridgePool(opts, nil, nil, logger)
require.NoError(t, err)
t.Cleanup(func() { pool.Shutdown(context.Background()) })
@@ -63,11 +63,11 @@ func TestPool(t *testing.T) {
require.NoError(t, err, "acquire pool instance")
require.Same(t, inst, instB)
metrics := pool.Metrics()
require.EqualValues(t, 1, metrics.KeysAdded())
require.EqualValues(t, 0, metrics.KeysEvicted())
require.EqualValues(t, 1, metrics.Hits())
require.EqualValues(t, 1, metrics.Misses())
cacheMetrics := pool.CacheMetrics()
require.EqualValues(t, 1, cacheMetrics.KeysAdded())
require.EqualValues(t, 0, cacheMetrics.KeysEvicted())
require.EqualValues(t, 1, cacheMetrics.Hits())
require.EqualValues(t, 1, cacheMetrics.Misses())
// This will get called again because a new instance will be created.
mcpProxy.EXPECT().Init(gomock.Any()).Times(1).Return(nil)
@@ -81,11 +81,11 @@ func TestPool(t *testing.T) {
require.NoError(t, err, "acquire pool instance")
require.NotSame(t, inst, inst2)
metrics = pool.Metrics()
require.EqualValues(t, 2, metrics.KeysAdded())
require.EqualValues(t, 1, metrics.KeysEvicted())
require.EqualValues(t, 1, metrics.Hits())
require.EqualValues(t, 2, metrics.Misses())
cacheMetrics = pool.CacheMetrics()
require.EqualValues(t, 2, cacheMetrics.KeysAdded())
require.EqualValues(t, 1, cacheMetrics.KeysEvicted())
require.EqualValues(t, 1, cacheMetrics.Hits())
require.EqualValues(t, 2, cacheMetrics.Misses())
// This will get called again because a new instance will be created.
mcpProxy.EXPECT().Init(gomock.Any()).Times(1).Return(nil)
@@ -99,11 +99,11 @@ func TestPool(t *testing.T) {
require.NoError(t, err, "acquire pool instance 2B")
require.NotSame(t, inst2, inst2B)
metrics = pool.Metrics()
require.EqualValues(t, 3, metrics.KeysAdded())
require.EqualValues(t, 2, metrics.KeysEvicted())
require.EqualValues(t, 1, metrics.Hits())
require.EqualValues(t, 3, metrics.Misses())
cacheMetrics = pool.CacheMetrics()
require.EqualValues(t, 3, cacheMetrics.KeysAdded())
require.EqualValues(t, 2, cacheMetrics.KeysEvicted())
require.EqualValues(t, 1, cacheMetrics.Hits())
require.EqualValues(t, 3, cacheMetrics.Misses())
// TODO: add test for expiry.
// This requires Go 1.25's [synctest](https://pkg.go.dev/testing/synctest) since the
+10
View File
@@ -57,6 +57,16 @@ func (t *recorderTranslation) RecordPromptUsage(ctx context.Context, req *aibrid
}
func (t *recorderTranslation) RecordTokenUsage(ctx context.Context, req *aibridge.TokenUsageRecord) error {
merged := req.Metadata
if merged == nil {
merged = aibridge.Metadata{}
}
// Merge the token usage values into metadata; later we might want to store some of these in their own fields.
for k, v := range req.ExtraTokenTypes {
merged[k] = v
}
_, err := t.client.RecordTokenUsage(ctx, &proto.RecordTokenUsageRequest{
InterceptionId: req.InterceptionID,
MsgId: req.MsgID,
+3 -3
View File
@@ -17,7 +17,7 @@ const maxInterceptionsLimit = 1000
func (r *RootCmd) aibridge() *serpent.Command {
cmd := &serpent.Command{
Use: "aibridge",
Short: "Manage AIBridge.",
Short: "Manage AI Bridge.",
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
@@ -31,7 +31,7 @@ func (r *RootCmd) aibridge() *serpent.Command {
func (r *RootCmd) aibridgeInterceptions() *serpent.Command {
cmd := &serpent.Command{
Use: "interceptions",
Short: "Manage AIBridge interceptions.",
Short: "Manage AI Bridge interceptions.",
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
@@ -55,7 +55,7 @@ func (r *RootCmd) aibridgeInterceptionsList() *serpent.Command {
return &serpent.Command{
Use: "list",
Short: "List AIBridge interceptions as JSON.",
Short: "List AI Bridge interceptions as JSON.",
Options: serpent.OptionSet{
{
Flag: "initiator",
+6 -3
View File
@@ -43,10 +43,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
InitiatorID: member.ID,
StartedAt: now.Add(-time.Hour),
}, &now)
interception2EndedAt := now.Add(time.Minute)
interception2 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: member.ID,
StartedAt: now,
}, nil)
}, &interception2EndedAt)
// Should not be returned because the user can't see it.
_ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: owner.UserID,
@@ -91,12 +92,13 @@ func TestAIBridgeListInterceptions(t *testing.T) {
now := dbtime.Now()
// This interception should be returned since it matches all filters.
goodInterceptionEndedAt := now.Add(time.Minute)
goodInterception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: member.ID,
Provider: "real-provider",
Model: "real-model",
StartedAt: now,
}, nil)
}, &goodInterceptionEndedAt)
// These interceptions should not be returned since they don't match the
// filters.
@@ -173,10 +175,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
now := dbtime.Now()
firstInterceptionEndedAt := now.Add(time.Minute)
firstInterception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: member.ID,
StartedAt: now,
}, nil)
}, &firstInterceptionEndedAt)
returnedInterception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: member.ID,
StartedAt: now.Add(-time.Hour),
+6 -1
View File
@@ -7,6 +7,8 @@ import (
"golang.org/x/xerrors"
"github.com/prometheus/client_golang/prometheus"
"github.com/coder/aibridge"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/aibridged"
@@ -31,8 +33,11 @@ func newAIBridgeDaemon(coderAPI *coderd.API) (*aibridged.Server, error) {
}, getBedrockConfig(coderAPI.DeploymentValues.AI.BridgeConfig.Bedrock)),
}
reg := prometheus.WrapRegistererWithPrefix("coder_aibridged_", coderAPI.PrometheusRegistry)
metrics := aibridge.NewMetrics(reg)
// Create pool for reusable stateful [aibridge.RequestBridge] instances (one per user).
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger.Named("pool")) // TODO: configurable.
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, metrics, logger.Named("pool")) // TODO: configurable size.
if err != nil {
return nil, xerrors.Errorf("create request pool: %w", err)
}
+1 -1
View File
@@ -107,7 +107,7 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
if provisionerKey != "" {
pkDetails, err := client.GetProvisionerKey(ctx, provisionerKey)
if err != nil {
return xerrors.New("unable to get provisioner key details")
return xerrors.Errorf("unable to get provisioner key details: %w", err)
}
for k, v := range pkDetails.Tags {
+1 -1
View File
@@ -14,7 +14,7 @@ USAGE:
$ coder templates init
SUBCOMMANDS:
aibridge Manage AIBridge.
aibridge Manage AI Bridge.
external-workspaces Create or manage external workspaces
features List Enterprise features
groups Manage groups
+2 -2
View File
@@ -3,10 +3,10 @@ coder v0.0.0-devel
USAGE:
coder aibridge
Manage AIBridge.
Manage AI Bridge.
SUBCOMMANDS:
interceptions Manage AIBridge interceptions.
interceptions Manage AI Bridge interceptions.
———
Run `coder --help` for a list of global options.
@@ -3,10 +3,10 @@ coder v0.0.0-devel
USAGE:
coder aibridge interceptions
Manage AIBridge interceptions.
Manage AI Bridge interceptions.
SUBCOMMANDS:
list List AIBridge interceptions as JSON.
list List AI Bridge interceptions as JSON.
———
Run `coder --help` for a list of global options.
@@ -3,7 +3,7 @@ coder v0.0.0-devel
USAGE:
coder aibridge interceptions list [flags]
List AIBridge interceptions as JSON.
List AI Bridge interceptions as JSON.
OPTIONS:
--after-id string

Some files were not shown because too many files have changed in this diff Show More