mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
Merge branch 'main' into aqandrew/add-group-page-avatar
This commit is contained in:
@@ -41,6 +41,41 @@
|
||||
dimension.
|
||||
- Avoid `ByX` names for grouped queries.
|
||||
|
||||
### Enum Changes Run in a Single Transaction
|
||||
|
||||
All migrations run inside one transaction (`pgTxnDriver`). Postgres forbids
|
||||
*using* an enum value added by `ALTER TYPE ... ADD VALUE` within the same
|
||||
transaction that added it, so it fails with `unsafe use of new value`.
|
||||
|
||||
Adding the value is fine; using it in the same batch is not. "Using it"
|
||||
includes a later migration that casts to it (`col::my_enum`), inserts or
|
||||
updates a row with it, or sets it as a column default. This only fails when a
|
||||
row actually materializes the new value, so fresh databases and CI pass while
|
||||
deployments with existing data break.
|
||||
|
||||
**MUST DO**: If any migration uses a newly added enum value, recreate the type
|
||||
instead of using `ADD VALUE`. A freshly created enum's values are usable
|
||||
immediately in the same transaction. Precedent: `000144_user_status_dormant`.
|
||||
|
||||
```sql
|
||||
CREATE TYPE new_my_enum AS ENUM ('existing', 'value', 'new_value');
|
||||
|
||||
ALTER TABLE my_table
|
||||
ALTER COLUMN col TYPE new_my_enum USING (col::text::new_my_enum);
|
||||
|
||||
DROP TYPE my_enum;
|
||||
|
||||
ALTER TYPE new_my_enum RENAME TO my_enum;
|
||||
```
|
||||
|
||||
Recreating produces an identical schema, so `make gen` yields no `dump.sql`
|
||||
diff and databases that already applied the migration see no drift.
|
||||
|
||||
**Testing**: `migrations.Stepper` commits each migration separately, so tests
|
||||
built on it cannot surface this. To catch it, seed a row using the new value,
|
||||
then apply the affected migrations in a single transaction (see
|
||||
`TestMigration000504AIProvidersBackfillEnumInSingleTxn`).
|
||||
|
||||
## Handling Nullable Fields
|
||||
|
||||
Use `sql.NullString`, `sql.NullBool`, etc. for optional database fields:
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
name: "Go cache"
|
||||
description: Restore and save Go build and module caches.
|
||||
inputs:
|
||||
cache-path:
|
||||
description: "Optional newline-delimited cache paths. Defaults to go env GOCACHE and GOMODCACHE."
|
||||
required: false
|
||||
default: ""
|
||||
key-prefix:
|
||||
description: "Prefix for the cache key."
|
||||
required: false
|
||||
default: "go"
|
||||
download-modules:
|
||||
description: "Whether to run go mod download after restoring cache."
|
||||
required: false
|
||||
default: "true"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Compute Go cache key
|
||||
id: go-cache
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -n "${INPUT_CACHE_PATH}" ]]; then
|
||||
paths="${INPUT_CACHE_PATH}"
|
||||
else
|
||||
paths="$(printf '%s\n%s' "$(go env GOCACHE)" "$(go env GOMODCACHE)")"
|
||||
fi
|
||||
|
||||
go_version="$(go env GOVERSION)"
|
||||
paths_hash="$(printf '%s\n' "${paths}" | git hash-object --stdin)"
|
||||
hash="$(
|
||||
{
|
||||
printf '%s\n' "${go_version}"
|
||||
for file in go.mod go.sum; do
|
||||
if [[ -f "${file}" ]]; then
|
||||
git hash-object "${file}"
|
||||
fi
|
||||
done
|
||||
} | git hash-object --stdin
|
||||
)"
|
||||
|
||||
{
|
||||
echo "path<<EOF"
|
||||
echo "${paths}"
|
||||
echo "EOF"
|
||||
echo "key=${INPUT_KEY_PREFIX}-${RUNNER_OS}-${RUNNER_ARCH}-${paths_hash}-${hash}"
|
||||
echo "restore-key=${INPUT_KEY_PREFIX}-${RUNNER_OS}-${RUNNER_ARCH}-${paths_hash}-"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
INPUT_CACHE_PATH: ${{ inputs.cache-path }}
|
||||
INPUT_KEY_PREFIX: ${{ inputs.key-prefix }}
|
||||
|
||||
- name: Restore Go cache, save on main
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ steps.go-cache.outputs.path }}
|
||||
key: ${{ steps.go-cache.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ steps.go-cache.outputs.restore-key }}
|
||||
|
||||
- name: Restore Go cache read-only
|
||||
if: ${{ github.ref != 'refs/heads/main' }}
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ steps.go-cache.outputs.path }}
|
||||
key: ${{ steps.go-cache.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ steps.go-cache.outputs.restore-key }}
|
||||
|
||||
- name: Download Go modules
|
||||
if: ${{ inputs.download-modules == 'true' }}
|
||||
shell: bash
|
||||
run: ./.github/scripts/retry.sh -- go mod download -x
|
||||
@@ -1,10 +0,0 @@
|
||||
name: "Install cosign"
|
||||
description: |
|
||||
Cosign Github Action.
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
|
||||
with:
|
||||
cosign-release: "v2.4.3"
|
||||
@@ -1,10 +0,0 @@
|
||||
name: "Install syft"
|
||||
description: |
|
||||
Downloads Syft to the Action tool cache and provides a reference.
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install syft
|
||||
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||||
with:
|
||||
syft-version: "v1.26.1"
|
||||
@@ -0,0 +1,59 @@
|
||||
name: "pnpm install"
|
||||
description: Restore pnpm store cache and install root plus workspace dependencies.
|
||||
inputs:
|
||||
directory:
|
||||
description: "Workspace directory to install after the repository root."
|
||||
required: false
|
||||
default: "site"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Compute pnpm cache key
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
store_path="$(pnpm store path --silent)"
|
||||
hash="$(
|
||||
for file in pnpm-lock.yaml "${INPUT_DIRECTORY}/pnpm-lock.yaml"; do
|
||||
if [[ -f "${file}" ]]; then
|
||||
git hash-object "${file}"
|
||||
fi
|
||||
done | git hash-object --stdin
|
||||
)"
|
||||
|
||||
{
|
||||
echo "store-path=${store_path}"
|
||||
echo "key=pnpm-${RUNNER_OS}-${RUNNER_ARCH}-${INPUT_DIRECTORY}-${hash}"
|
||||
echo "restore-key=pnpm-${RUNNER_OS}-${RUNNER_ARCH}-${INPUT_DIRECTORY}-"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
INPUT_DIRECTORY: ${{ inputs.directory }}
|
||||
|
||||
- name: Restore and save pnpm cache
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.store-path }}
|
||||
key: ${{ steps.pnpm-cache.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ steps.pnpm-cache.outputs.restore-key }}
|
||||
|
||||
- name: Restore pnpm cache
|
||||
if: ${{ github.ref != 'refs/heads/main' }}
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.store-path }}
|
||||
key: ${{ steps.pnpm-cache.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ steps.pnpm-cache.outputs.restore-key }}
|
||||
|
||||
- name: Install root node_modules
|
||||
shell: bash
|
||||
run: ./scripts/pnpm_install.sh
|
||||
|
||||
- name: Install node_modules
|
||||
shell: bash
|
||||
run: "${GITHUB_WORKSPACE}/scripts/pnpm_install.sh"
|
||||
working-directory: ${{ github.workspace }}/${{ inputs.directory }}
|
||||
@@ -1,12 +0,0 @@
|
||||
name: "Setup Go tools"
|
||||
description: |
|
||||
Set up tools for `make gen`, `offlinedocs` and Schmoder CI.
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: go install tools
|
||||
shell: bash
|
||||
run: |
|
||||
./.github/scripts/retry.sh -- go install tool
|
||||
# NOTE: protoc-gen-go cannot be installed with `go get`
|
||||
./.github/scripts/retry.sh -- go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30
|
||||
@@ -1,32 +0,0 @@
|
||||
name: "Setup Go"
|
||||
description: |
|
||||
Sets up the Go environment for tests, builds, etc.
|
||||
inputs:
|
||||
version:
|
||||
description: "The Go version to use."
|
||||
default: "1.26.2"
|
||||
use-cache:
|
||||
description: "Whether to use the cache."
|
||||
default: "true"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
|
||||
with:
|
||||
go-version: ${{ inputs.version }}
|
||||
cache: ${{ inputs.use-cache }}
|
||||
|
||||
- name: Install gotestsum
|
||||
shell: bash
|
||||
run: ./.github/scripts/retry.sh -- go install gotest.tools/gotestsum@0d9599e513d70e5792bb9334869f82f6e8b53d4d # main as of 2025-05-15
|
||||
|
||||
- name: Install mtimehash
|
||||
shell: bash
|
||||
run: ./.github/scripts/retry.sh -- go install github.com/slsyy/mtimehash/cmd/mtimehash@a6b5da4ed2c4a40e7b805534b004e9fde7b53ce0 # v1.0.0
|
||||
|
||||
# It isn't necessary that we ever do this, but it helps
|
||||
# separate the "setup" from the "run" times.
|
||||
- name: go mod download
|
||||
shell: bash
|
||||
run: ./.github/scripts/retry.sh -- go mod download -x
|
||||
@@ -0,0 +1,168 @@
|
||||
name: Setup mise
|
||||
description: Install mise tools from SHA256-pinned binaries, with CI-layer caching.
|
||||
inputs:
|
||||
install-args:
|
||||
description: Tool names or extra arguments passed to mise install. --locked is added by default.
|
||||
required: false
|
||||
default: ""
|
||||
locked:
|
||||
description: Whether to pass --locked to mise install.
|
||||
required: false
|
||||
default: "true"
|
||||
cache-key-prefix:
|
||||
description: Prefix for mise tool cache keys.
|
||||
required: false
|
||||
default: mise-ci-v1
|
||||
mise-version:
|
||||
description: mise version to install.
|
||||
required: false
|
||||
default: "2026.5.12"
|
||||
mise-sha256:
|
||||
description: SHA256 checksum for the mise binary.
|
||||
required: false
|
||||
default: ""
|
||||
use-cache:
|
||||
description: Whether to restore and save mise tool caches.
|
||||
required: false
|
||||
default: "true"
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Compute mise cache key
|
||||
id: cache-key
|
||||
shell: bash
|
||||
env:
|
||||
CACHE_KEY_PREFIX: ${{ inputs.cache-key-prefix }}
|
||||
INPUT_INSTALL_ARGS: ${{ inputs.install-args }}
|
||||
INPUT_LOCKED: ${{ inputs.locked }}
|
||||
MISE_VERSION: ${{ inputs.mise-version }}
|
||||
RUNNER_ARCH: ${{ runner.arch }}
|
||||
RUNNER_OS: ${{ runner.os }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
case "${INPUT_LOCKED}" in
|
||||
true)
|
||||
if [[ -n "${INPUT_INSTALL_ARGS}" ]]; then
|
||||
install_args="--locked ${INPUT_INSTALL_ARGS}"
|
||||
else
|
||||
install_args="--locked"
|
||||
fi
|
||||
;;
|
||||
false)
|
||||
install_args="${INPUT_INSTALL_ARGS}"
|
||||
;;
|
||||
*)
|
||||
echo "::error::locked must be true or false."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
install_args_hash="$(printf '%s' "$install_args" | git hash-object --stdin)"
|
||||
files_hash="$(git hash-object mise.toml mise.lock | git hash-object --stdin)"
|
||||
key="${CACHE_KEY_PREFIX}-${RUNNER_OS}-${RUNNER_ARCH}-${MISE_VERSION}-${install_args_hash}-${files_hash}"
|
||||
restore_key="${CACHE_KEY_PREFIX}-${RUNNER_OS}-${RUNNER_ARCH}-${MISE_VERSION}-${install_args_hash}-"
|
||||
|
||||
{
|
||||
echo "install-args<<EOF"
|
||||
echo "${install_args}"
|
||||
echo "EOF"
|
||||
echo "key=$key"
|
||||
echo "restore-key=$restore_key"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Select mise checksum
|
||||
id: checksum
|
||||
shell: bash
|
||||
env:
|
||||
CHECKSUMS_FILE: ${{ github.action_path }}/checksums.toml
|
||||
INPUT_MISE_SHA256: ${{ inputs.mise-sha256 }}
|
||||
MISE_CHECKSUM_SCRIPT: ${{ github.workspace }}/scripts/mise_checksum.sh
|
||||
MISE_VERSION: ${{ inputs.mise-version }}
|
||||
RUNNER_ARCH: ${{ runner.arch }}
|
||||
RUNNER_OS: ${{ runner.os }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
checksum="${INPUT_MISE_SHA256}"
|
||||
if [[ -z "${checksum}" ]]; then
|
||||
case "${RUNNER_OS}-${RUNNER_ARCH}" in
|
||||
Linux-X64)
|
||||
target="linux-x64"
|
||||
;;
|
||||
Linux-ARM64)
|
||||
target="linux-arm64"
|
||||
;;
|
||||
macOS-X64)
|
||||
target="macos-x64"
|
||||
;;
|
||||
macOS-ARM64)
|
||||
target="macos-arm64"
|
||||
;;
|
||||
Windows-X64)
|
||||
target="windows-x64"
|
||||
;;
|
||||
*)
|
||||
echo "::error::No mise checksum is pinned for ${RUNNER_OS}-${RUNNER_ARCH}."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
checksum="$("${MISE_CHECKSUM_SCRIPT}" "${CHECKSUMS_FILE}" "${MISE_VERSION}" "${target}")"
|
||||
if [[ -z "${checksum}" ]]; then
|
||||
echo "::error::No mise checksum is pinned for mise ${MISE_VERSION} on ${target}."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "sha256=${checksum}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Configure mise data directory
|
||||
id: mise-data-dir
|
||||
shell: bash
|
||||
env:
|
||||
RUNNER_OS: ${{ runner.os }}
|
||||
run: | # zizmor: ignore[github-env] MISE_DATA_DIR uses only runner-provided paths.
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||
data_dir="${LOCALAPPDATA:-${USERPROFILE}\\AppData\\Local}\\mise"
|
||||
else
|
||||
data_dir="${RUNNER_TEMP}/mise-data"
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'path=%s\n' "${data_dir}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
printf 'MISE_DATA_DIR=%s\n' "${data_dir}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cache mise tools
|
||||
if: ${{ inputs.use-cache == 'true' && github.ref == 'refs/heads/main' }}
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/mise
|
||||
${{ steps.mise-data-dir.outputs.path }}
|
||||
key: ${{ steps.cache-key.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ steps.cache-key.outputs.restore-key }}
|
||||
|
||||
- name: Restore mise tools
|
||||
if: ${{ inputs.use-cache == 'true' && github.ref != 'refs/heads/main' }}
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/mise
|
||||
${{ steps.mise-data-dir.outputs.path }}
|
||||
key: ${{ steps.cache-key.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ steps.cache-key.outputs.restore-key }}
|
||||
|
||||
- name: Install mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
with:
|
||||
version: ${{ inputs.mise-version }}
|
||||
sha256: ${{ steps.checksum.outputs.sha256 }}
|
||||
mise_dir: ${{ steps.mise-data-dir.outputs.path }}
|
||||
install_args: ${{ steps.cache-key.outputs.install-args }}
|
||||
cache: "false"
|
||||
@@ -0,0 +1,9 @@
|
||||
# SHA256 hashes of the extracted mise binary verified by jdx/mise-action.
|
||||
# Keys use the GitHub runner target for each release artifact.
|
||||
|
||||
["2026.5.12"]
|
||||
linux-x64 = "a238972a3162d710b85b28c324372e96ca4e4b486c81fe78695000d9fbc77c48"
|
||||
linux-arm64 = "fd2d5227a8ad0b1e359c70527a8345a9ada72077f8dcbb559371653c3d95464f"
|
||||
macos-x64 = "de57e8dc82bbd880a69c9bc8aee06b9dcc578184b3e5cf86fcef80635d6a90b4"
|
||||
macos-arm64 = "e777070540ffe22cf8b2b9f88aed88b461d0887d940c4f1c1a97359463cde6e1"
|
||||
windows-x64 = "adf1b4c9f51e7d15cff723056fcd8fd51f40ebacadcca97fd5758c44d469d5ea"
|
||||
@@ -1,44 +0,0 @@
|
||||
name: "Setup Node"
|
||||
description: |
|
||||
Sets up the node environment for tests, builds, etc.
|
||||
inputs:
|
||||
directory:
|
||||
description: |
|
||||
The directory to run the setup in.
|
||||
required: false
|
||||
default: "site"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@739bfe42ca9233c5e6aca07c1a25a9d34aca49b0 # v6.0.7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 22.19.0
|
||||
# See https://github.com/actions/setup-node#caching-global-packages-data
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: ${{ inputs.directory }}/pnpm-lock.yaml
|
||||
|
||||
- name: Verify Node
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
expected="v22.19.0"
|
||||
actual="$(node --version)"
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
echo "::error::Expected Node.js $expected, but got $actual from $(command -v node)."
|
||||
exit 1
|
||||
fi
|
||||
echo "Node.js $actual is active at $(command -v node)."
|
||||
|
||||
- name: Install root node_modules
|
||||
shell: bash
|
||||
run: ./scripts/pnpm_install.sh
|
||||
|
||||
- name: Install node_modules
|
||||
shell: bash
|
||||
run: ../scripts/pnpm_install.sh
|
||||
working-directory: ${{ inputs.directory }}
|
||||
@@ -1,17 +0,0 @@
|
||||
name: Setup sqlc
|
||||
description: |
|
||||
Sets up the sqlc environment for tests, builds, etc.
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup sqlc
|
||||
# uses: sqlc-dev/setup-sqlc@c0209b9199cd1cce6a14fc27cabcec491b651761 # v4.0.0
|
||||
# with:
|
||||
# sqlc-version: "1.30.0"
|
||||
|
||||
# Switched to coder/sqlc fork to fix ambiguous column bug, see:
|
||||
# - https://github.com/coder/sqlc/pull/1
|
||||
# - https://github.com/sqlc-dev/sqlc/pull/4159
|
||||
shell: bash
|
||||
run: |
|
||||
./.github/scripts/retry.sh -- env CGO_ENABLED=1 go install github.com/coder/sqlc/cmd/sqlc@337309bfb9524f38466a5090e310040fc7af0203
|
||||
@@ -1,11 +0,0 @@
|
||||
name: "Setup Terraform"
|
||||
description: |
|
||||
Sets up Terraform for tests, builds, etc.
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Terraform
|
||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
||||
with:
|
||||
terraform_version: 1.15.5
|
||||
terraform_wrapper: false
|
||||
+155
-149
@@ -151,8 +151,13 @@ jobs:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "node pnpm"
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
uses: ./.github/actions/pnpm-install
|
||||
|
||||
- name: Check docs
|
||||
run: pnpm check-docs
|
||||
@@ -171,8 +176,10 @@ jobs:
|
||||
# # See: https://github.com/stefanzweifel/git-auto-commit-action?tab=readme-ov-file#commits-made-by-this-action-do-not-trigger-new-workflow-runs
|
||||
# token: ${{ secrets.CDRCI_GITHUB_TOKEN }}
|
||||
|
||||
# - name: Setup Go
|
||||
# uses: ./.github/actions/setup-go
|
||||
# - name: Set up mise tools
|
||||
# uses: ./.github/actions/setup-mise
|
||||
# with:
|
||||
# install-args: "go"
|
||||
|
||||
# - name: Update Nix Flake SRI Hash
|
||||
# run: ./scripts/update-flake.sh
|
||||
@@ -208,18 +215,22 @@ jobs:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go node pnpm helm actionlint aqua:crate-ci/typos"
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Install pnpm dependencies
|
||||
uses: ./.github/actions/pnpm-install
|
||||
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:github.com/golangci/golangci-lint/cmd/golangci-lint go:github.com/coder/paralleltestctx/cmd/paralleltestctx
|
||||
|
||||
- name: Get golangci-lint cache dir
|
||||
run: |
|
||||
# mise.toml is the source of truth for tool versions baked into
|
||||
# the dogfood image; pull the same version for the lint job.
|
||||
linter_ver=$(grep -Eo '^golangci-lint = "[^"]+"' mise.toml | sed -E 's/.*"([^"]+)"/\1/')
|
||||
./.github/scripts/retry.sh -- go install "github.com/golangci/golangci-lint/cmd/golangci-lint@v$linter_ver"
|
||||
dir=$(golangci-lint cache status | awk '/Dir/ { print $2 }')
|
||||
echo "LINT_CACHE_DIR=$dir" >> "$GITHUB_ENV"
|
||||
|
||||
@@ -239,35 +250,13 @@ jobs:
|
||||
|
||||
# Check for any typos
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1
|
||||
with:
|
||||
config: .github/workflows/typos.toml
|
||||
run: typos --config .github/workflows/typos.toml
|
||||
|
||||
- name: Fix the typos
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
echo "::notice:: you can automatically fix typos from your CLI:
|
||||
cargo install typos-cli
|
||||
typos -c .github/workflows/typos.toml -w"
|
||||
|
||||
# Needed for helm chart linting
|
||||
- name: Install helm
|
||||
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
|
||||
with:
|
||||
version: v3.9.2
|
||||
continue-on-error: true
|
||||
id: setup-helm
|
||||
|
||||
- name: Install helm (fallback)
|
||||
if: steps.setup-helm.outcome == 'failure'
|
||||
# Fallback to Buildkite's apt repository if get.helm.sh is down.
|
||||
# See: https://github.com/coder/internal/issues/1109
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL https://packages.buildkite.com/helm-linux/helm-debian/gpgkey | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
|
||||
echo "deb [signed-by=/usr/share/keyrings/helm.gpg] https://packages.buildkite.com/helm-linux/helm-debian/any/ any main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y helm=3.9.2-1
|
||||
mise exec aqua:crate-ci/typos -- typos -c .github/workflows/typos.toml -w"
|
||||
|
||||
- name: Verify helm version
|
||||
run: helm version --short
|
||||
@@ -287,15 +276,11 @@ jobs:
|
||||
key: ${{ steps.golangci-lint-cache.outputs.cache-primary-key }}
|
||||
|
||||
- name: Check workflow files
|
||||
run: |
|
||||
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 1.7.4
|
||||
./actionlint -color -shellcheck= -ignore "set-output"
|
||||
run: actionlint -color -shellcheck= -ignore "set-output"
|
||||
shell: bash
|
||||
|
||||
- name: Check for unstaged files
|
||||
run: |
|
||||
rm -f ./actionlint ./typos
|
||||
./scripts/check_unstaged.sh
|
||||
run: ./scripts/check_unstaged.sh
|
||||
shell: bash
|
||||
|
||||
lint-actions:
|
||||
@@ -303,7 +288,7 @@ jobs:
|
||||
# Only run this job if changes to CI workflow files are detected. This job
|
||||
# can flake as it reaches out to GitHub to check referenced actions.
|
||||
if: needs.changes.outputs.ci == 'true'
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-24.04-8' || 'ubuntu-24.04' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
@@ -316,8 +301,10 @@ jobs:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "actionlint zizmor"
|
||||
|
||||
- name: make lint/actions
|
||||
run: make --output-sync=line -j lint/actions
|
||||
@@ -341,30 +328,19 @@ jobs:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go node pnpm terraform protoc protoc-gen-go"
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Install pnpm dependencies
|
||||
uses: ./.github/actions/pnpm-install
|
||||
|
||||
- name: Setup sqlc
|
||||
uses: ./.github/actions/setup-sqlc
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: ./.github/actions/setup-tf
|
||||
|
||||
- name: go install tools
|
||||
uses: ./.github/actions/setup-go-tools
|
||||
|
||||
- name: Install Protoc
|
||||
run: |
|
||||
mkdir -p /tmp/proto
|
||||
pushd /tmp/proto
|
||||
curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.4/protoc-23.4-linux-x86_64.zip
|
||||
unzip protoc.zip
|
||||
sudo cp -r ./bin/* /usr/local/bin
|
||||
sudo cp -r ./include /usr/local/bin/include
|
||||
popd
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:storj.io/drpc/cmd/protoc-gen-go-drpc go:github.com/coder/sqlc/cmd/sqlc
|
||||
|
||||
- name: make gen
|
||||
timeout-minutes: 8
|
||||
@@ -396,24 +372,26 @@ jobs:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Check Go version
|
||||
run: IGNORE_NIX=true ./scripts/check_go_versions.sh
|
||||
|
||||
# Use default Go version
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go node pnpm terraform"
|
||||
|
||||
- name: Install shfmt
|
||||
run: ./.github/scripts/retry.sh -- go install mvdan.cc/sh/v3/cmd/shfmt@v3.7.0
|
||||
- name: Install pnpm dependencies
|
||||
uses: ./.github/actions/pnpm-install
|
||||
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:mvdan.cc/sh/v3/cmd/shfmt
|
||||
|
||||
- name: make fmt
|
||||
timeout-minutes: 7
|
||||
run: |
|
||||
PATH="${PATH}:$(go env GOPATH)/bin" \
|
||||
make --output-sync -j -B fmt
|
||||
run: make --output-sync -j -B fmt
|
||||
|
||||
- name: Check for unstaged files
|
||||
run: ./scripts/check_unstaged.sh
|
||||
@@ -476,13 +454,18 @@ jobs:
|
||||
- name: Setup GNU tools (macOS)
|
||||
uses: ./.github/actions/setup-gnu-tools
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
use-cache: true
|
||||
install-args: "go terraform"
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: ./.github/actions/setup-tf
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-path: ${{ steps.go-paths.outputs.cached-dirs }}
|
||||
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:gotest.tools/gotestsum go:github.com/slsyy/mtimehash/cmd/mtimehash
|
||||
|
||||
- name: Download Test Cache
|
||||
id: download-cache
|
||||
@@ -651,11 +634,16 @@ jobs:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go terraform"
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: ./.github/actions/setup-tf
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:gotest.tools/gotestsum
|
||||
|
||||
- name: Download Test Cache
|
||||
id: download-cache
|
||||
@@ -720,11 +708,16 @@ jobs:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go terraform"
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: ./.github/actions/setup-tf
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:gotest.tools/gotestsum
|
||||
|
||||
- name: Download Test Cache
|
||||
id: download-cache
|
||||
@@ -799,8 +792,13 @@ jobs:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go"
|
||||
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
# Used by some integration tests.
|
||||
- name: Install Nginx
|
||||
@@ -826,8 +824,13 @@ jobs:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "node pnpm"
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
uses: ./.github/actions/pnpm-install
|
||||
|
||||
- run: pnpm test:ci --max-workers "$(nproc)"
|
||||
working-directory: site
|
||||
@@ -859,11 +862,16 @@ jobs:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go node pnpm"
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Install pnpm dependencies
|
||||
uses: ./.github/actions/pnpm-install
|
||||
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
# Assume that the checked-in versions are up-to-date
|
||||
- run: make gen/mark-fresh
|
||||
@@ -951,8 +959,13 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "node pnpm"
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
uses: ./.github/actions/pnpm-install
|
||||
|
||||
# This step is not meant for mainline because any detected changes to
|
||||
# storybook snapshots will require manual approval/review in order for
|
||||
@@ -1030,29 +1043,21 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go node pnpm protoc protoc-gen-go"
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
uses: ./.github/actions/pnpm-install
|
||||
with:
|
||||
directory: offlinedocs
|
||||
|
||||
- name: Install Protoc
|
||||
run: |
|
||||
mkdir -p /tmp/proto
|
||||
pushd /tmp/proto
|
||||
curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.4/protoc-23.4-linux-x86_64.zip
|
||||
unzip protoc.zip
|
||||
sudo cp -r ./bin/* /usr/local/bin
|
||||
sudo cp -r ./include /usr/local/bin/include
|
||||
popd
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Install go tools
|
||||
uses: ./.github/actions/setup-go-tools
|
||||
|
||||
- name: Setup sqlc
|
||||
uses: ./.github/actions/setup-sqlc
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:storj.io/drpc/cmd/protoc-gen-go-drpc go:github.com/coder/sqlc/cmd/sqlc
|
||||
|
||||
- name: Format
|
||||
run: |
|
||||
@@ -1144,17 +1149,19 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go node pnpm"
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Install pnpm dependencies
|
||||
uses: ./.github/actions/pnpm-install
|
||||
|
||||
- name: Install go-winres
|
||||
run: ./.github/scripts/retry.sh -- go install github.com/tc-hib/go-winres@d743268d7ea168077ddd443c4240562d4f5e8c3e # v0.3.3
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
- name: Install nfpm
|
||||
run: ./.github/scripts/retry.sh -- go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.35.1
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:github.com/tc-hib/go-winres go:github.com/goreleaser/nfpm/v2/cmd/nfpm
|
||||
|
||||
- name: Install zstd
|
||||
run: sudo apt-get install -y zstd
|
||||
@@ -1205,13 +1212,19 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
use-cache: false
|
||||
install-args: "go node pnpm cosign syft"
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
uses: ./.github/actions/pnpm-install
|
||||
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:github.com/tc-hib/go-winres go:github.com/goreleaser/nfpm/v2/cmd/nfpm
|
||||
|
||||
- name: Install rcodesign
|
||||
run: |
|
||||
@@ -1241,21 +1254,9 @@ jobs:
|
||||
distribution: "zulu"
|
||||
java-version: "11.0"
|
||||
|
||||
- name: Install go-winres
|
||||
run: ./.github/scripts/retry.sh -- go install github.com/tc-hib/go-winres@d743268d7ea168077ddd443c4240562d4f5e8c3e # v0.3.3
|
||||
|
||||
- name: Install nfpm
|
||||
run: ./.github/scripts/retry.sh -- go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.35.1
|
||||
|
||||
- name: Install zstd
|
||||
run: sudo apt-get install -y zstd
|
||||
|
||||
- name: Install cosign
|
||||
uses: ./.github/actions/install-cosign
|
||||
|
||||
- name: Install syft
|
||||
uses: ./.github/actions/install-syft
|
||||
|
||||
- name: Setup Windows EV Signing Certificate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -1579,11 +1580,16 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go"
|
||||
|
||||
- name: Setup sqlc
|
||||
uses: ./.github/actions/setup-sqlc
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:github.com/coder/sqlc/cmd/sqlc
|
||||
|
||||
- name: Setup and run sqlc vet
|
||||
run: |
|
||||
|
||||
@@ -71,9 +71,6 @@ jobs:
|
||||
packages: write # push the dogfood base image to ghcr.io/coder/oss-dogfood-base
|
||||
env:
|
||||
# MISE_EXPERIMENTAL opts into the experimental `oci` subcommand.
|
||||
# Trust is set via a config file (see the Install mise step
|
||||
# below) rather than MISE_TRUSTED_CONFIG_PATHS so the workspace
|
||||
# template can keep parity with the same file-based approach.
|
||||
MISE_EXPERIMENTAL: "1"
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -135,32 +132,9 @@ jobs:
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
if: matrix.image-version != 'nix'
|
||||
|
||||
- name: Install mise
|
||||
if: matrix.image-version != 'nix'
|
||||
# MISE_VERSION + MISE_SHA256 match dogfood/coder/ubuntu-*/Dockerfile.base
|
||||
# so the mise binary baking the image is the same one a workspace
|
||||
# ships with. `min_version` in mise.toml catches downgrades.
|
||||
# Write trust config to ~/.config/mise/conf.d/ instead of using
|
||||
# MISE_TRUSTED_CONFIG_PATHS so the same file-based approach
|
||||
# works in workspaces (where the user owns the file).
|
||||
env:
|
||||
MISE_VERSION: v2026.5.12
|
||||
MISE_SHA256: a238972a3162d710b85b28c324372e96ca4e4b486c81fe78695000d9fbc77c48
|
||||
WORKSPACE: ${{ github.workspace }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl --silent --show-error --location --fail \
|
||||
"https://github.com/jdx/mise/releases/download/${MISE_VERSION}/mise-${MISE_VERSION}-linux-x64" \
|
||||
--output /tmp/mise
|
||||
echo "${MISE_SHA256} /tmp/mise" | sha256sum -c
|
||||
sudo install -m 0755 /tmp/mise /usr/local/bin/mise
|
||||
rm /tmp/mise
|
||||
mise --version
|
||||
mkdir -p "$HOME/.config/mise/conf.d"
|
||||
cat > "$HOME/.config/mise/conf.d/00-ci-trust.toml" <<EOF
|
||||
[settings]
|
||||
trusted_config_paths = ["$WORKSPACE"]
|
||||
EOF
|
||||
- name: Set up mise tools
|
||||
if: matrix.image-version != 'nix' && !github.event.pull_request.head.repo.fork
|
||||
uses: ./.github/actions/setup-mise
|
||||
|
||||
- name: Compute image SHAs
|
||||
# Match the fork guard on the downstream consumers of these
|
||||
@@ -216,25 +190,6 @@ jobs:
|
||||
ghcr.io/coder/oss-dogfood-base:${{ matrix.image-version }}-${{ steps.shas.outputs.base_sha }}
|
||||
ghcr.io/coder/oss-dogfood-base:${{ matrix.image-version }}-${{ steps.docker-tag-name.outputs.tag }}
|
||||
|
||||
- name: Install mise tools
|
||||
if: matrix.image-version != 'nix' && !github.event.pull_request.head.repo.fork
|
||||
# `mise oci build` packages already-installed tools into OCI
|
||||
# layers; it does not install them. Run `mise install` first so
|
||||
# the tools land in MISE_DATA_DIR on the runner.
|
||||
# github_token raises aqua's API quota during tool installs.
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# --locked refuses to resolve URLs at install time and forces
|
||||
# the runner to consume what mise.lock already committed,
|
||||
# so a forgotten lockfile entry fails CI instead of silently
|
||||
# being added on next run.
|
||||
mise install --yes --locked
|
||||
# Put mise's shims dir on PATH for subsequent steps so
|
||||
# `mise oci push --tool crane` can find crane (and any other
|
||||
# mise-managed binary it shells out to).
|
||||
echo "$HOME/.local/share/mise/shims" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Build mise oci layer
|
||||
if: matrix.image-version != 'nix' && !github.event.pull_request.head.repo.fork
|
||||
env:
|
||||
@@ -360,8 +315,10 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: ./.github/actions/setup-tf
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "terraform"
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
|
||||
|
||||
@@ -39,12 +39,16 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Set up Go
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go"
|
||||
|
||||
- name: Install whichtests
|
||||
shell: bash
|
||||
run: ./.github/scripts/retry.sh -- go install github.com/coder/whichtests@ec33bab1ec04cd86beb7a61a069db4463dba63f5
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:github.com/coder/whichtests
|
||||
|
||||
- name: Select changed tests
|
||||
id: selector
|
||||
@@ -57,9 +61,11 @@ jobs:
|
||||
--coalesce \
|
||||
--out-matrix "$RUNNER_TEMP/flake-matrix.json"
|
||||
|
||||
- name: Setup Terraform
|
||||
- name: Set up Terraform
|
||||
if: ${{ fromJSON(steps.selector.outputs.matrix).include[0] != null }}
|
||||
uses: ./.github/actions/setup-tf
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "terraform"
|
||||
|
||||
- name: Run targeted Go flake checks
|
||||
id: flake_check
|
||||
|
||||
@@ -62,11 +62,16 @@ jobs:
|
||||
- name: Setup GNU tools (macOS)
|
||||
uses: ./.github/actions/setup-gnu-tools
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go terraform"
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: ./.github/actions/setup-tf
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:gotest.tools/gotestsum
|
||||
|
||||
- name: Setup Embedded Postgres Cache Paths
|
||||
id: embedded-pg-cache
|
||||
|
||||
@@ -238,14 +238,19 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go node pnpm"
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Install pnpm dependencies
|
||||
uses: ./.github/actions/pnpm-install
|
||||
|
||||
- name: Setup sqlc
|
||||
uses: ./.github/actions/setup-sqlc
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:github.com/coder/sqlc/cmd/sqlc
|
||||
|
||||
- name: GHCR Login
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
|
||||
@@ -172,13 +172,16 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
use-cache: false
|
||||
install-args: "go node pnpm helm cosign syft"
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Install pnpm dependencies
|
||||
uses: ./.github/actions/pnpm-install
|
||||
|
||||
- name: Install Go mise tools
|
||||
run: ./.github/scripts/retry.sh -- mise install --locked go:github.com/tc-hib/go-winres go:github.com/goreleaser/nfpm/v2/cmd/nfpm
|
||||
|
||||
# Necessary for signing Windows binaries.
|
||||
- name: Setup Java
|
||||
@@ -187,19 +190,9 @@ jobs:
|
||||
distribution: "zulu"
|
||||
java-version: "11.0"
|
||||
|
||||
- name: Install go-winres
|
||||
run: ./.github/scripts/retry.sh -- go install github.com/tc-hib/go-winres@d743268d7ea168077ddd443c4240562d4f5e8c3e # v0.3.3
|
||||
|
||||
- name: Install nsis and zstd
|
||||
run: sudo apt-get install -y nsis zstd
|
||||
|
||||
- name: Install nfpm
|
||||
run: |
|
||||
set -euo pipefail
|
||||
wget -O /tmp/nfpm.deb https://github.com/goreleaser/nfpm/releases/download/v2.35.1/nfpm_2.35.1_amd64.deb
|
||||
sudo dpkg -i /tmp/nfpm.deb
|
||||
rm /tmp/nfpm.deb
|
||||
|
||||
- name: Install rcodesign
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -210,12 +203,6 @@ jobs:
|
||||
apple-codesign-0.22.0-x86_64-unknown-linux-musl/rcodesign
|
||||
rm /tmp/rcodesign.tar.gz
|
||||
|
||||
- name: Install cosign
|
||||
uses: ./.github/actions/install-cosign
|
||||
|
||||
- name: Install syft
|
||||
uses: ./.github/actions/install-syft
|
||||
|
||||
- name: Setup Apple Developer certificate and API key
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -36,8 +36,13 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "go"
|
||||
|
||||
- name: Restore Go cache
|
||||
uses: ./.github/actions/go-cache
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v3.29.5
|
||||
|
||||
@@ -14,7 +14,54 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prepare-linkspector-browser:
|
||||
# later versions of Ubuntu have disabled unprivileged user namespaces, which are required by the action
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
CHROME_BUILD_ID: "145.0.7632.77"
|
||||
outputs:
|
||||
browser-cache-key: ${{ steps.browser-versions.outputs.cache-key }}
|
||||
chrome-path: ${{ steps.install-chrome.outputs.path }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up mise tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
with:
|
||||
install-args: "node npm:@puppeteer/browsers"
|
||||
|
||||
- name: Get browser versions
|
||||
id: browser-versions
|
||||
run: |
|
||||
set -euo pipefail
|
||||
installer_version="$(mise current npm:@puppeteer/browsers)"
|
||||
echo "cache-key=puppeteer-${RUNNER_OS}-${RUNNER_ARCH}-browsers-${installer_version}-chrome-${CHROME_BUILD_ID}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore Puppeteer browser cache
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.cache/puppeteer
|
||||
key: ${{ steps.browser-versions.outputs.cache-key }}
|
||||
|
||||
- name: Install Linkspector Chrome
|
||||
id: install-chrome
|
||||
run: |
|
||||
set -euo pipefail
|
||||
chrome_path="$(browsers install "chrome@${CHROME_BUILD_ID}" --path "${HOME}/.cache/puppeteer" --format '{{path}}')"
|
||||
echo "path=${chrome_path}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
check-docs:
|
||||
needs: prepare-linkspector-browser
|
||||
# later versions of Ubuntu have disabled unprivileged user namespaces, which are required by the action
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
@@ -54,10 +101,21 @@ jobs:
|
||||
corepack enable pnpm
|
||||
mkdir -p "$(pnpm store path --silent)"
|
||||
|
||||
- name: Restore Puppeteer browser cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.cache/puppeteer
|
||||
key: ${{ needs.prepare-linkspector-browser.outputs.browser-cache-key }}
|
||||
|
||||
- name: Check Markdown links
|
||||
uses: umbrelladocs/action-linkspector@37c85bcde51b30bf929936502bac6bfb7e8f0a4d # v1.4.1
|
||||
uses: umbrelladocs/action-linkspector@036f295d12b67b0c4b445bc83db0538afb78db69 # v1.5.2
|
||||
id: markdown-link-check
|
||||
# checks all markdown files from /docs including all subfolders
|
||||
env:
|
||||
# Use the Chrome build prepared from mise-pinned Puppeteer instead
|
||||
# of letting linkspector download a mutable browser at runtime.
|
||||
# See: https://github.com/UmbrellaDocs/action-linkspector/issues/62
|
||||
PUPPETEER_EXECUTABLE_PATH: ${{ needs.prepare-linkspector-browser.outputs.chrome-path }}
|
||||
with:
|
||||
reporter: github-pr-review
|
||||
config_file: ".github/.linkspector.yml"
|
||||
|
||||
@@ -728,11 +728,11 @@ endif
|
||||
# GitHub Actions linters are run in a separate CI job (lint-actions) that only
|
||||
# triggers when workflow files change, so we skip them here when CI=true.
|
||||
LINT_ACTIONS_TARGETS := $(if $(CI),,lint/actions/actionlint)
|
||||
lint: lint/shellcheck lint/go lint/ts lint/examples lint/helm lint/site-icons lint/markdown lint/check-scopes lint/migrations lint/bootstrap lint/architecture lint/emdash lint/agents $(LINT_ACTIONS_TARGETS)
|
||||
lint: lint/shellcheck lint/go lint/ts lint/examples lint/helm lint/site-icons lint/markdown lint/check-scopes lint/migrations lint/bootstrap lint/architecture lint/emdash lint/agents lint/mise-versions $(LINT_ACTIONS_TARGETS)
|
||||
.PHONY: lint
|
||||
|
||||
# Subset of lint that does not require Go or Node toolchains.
|
||||
lint-light: lint/shellcheck lint/markdown lint/helm lint/bootstrap lint/migrations lint/actions/actionlint lint/typos lint/emdash
|
||||
# Fast lint subset for lightweight hooks. Some targets use mise-managed tools.
|
||||
lint-light: lint/shellcheck lint/markdown lint/helm lint/bootstrap lint/migrations lint/actions/actionlint lint/typos lint/emdash lint/mise-versions
|
||||
.PHONY: lint-light
|
||||
|
||||
lint/site-icons:
|
||||
@@ -745,9 +745,8 @@ lint/ts: site/node_modules/.installed
|
||||
.PHONY: lint/ts
|
||||
|
||||
lint/go:
|
||||
linter_ver=$$(grep -Eo '^golangci-lint = "[^"]+"' mise.toml | sed -E 's/.*"([^"]+)"/\1/')
|
||||
go run github.com/golangci/golangci-lint/cmd/golangci-lint@v$$linter_ver run
|
||||
go tool github.com/coder/paralleltestctx/cmd/paralleltestctx -custom-funcs="testutil.Context,chatdTestContext" ./...
|
||||
golangci-lint run
|
||||
paralleltestctx -custom-funcs="testutil.Context,chatdTestContext" ./...
|
||||
go run ./scripts/intxcheck ./...
|
||||
.PHONY: lint/go
|
||||
|
||||
@@ -790,16 +789,27 @@ lint/actions: lint/actions/actionlint lint/actions/zizmor
|
||||
.PHONY: lint/actions
|
||||
|
||||
lint/actions/actionlint:
|
||||
go tool github.com/rhysd/actionlint/cmd/actionlint
|
||||
mise exec actionlint -- actionlint
|
||||
.PHONY: lint/actions/actionlint
|
||||
|
||||
# zizmor uses GH_TOKEN to fetch imported workflows from GitHub; without it,
|
||||
# external action references are skipped silently.
|
||||
lint/actions/zizmor:
|
||||
./scripts/zizmor.sh \
|
||||
@set -euo pipefail; \
|
||||
if [ -z "$${GH_TOKEN:-}" ] && command -v gh >/dev/null 2>&1; then \
|
||||
GH_TOKEN="$$(gh auth token 2>/dev/null || true)"; \
|
||||
export GH_TOKEN; \
|
||||
fi; \
|
||||
mise exec zizmor -- zizmor \
|
||||
--strict-collection \
|
||||
--persona=regular \
|
||||
.
|
||||
.PHONY: lint/actions/zizmor
|
||||
|
||||
lint/mise-versions:
|
||||
./scripts/check_mise_versions.sh
|
||||
.PHONY: lint/mise-versions
|
||||
|
||||
# Verify api_key_scope enum contains all RBAC <resource>:<action> values.
|
||||
lint/check-scopes: coderd/database/dump.sql | _gen/bin/check-scopes
|
||||
_gen/bin/check-scopes
|
||||
@@ -811,28 +821,8 @@ lint/migrations:
|
||||
./scripts/check_pg_schema.sh "Fixtures" $(FIXTURE_FILES)
|
||||
.PHONY: lint/migrations
|
||||
|
||||
TYPOS_VERSION := $(shell grep -oP 'crate-ci/typos@\S+\s+\#\s+v\K[0-9.]+' .github/workflows/ci.yaml)
|
||||
|
||||
# Map uname values to typos release asset names.
|
||||
TYPOS_ARCH := $(shell uname -m)
|
||||
# typos release assets use aarch64, but macOS ARM reports arm64 via uname -m.
|
||||
ifeq ($(TYPOS_ARCH),arm64)
|
||||
TYPOS_ARCH := aarch64
|
||||
endif
|
||||
ifeq ($(shell uname -s),Darwin)
|
||||
TYPOS_OS := apple-darwin
|
||||
else
|
||||
TYPOS_OS := unknown-linux-musl
|
||||
endif
|
||||
|
||||
build/typos-$(TYPOS_VERSION):
|
||||
mkdir -p build/
|
||||
curl -sSfL "https://github.com/crate-ci/typos/releases/download/v$(TYPOS_VERSION)/typos-v$(TYPOS_VERSION)-$(TYPOS_ARCH)-$(TYPOS_OS).tar.gz" \
|
||||
| tar -xzf - -C build/ ./typos
|
||||
mv build/typos "$@"
|
||||
|
||||
lint/typos: build/typos-$(TYPOS_VERSION)
|
||||
build/typos-$(TYPOS_VERSION) --config .github/workflows/typos.toml
|
||||
lint/typos:
|
||||
typos --config .github/workflows/typos.toml
|
||||
.PHONY: lint/typos
|
||||
|
||||
# pre-commit and pre-push mirror CI checks locally.
|
||||
|
||||
@@ -975,15 +975,19 @@ func (m *Manager) createTransport(ctx context.Context, cfg ServerConfig) (transp
|
||||
}),
|
||||
), nil
|
||||
case "http", "":
|
||||
return transport.NewStreamableHTTP(
|
||||
cfg.URL,
|
||||
transport.WithHTTPHeaders(cfg.Headers),
|
||||
)
|
||||
var opts []transport.StreamableHTTPCOption
|
||||
opts = append(opts, transport.WithHTTPHeaders(cfg.Headers))
|
||||
if c := mcpHTTPClient(); c != nil {
|
||||
opts = append(opts, transport.WithHTTPBasicClient(c))
|
||||
}
|
||||
return transport.NewStreamableHTTP(cfg.URL, opts...)
|
||||
case "sse":
|
||||
return transport.NewSSE(
|
||||
cfg.URL,
|
||||
transport.WithHeaders(cfg.Headers),
|
||||
)
|
||||
var sseOpts []transport.ClientOption
|
||||
sseOpts = append(sseOpts, transport.WithHeaders(cfg.Headers))
|
||||
if c := mcpHTTPClient(); c != nil {
|
||||
sseOpts = append(sseOpts, transport.WithHTTPClient(c))
|
||||
}
|
||||
return transport.NewSSE(cfg.URL, sseOpts...)
|
||||
default:
|
||||
return nil, xerrors.Errorf("unsupported transport %q", cfg.Transport)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package agentmcp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mcpHTTPClient returns an isolated *http.Client when running
|
||||
// inside tests, or nil for production. During tests,
|
||||
// httptest.Server.Close() calls
|
||||
// http.DefaultTransport.CloseIdleConnections(), which disrupts
|
||||
// any MCP client sharing that transport. When DefaultTransport
|
||||
// is a *http.Transport it is cloned; otherwise a minimal
|
||||
// transport with ProxyFromEnvironment is created as a fallback.
|
||||
func mcpHTTPClient() *http.Client {
|
||||
if !testing.Testing() {
|
||||
return nil
|
||||
}
|
||||
if dt, ok := http.DefaultTransport.(*http.Transport); ok {
|
||||
return &http.Client{Transport: dt.Clone()}
|
||||
}
|
||||
return &http.Client{Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mcpHTTPClient returns an isolated *http.Client when running
|
||||
// inside tests, or nil for production. During tests,
|
||||
// httptest.Server.Close() calls
|
||||
// http.DefaultTransport.CloseIdleConnections(), which disrupts
|
||||
// any MCP client sharing that transport. When DefaultTransport
|
||||
// is a *http.Transport it is cloned; otherwise a minimal
|
||||
// transport with ProxyFromEnvironment is created as a fallback.
|
||||
func mcpHTTPClient() *http.Client {
|
||||
if !testing.Testing() {
|
||||
return nil
|
||||
}
|
||||
if dt, ok := http.DefaultTransport.(*http.Transport); ok {
|
||||
return &http.Client{Transport: dt.Clone()}
|
||||
}
|
||||
return &http.Client{Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}}
|
||||
}
|
||||
@@ -39,6 +39,17 @@ func NewStreamableHTTPServerProxy(serverName, serverURL string, headers map[stri
|
||||
opts = append(opts, transport.WithHTTPHeaders(headers))
|
||||
}
|
||||
|
||||
// Prepend an isolated HTTP client when running in tests so
|
||||
// httptest.Server.Close() does not disrupt this proxy's
|
||||
// connections via http.DefaultTransport.CloseIdleConnections().
|
||||
// Caller-provided WithHTTPBasicClient in opts overrides this
|
||||
// (last-wins).
|
||||
if c := mcpHTTPClient(); c != nil {
|
||||
opts = append([]transport.StreamableHTTPCOption{
|
||||
transport.WithHTTPBasicClient(c),
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
mcpClient, err := client.NewStreamableHttpClient(serverURL, opts...)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("create streamable http client: %w", err)
|
||||
|
||||
+32
-3
@@ -2979,6 +2979,11 @@ func parseExternalAuthProvidersFromEnv(prefix string, environ []string) ([]coder
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
const (
|
||||
aiGatewayProviderEnvPrefix = "CODER_AI_GATEWAY_PROVIDER_"
|
||||
aiBridgeProviderEnvPrefix = "CODER_AIBRIDGE_PROVIDER_"
|
||||
)
|
||||
|
||||
// ReadAIProvidersFromEnv parses CODER_AI_GATEWAY_PROVIDER_<N>_<KEY>
|
||||
// environment variables into a slice of AIProviderConfig.
|
||||
// Deprecated alias env vars with the CODER_AIBRIDGE_PROVIDER_<N>_<KEY>
|
||||
@@ -2986,16 +2991,22 @@ func parseExternalAuthProvidersFromEnv(prefix string, environ []string) ([]coder
|
||||
//
|
||||
// This follows the same indexed pattern as ReadExternalAuthProvidersFromEnv.
|
||||
func ReadAIProvidersFromEnv(logger slog.Logger, environ []string) ([]codersdk.AIProviderConfig, error) {
|
||||
providers, err := readAIProvidersForPrefix(logger, environ, "CODER_AIBRIDGE_PROVIDER_")
|
||||
providers, err := readAIProvidersForPrefix(logger, environ, aiBridgeProviderEnvPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gatewayProviders, err := readAIProvidersForPrefix(logger, environ, "CODER_AI_GATEWAY_PROVIDER_")
|
||||
gatewayProviders, err := readAIProvidersForPrefix(logger, environ, aiGatewayProviderEnvPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(providers) > 0 && len(gatewayProviders) > 0 {
|
||||
return nil, xerrors.New("cannot mix CODER_AIBRIDGE_PROVIDER_* and CODER_AI_GATEWAY_PROVIDER_* environment variables, please consolidate onto CODER_AI_GATEWAY_PROVIDER_*")
|
||||
return nil, xerrors.Errorf("cannot mix %s* and %s* environment variables, please consolidate onto %s*", aiBridgeProviderEnvPrefix, aiGatewayProviderEnvPrefix, aiGatewayProviderEnvPrefix)
|
||||
}
|
||||
var activePrefix string
|
||||
if len(providers) > 0 {
|
||||
activePrefix = aiBridgeProviderEnvPrefix
|
||||
} else if len(gatewayProviders) > 0 {
|
||||
activePrefix = aiGatewayProviderEnvPrefix
|
||||
}
|
||||
providers = append(providers, gatewayProviders...)
|
||||
|
||||
@@ -3077,9 +3088,27 @@ func ReadAIProvidersFromEnv(logger slog.Logger, environ []string) ([]codersdk.AI
|
||||
names[p.Name] = i
|
||||
}
|
||||
|
||||
warnIfAIProvidersConfiguredFromEnv(context.Background(), logger, activePrefix, providers)
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func warnIfAIProvidersConfiguredFromEnv(ctx context.Context, logger slog.Logger, prefix string, providers []codersdk.AIProviderConfig) {
|
||||
if len(providers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if prefix == "" {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Warn(ctx,
|
||||
"ai provider environment variables are deprecated for provider management and only seed provider configuration at startup",
|
||||
slog.F("env_prefix", prefix),
|
||||
slog.F("replacement", "Manage AI Providers from the Coder UI or HTTP API."),
|
||||
)
|
||||
}
|
||||
|
||||
// readAIProvidersForPrefix parses provider env vars under a single
|
||||
// indexed prefix (e.g. CODER_AI_GATEWAY_PROVIDER_) into a slice of
|
||||
// AIProviderConfig. Per-field syntax errors and unknown keys are
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -575,6 +576,58 @@ func TestValidateLegacyAIBridgeConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarnIfAIProvidersConfiguredFromEnv(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("NoProviders", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sink := testutil.NewFakeSink(t)
|
||||
warnIfAIProvidersConfiguredFromEnv(context.Background(), sink.Logger(), aiGatewayProviderEnvPrefix, nil)
|
||||
|
||||
require.Empty(t, sink.Entries())
|
||||
})
|
||||
|
||||
t.Run("EmptyPrefix", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sink := testutil.NewFakeSink(t)
|
||||
warnIfAIProvidersConfiguredFromEnv(context.Background(), sink.Logger(), "", []codersdk.AIProviderConfig{{Type: "openai", Name: "openai"}})
|
||||
|
||||
require.Empty(t, sink.Entries())
|
||||
})
|
||||
|
||||
t.Run("AIGatewayPrefix", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sink := testutil.NewFakeSink(t)
|
||||
warnIfAIProvidersConfiguredFromEnv(context.Background(), sink.Logger(), aiGatewayProviderEnvPrefix, []codersdk.AIProviderConfig{{Type: "openai", Name: "openai"}})
|
||||
|
||||
entries := sink.Entries(func(e slog.SinkEntry) bool {
|
||||
return e.Message == "ai provider environment variables are deprecated for provider management and only seed provider configuration at startup"
|
||||
})
|
||||
require.Len(t, entries, 1)
|
||||
require.Len(t, entries[0].Fields, 2)
|
||||
assertFieldValue(t, entries[0].Fields, "env_prefix", aiGatewayProviderEnvPrefix)
|
||||
assertFieldValue(t, entries[0].Fields, "replacement", "Manage AI Providers from the Coder UI or HTTP API.")
|
||||
})
|
||||
|
||||
t.Run("AIBridgePrefix", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sink := testutil.NewFakeSink(t)
|
||||
warnIfAIProvidersConfiguredFromEnv(context.Background(), sink.Logger(), aiBridgeProviderEnvPrefix, []codersdk.AIProviderConfig{{Type: "openai", Name: "openai"}})
|
||||
|
||||
entries := sink.Entries(func(e slog.SinkEntry) bool {
|
||||
return e.Message == "ai provider environment variables are deprecated for provider management and only seed provider configuration at startup"
|
||||
})
|
||||
require.Len(t, entries, 1)
|
||||
require.Len(t, entries[0].Fields, 2)
|
||||
assertFieldValue(t, entries[0].Fields, "env_prefix", aiBridgeProviderEnvPrefix)
|
||||
assertFieldValue(t, entries[0].Fields, "replacement", "Manage AI Providers from the Coder UI or HTTP API.")
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildAIProviderFromRowSetsAPIDumpDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -721,3 +774,14 @@ func mustMarshalSettings(s codersdk.AIProviderSettings) sql.NullString {
|
||||
}
|
||||
return sql.NullString{String: string(data), Valid: true}
|
||||
}
|
||||
|
||||
func assertFieldValue(t *testing.T, fields slog.Map, name string, expected interface{}) {
|
||||
t.Helper()
|
||||
for _, f := range fields {
|
||||
if f.Name == name {
|
||||
assert.Equal(t, expected, f.Value)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("field %q not found", name)
|
||||
}
|
||||
|
||||
+35
-21
@@ -237,7 +237,10 @@ func Test_TaskSend(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given: An initializing task (workspace running, no agent
|
||||
// connected).
|
||||
// connected). Close the agent, pause, then resume so the
|
||||
// workspace is started but no agent is connected. The
|
||||
// command enters waitForTaskIdle directly (initializing
|
||||
// path), where we verify it handles an external pause.
|
||||
setupCtx := testutil.Context(t, testutil.WaitLong)
|
||||
setup := setupCLITaskTest(setupCtx, t, nil)
|
||||
|
||||
@@ -246,8 +249,6 @@ func Test_TaskSend(t *testing.T) {
|
||||
resumeTask(setupCtx, t, setup.userClient, setup.task)
|
||||
|
||||
// Set up mock clock and traps before starting the command.
|
||||
// Without a mock clock the poll can race with the stop build
|
||||
// and see a transient 'unknown' status instead of 'paused'.
|
||||
mClock := quartz.NewMock(t)
|
||||
tickTrap := mClock.Trap().NewTicker("task_send", "poll")
|
||||
resetTrap := mClock.Trap().TickerReset("task_send", "poll")
|
||||
@@ -271,23 +272,28 @@ func Test_TaskSend(t *testing.T) {
|
||||
tickCall.MustRelease(ctx)
|
||||
tickTrap.Close()
|
||||
|
||||
// Fire the immediate first poll (time.Nanosecond initial interval).
|
||||
// This poll sees 'initializing' because no agent is connected.
|
||||
// Fire the first poll. The goroutine calls ticker.Reset
|
||||
// which the trap catches, freezing the goroutine BEFORE
|
||||
// client.TaskByID runs. Release it so the first poll
|
||||
// sees 'initializing' and continues.
|
||||
mClock.Advance(time.Nanosecond).MustWait(ctx)
|
||||
|
||||
// Wait for Reset (confirms first poll completed).
|
||||
resetCall := resetTrap.MustWait(ctx)
|
||||
resetCall.MustRelease(ctx)
|
||||
resetTrap.Close()
|
||||
|
||||
// Pause the task while waitForTaskIdle is polling. Since
|
||||
// no agent is connected, the task stays initializing until
|
||||
// we pause it, at which point the status becomes paused.
|
||||
// Fire the second poll. The goroutine is again frozen at
|
||||
// ticker.Reset by the trap.
|
||||
mClock.Advance(5 * time.Second).MustWait(ctx)
|
||||
resetCall = resetTrap.MustWait(ctx)
|
||||
|
||||
// While the goroutine is frozen (before client.TaskByID),
|
||||
// pause the task. The stop build completes, so the DB has
|
||||
// (stop, succeeded) = 'paused'.
|
||||
pauseTask(ctx, t, setup.userClient, setup.task)
|
||||
|
||||
// Fire second poll at the regular 5s interval. The stop
|
||||
// build has completed, so the poll sees 'paused'.
|
||||
mClock.Advance(5 * time.Second).MustWait(ctx)
|
||||
// Release the trap. The goroutine unfreezes and
|
||||
// client.TaskByID deterministically sees 'paused'.
|
||||
resetCall.MustRelease(ctx)
|
||||
resetTrap.Close()
|
||||
|
||||
// Then: The command should fail because the task was paused.
|
||||
err := w.Wait()
|
||||
@@ -328,23 +334,31 @@ func Test_TaskSend(t *testing.T) {
|
||||
tickCall.MustRelease(ctx)
|
||||
tickTrap.Close()
|
||||
|
||||
// Fire the immediate first poll (time.Nanosecond initial interval).
|
||||
// Fire the first poll. The goroutine calls ticker.Reset
|
||||
// which the trap catches, freezing the goroutine BEFORE
|
||||
// client.TaskByID runs. Release it so the first poll
|
||||
// sees "working" and continues.
|
||||
mClock.Advance(time.Nanosecond).MustWait(ctx)
|
||||
|
||||
// Wait for Reset (confirms first poll completed and saw "working").
|
||||
resetCall := resetTrap.MustWait(ctx)
|
||||
resetCall.MustRelease(ctx)
|
||||
resetTrap.Close()
|
||||
|
||||
// Transition the app back to idle so waitForTaskIdle proceeds.
|
||||
// Fire the second poll. The goroutine is again frozen
|
||||
// at ticker.Reset by the trap.
|
||||
mClock.Advance(5 * time.Second).MustWait(ctx)
|
||||
resetCall = resetTrap.MustWait(ctx)
|
||||
|
||||
// While the goroutine is frozen (before client.TaskByID),
|
||||
// transition the app to idle.
|
||||
require.NoError(t, agentClient.PatchAppStatus(ctx, agentsdk.PatchAppStatus{
|
||||
AppSlug: "task-sidebar",
|
||||
State: codersdk.WorkspaceAppStatusStateIdle,
|
||||
Message: "ready",
|
||||
}))
|
||||
|
||||
// Fire second poll at the regular 5s interval.
|
||||
mClock.Advance(5 * time.Second).MustWait(ctx)
|
||||
// Release the trap. The goroutine unfreezes and
|
||||
// client.TaskByID deterministically sees "idle".
|
||||
resetCall.MustRelease(ctx)
|
||||
resetTrap.Close()
|
||||
|
||||
// Then: The command should complete successfully.
|
||||
require.NoError(t, w.Wait())
|
||||
|
||||
+44
-18
@@ -124,35 +124,55 @@ AI GATEWAY OPTIONS:
|
||||
disabled, only centralized key authentication is permitted.
|
||||
|
||||
--ai-gateway-anthropic-base-url string, $CODER_AI_GATEWAY_ANTHROPIC_BASE_URL (default: https://api.anthropic.com/)
|
||||
The base URL of the Anthropic API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The base URL of the Anthropic
|
||||
API.
|
||||
|
||||
--ai-gateway-anthropic-key string, $CODER_AI_GATEWAY_ANTHROPIC_KEY
|
||||
The key to authenticate against the Anthropic API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The key to authenticate
|
||||
against the Anthropic API.
|
||||
|
||||
--ai-gateway-bedrock-access-key string, $CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY
|
||||
The access key to authenticate against the AWS Bedrock API.
|
||||
|
||||
--ai-gateway-bedrock-access-key-secret string, $CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY_SECRET
|
||||
The access key secret to use with the access key to authenticate
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The access key to authenticate
|
||||
against the AWS Bedrock API.
|
||||
|
||||
--ai-gateway-bedrock-access-key-secret string, $CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY_SECRET
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The access key secret to use
|
||||
with the access key to authenticate against the AWS Bedrock API.
|
||||
|
||||
--ai-gateway-bedrock-base-url string, $CODER_AI_GATEWAY_BEDROCK_BASE_URL
|
||||
The base URL to use for the AWS Bedrock API. Use this setting to
|
||||
specify an exact URL to use. Takes precedence over
|
||||
CODER_AI_GATEWAY_BEDROCK_REGION.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The base URL to use for the
|
||||
AWS Bedrock API. Use this setting to specify an exact URL to use.
|
||||
Takes precedence over CODER_AI_GATEWAY_BEDROCK_REGION.
|
||||
|
||||
--ai-gateway-bedrock-model string, $CODER_AI_GATEWAY_BEDROCK_MODEL (default: global.anthropic.claude-sonnet-4-5-20250929-v1:0)
|
||||
The model to use when making requests to the AWS Bedrock API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The model to use when making
|
||||
requests to the AWS Bedrock API.
|
||||
|
||||
--ai-gateway-bedrock-region string, $CODER_AI_GATEWAY_BEDROCK_REGION
|
||||
The AWS Bedrock API region to use. Constructs a base URL to use for
|
||||
the AWS Bedrock API in the form of
|
||||
'https://bedrock-runtime.<region>.amazonaws.com'.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The AWS Bedrock API region to
|
||||
use. Constructs a base URL to use for the AWS Bedrock API in the form
|
||||
of 'https://bedrock-runtime.<region>.amazonaws.com'.
|
||||
|
||||
--ai-gateway-bedrock-small-fastmodel string, $CODER_AI_GATEWAY_BEDROCK_SMALL_FAST_MODEL (default: global.anthropic.claude-haiku-4-5-20251001-v1:0)
|
||||
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
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. 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.
|
||||
|
||||
--ai-gateway-circuit-breaker-enabled bool, $CODER_AI_GATEWAY_CIRCUIT_BREAKER_ENABLED (default: false)
|
||||
@@ -171,10 +191,16 @@ AI GATEWAY OPTIONS:
|
||||
to disable (unlimited).
|
||||
|
||||
--ai-gateway-openai-base-url string, $CODER_AI_GATEWAY_OPENAI_BASE_URL (default: https://api.openai.com/v1/)
|
||||
The base URL of the OpenAI API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The base URL of the OpenAI
|
||||
API.
|
||||
|
||||
--ai-gateway-openai-key string, $CODER_AI_GATEWAY_OPENAI_KEY
|
||||
The key to authenticate against the OpenAI API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The key to authenticate
|
||||
against the OpenAI API.
|
||||
|
||||
--ai-gateway-rate-limit int, $CODER_AI_GATEWAY_RATE_LIMIT (default: 0)
|
||||
Maximum number of AI Gateway requests per second per replica. Set to 0
|
||||
|
||||
+25
-9
@@ -874,25 +874,41 @@ ai_gateway:
|
||||
# Whether to start an in-memory AI Gateway instance.
|
||||
# (default: true, type: bool)
|
||||
enabled: true
|
||||
# The base URL of the OpenAI API.
|
||||
# Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this
|
||||
# option seeds provider configuration at startup only exactly once. It will not be
|
||||
# used in service runtime. The base URL of the OpenAI API.
|
||||
# (default: https://api.openai.com/v1/, type: string)
|
||||
openai_base_url: https://api.openai.com/v1/
|
||||
# The base URL of the Anthropic API.
|
||||
# Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this
|
||||
# option seeds provider configuration at startup only exactly once. It will not be
|
||||
# used in service runtime. The base URL of the Anthropic API.
|
||||
# (default: https://api.anthropic.com/, type: string)
|
||||
anthropic_base_url: https://api.anthropic.com/
|
||||
# The base URL to use for the AWS Bedrock API. Use this setting to specify an
|
||||
# exact URL to use. Takes precedence over CODER_AI_GATEWAY_BEDROCK_REGION.
|
||||
# Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this
|
||||
# option seeds provider configuration at startup only exactly once. It will not be
|
||||
# used in service runtime. The base URL to use for the AWS Bedrock API. Use this
|
||||
# setting to specify an exact URL to use. Takes precedence over
|
||||
# CODER_AI_GATEWAY_BEDROCK_REGION.
|
||||
# (default: <unset>, type: string)
|
||||
bedrock_base_url: ""
|
||||
# The AWS Bedrock API region to use. Constructs a base URL to use for the AWS
|
||||
# Bedrock API in the form of 'https://bedrock-runtime.<region>.amazonaws.com'.
|
||||
# Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this
|
||||
# option seeds provider configuration at startup only exactly once. It will not be
|
||||
# used in service runtime. The AWS Bedrock API region to use. Constructs a base
|
||||
# URL to use for the AWS Bedrock API in the form of
|
||||
# 'https://bedrock-runtime.<region>.amazonaws.com'.
|
||||
# (default: <unset>, type: string)
|
||||
bedrock_region: ""
|
||||
# The model to use when making requests to the AWS Bedrock API.
|
||||
# Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this
|
||||
# option seeds provider configuration at startup only exactly once. It will not be
|
||||
# used in service runtime. The model to use when making requests to the AWS
|
||||
# Bedrock API.
|
||||
# (default: global.anthropic.claude-sonnet-4-5-20250929-v1:0, type: string)
|
||||
bedrock_model: global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
||||
# 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
|
||||
# Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this
|
||||
# option seeds provider configuration at startup only exactly once. It will not be
|
||||
# used in service runtime. 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.
|
||||
# (default: global.anthropic.claude-haiku-4-5-20251001-v1:0, type: string)
|
||||
bedrock_small_fast_model: global.anthropic.claude-haiku-4-5-20251001-v1:0
|
||||
|
||||
@@ -340,6 +340,10 @@ func (api *API) aiProvidersUpdate(rw http.ResponseWriter, r *http.Request) {
|
||||
return errBedrockRejectsAPIKeys
|
||||
}
|
||||
|
||||
if req.APIKeys != nil && old.Type == database.AiProviderTypeCopilot && len(*req.APIKeys) > 0 {
|
||||
return errCopilotRejectsAPIKeys
|
||||
}
|
||||
|
||||
displayName := old.DisplayName
|
||||
if req.DisplayName != nil {
|
||||
// Empty string clears the column.
|
||||
@@ -383,6 +387,12 @@ func (api *API) aiProvidersUpdate(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, errCopilotRejectsAPIKeys) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Copilot providers do not accept api_keys; they authenticate via request-time GitHub OAuth tokens.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, errAIProviderBedrockTypeMismatch) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Bedrock settings are only valid for type=anthropic or type=bedrock.",
|
||||
@@ -483,6 +493,12 @@ func (api *API) publishAIProvidersChanged(ctx context.Context) {
|
||||
// Bedrock-typed provider; the outer handler translates it into a 400.
|
||||
var errBedrockRejectsAPIKeys = xerrors.New("bedrock providers do not accept api_keys")
|
||||
|
||||
// errCopilotRejectsAPIKeys is the sentinel returned from inside the
|
||||
// update transaction when a caller attempts to attach api_keys to a
|
||||
// Copilot-typed provider; the outer handler translates it into a 400.
|
||||
// Copilot authenticates via request-time GitHub OAuth tokens.
|
||||
var errCopilotRejectsAPIKeys = xerrors.New("copilot providers do not accept api_keys")
|
||||
|
||||
// errAIProviderBedrockTypeMismatch is the sentinel returned from
|
||||
// inside the update transaction when the post-merge settings carry a
|
||||
// Bedrock block but the provider is not anthropic- or bedrock-typed;
|
||||
|
||||
@@ -889,6 +889,75 @@ func TestAIProvidersKeyManagement(t *testing.T) {
|
||||
require.Contains(t, sdkErr.Message, "Bedrock providers do not accept api_keys")
|
||||
})
|
||||
|
||||
t.Run("CopilotCreateWithoutKeys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
//nolint:gocritic // Owner role is the audience for this endpoint.
|
||||
provider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
|
||||
Type: codersdk.AIProviderTypeCopilot,
|
||||
Name: "keys-copilot",
|
||||
Enabled: true,
|
||||
BaseURL: "https://api.business.githubcopilot.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.AIProviderTypeCopilot, provider.Type)
|
||||
require.Empty(t, provider.APIKeys)
|
||||
})
|
||||
|
||||
t.Run("CopilotRejectsCreateWithKeys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
//nolint:gocritic // Owner role is the audience for this endpoint.
|
||||
_, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
|
||||
Type: codersdk.AIProviderTypeCopilot,
|
||||
Name: "keys-copilot-create",
|
||||
Enabled: true,
|
||||
BaseURL: "https://api.business.githubcopilot.com",
|
||||
APIKeys: []string{"sk-should-be-rejected"}, //nolint:gosec // test fixture, not a real credential
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
require.Len(t, sdkErr.Validations, 1)
|
||||
require.Equal(t, "api_keys", sdkErr.Validations[0].Field)
|
||||
require.Contains(t, sdkErr.Validations[0].Detail, "type=copilot does not accept api_keys")
|
||||
})
|
||||
|
||||
t.Run("CopilotRejectsUpdateWithKeys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
//nolint:gocritic // Owner role is the audience for this endpoint.
|
||||
provider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
|
||||
Type: codersdk.AIProviderTypeCopilot,
|
||||
Name: "keys-copilot-update",
|
||||
Enabled: true,
|
||||
BaseURL: "https://api.business.githubcopilot.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
rejected := []codersdk.AIProviderKeyMutation{
|
||||
{APIKey: ptr.Ref("sk-copilot-no")}, //nolint:gosec // test fixture, not a real credential
|
||||
}
|
||||
_, err = client.UpdateAIProvider(ctx, provider.Name, codersdk.UpdateAIProviderRequest{
|
||||
APIKeys: &rejected,
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
require.Contains(t, sdkErr.Message, "Copilot providers do not accept api_keys")
|
||||
})
|
||||
|
||||
t.Run("EmptyKeyRejected", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
-- No-op: Postgres does not allow removing enum values safely.
|
||||
-- Matches the precedent in 000495_ai_providers.down.sql for ALTER
|
||||
-- TYPE resource_type / api_key_scope ADD VALUE.
|
||||
-- No-op: the up recreates ai_provider_type with a wider value set, but the
|
||||
-- down does not narrow it back. Narrowing would drop rows that already use the
|
||||
-- new values, and 000495_ai_providers.down.sql drops the type wholesale when
|
||||
-- migrating all the way down.
|
||||
|
||||
@@ -7,9 +7,27 @@
|
||||
-- OpenAI-compatible endpoints. Native gateway-side support for these
|
||||
-- providers comes later, at which point this enum already carries the
|
||||
-- right discriminator and no further migration is needed.
|
||||
ALTER TYPE ai_provider_type ADD VALUE IF NOT EXISTS 'azure';
|
||||
ALTER TYPE ai_provider_type ADD VALUE IF NOT EXISTS 'bedrock';
|
||||
ALTER TYPE ai_provider_type ADD VALUE IF NOT EXISTS 'google';
|
||||
ALTER TYPE ai_provider_type ADD VALUE IF NOT EXISTS 'openai-compat';
|
||||
ALTER TYPE ai_provider_type ADD VALUE IF NOT EXISTS 'openrouter';
|
||||
ALTER TYPE ai_provider_type ADD VALUE IF NOT EXISTS 'vercel';
|
||||
--
|
||||
-- Recreate the type rather than using ALTER TYPE ... ADD VALUE. Postgres
|
||||
-- forbids using a value added by ADD VALUE within the same transaction, and
|
||||
-- all migrations run in one transaction. 000504 casts existing chat_providers
|
||||
-- rows to these new values in that same transaction, so ADD VALUE fails with
|
||||
-- "unsafe use of new value". A freshly created enum's values are usable
|
||||
-- immediately, so the cast in 000504 succeeds.
|
||||
CREATE TYPE new_ai_provider_type AS ENUM (
|
||||
'openai',
|
||||
'anthropic',
|
||||
'azure',
|
||||
'bedrock',
|
||||
'google',
|
||||
'openai-compat',
|
||||
'openrouter',
|
||||
'vercel'
|
||||
);
|
||||
|
||||
ALTER TABLE ai_providers
|
||||
ALTER COLUMN type TYPE new_ai_provider_type USING (type::text::new_ai_provider_type);
|
||||
|
||||
DROP TYPE ai_provider_type;
|
||||
|
||||
ALTER TYPE new_ai_provider_type RENAME TO ai_provider_type;
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -1502,6 +1503,85 @@ func TestMigration000504AIProvidersBackfillOverridesNameConflict(t *testing.T) {
|
||||
require.True(t, fresh.Enabled)
|
||||
}
|
||||
|
||||
// TestMigration000504AIProvidersBackfillEnumInSingleTxn reproduces the
|
||||
// production migration path, where every pending migration runs inside a
|
||||
// single transaction (see pgTxnDriver). Migration 000499 widens
|
||||
// ai_provider_type with ALTER TYPE ... ADD VALUE, and 000504 casts existing
|
||||
// chat_providers rows to that enum. Postgres forbids using an enum value
|
||||
// added by ADD VALUE within the same transaction, so when a legacy provider
|
||||
// uses one of the new values (for example openai-compat) the batch fails with
|
||||
// "unsafe use of new value". The per-step Stepper used by the other tests
|
||||
// commits each migration separately and cannot surface this.
|
||||
func TestMigration000504AIProvidersBackfillEnumInSingleTxn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqlDB := testSQLDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
||||
|
||||
// Apply everything through 498 and commit, so chat_providers exists and is
|
||||
// populated before the batch under test runs, matching a deployment that
|
||||
// ran an earlier migration batch before this one.
|
||||
applyMigrationsInTxn(ctx, t, sqlDB, 1, 498)
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
providerID := uuid.New()
|
||||
|
||||
// A legacy provider whose type is one of the values added in 000499.
|
||||
_, err := sqlDB.ExecContext(ctx, `
|
||||
INSERT INTO chat_providers (id, provider, display_name, api_key, enabled, base_url, created_at, updated_at)
|
||||
VALUES ($1, 'openai-compat', 'OpenAI Compatible', '', TRUE, 'https://api.example.com/v1', $2, $2)
|
||||
`, providerID, now)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Apply 000499 through 000504 in a single transaction, as production does.
|
||||
applyMigrationsInTxn(ctx, t, sqlDB, 499, 504)
|
||||
|
||||
var typ string
|
||||
err = sqlDB.QueryRowContext(ctx,
|
||||
`SELECT type FROM ai_providers WHERE id = $1`, providerID,
|
||||
).Scan(&typ)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "openai-compat", typ)
|
||||
}
|
||||
|
||||
// applyMigrationsInTxn executes the up SQL for every migration whose version is
|
||||
// in [from, to] inside a single transaction, mirroring pgTxnDriver. The whole
|
||||
// batch commits or rolls back together.
|
||||
func applyMigrationsInTxn(ctx context.Context, t *testing.T, sqlDB *sql.DB, from, to int) {
|
||||
t.Helper()
|
||||
|
||||
entries, err := os.ReadDir(".")
|
||||
require.NoError(t, err)
|
||||
|
||||
var files []string
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if !strings.HasSuffix(name, ".up.sql") {
|
||||
continue
|
||||
}
|
||||
var version int
|
||||
if _, err := fmt.Sscanf(name, "%06d_", &version); err != nil {
|
||||
continue
|
||||
}
|
||||
if version >= from && version <= to {
|
||||
files = append(files, name)
|
||||
}
|
||||
}
|
||||
slices.Sort(files)
|
||||
|
||||
tx, err := sqlDB.BeginTx(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
defer tx.Rollback()
|
||||
|
||||
for _, name := range files {
|
||||
query, err := os.ReadFile(name)
|
||||
require.NoError(t, err)
|
||||
_, err = tx.ExecContext(ctx, string(query))
|
||||
require.NoErrorf(t, err, "apply migration %s", name)
|
||||
}
|
||||
require.NoError(t, tx.Commit())
|
||||
}
|
||||
|
||||
func TestMigration000498SoftDeleteStaleWorkspaceAgents(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
+107
-30
@@ -12,6 +12,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -57,11 +58,10 @@ func TestMCPHTTP_E2E_ClientIntegration(t *testing.T) {
|
||||
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
|
||||
|
||||
// Configure client with authentication headers using RFC 6750 Bearer token
|
||||
mcpClient, err := mcpclient.NewStreamableHttpClient(mcpURL,
|
||||
mcpClient := newIsolatedMCPClient(t, mcpURL,
|
||||
transport.WithHTTPHeaders(map[string]string{
|
||||
"Authorization": "Bearer " + coderClient.SessionToken(),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if closeErr := mcpClient.Close(); closeErr != nil {
|
||||
t.Logf("Failed to close MCP client: %v", closeErr)
|
||||
@@ -72,7 +72,7 @@ func TestMCPHTTP_E2E_ClientIntegration(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
// Start client
|
||||
err = mcpClient.Start(ctx)
|
||||
err := mcpClient.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initialize connection
|
||||
@@ -190,8 +190,7 @@ func TestMCPHTTP_E2E_UnauthenticatedAccess(t *testing.T) {
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode, "Should get HTTP 401 for unauthenticated access")
|
||||
|
||||
// Also test with MCP client to ensure it handles the error gracefully
|
||||
mcpClient, err := mcpclient.NewStreamableHttpClient(mcpURL)
|
||||
require.NoError(t, err, "Should be able to create MCP client without authentication")
|
||||
mcpClient := newIsolatedMCPClient(t, mcpURL)
|
||||
defer func() {
|
||||
if closeErr := mcpClient.Close(); closeErr != nil {
|
||||
t.Logf("Failed to close MCP client: %v", closeErr)
|
||||
@@ -245,11 +244,10 @@ func TestMCPHTTP_E2E_ToolWithWorkspace(t *testing.T) {
|
||||
coderdtest.NewWorkspaceAgentWaiter(t, coderClient, r.Workspace.ID).Wait()
|
||||
|
||||
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
|
||||
mcpClient, err := mcpclient.NewStreamableHttpClient(mcpURL,
|
||||
mcpClient := newIsolatedMCPClient(t, mcpURL,
|
||||
transport.WithHTTPHeaders(map[string]string{
|
||||
"Authorization": "Bearer " + coderClient.SessionToken(),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if closeErr := mcpClient.Close(); closeErr != nil {
|
||||
t.Logf("Failed to close MCP client: %v", closeErr)
|
||||
@@ -260,7 +258,7 @@ func TestMCPHTTP_E2E_ToolWithWorkspace(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
require.NoError(t, mcpClient.Start(ctx))
|
||||
_, err = mcpClient.Initialize(ctx, mcp.InitializeRequest{
|
||||
_, err := mcpClient.Initialize(ctx, mcp.InitializeRequest{
|
||||
Params: mcp.InitializeParams{
|
||||
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
|
||||
ClientInfo: mcp.Implementation{
|
||||
@@ -307,11 +305,10 @@ func TestMCPHTTP_E2E_ErrorHandling(t *testing.T) {
|
||||
|
||||
// Create MCP client
|
||||
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
|
||||
mcpClient, err := mcpclient.NewStreamableHttpClient(mcpURL,
|
||||
mcpClient := newIsolatedMCPClient(t, mcpURL,
|
||||
transport.WithHTTPHeaders(map[string]string{
|
||||
"Authorization": "Bearer " + coderClient.SessionToken(),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if closeErr := mcpClient.Close(); closeErr != nil {
|
||||
t.Logf("Failed to close MCP client: %v", closeErr)
|
||||
@@ -322,7 +319,7 @@ func TestMCPHTTP_E2E_ErrorHandling(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
// Start and initialize client
|
||||
err = mcpClient.Start(ctx)
|
||||
err := mcpClient.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
initReq := mcp.InitializeRequest{
|
||||
@@ -366,11 +363,10 @@ func TestMCPHTTP_E2E_ConcurrentRequests(t *testing.T) {
|
||||
|
||||
// Create MCP client
|
||||
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
|
||||
mcpClient, err := mcpclient.NewStreamableHttpClient(mcpURL,
|
||||
mcpClient := newIsolatedMCPClient(t, mcpURL,
|
||||
transport.WithHTTPHeaders(map[string]string{
|
||||
"Authorization": "Bearer " + coderClient.SessionToken(),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if closeErr := mcpClient.Close(); closeErr != nil {
|
||||
t.Logf("Failed to close MCP client: %v", closeErr)
|
||||
@@ -381,7 +377,7 @@ func TestMCPHTTP_E2E_ConcurrentRequests(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
// Start and initialize client
|
||||
err = mcpClient.Start(ctx)
|
||||
err := mcpClient.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
initReq := mcp.InitializeRequest{
|
||||
@@ -520,11 +516,10 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
|
||||
sessionToken := coderClient.SessionToken()
|
||||
|
||||
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
|
||||
mcpClient, err := mcpclient.NewStreamableHttpClient(mcpURL,
|
||||
mcpClient := newIsolatedMCPClient(t, mcpURL,
|
||||
transport.WithHTTPHeaders(map[string]string{
|
||||
"Authorization": "Bearer " + sessionToken,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if closeErr := mcpClient.Close(); closeErr != nil {
|
||||
t.Logf("Failed to close MCP client: %v", closeErr)
|
||||
@@ -669,11 +664,10 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
|
||||
|
||||
// Step 3: Use access token to authenticate with MCP endpoint
|
||||
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
|
||||
mcpClient, err := mcpclient.NewStreamableHttpClient(mcpURL,
|
||||
mcpClient := newIsolatedMCPClient(t, mcpURL,
|
||||
transport.WithHTTPHeaders(map[string]string{
|
||||
"Authorization": "Bearer " + accessToken,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if closeErr := mcpClient.Close(); closeErr != nil {
|
||||
t.Logf("Failed to close MCP client: %v", closeErr)
|
||||
@@ -762,11 +756,10 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
|
||||
t.Logf("Successfully refreshed token: %s...", newAccessToken[:10])
|
||||
|
||||
// Step 5: Use new access token to create another MCP connection
|
||||
newMcpClient, err := mcpclient.NewStreamableHttpClient(mcpURL,
|
||||
newMcpClient := newIsolatedMCPClient(t, mcpURL,
|
||||
transport.WithHTTPHeaders(map[string]string{
|
||||
"Authorization": "Bearer " + newAccessToken,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if closeErr := newMcpClient.Close(); closeErr != nil {
|
||||
t.Logf("Failed to close new MCP client: %v", closeErr)
|
||||
@@ -990,11 +983,10 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
|
||||
t.Logf("Successfully obtained access token: %s...", accessToken[:10])
|
||||
|
||||
// Step 5: Use access token to get user information via MCP
|
||||
mcpClient, err := mcpclient.NewStreamableHttpClient(mcpURL,
|
||||
mcpClient := newIsolatedMCPClient(t, mcpURL,
|
||||
transport.WithHTTPHeaders(map[string]string{
|
||||
"Authorization": "Bearer " + accessToken,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if closeErr := mcpClient.Close(); closeErr != nil {
|
||||
t.Logf("Failed to close MCP client: %v", closeErr)
|
||||
@@ -1088,11 +1080,10 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
|
||||
t.Logf("Successfully refreshed token: %s...", newAccessToken[:10])
|
||||
|
||||
// Step 7: Use refreshed token to get user information again via MCP
|
||||
newMcpClient, err := mcpclient.NewStreamableHttpClient(mcpURL,
|
||||
newMcpClient := newIsolatedMCPClient(t, mcpURL,
|
||||
transport.WithHTTPHeaders(map[string]string{
|
||||
"Authorization": "Bearer " + newAccessToken,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if closeErr := newMcpClient.Close(); closeErr != nil {
|
||||
t.Logf("Failed to close new MCP client: %v", closeErr)
|
||||
@@ -1268,11 +1259,10 @@ func TestMCPHTTP_E2E_ChatGPTEndpoint(t *testing.T) {
|
||||
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint + "?toolset=chatgpt"
|
||||
|
||||
// Configure client with authentication headers using RFC 6750 Bearer token
|
||||
mcpClient, err := mcpclient.NewStreamableHttpClient(mcpURL,
|
||||
mcpClient := newIsolatedMCPClient(t, mcpURL,
|
||||
transport.WithHTTPHeaders(map[string]string{
|
||||
"Authorization": "Bearer " + coderClient.SessionToken(),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
if closeErr := mcpClient.Close(); closeErr != nil {
|
||||
t.Logf("Failed to close MCP client: %v", closeErr)
|
||||
@@ -1283,7 +1273,7 @@ func TestMCPHTTP_E2E_ChatGPTEndpoint(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
// Start client
|
||||
err = mcpClient.Start(ctx)
|
||||
err := mcpClient.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initialize connection
|
||||
@@ -1433,11 +1423,10 @@ func TestMCPHTTP_E2E_WorkspaceSSHAuthz(t *testing.T) {
|
||||
|
||||
// Connect with the template-admin user.
|
||||
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
|
||||
mcpClient, err := mcpclient.NewStreamableHttpClient(mcpURL,
|
||||
mcpClient := newIsolatedMCPClient(t, mcpURL,
|
||||
transport.WithHTTPHeaders(map[string]string{
|
||||
"Authorization": "Bearer " + tmplAdminClient.SessionToken(),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = mcpClient.Close()
|
||||
}()
|
||||
@@ -1446,7 +1435,7 @@ func TestMCPHTTP_E2E_WorkspaceSSHAuthz(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
require.NoError(t, mcpClient.Start(ctx))
|
||||
_, err = mcpClient.Initialize(ctx, mcp.InitializeRequest{
|
||||
_, err := mcpClient.Initialize(ctx, mcp.InitializeRequest{
|
||||
Params: mcp.InitializeParams{
|
||||
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
|
||||
ClientInfo: mcp.Implementation{
|
||||
@@ -1489,3 +1478,91 @@ func mustParseURL(t *testing.T, rawURL string) *url.URL {
|
||||
require.NoError(t, err, "Failed to parse URL %q", rawURL)
|
||||
return u
|
||||
}
|
||||
|
||||
// newIsolatedMCPClient creates a streamable HTTP MCP client that uses
|
||||
// an isolated http.Transport cloned from http.DefaultTransport.
|
||||
// This prevents httptest.Server.Close() (which calls
|
||||
// http.DefaultTransport.CloseIdleConnections()) from disrupting the
|
||||
// client's connections during parallel tests.
|
||||
func newIsolatedMCPClient(t *testing.T, mcpURL string, opts ...transport.StreamableHTTPCOption) *mcpclient.Client {
|
||||
t.Helper()
|
||||
isolated := coderdtest.NewIsolatedHTTPClient(nil)
|
||||
opts = append([]transport.StreamableHTTPCOption{transport.WithHTTPBasicClient(isolated)}, opts...)
|
||||
client, err := mcpclient.NewStreamableHttpClient(mcpURL, opts...)
|
||||
require.NoError(t, err)
|
||||
return client
|
||||
}
|
||||
|
||||
// sentinelTransport wraps an http.RoundTripper and counts how many
|
||||
// requests flow through it. Used as a test sentinel to verify
|
||||
// whether a client is (or is not) using http.DefaultTransport.
|
||||
type sentinelTransport struct {
|
||||
inner http.RoundTripper
|
||||
hits atomic.Int64
|
||||
}
|
||||
|
||||
func (s *sentinelTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
s.hits.Add(1)
|
||||
return s.inner.RoundTrip(req)
|
||||
}
|
||||
|
||||
// TestMCPHTTP_E2E_TransportIsolation verifies that the
|
||||
// newIsolatedMCPClient helper creates clients that do NOT route
|
||||
// requests through http.DefaultTransport, while raw
|
||||
// mcpclient.NewStreamableHttpClient (without explicit
|
||||
// WithHTTPBasicClient) does use it.
|
||||
//
|
||||
//nolint:paralleltest // Mutates http.DefaultTransport.
|
||||
func TestMCPHTTP_E2E_TransportIsolation(t *testing.T) {
|
||||
// Replace DefaultTransport with a counting sentinel.
|
||||
original := http.DefaultTransport
|
||||
sentinel := &sentinelTransport{inner: original}
|
||||
http.DefaultTransport = sentinel
|
||||
t.Cleanup(func() { http.DefaultTransport = original })
|
||||
|
||||
coderClient, closer, api := coderdtest.NewWithAPI(t, nil)
|
||||
t.Cleanup(func() { closer.Close() })
|
||||
_ = coderdtest.CreateFirstUser(t, coderClient)
|
||||
|
||||
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
|
||||
authOpt := transport.WithHTTPHeaders(map[string]string{
|
||||
"Authorization": "Bearer " + coderClient.SessionToken(),
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
initReq := mcp.InitializeRequest{
|
||||
Params: mcp.InitializeParams{
|
||||
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
|
||||
ClientInfo: mcp.Implementation{Name: "sentinel-test", Version: "1.0.0"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("RawClientUsesDefaultTransport", func(t *testing.T) {
|
||||
sentinel.hits.Store(0)
|
||||
rawClient, err := mcpclient.NewStreamableHttpClient(mcpURL, authOpt)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = rawClient.Close() }()
|
||||
|
||||
require.NoError(t, rawClient.Start(ctx))
|
||||
_, err = rawClient.Initialize(ctx, initReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Greater(t, sentinel.hits.Load(), int64(0),
|
||||
"raw client should route requests through http.DefaultTransport")
|
||||
})
|
||||
|
||||
t.Run("IsolatedClientBypassesDefaultTransport", func(t *testing.T) {
|
||||
sentinel.hits.Store(0)
|
||||
isoClient := newIsolatedMCPClient(t, mcpURL, authOpt)
|
||||
defer func() { _ = isoClient.Close() }()
|
||||
|
||||
require.NoError(t, isoClient.Start(ctx))
|
||||
_, err := isoClient.Initialize(ctx, initReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, int64(0), sentinel.hits.Load(),
|
||||
"isolated client must NOT route requests through http.DefaultTransport")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,14 +35,22 @@ const (
|
||||
"- Key decisions made and their rationale\n" +
|
||||
"- Concrete technical details: file paths, function names, " +
|
||||
"commands, APIs, and configurations\n" +
|
||||
"- Errors encountered and how they were resolved\n" +
|
||||
"- Errors encountered and how they were resolved. Keep error " +
|
||||
"notes specific: name the file, the error, and the fix. Do not " +
|
||||
"generalize from a specific failure to a blanket tool-avoidance " +
|
||||
"rule (e.g. \"tool X is unreliable\" or \"always use Y instead " +
|
||||
"of Z\")\n" +
|
||||
"- Current state of the work: what is DONE, what is IN PROGRESS, " +
|
||||
"and what REMAINS to be done\n" +
|
||||
"- The specific action the assistant was performing or about to " +
|
||||
"perform when this summary was triggered\n\n" +
|
||||
"Be dense and factual. Every sentence should convey essential " +
|
||||
"context for continuation. Do not include pleasantries or " +
|
||||
"conversational filler."
|
||||
"conversational filler. For content that can be reproduced " +
|
||||
"(repo files, command output, API responses), reference how to " +
|
||||
"obtain it (file path, command, URL) rather than inlining the " +
|
||||
"full content. Include brief inline summaries when the content " +
|
||||
"itself would exceed a few lines."
|
||||
defaultCompactionSystemSummaryPrefix = "The following is a summary of " +
|
||||
"the earlier conversation. The assistant was actively working when " +
|
||||
"the context was compacted. Continue the work described below:"
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -238,7 +239,7 @@ func CreateWorkspace(db database.Store, organizationID, chatID uuid.UUID, option
|
||||
)
|
||||
}
|
||||
|
||||
workspace, err := options.CreateFn(ctx, ownerID, createReq)
|
||||
workspace, err := createWorkspaceWithNameRetry(ctx, ownerID, createReq, options.CreateFn)
|
||||
if err != nil {
|
||||
if responseErr, ok := httperror.IsResponder(err); ok {
|
||||
_, resp := responseErr.Response()
|
||||
@@ -703,6 +704,41 @@ func waitForAgentReady(
|
||||
}
|
||||
}
|
||||
|
||||
func createWorkspaceWithNameRetry(
|
||||
ctx context.Context,
|
||||
ownerID uuid.UUID,
|
||||
req codersdk.CreateWorkspaceRequest,
|
||||
createFn CreateWorkspaceFn,
|
||||
) (codersdk.Workspace, error) {
|
||||
workspace, err := createFn(ctx, ownerID, req)
|
||||
if err == nil {
|
||||
return workspace, nil
|
||||
}
|
||||
if !isWorkspaceNameConflict(err) {
|
||||
return codersdk.Workspace{}, err
|
||||
}
|
||||
|
||||
req.Name = generatedWorkspaceName(req.Name)
|
||||
return createFn(ctx, ownerID, req)
|
||||
}
|
||||
|
||||
func isWorkspaceNameConflict(err error) bool {
|
||||
responseErr, ok := httperror.IsResponder(err)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
status, resp := responseErr.Response()
|
||||
if status != http.StatusConflict {
|
||||
return false
|
||||
}
|
||||
for _, validation := range resp.Validations {
|
||||
if validation.Field == "name" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func generatedWorkspaceName(seed string) string {
|
||||
base := codersdk.UsernameFrom(strings.TrimSpace(strings.ToLower(seed)))
|
||||
if strings.TrimSpace(base) == "" {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -855,6 +856,70 @@ func TestCreateWorkspace_ResponderErrorPreservesStructuredFields(t *testing.T) {
|
||||
}}, result.Validations)
|
||||
}
|
||||
|
||||
func TestCreateWorkspaceWithNameRetry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("NameConflictRetriesWithGeneratedName", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var names []string
|
||||
workspace, err := createWorkspaceWithNameRetry(
|
||||
context.Background(),
|
||||
uuid.New(),
|
||||
codersdk.CreateWorkspaceRequest{Name: "fun-dashboard"},
|
||||
func(_ context.Context, _ uuid.UUID, req codersdk.CreateWorkspaceRequest) (codersdk.Workspace, error) {
|
||||
names = append(names, req.Name)
|
||||
if len(names) == 1 {
|
||||
return codersdk.Workspace{}, workspaceNameConflictError(req.Name)
|
||||
}
|
||||
|
||||
require.Regexp(t, `^fun-dashboard-[0-9a-f]{4}$`, req.Name)
|
||||
return codersdk.Workspace{Name: req.Name}, nil
|
||||
},
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, names, 2)
|
||||
require.Equal(t, "fun-dashboard", names[0])
|
||||
require.Equal(t, names[1], workspace.Name)
|
||||
})
|
||||
|
||||
t.Run("OtherConflictDoesNotRetry", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
calls := 0
|
||||
wantErr := httperror.NewResponseError(http.StatusConflict, codersdk.Response{
|
||||
Message: "quota exceeded",
|
||||
Validations: []codersdk.ValidationError{{
|
||||
Field: "quota",
|
||||
Detail: "quota exceeded",
|
||||
}},
|
||||
})
|
||||
_, err := createWorkspaceWithNameRetry(
|
||||
context.Background(),
|
||||
uuid.New(),
|
||||
codersdk.CreateWorkspaceRequest{Name: "fun-dashboard"},
|
||||
func(context.Context, uuid.UUID, codersdk.CreateWorkspaceRequest) (codersdk.Workspace, error) {
|
||||
calls++
|
||||
return codersdk.Workspace{}, wantErr
|
||||
},
|
||||
)
|
||||
|
||||
require.Same(t, wantErr, err)
|
||||
require.Equal(t, 1, calls)
|
||||
})
|
||||
}
|
||||
|
||||
func workspaceNameConflictError(name string) error {
|
||||
return httperror.NewResponseError(http.StatusConflict, codersdk.Response{
|
||||
Message: fmt.Sprintf("Workspace %q already exists.", name),
|
||||
Validations: []codersdk.ValidationError{{
|
||||
Field: "name",
|
||||
Detail: "This value is already in use and should be unique.",
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateWorkspace_GlobalTTL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -285,31 +285,24 @@ func createTransport(
|
||||
cfg database.MCPServerConfig,
|
||||
headers map[string]string,
|
||||
) (transport.Interface, error) {
|
||||
// Each connection gets its own HTTP client with a dedicated
|
||||
// transport so that httptest.Server.Close() (which calls
|
||||
// CloseIdleConnections on http.DefaultTransport) does not
|
||||
// disrupt unrelated connections during parallel tests.
|
||||
var httpClient *http.Client
|
||||
if dt, ok := http.DefaultTransport.(*http.Transport); ok {
|
||||
httpClient = &http.Client{Transport: dt.Clone()}
|
||||
} else {
|
||||
httpClient = &http.Client{}
|
||||
}
|
||||
httpClient := mcpHTTPClient()
|
||||
|
||||
switch cfg.Transport {
|
||||
case "sse":
|
||||
return transport.NewSSE(
|
||||
cfg.Url,
|
||||
transport.WithHeaders(headers),
|
||||
transport.WithHTTPClient(httpClient),
|
||||
)
|
||||
var opts []transport.ClientOption
|
||||
opts = append(opts, transport.WithHeaders(headers))
|
||||
if httpClient != nil {
|
||||
opts = append(opts, transport.WithHTTPClient(httpClient))
|
||||
}
|
||||
return transport.NewSSE(cfg.Url, opts...)
|
||||
case "", "streamable_http":
|
||||
// Default to streamable HTTP, the newer transport.
|
||||
return transport.NewStreamableHTTP(
|
||||
cfg.Url,
|
||||
transport.WithHTTPHeaders(headers),
|
||||
transport.WithHTTPBasicClient(httpClient),
|
||||
)
|
||||
var opts []transport.StreamableHTTPCOption
|
||||
opts = append(opts, transport.WithHTTPHeaders(headers))
|
||||
if httpClient != nil {
|
||||
opts = append(opts, transport.WithHTTPBasicClient(httpClient))
|
||||
}
|
||||
return transport.NewStreamableHTTP(cfg.Url, opts...)
|
||||
default:
|
||||
return nil, xerrors.Errorf(
|
||||
"unsupported transport %q", cfg.Transport,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package mcpclient
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mcpHTTPClient returns an isolated *http.Client when running
|
||||
// inside tests, or nil for production. During tests,
|
||||
// httptest.Server.Close() calls
|
||||
// http.DefaultTransport.CloseIdleConnections(), which disrupts
|
||||
// any MCP client sharing that transport. When DefaultTransport
|
||||
// is a *http.Transport it is cloned; otherwise a minimal
|
||||
// transport with ProxyFromEnvironment is created as a fallback.
|
||||
func mcpHTTPClient() *http.Client {
|
||||
if !testing.Testing() {
|
||||
return nil
|
||||
}
|
||||
if dt, ok := http.DefaultTransport.(*http.Transport); ok {
|
||||
return &http.Client{Transport: dt.Clone()}
|
||||
}
|
||||
return &http.Client{Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}}
|
||||
}
|
||||
+10
-2
@@ -188,8 +188,9 @@ type AIProviderKey struct {
|
||||
|
||||
// CreateAIProviderRequest is the payload for creating a new AI
|
||||
// provider. Name and Type are required. APIKeys carries the plaintext
|
||||
// keys for OpenAI/Anthropic providers; Bedrock providers authenticate
|
||||
// via Settings and must omit APIKeys.
|
||||
// keys for OpenAI/Anthropic providers; Bedrock and Copilot providers
|
||||
// must omit APIKeys (Bedrock authenticates via Settings, Copilot via
|
||||
// request-time GitHub OAuth tokens).
|
||||
type CreateAIProviderRequest struct {
|
||||
Type AIProviderType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
@@ -209,6 +210,7 @@ func (req CreateAIProviderRequest) Validate() []ValidationError {
|
||||
AIProviderTypeAnthropic,
|
||||
AIProviderTypeAzure,
|
||||
AIProviderTypeBedrock,
|
||||
AIProviderTypeCopilot,
|
||||
AIProviderTypeGoogle,
|
||||
AIProviderTypeOpenAICompat,
|
||||
AIProviderTypeOpenrouter,
|
||||
@@ -244,6 +246,12 @@ func (req CreateAIProviderRequest) Validate() []ValidationError {
|
||||
Detail: "type=bedrock does not accept api_keys",
|
||||
})
|
||||
}
|
||||
if req.Type == AIProviderTypeCopilot && len(req.APIKeys) > 0 {
|
||||
validations = append(validations, ValidationError{
|
||||
Field: "api_keys",
|
||||
Detail: "type=copilot does not accept api_keys",
|
||||
})
|
||||
}
|
||||
return validations
|
||||
}
|
||||
|
||||
|
||||
+25
-26
@@ -1700,6 +1700,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
|
||||
}
|
||||
|
||||
// AI Gateway options
|
||||
aiGatewayProviderSeedingDeprecated := "Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this option seeds provider configuration at startup only exactly once. It will not be used in service runtime. "
|
||||
aiGatewayEnabled := serpent.Option{
|
||||
Name: "AI Gateway Enabled",
|
||||
Description: "Whether to start an in-memory AI Gateway instance.",
|
||||
@@ -1712,7 +1713,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
|
||||
}
|
||||
aiGatewayOpenAIBaseURL := serpent.Option{
|
||||
Name: "AI Gateway OpenAI Base URL",
|
||||
Description: "The base URL of the OpenAI API.",
|
||||
Description: aiGatewayProviderSeedingDeprecated + "The base URL of the OpenAI API.",
|
||||
Flag: "ai-gateway-openai-base-url",
|
||||
Env: "CODER_AI_GATEWAY_OPENAI_BASE_URL",
|
||||
Value: &c.AI.BridgeConfig.LegacyOpenAI.BaseURL,
|
||||
@@ -1722,7 +1723,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
|
||||
}
|
||||
aiGatewayOpenAIKey := serpent.Option{
|
||||
Name: "AI Gateway OpenAI Key",
|
||||
Description: "The key to authenticate against the OpenAI API.",
|
||||
Description: aiGatewayProviderSeedingDeprecated + "The key to authenticate against the OpenAI API.",
|
||||
Flag: "ai-gateway-openai-key",
|
||||
Env: "CODER_AI_GATEWAY_OPENAI_KEY",
|
||||
Value: &c.AI.BridgeConfig.LegacyOpenAI.Key,
|
||||
@@ -1732,7 +1733,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
|
||||
}
|
||||
aiGatewayAnthropicBaseURL := serpent.Option{
|
||||
Name: "AI Gateway Anthropic Base URL",
|
||||
Description: "The base URL of the Anthropic API.",
|
||||
Description: aiGatewayProviderSeedingDeprecated + "The base URL of the Anthropic API.",
|
||||
Flag: "ai-gateway-anthropic-base-url",
|
||||
Env: "CODER_AI_GATEWAY_ANTHROPIC_BASE_URL",
|
||||
Value: &c.AI.BridgeConfig.LegacyAnthropic.BaseURL,
|
||||
@@ -1742,7 +1743,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
|
||||
}
|
||||
aiGatewayAnthropicKey := serpent.Option{
|
||||
Name: "AI Gateway Anthropic Key",
|
||||
Description: "The key to authenticate against the Anthropic API.",
|
||||
Description: aiGatewayProviderSeedingDeprecated + "The key to authenticate against the Anthropic API.",
|
||||
Flag: "ai-gateway-anthropic-key",
|
||||
Env: "CODER_AI_GATEWAY_ANTHROPIC_KEY",
|
||||
Value: &c.AI.BridgeConfig.LegacyAnthropic.Key,
|
||||
@@ -1751,30 +1752,28 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
|
||||
Annotations: serpent.Annotations{}.Mark(annotationSecretKey, "true"),
|
||||
}
|
||||
aiGatewayBedrockBaseURL := serpent.Option{
|
||||
Name: "AI Gateway Bedrock Base URL",
|
||||
Description: "The base URL to use for the AWS Bedrock API. Use this setting to specify an exact URL to use. Takes precedence " +
|
||||
"over CODER_AI_GATEWAY_BEDROCK_REGION.",
|
||||
Flag: "ai-gateway-bedrock-base-url",
|
||||
Env: "CODER_AI_GATEWAY_BEDROCK_BASE_URL",
|
||||
Value: &c.AI.BridgeConfig.LegacyBedrock.BaseURL,
|
||||
Default: "",
|
||||
Group: &deploymentGroupAIGateway,
|
||||
YAML: "bedrock_base_url",
|
||||
Name: "AI Gateway Bedrock Base URL",
|
||||
Description: aiGatewayProviderSeedingDeprecated + "The base URL to use for the AWS Bedrock API. Use this setting to specify an exact URL to use. Takes precedence over CODER_AI_GATEWAY_BEDROCK_REGION.",
|
||||
Flag: "ai-gateway-bedrock-base-url",
|
||||
Env: "CODER_AI_GATEWAY_BEDROCK_BASE_URL",
|
||||
Value: &c.AI.BridgeConfig.LegacyBedrock.BaseURL,
|
||||
Default: "",
|
||||
Group: &deploymentGroupAIGateway,
|
||||
YAML: "bedrock_base_url",
|
||||
}
|
||||
aiGatewayBedrockRegion := serpent.Option{
|
||||
Name: "AI Gateway Bedrock Region",
|
||||
Description: "The AWS Bedrock API region to use. Constructs a base URL to use for the AWS Bedrock API in the form of " +
|
||||
"'https://bedrock-runtime.<region>.amazonaws.com'.",
|
||||
Flag: "ai-gateway-bedrock-region",
|
||||
Env: "CODER_AI_GATEWAY_BEDROCK_REGION",
|
||||
Value: &c.AI.BridgeConfig.LegacyBedrock.Region,
|
||||
Default: "",
|
||||
Group: &deploymentGroupAIGateway,
|
||||
YAML: "bedrock_region",
|
||||
Name: "AI Gateway Bedrock Region",
|
||||
Description: aiGatewayProviderSeedingDeprecated + "The AWS Bedrock API region to use. Constructs a base URL to use for the AWS Bedrock API in the form of 'https://bedrock-runtime.<region>.amazonaws.com'.",
|
||||
Flag: "ai-gateway-bedrock-region",
|
||||
Env: "CODER_AI_GATEWAY_BEDROCK_REGION",
|
||||
Value: &c.AI.BridgeConfig.LegacyBedrock.Region,
|
||||
Default: "",
|
||||
Group: &deploymentGroupAIGateway,
|
||||
YAML: "bedrock_region",
|
||||
}
|
||||
aiGatewayBedrockAccessKey := serpent.Option{
|
||||
Name: "AI Gateway Bedrock Access Key",
|
||||
Description: "The access key to authenticate against the AWS Bedrock API.",
|
||||
Description: aiGatewayProviderSeedingDeprecated + "The access key to authenticate against the AWS Bedrock API.",
|
||||
Flag: "ai-gateway-bedrock-access-key",
|
||||
Env: "CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY",
|
||||
Value: &c.AI.BridgeConfig.LegacyBedrock.AccessKey,
|
||||
@@ -1784,7 +1783,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
|
||||
}
|
||||
aiGatewayBedrockAccessKeySecret := serpent.Option{
|
||||
Name: "AI Gateway Bedrock Access Key Secret",
|
||||
Description: "The access key secret to use with the access key to authenticate against the AWS Bedrock API.",
|
||||
Description: aiGatewayProviderSeedingDeprecated + "The access key secret to use with the access key to authenticate against the AWS Bedrock API.",
|
||||
Flag: "ai-gateway-bedrock-access-key-secret",
|
||||
Env: "CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY_SECRET",
|
||||
Value: &c.AI.BridgeConfig.LegacyBedrock.AccessKeySecret,
|
||||
@@ -1794,7 +1793,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
|
||||
}
|
||||
aiGatewayBedrockModel := serpent.Option{
|
||||
Name: "AI Gateway Bedrock Model",
|
||||
Description: "The model to use when making requests to the AWS Bedrock API.",
|
||||
Description: aiGatewayProviderSeedingDeprecated + "The model to use when making requests to the AWS Bedrock API.",
|
||||
Flag: "ai-gateway-bedrock-model",
|
||||
Env: "CODER_AI_GATEWAY_BEDROCK_MODEL",
|
||||
Value: &c.AI.BridgeConfig.LegacyBedrock.Model,
|
||||
@@ -1804,7 +1803,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
|
||||
}
|
||||
aiGatewayBedrockSmallFastModel := serpent.Option{
|
||||
Name: "AI Gateway 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.",
|
||||
Description: aiGatewayProviderSeedingDeprecated + "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: "ai-gateway-bedrock-small-fastmodel",
|
||||
Env: "CODER_AI_GATEWAY_BEDROCK_SMALL_FAST_MODEL",
|
||||
Value: &c.AI.BridgeConfig.LegacyBedrock.SmallFastModel,
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -1048,9 +1048,6 @@ func TestTools(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("WorkspaceSSHExec", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("WorkspaceSSHExec is not supported on Windows")
|
||||
}
|
||||
// Setup workspace exactly like main SSH tests
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
||||
|
||||
@@ -1076,7 +1073,7 @@ func TestTools(t *testing.T) {
|
||||
// Test output trimming
|
||||
result, err = testTool(t, toolsdk.WorkspaceBash, tb, toolsdk.WorkspaceBashArgs{
|
||||
Workspace: workspace.Name,
|
||||
Command: "echo -e '\\n test with whitespace \\n'",
|
||||
Command: "echo ' test with whitespace '",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, result.ExitCode)
|
||||
@@ -2576,29 +2573,31 @@ func TestMain(m *testing.M) {
|
||||
var untested []string
|
||||
for _, tool := range toolsdk.All {
|
||||
if tested, ok := testedTools.Load(tool.Name); !ok || !tested.(bool) {
|
||||
// Test is skipped on Windows
|
||||
if runtime.GOOS == "windows" && tool.Name == "coder_workspace_bash" {
|
||||
continue
|
||||
}
|
||||
untested = append(untested, tool.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(untested) > 0 && code == 0 {
|
||||
code = 1
|
||||
println("The following tools were not tested:")
|
||||
_, _ = fmt.Fprintln(os.Stderr, "The following tools were not tested:")
|
||||
for _, tool := range untested {
|
||||
println(" - " + tool)
|
||||
_, _ = fmt.Fprintf(os.Stderr, " - %s\n", tool)
|
||||
}
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Please ensure that all tools are tested using testTool().")
|
||||
_, _ = fmt.Fprintln(os.Stderr, "If you just added a new tool, please add a test for it.")
|
||||
// Only fail when the full suite ran. When -run filters to a
|
||||
// subset (e.g. CI flake checks use -run ^TestTools), tools
|
||||
// covered by other top-level functions appear untested.
|
||||
if f := flag.Lookup("test.run"); f == nil || f.Value.String() == "" {
|
||||
code = 1
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "NOTE: if you just ran an individual test, this is expected.")
|
||||
}
|
||||
println("Please ensure that all tools are tested using testTool().")
|
||||
println("If you just added a new tool, please add a test for it.")
|
||||
println("NOTE: if you just ran an individual test, this is expected.")
|
||||
}
|
||||
|
||||
// Check for goroutine leaks. Below is adapted from goleak.VerifyTestMain:
|
||||
if code == 0 {
|
||||
if err := goleak.Find(testutil.GoleakOptions...); err != nil {
|
||||
println("goleak: Errors on successful test run: ", err.Error())
|
||||
_, _ = fmt.Fprintln(os.Stderr, "goleak: Errors on successful test run:", err.Error())
|
||||
code = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,11 @@ Learn more [how Nix works](https://nixos.org/guides/how-nix-works).
|
||||
|
||||
If you're not using the Nix environment, you can launch a local [DevContainer](https://github.com/coder/coder/tree/main/.devcontainer) to get a fully configured development environment.
|
||||
|
||||
DevContainers are supported in tools like **VS Code** and **GitHub Codespaces**, and come preloaded with all required dependencies: Docker, Go, Node.js with `pnpm`, and `make`.
|
||||
DevContainers are supported in tools like **VS Code** and **GitHub Codespaces**, and come preloaded with all required dependencies: Docker, Go, Node.js with `pnpm`, `mise`, and `make`.
|
||||
|
||||
For manual setup outside Nix and DevContainers, install Docker, `mise`, and
|
||||
`make`. Run `mise install` from the repository root to install Go, Node.js
|
||||
with `pnpm`, and development tools at the versions pinned in `mise.toml`.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -169,9 +169,9 @@ There are two types of fixtures that are used to test that migrations don't
|
||||
break existing Coder deployments:
|
||||
|
||||
* Partial fixtures
|
||||
[`migrations/testdata/fixtures`](../../../coderd/database/migrations/testdata/fixtures)
|
||||
[`migrations/testdata/fixtures`](https://github.com/coder/coder/tree/main/coderd/database/migrations/testdata/fixtures)
|
||||
* Full database dumps
|
||||
[`migrations/testdata/full_dumps`](../../../coderd/database/migrations/testdata/full_dumps)
|
||||
[`migrations/testdata/full_dumps`](https://github.com/coder/coder/tree/main/coderd/database/migrations/testdata/full_dumps)
|
||||
|
||||
Both types behave like database migrations (they also
|
||||
[`migrate`](https://github.com/golang-migrate/migrate)). Their behavior mirrors
|
||||
@@ -194,7 +194,7 @@ To add a new partial fixture, run the following command:
|
||||
```
|
||||
|
||||
Then add some queries to insert data and commit the file to the repo. See
|
||||
[`000024_example.up.sql`](../../../coderd/database/migrations/testdata/fixtures/000024_example.up.sql)
|
||||
[`000024_example.up.sql`](https://github.com/coder/coder/blob/main/coderd/database/migrations/testdata/fixtures/000024_example.up.sql)
|
||||
for an example.
|
||||
|
||||
To create a full dump, run a fully fledged Coder deployment and use it to
|
||||
|
||||
@@ -142,7 +142,7 @@ as OpenAI and Anthropic. Users authenticate through Coder instead of managing se
|
||||
provider API keys. All prompts, token usage, and tool invocations are recorded
|
||||
for compliance and cost tracking.
|
||||
|
||||
Learn more: [AI Gateway](../../ai-coder/ai-gateway)
|
||||
Learn more: [AI Gateway](../../ai-coder/ai-gateway/index.md)
|
||||
|
||||
### Agent Firewall
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ In your Terraform module, enable Agent Firewall with minimal configuration:
|
||||
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "dev.registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.7.0"
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "5.2.0"
|
||||
enable_boundary = true
|
||||
}
|
||||
```
|
||||
@@ -59,7 +59,7 @@ Claude Code module, use the following minimal configuration:
|
||||
|
||||
```yaml
|
||||
allowlist:
|
||||
- "domain=dev.coder.com" # Required - use your Coder deployment domain
|
||||
- "domain=coder.example.com" # Required - use your Coder deployment domain
|
||||
- "domain=api.anthropic.com" # Required - API endpoint for Claude
|
||||
- "domain=statsig.anthropic.com" # Required - Feature flags and analytics
|
||||
- "domain=claude.ai" # Recommended - WebFetch/WebSearch features
|
||||
@@ -225,5 +225,5 @@ such as Grafana Loki.
|
||||
Example of an allowed request (assuming stderr):
|
||||
|
||||
```console
|
||||
2026-01-16 00:11:40.564 [info] coderd.agentrpc: boundary_request owner=joe workspace_name=some-task-c88d agent_name=dev decision=allow workspace_id=f2bd4e9f-7e27-49fc-961e-be4d1c2aa987 http_method=GET http_url=https://dev.coder.com event_time=2026-01-16T00:11:39.388607657Z matched_rule=domain=dev.coder.com request_id=9f30d667-1fc9-47ba-b9e5-8eac46e0abef trace=478b2b45577307c4fd1bcfc64fad6ffb span=9ece4bc70c311edb
|
||||
2026-01-16 00:11:40.564 [info] coderd.agentrpc: boundary_request owner=joe workspace_name=some-task-c88d agent_name=dev decision=allow workspace_id=f2bd4e9f-7e27-49fc-961e-be4d1c2aa987 http_method=GET http_url=https://coder.example.com event_time=2026-01-16T00:11:39.388607657Z matched_rule=domain=coder.example.com request_id=9f30d667-1fc9-47ba-b9e5-8eac46e0abef trace=478b2b45577307c4fd1bcfc64fad6ffb span=9ece4bc70c311edb
|
||||
```
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Chat Sharing
|
||||
|
||||
Chat sharing lets you give other users or groups read-only access to a Coder Agents conversation.
|
||||
|
||||
## Share a chat
|
||||
|
||||
1. Open the chat you want to share on the **Agents** page. Only top-level chats can be shared; sub-agent chats inherit sharing from their parent.
|
||||
1. Click the share icon in the chat top bar.
|
||||
1. Click the **Search for user or group** field.
|
||||
1. Search for and select a user or group.
|
||||
1. Click **Add member** to grant **Read** access.
|
||||
1. Copy the chat URL from your browser and send it to the recipients.
|
||||
|
||||
Coder does not create a separate share link or notify recipients. They must open the chat from the URL you send them.
|
||||
|
||||
## Shared chat access
|
||||
|
||||
Viewers can open the chat from a direct link, view messages, stream live updates, and download chat attachments. They reach sub-agent chats by following sub-agent links inside the parent chat or by opening a direct URL.
|
||||
|
||||
Shared chats do not appear in the viewer's normal chat list. Viewers have read-only access: they cannot send or edit messages, regenerate the chat title, archive the chat, or change its sharing settings.
|
||||
|
||||
## Disable chat sharing
|
||||
|
||||
Administrators can disable chat sharing for a deployment with `--disable-chat-sharing`, `CODER_DISABLE_CHAT_SHARING`, or `disableChatSharing`. When disabled, only chat owners can access their chats.
|
||||
@@ -92,6 +92,10 @@ When no user credential is present, AI Gateway falls back to the admin-configure
|
||||
This approach offers centralized keys as a default,
|
||||
while allowing individual users to bring their own key.
|
||||
|
||||
> [!NOTE]
|
||||
> When a BYOK credential is present, [key failover](./providers.md#key-failover)
|
||||
> is skipped.
|
||||
|
||||
Visit individual [client pages](./clients/index.md) for configuration details.
|
||||
|
||||
### Enable or disable BYOK
|
||||
|
||||
@@ -15,6 +15,46 @@ We provide an example Grafana dashboard that you can import as a starting point
|
||||
|
||||
These logs and metrics can be used to determine usage patterns, track costs, and evaluate tooling adoption.
|
||||
|
||||
## Provider metrics
|
||||
|
||||
`aibridged` (the in-process daemon) and `aibridgeproxyd` (the external
|
||||
proxy) each export Prometheus metrics describing the configured
|
||||
provider pool and its reload loop. See
|
||||
[Provider Configuration](./providers.md) for the lifecycle these
|
||||
metrics describe.
|
||||
|
||||
| Metric | Type | Labels | Purpose |
|
||||
|------------------------------------------------------------------------|---------|--------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `coder_aibridged_provider_info` | gauge | `provider_name`, `provider_type`, `status` | One series per configured provider. Value is always `1`; the `status` label (`enabled`, `disabled`, `error`) carries the alertable signal. |
|
||||
| `coder_aibridged_providers_last_reload_timestamp_seconds` | gauge | | Unix timestamp of the last reload attempt, success or failure. |
|
||||
| `coder_aibridged_providers_last_reload_success_timestamp_seconds` | gauge | | Unix timestamp of the last reload that successfully refreshed the pool. |
|
||||
| `coder_aibridgeproxyd_provider_info` | gauge | `provider_name`, `provider_type`, `status` | Same shape as `aibridged_provider_info` but reported by the external proxy. |
|
||||
| `coder_aibridgeproxyd_providers_last_reload_timestamp_seconds` | gauge | | Last reload attempt timestamp in `aibridgeproxyd`. |
|
||||
| `coder_aibridgeproxyd_providers_last_reload_success_timestamp_seconds` | gauge | | Last successful reload timestamp in `aibridgeproxyd`. |
|
||||
| `coder_aibridgeproxyd_connect_sessions_total` | counter | `type` (`mitm`, `tunneled`) | CONNECT sessions established by the proxy. |
|
||||
| `coder_aibridgeproxyd_mitm_requests_total` | counter | `provider` | MITM requests handled. |
|
||||
| `coder_aibridgeproxyd_inflight_mitm_requests` | gauge | `provider` | In-flight MITM requests. |
|
||||
| `coder_aibridgeproxyd_mitm_responses_total` | counter | `code`, `provider` | MITM responses by HTTP status code. |
|
||||
|
||||
### Suggested alerts
|
||||
|
||||
Alert on any provider entering a non-`enabled` status:
|
||||
|
||||
```promql
|
||||
sum by (provider_name, status) (coder_aibridged_provider_info{status!="enabled"}) > 0
|
||||
```
|
||||
|
||||
Alert when the reload loop is firing but failing to refresh the pool
|
||||
for longer than a few minutes:
|
||||
|
||||
```promql
|
||||
(coder_aibridged_providers_last_reload_timestamp_seconds
|
||||
- coder_aibridged_providers_last_reload_success_timestamp_seconds) > 300
|
||||
```
|
||||
|
||||
Repeat the same query against `coder_aibridgeproxyd_*` if you run the
|
||||
external proxy.
|
||||
|
||||
## Structured Logging
|
||||
|
||||
AI Bridge can emit structured logs for every interception event to your
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
# Provider Configuration
|
||||
|
||||
> [!NOTE]
|
||||
> AI Gateway requires the [AI Governance Add-On](../ai-governance.md).
|
||||
|
||||
Providers are deployment-scoped and managed from the dashboard or the
|
||||
[AI Providers API](../../reference/api/aiproviders.md). See
|
||||
[Setup](./setup.md#configure-providers) for the steps to add, edit, and
|
||||
disable a provider.
|
||||
|
||||
This page covers the provider types AI Gateway supports, the setup
|
||||
considerations for each, how a provider's lifecycle affects request
|
||||
handling, and how to monitor providers.
|
||||
|
||||
## Database management of providers
|
||||
|
||||
> [!NOTE]
|
||||
> Since v2.34, provider environment variables and flags, including
|
||||
> `CODER_AI_GATEWAY_PROVIDER_<N>_*`, `CODER_AI_GATEWAY_OPENAI_*`,
|
||||
> `CODER_AI_GATEWAY_ANTHROPIC_*`, and their `--aibridge/ai-gateway-*`
|
||||
> equivalents, are deprecated. Provider configuration is now stored in
|
||||
> the database, and any environment variables set on startup are used to
|
||||
> seed it.
|
||||
>
|
||||
> This is a once-off operation. The environment variables have no effect
|
||||
> once seeding has completed.
|
||||
>
|
||||
> **Any changes to the provider environment variables after seeding will
|
||||
> cause the server to fail to start, to prevent operators from updating a
|
||||
> configuration that is ineffectual.**
|
||||
>
|
||||
> The environment variables can be safely removed once seeding has
|
||||
> completed. Visit `https://<your-coder-host>/ai/settings` to see which
|
||||
> providers have been seeded.
|
||||
|
||||
After seeding, manage providers through the dashboard or API. A provider
|
||||
that has been edited or removed there is not recreated or overwritten
|
||||
from the environment on the next restart.
|
||||
|
||||
## Provider types
|
||||
|
||||
AI Gateway speaks two upstream API formats: the **OpenAI** format
|
||||
(Chat Completions and Responses) and the **Anthropic** format
|
||||
(Messages). Every provider type maps to one of these.
|
||||
|
||||
| Type | API format | Setup notes |
|
||||
|-----------------|------------|-------------------------------------------------------------------|
|
||||
| `openai` | OpenAI | Native OpenAI, or any OpenAI-compatible endpoint via the base URL |
|
||||
| `anthropic` | Anthropic | Native Anthropic, or an Anthropic-compatible broker |
|
||||
| `bedrock` | Anthropic | Anthropic models hosted on AWS Bedrock; authenticates via AWS |
|
||||
| `copilot` | OpenAI | GitHub Copilot; authenticates via each user's GitHub OAuth token |
|
||||
| `azure` | OpenAI | OpenAI-compatible endpoint only |
|
||||
| `google` | OpenAI | OpenAI-compatible endpoint only |
|
||||
| `openrouter` | OpenAI | OpenAI-compatible endpoint only |
|
||||
| `vercel` | OpenAI | OpenAI-compatible endpoint only |
|
||||
| `openai-compat` | OpenAI | Generic OpenAI-compatible endpoint |
|
||||
|
||||
`azure`, `google`, `openrouter`, `vercel`, and `openai-compat` are
|
||||
supported only as OpenAI-compatible endpoints: AI Gateway sends them
|
||||
OpenAI-format requests, so each must expose an OpenAI-compatible API at
|
||||
its base URL. They have no provider-specific integration beyond that.
|
||||
|
||||
### OpenAI
|
||||
|
||||
Set the base URL to the upstream endpoint and provide an API key. The
|
||||
default `https://api.openai.com/v1/` targets the native OpenAI service;
|
||||
point it at any OpenAI-compatible endpoint (for example, a hosted proxy
|
||||
or LiteLLM deployment) when needed.
|
||||
|
||||
If you create an [OpenAI key](https://platform.openai.com/api-keys)
|
||||
with minimal privileges, this is the minimum required set:
|
||||
|
||||

|
||||
|
||||
### Anthropic
|
||||
|
||||
Set the base URL and provide an API key. The default
|
||||
`https://api.anthropic.com/` targets Anthropic's public API; override it
|
||||
for Anthropic-compatible brokers.
|
||||
|
||||
Anthropic does not allow [API keys](https://console.anthropic.com/settings/keys)
|
||||
to have restricted permissions at the time of writing (June 2026).
|
||||
|
||||
### Amazon Bedrock
|
||||
|
||||
Bedrock providers serve Anthropic models hosted on AWS and authenticate
|
||||
with AWS credentials rather than a registered API key. Configure:
|
||||
|
||||
- A **region** (or a full base URL when routing through a proxy or a
|
||||
non-standard endpoint that does not follow the
|
||||
`https://bedrock-runtime.<region>.amazonaws.com` format).
|
||||
- The **model** and **small fast model** identifiers.
|
||||
|
||||
Do not attach API keys to a Bedrock provider.
|
||||
|
||||
AI Gateway resolves AWS credentials one of two ways:
|
||||
|
||||
- **AWS SDK default credential chain (recommended).** When no explicit
|
||||
credentials are configured, the AWS SDK resolves them automatically
|
||||
from the environment: IAM Roles (instance profiles, IRSA, ECS task
|
||||
roles), shared config files, environment variables, SSO, and more.
|
||||
Attaching an IAM Role to the compute running Coder follows
|
||||
[AWS best practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)
|
||||
for temporary credentials. The role must permit `bedrock:InvokeModel`
|
||||
and `bedrock:InvokeModelWithResponseStream` for the configured models.
|
||||
- **Static credentials.** Provide an access key and secret for an IAM
|
||||
user with the same Bedrock permissions.
|
||||
|
||||
### GitHub Copilot
|
||||
|
||||
GitHub Copilot offers three plans: Individual, Business, and Enterprise,
|
||||
each with its own API endpoint. Add one `copilot` provider per plan your
|
||||
organization uses, setting the base URL accordingly:
|
||||
|
||||
| Plan | Base URL |
|
||||
|------------|--------------------------------------------|
|
||||
| Individual | `https://api.individual.githubcopilot.com` |
|
||||
| Business | `https://api.business.githubcopilot.com` |
|
||||
| Enterprise | `https://api.enterprise.githubcopilot.com` |
|
||||
|
||||
Copilot providers authenticate with each user's request-time GitHub
|
||||
OAuth token, so do not attach API keys. For client-side setup (proxy,
|
||||
certificates, IDE configuration), see
|
||||
[GitHub Copilot client configuration](./clients/copilot.md).
|
||||
|
||||
### OpenAI-compatible providers
|
||||
|
||||
Azure-hosted OpenAI, Google, OpenRouter, Vercel, and any other
|
||||
OpenAI-compatible service are configured with the matching type (or the
|
||||
generic `openai-compat`), the provider's OpenAI-compatible base URL, and
|
||||
an API key.
|
||||
|
||||
> [!NOTE]
|
||||
> See the [Supported APIs](./reference.md#supported-apis) section for
|
||||
> precise endpoint coverage and interception behavior.
|
||||
|
||||
## Provider lifecycle
|
||||
|
||||
Every provider carries an explicit status, surfaced through the
|
||||
[`provider_info`](./monitoring.md#provider-metrics) metric and the API:
|
||||
|
||||
| Status | Meaning | Effect on requests |
|
||||
|------------|-------------------------------------------------------------------------------|--------------------------------------------------|
|
||||
| `enabled` | Configuration is valid and the provider is serving traffic | Requests are proxied to the upstream |
|
||||
| `disabled` | The provider exists but has been turned off | Requests are rejected with a non-retryable error |
|
||||
| `error` | The provider is enabled but cannot be built (missing credentials, bad config) | Requests fail; the error is surfaced in metrics |
|
||||
|
||||
Disabling a provider does not delete it, its credentials, or its
|
||||
historical interception data. Re-enabling restores it to service.
|
||||
|
||||
## Monitoring and reloads
|
||||
|
||||
Provider configuration changes take effect automatically, without
|
||||
restarting `coderd`. AI Gateway records the timestamp of each reload
|
||||
attempt and each successful reload, exposed as Prometheus metrics:
|
||||
|
||||
- `coder_aibridged_providers_last_reload_timestamp_seconds`
|
||||
- `coder_aibridged_providers_last_reload_success_timestamp_seconds`
|
||||
|
||||
If you run the [external proxy](./ai-gateway-proxy/index.md), it exposes
|
||||
the same pair under the `coder_aibridgeproxyd_` prefix.
|
||||
|
||||
A growing gap between the attempt and success timestamps means reloads
|
||||
are firing but failing to apply. Alert on that gap rather than on a
|
||||
single failure, which may resolve on the next change. See
|
||||
[Monitoring](./monitoring.md#provider-metrics) for the full metric list
|
||||
and sample alert queries.
|
||||
|
||||
## Key failover
|
||||
|
||||
You can configure multiple centralized API keys for a single provider instance
|
||||
so that AI Gateway automatically retries with the next key when one fails. This
|
||||
is transparent to end users, and clients see no difference in behavior or need
|
||||
any configuration changes.
|
||||
|
||||
Key failover is supported for **OpenAI** and **Anthropic** providers. Amazon
|
||||
Bedrock and GitHub Copilot do not support key failover.
|
||||
|
||||
Multiple keys can be added per provider through the
|
||||
[AI Providers API](../../reference/api/aiproviders.md). Each provider supports
|
||||
a maximum of **5 keys**.
|
||||
|
||||
### Failover behavior
|
||||
|
||||
Every request starts with the first key in the list. If a key is rate-limited
|
||||
or returns an authentication error, AI Gateway automatically retries the request
|
||||
with the next available key.
|
||||
|
||||
> [!WARNING]
|
||||
> A key that fails with an authentication error (`401 Unauthorized` or
|
||||
> `403 Forbidden`) is permanently disabled and will not be used again until the
|
||||
> server is restarted or the provider configuration is reloaded.
|
||||
|
||||
If all keys in the pool are exhausted, AI Gateway returns:
|
||||
|
||||
- `429 Too Many Requests` when at least one key is rate-limited, with a `Retry-After` header set to the shortest cooldown across all keys.
|
||||
- `502 Bad Gateway` when every key has failed permanently.
|
||||
|
||||
## Bring Your Own Key
|
||||
|
||||
A provider's configured credentials are the centralized default. When
|
||||
Bring Your Own Key (BYOK) is enabled, a user's own credential takes
|
||||
precedence over the provider's for that user's requests, and AI Gateway
|
||||
falls back to the provider credentials when the user has none. See
|
||||
[Authentication](./auth.md#bring-your-own-key-byok) for the BYOK flow
|
||||
and how to enable or disable it.
|
||||
|
||||
## Failure modes
|
||||
|
||||
| Symptom | Likely cause | Corrective action |
|
||||
|------------------------------------------------|------------------------------------------------------------|------------------------------------------|
|
||||
| Startup fails referencing an existing provider | Env config drifted from a provider already in the database | Remove the provider env vars and restart |
|
||||
| Provider returns errors with no upstream call | The provider is `disabled` or in `error` status | Consult the server logs for details |
|
||||
| Configuration changes not taking effect | Reloads are firing but failing to apply | Consult the server logs for details |
|
||||
@@ -2,20 +2,17 @@
|
||||
|
||||
AI Gateway runs inside the Coder control plane (`coderd`), requiring no separate compute to deploy or scale. Once enabled, `coderd` runs the `aibridged` in-memory and brokers traffic to your configured AI providers on behalf of authenticated users.
|
||||
|
||||
**Required**:
|
||||
|
||||
1. The [AI Governance Add-On](../ai-governance.md) license.
|
||||
1. Feature must be [enabled](#activation) using the server flag
|
||||
1. One or more [providers](#configure-providers) API key(s) must be configured
|
||||
|
||||
> [!NOTE]
|
||||
> AI Gateway environment variables and CLI flags have migrated to the new
|
||||
> `CODER_AI_GATEWAY_*` and `--ai-gateway-*` naming scheme. The earlier
|
||||
> `CODER_AIBRIDGE_*` and `--aibridge-*` names continue to work as aliases.
|
||||
> Since v2.34, provider environment variables and flags are deprecated.
|
||||
> Provider configuration is now stored in the database, and any
|
||||
> environment variables set on startup are used to seed it once. See
|
||||
> [Database management of providers](./providers.md#database-management-of-providers)
|
||||
> for details.
|
||||
|
||||
## Activation
|
||||
|
||||
You will need to enable AI Gateway explicitly:
|
||||
AI Gateway must be enabled in deployment config before users can authenticate
|
||||
to it.
|
||||
|
||||
```sh
|
||||
export CODER_AI_GATEWAY_ENABLED=true
|
||||
@@ -24,224 +21,41 @@ coder server
|
||||
coder server --ai-gateway-enabled=true
|
||||
```
|
||||
|
||||
_AI Gateway is enabled by default as of v2.34._
|
||||
|
||||
## Configure Providers
|
||||
|
||||
AI Gateway proxies requests to upstream LLM APIs. Configure at least one provider before exposing AI Gateway to end users.
|
||||
Configure at least one provider before exposing AI Gateway to end users.
|
||||
|
||||
<div class="tabs">
|
||||
Providers are deployment-scoped. Add them from the dashboard or the
|
||||
[AI Providers API](../../reference/api/aiproviders.md). Changes take effect
|
||||
without restarting `coderd`.
|
||||
|
||||
### OpenAI
|
||||
### Dashboard
|
||||
|
||||
Set the following when routing [OpenAI-compatible](https://coder.com/docs/reference/cli/server#--ai-gateway-openai-key) traffic through AI Gateway:
|
||||
1. Navigate to **Admin settings** > **AI**
|
||||
1. Select **Providers**
|
||||
1. Click **Add provider**
|
||||
1. Select the provider type
|
||||
1. Enter a unique lowercase name, the upstream endpoint, and the credentials
|
||||
1. Save the provider
|
||||
|
||||
- `CODER_AI_GATEWAY_OPENAI_KEY` or `--ai-gateway-openai-key`
|
||||
- `CODER_AI_GATEWAY_OPENAI_BASE_URL` or `--ai-gateway-openai-base-url`
|
||||
|
||||
The default base URL (`https://api.openai.com/v1/`) works for the native OpenAI service. Point the base URL at your preferred OpenAI-compatible endpoint (for example, a hosted proxy or LiteLLM deployment) when needed.
|
||||
|
||||
If you'd like to create an [OpenAI key](https://platform.openai.com/api-keys) with minimal privileges, this is the minimum required set:
|
||||
|
||||

|
||||
|
||||
### Anthropic
|
||||
|
||||
Set the following when routing [Anthropic-compatible](https://coder.com/docs/reference/cli/server#--ai-gateway-anthropic-key) traffic through AI Gateway:
|
||||
|
||||
- `CODER_AI_GATEWAY_ANTHROPIC_KEY` or `--ai-gateway-anthropic-key`
|
||||
- `CODER_AI_GATEWAY_ANTHROPIC_BASE_URL` or `--ai-gateway-anthropic-base-url`
|
||||
|
||||
The default base URL (`https://api.anthropic.com/`) targets Anthropic's public API. Override it for Anthropic-compatible brokers.
|
||||
|
||||
Anthropic does not allow [API keys](https://console.anthropic.com/settings/keys) to have restricted permissions at the time of writing (Nov 2025).
|
||||
|
||||
### Amazon Bedrock
|
||||
|
||||
Set the following when routing [Amazon Bedrock](https://coder.com/docs/reference/cli/server#--ai-gateway-bedrock-region) traffic through AI Gateway:
|
||||
|
||||
**Required:**
|
||||
|
||||
- `CODER_AI_GATEWAY_BEDROCK_REGION` or `--ai-gateway-bedrock-region`.
|
||||
Alternatively, set `CODER_AI_GATEWAY_BEDROCK_BASE_URL` or `--ai-gateway-bedrock-base-url` to a full URL (e.g., when routing through a proxy between AI Gateway and AWS Bedrock or using a non-standard endpoint that doesn't follow the `https://bedrock-runtime.<region>.amazonaws.com` format).
|
||||
If both are set, `CODER_AI_GATEWAY_BEDROCK_BASE_URL` takes precedence.
|
||||
- `CODER_AI_GATEWAY_BEDROCK_MODEL` or `--ai-gateway-bedrock-model`
|
||||
- `CODER_AI_GATEWAY_BEDROCK_SMALL_FAST_MODEL` or `--ai-gateway-bedrock-small-fastmodel`
|
||||
Each provider gets its own AI Gateway route at
|
||||
`/api/v2/aibridge/<provider-name>/`.
|
||||
|
||||
> [!NOTE]
|
||||
> These Bedrock settings configure AI Gateway only. To configure Bedrock as an
|
||||
> Agents provider, see [Configuring AWS Bedrock](../agents/models.md#configuring-aws-bedrock).
|
||||
> Provider names must be unique and use lowercase, hyphen-separated identifiers
|
||||
> such as `anthropic-corp` or `azure-openai`. Once deleted, another provider
|
||||
> may reuse the name.
|
||||
|
||||
**Optional:**
|
||||

|
||||
|
||||
- `CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY` or `--ai-gateway-bedrock-access-key`
|
||||
- `CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY_SECRET` or `--ai-gateway-bedrock-access-key-secret`
|
||||

|
||||
|
||||
#### Authentication
|
||||
Open an existing provider to rotate credentials, update its endpoint, or
|
||||
disable it without restarting `coderd`.
|
||||
|
||||
AI Gateway supports two credential configuration paths:
|
||||
|
||||
##### AWS SDK default credential chain (recommended)
|
||||
|
||||
When no credentials are set in AI Gateway config, the AWS SDK resolves them automatically from the environment.
|
||||
This includes IAM Roles (instance profiles, IRSA, ECS task roles), shared config files, environment variables, SSO, and more.
|
||||
|
||||
**IAM Roles are the recommended approach** when AI Gateway runs on AWS infrastructure.
|
||||
Attach an IAM Role with Bedrock permissions to the compute running AI Gateway (EC2 instance, EKS pod via IRSA, or ECS task), no credentials need to be configured in AI Gateway itself.
|
||||
|
||||
The IAM Role must have permission to invoke the Bedrock models configured for AI Gateway (`bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream`).
|
||||
See [Amazon Bedrock identity-based policy examples](https://docs.aws.amazon.com/bedrock/latest/userguide/security_iam_id-based-policy-examples.html) for policy examples,
|
||||
and [AWS IAM role creation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-service.html) for general guidance on attaching roles to AWS services.
|
||||
|
||||
This aligns with [AWS best practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) for using temporary credentials instead of long-lived access keys.
|
||||
|
||||
##### Static credentials
|
||||
|
||||
For deployments when explicit credentials are preferred, provide an access key and secret for an IAM User:
|
||||
|
||||
1. **Choose a region** where you want to use Bedrock.
|
||||
|
||||
2. **Generate API keys** in the [AWS Bedrock console](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/api-keys/long-term/create) (replace `us-east-1` in the URL with your chosen region):
|
||||
- Choose an expiry period for the key.
|
||||
- Click **Generate**.
|
||||
- This creates an IAM user with strictly-scoped permissions for Bedrock access.
|
||||
|
||||
3. **Create an access key** for the IAM user:
|
||||
- After generating the API key, click **"You can directly modify permissions for the IAM user associated"**.
|
||||
- In the IAM user page, navigate to the **Security credentials** tab.
|
||||
- Under **Access keys**, click **Create access key**.
|
||||
- Select **"Application running outside AWS"** as the use case.
|
||||
- Click **Next**.
|
||||
- Add a description like "Coder AI Gateway token".
|
||||
- Click **Create access key**.
|
||||
- Save both the access key ID and secret access key securely.
|
||||
|
||||
4. **Configure your Coder deployment** with the credentials:
|
||||
|
||||
```sh
|
||||
export CODER_AI_GATEWAY_BEDROCK_REGION=us-east-1
|
||||
export CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY=<your-access-key-id>
|
||||
export CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY_SECRET=<your-secret-access-key>
|
||||
coder server
|
||||
```
|
||||
|
||||
### GitHub Copilot
|
||||
|
||||
GitHub Copilot offers three plans: Individual, Business, and Enterprise,
|
||||
each with its own API endpoint. Configure one or more `copilot` providers
|
||||
using the [indexed provider format](#multiple-instances-of-the-same-provider)
|
||||
depending on which plans your organization uses.
|
||||
Copilot providers use OAuth app installations for authentication rather than
|
||||
static API keys.
|
||||
|
||||
```sh
|
||||
# GitHub Copilot (Individual)
|
||||
export CODER_AI_GATEWAY_PROVIDER_0_TYPE=copilot
|
||||
export CODER_AI_GATEWAY_PROVIDER_0_NAME=copilot
|
||||
|
||||
# GitHub Copilot Business
|
||||
export CODER_AI_GATEWAY_PROVIDER_1_TYPE=copilot
|
||||
export CODER_AI_GATEWAY_PROVIDER_1_NAME=copilot-business
|
||||
export CODER_AI_GATEWAY_PROVIDER_1_BASE_URL=https://api.business.githubcopilot.com
|
||||
|
||||
# GitHub Copilot Enterprise
|
||||
export CODER_AI_GATEWAY_PROVIDER_2_TYPE=copilot
|
||||
export CODER_AI_GATEWAY_PROVIDER_2_NAME=copilot-enterprise
|
||||
export CODER_AI_GATEWAY_PROVIDER_2_BASE_URL=https://api.enterprise.githubcopilot.com
|
||||
```
|
||||
|
||||
The default base URL targets the individual Copilot API
|
||||
(`api.individual.githubcopilot.com`). Override `CODER_AI_GATEWAY_PROVIDER_<N>_BASE_URL`
|
||||
for Business or Enterprise tiers as shown above.
|
||||
|
||||
For client-side setup (proxy, certificates, IDE configuration), see
|
||||
[GitHub Copilot client configuration](./clients/copilot.md).
|
||||
|
||||
### ChatGPT
|
||||
|
||||
Configure a ChatGPT provider by creating an `openai`-typed instance with the
|
||||
ChatGPT Codex base URL:
|
||||
|
||||
```sh
|
||||
export CODER_AI_GATEWAY_PROVIDER_0_TYPE=openai
|
||||
export CODER_AI_GATEWAY_PROVIDER_0_NAME=chatgpt
|
||||
export CODER_AI_GATEWAY_PROVIDER_0_BASE_URL=https://chatgpt.com/backend-api/codex
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
> See the [Supported APIs](./reference.md#supported-apis) section below for precise endpoint coverage and interception behavior.
|
||||
|
||||
### Multiple instances of the same provider
|
||||
|
||||
You can configure multiple instances of the same provider type, for example, to
|
||||
route different teams to separate API keys, use different base URLs per region, or
|
||||
connect to both a direct API and a proxy simultaneously. Use indexed environment
|
||||
variables following the pattern `CODER_AI_GATEWAY_PROVIDER_<N>_<KEY>`:
|
||||
|
||||
```sh
|
||||
# Anthropic routed through a corporate proxy
|
||||
export CODER_AI_GATEWAY_PROVIDER_0_TYPE=anthropic
|
||||
export CODER_AI_GATEWAY_PROVIDER_0_NAME=anthropic-corp
|
||||
export CODER_AI_GATEWAY_PROVIDER_0_KEY=sk-ant-corp-xxx
|
||||
export CODER_AI_GATEWAY_PROVIDER_0_BASE_URL=https://llm-proxy.internal.example.com/anthropic
|
||||
|
||||
# Anthropic direct (for teams that need direct access)
|
||||
export CODER_AI_GATEWAY_PROVIDER_1_TYPE=anthropic
|
||||
export CODER_AI_GATEWAY_PROVIDER_1_NAME=anthropic-direct
|
||||
export CODER_AI_GATEWAY_PROVIDER_1_KEY=sk-ant-direct-yyy
|
||||
|
||||
# Azure-hosted OpenAI deployment
|
||||
export CODER_AI_GATEWAY_PROVIDER_2_TYPE=openai
|
||||
export CODER_AI_GATEWAY_PROVIDER_2_NAME=azure-openai
|
||||
export CODER_AI_GATEWAY_PROVIDER_2_KEY=azure-key-zzz
|
||||
export CODER_AI_GATEWAY_PROVIDER_2_BASE_URL=https://my-deployment.openai.azure.com/
|
||||
|
||||
# Anthropic via AWS Bedrock
|
||||
export CODER_AI_GATEWAY_PROVIDER_3_TYPE=anthropic
|
||||
export CODER_AI_GATEWAY_PROVIDER_3_NAME=anthropic-bedrock
|
||||
export CODER_AI_GATEWAY_PROVIDER_3_BEDROCK_REGION=us-west-2
|
||||
export CODER_AI_GATEWAY_PROVIDER_3_BEDROCK_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
|
||||
export CODER_AI_GATEWAY_PROVIDER_3_BEDROCK_ACCESS_KEY_SECRET=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
||||
|
||||
coder server
|
||||
```
|
||||
|
||||
Each provider instance gets a unique route based on its `NAME`. Clients send
|
||||
requests to `/api/v2/aibridge/<NAME>/` to target a specific instance:
|
||||
|
||||
| Instance name | Route |
|
||||
|---------------------|-----------------------------------------------------|
|
||||
| `anthropic-corp` | `/api/v2/aibridge/anthropic-corp/v1/messages` |
|
||||
| `anthropic-direct` | `/api/v2/aibridge/anthropic-direct/v1/messages` |
|
||||
| `azure-openai` | `/api/v2/aibridge/azure-openai/v1/chat/completions` |
|
||||
| `anthropic-bedrock` | `/api/v2/aibridge/anthropic-bedrock/v1/messages` |
|
||||
|
||||
**Supported keys per provider:**
|
||||
|
||||
| Key | Required | Description |
|
||||
|------------|----------|------------------------------------------------------|
|
||||
| `TYPE` | Yes | Provider type: `openai`, `anthropic`, or `copilot` |
|
||||
| `NAME` | No | Unique instance name for routing. Defaults to `TYPE` |
|
||||
| `KEY` | No | API key for upstream authentication (alias: `KEYS`) |
|
||||
| `BASE_URL` | No | Base URL of the upstream API |
|
||||
|
||||
For `anthropic` providers using AWS Bedrock, the following keys are also
|
||||
available: `BEDROCK_BASE_URL`, `BEDROCK_REGION`,
|
||||
`BEDROCK_ACCESS_KEY` (alias: `BEDROCK_ACCESS_KEYS`),
|
||||
`BEDROCK_ACCESS_KEY_SECRET` (alias: `BEDROCK_ACCESS_KEY_SECRETS`),
|
||||
`BEDROCK_MODEL`, `BEDROCK_SMALL_FAST_MODEL`.
|
||||
|
||||
> [!NOTE]
|
||||
> Indices must be contiguous and start at `0`. Each instance must have a unique
|
||||
> `NAME`. If two instances of the same `TYPE` omit `NAME`, they will both
|
||||
> default to the type name and fail with a duplicate name error.
|
||||
>
|
||||
> The legacy single-provider environment variables (`CODER_AI_GATEWAY_OPENAI_KEY`,
|
||||
> `CODER_AI_GATEWAY_ANTHROPIC_KEY`, etc.) continue to work. However, setting
|
||||
> both a legacy variable and an indexed provider with the same default name
|
||||
> (e.g. `CODER_AI_GATEWAY_OPENAI_KEY` and an indexed provider named `openai`)
|
||||
> will produce a startup error. Remove one or the other to resolve the
|
||||
> conflict.
|
||||

|
||||
|
||||
## API Dumps
|
||||
|
||||
|
||||
@@ -51,12 +51,10 @@ being used across the organization. AI Gateway provides audit trails of prompts,
|
||||
token usage, and tool invocations, giving administrators insight into AI
|
||||
adoption patterns and potential issues.
|
||||
|
||||
### Restricting agent network and command access
|
||||
### Restricting agent network access
|
||||
|
||||
AI agents can make arbitrary network requests, potentially accessing
|
||||
unauthorized services or exfiltrating data. They can also execute destructive
|
||||
commands within a workspace. Agent Firewall enforces process-level policies
|
||||
that restrict which domains agents can reach and what actions they can perform,
|
||||
AI agents can make arbitrary network requests, potentially accessing unauthorized services or exfiltrating data.
|
||||
Agent Firewall enforces process-level policies that restrict which domains agents can reach and what actions they can perform,
|
||||
preventing unintended data exposure and destructive operations like `rm -rf`.
|
||||
|
||||
### Centralizing API key management
|
||||
|
||||
@@ -17,7 +17,7 @@ Coder Tasks is Coder's platform for managing coding agents. With Coder Tasks, yo
|
||||
|
||||
Coder Tasks Dashboard view to see all available tasks.
|
||||
|
||||
Coder Tasks allows you and your organization to build and automate workflows to fully leverage AI. Tasks operate through Coder Workspaces. We support interacting with an agent through the Task UI and CLI. Some Tasks can also be accessed through the Coder Workspace IDE; see [connect via an IDE](../user-guides/workspace-access).
|
||||
Coder Tasks allows you and your organization to build and automate workflows to fully leverage AI. Tasks operate through Coder Workspaces. We support interacting with an agent through the Task UI and CLI. Some Tasks can also be accessed through the Coder Workspace IDE; see [connect via an IDE](../user-guides/workspace-access/index.md).
|
||||
|
||||
## Why Use Tasks?
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Coder Tasks is an interface for running & managing coding agents such as Claude
|
||||
|
||||

|
||||
|
||||
Coder Tasks is best for cases where the IDE is secondary, such as prototyping or running long-running background jobs. However, tasks run inside full workspaces so developers can [connect via an IDE](../user-guides/workspace-access) to take a task to completion.
|
||||
Coder Tasks is best for cases where the IDE is secondary, such as prototyping or running long-running background jobs. However, tasks run inside full workspaces so developers can [connect via an IDE](../user-guides/workspace-access/index.md) to take a task to completion.
|
||||
|
||||
You can also interact with Coder Tasks from your IDE. The [Coder extension for VS Code](https://marketplace.visualstudio.com/items?itemName=coder.coder-remote) (and compatible forks like Cursor) enables you to create, monitor, and manage Tasks directly from the IDE, eliminating the need to context-switch to a browser. After logging in, you get access to a dedicated Tasks view in the sidebar that lets you select a template, configure parameters, prompt an agent, and track task status or download logs. Your tasks run in Coder workspaces with access to your repos, credentials, and internal network.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ The [AI Governance Add-On](./ai-governance.md) requires reporting usage data to
|
||||
- number of agent workspace builds consumed
|
||||
- number of AI Governance seats consumed
|
||||
|
||||
No user-identifiable information or additional metrics are sent to Tallyman. This information is also shared with [Metronome](https://metronome.com), a Stripe product and Coder partner for usage-based and reporting.
|
||||
No user-identifiable information or additional metrics are sent to Tallyman. This information is also shared with [Metronome](https://metronome.com), a Stripe product and Coder partner for usage-based billing and reporting.
|
||||
|
||||
To send usage data, your Coder deployment must be able to make outbound HTTPS requests to `https://tallyman-prod.coder.com`. Usage data is sent approximately every 17 minutes and can be monitored via `coderd` logs.
|
||||
|
||||
@@ -17,7 +17,7 @@ Example of a successful request (requires debug logging enabled [`CODER_LOG_FILT
|
||||
|
||||
Example of a request payload:
|
||||
|
||||
```sh
|
||||
```txt
|
||||
POST /api/v1/events/ingest HTTP/1.1
|
||||
Host: tallyman-prod.coder.com
|
||||
Content-Type: application/json
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -12,7 +12,7 @@ For upgrade recommendations and troubleshooting, see
|
||||
## Reinstall Coder to upgrade
|
||||
|
||||
To upgrade your Coder server, reinstall Coder using your original method
|
||||
of [install](../install).
|
||||
of [install](../install/index.md).
|
||||
|
||||
### Coder install script
|
||||
|
||||
|
||||
+52
-21
@@ -1007,6 +1007,12 @@
|
||||
"path": "./ai-coder/agents/architecture.md",
|
||||
"state": ["beta"]
|
||||
},
|
||||
{
|
||||
"title": "Chat Sharing",
|
||||
"description": "Share Coder Agents conversations with users and groups",
|
||||
"path": "./ai-coder/agents/chat-sharing.md",
|
||||
"state": ["beta"]
|
||||
},
|
||||
{
|
||||
"title": "Models",
|
||||
"description": "Configure LLM providers and models for Coder Agents",
|
||||
@@ -1144,85 +1150,99 @@
|
||||
{
|
||||
"title": "Setup",
|
||||
"description": "How to set up and configure AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/setup.md"
|
||||
"path": "./ai-coder/ai-gateway/setup.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "Authentication",
|
||||
"description": "Learn how to authenticate against AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/auth.md"
|
||||
"path": "./ai-coder/ai-gateway/auth.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "Client Configuration",
|
||||
"description": "How to configure your AI coding tools to use AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/clients/index.md",
|
||||
"state": ["ai governance add-on"],
|
||||
"children": [
|
||||
{
|
||||
"title": "Coder Agents",
|
||||
"description": "Route Coder Agents traffic through AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/clients/coder-agents.md"
|
||||
"path": "./ai-coder/ai-gateway/clients/coder-agents.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "Claude Code",
|
||||
"description": "Configure Claude Code to use AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/clients/claude-code.md"
|
||||
"path": "./ai-coder/ai-gateway/clients/claude-code.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "Codex",
|
||||
"description": "Configure Codex to use AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/clients/codex.md"
|
||||
"path": "./ai-coder/ai-gateway/clients/codex.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "Mux",
|
||||
"description": "Configure Mux to use AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/clients/mux.md"
|
||||
"path": "./ai-coder/ai-gateway/clients/mux.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "OpenCode",
|
||||
"description": "Configure OpenCode to use AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/clients/opencode.md"
|
||||
"path": "./ai-coder/ai-gateway/clients/opencode.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "Factory",
|
||||
"description": "Configure Factory to use AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/clients/factory.md"
|
||||
"path": "./ai-coder/ai-gateway/clients/factory.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "Cline",
|
||||
"description": "Configure Cline to use AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/clients/cline.md"
|
||||
"path": "./ai-coder/ai-gateway/clients/cline.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "Kilo Code",
|
||||
"description": "Configure Kilo Code to use AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/clients/kilo-code.md"
|
||||
"path": "./ai-coder/ai-gateway/clients/kilo-code.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "VS Code",
|
||||
"description": "Configure VS Code to use AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/clients/vscode.md"
|
||||
"path": "./ai-coder/ai-gateway/clients/vscode.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "JetBrains",
|
||||
"description": "Configure JetBrains IDEs to use AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/clients/jetbrains.md"
|
||||
"path": "./ai-coder/ai-gateway/clients/jetbrains.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "Zed",
|
||||
"description": "Configure Zed to use AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/clients/zed.md"
|
||||
"path": "./ai-coder/ai-gateway/clients/zed.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "GitHub Copilot",
|
||||
"description": "Configure GitHub Copilot to use AI Gateway via AI Gateway Proxy",
|
||||
"path": "./ai-coder/ai-gateway/clients/copilot.md"
|
||||
"path": "./ai-coder/ai-gateway/clients/copilot.md",
|
||||
"state": ["ai governance add-on"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "MCP Tools Injection",
|
||||
"description": "How to configure MCP servers for tools injection through AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/mcp.md",
|
||||
"state": ["early access"]
|
||||
"path": "./ai-coder/ai-gateway/mcp.md"
|
||||
},
|
||||
{
|
||||
"title": "AI Gateway Proxy",
|
||||
@@ -1233,31 +1253,42 @@
|
||||
{
|
||||
"title": "Setup",
|
||||
"description": "How to set up and configure AI Gateway Proxy",
|
||||
"path": "./ai-coder/ai-gateway/ai-gateway-proxy/setup.md"
|
||||
"path": "./ai-coder/ai-gateway/ai-gateway-proxy/setup.md",
|
||||
"state": ["ai governance add-on"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Provider Configuration",
|
||||
"description": "How AI Gateway stores, seeds, and reloads provider configuration",
|
||||
"path": "./ai-coder/ai-gateway/providers.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "Auditing AI Sessions",
|
||||
"description": "How to audit AI sessions",
|
||||
"path": "./ai-coder/ai-gateway/audit.md"
|
||||
"path": "./ai-coder/ai-gateway/audit.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "Monitoring",
|
||||
"description": "How to monitor AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/monitoring.md"
|
||||
"path": "./ai-coder/ai-gateway/monitoring.md",
|
||||
"state": ["ai governance add-on"]
|
||||
},
|
||||
{
|
||||
"title": "Reference",
|
||||
"description": "Technical reference for AI Gateway",
|
||||
"path": "./ai-coder/ai-gateway/reference.md"
|
||||
"path": "./ai-coder/ai-gateway/reference.md",
|
||||
"state": ["ai governance add-on"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Usage Data Reporting",
|
||||
"description": "Configure AI usage data reporting",
|
||||
"path": "./ai-coder/usage-data-reporting.md"
|
||||
"path": "./ai-coder/usage-data-reporting.md",
|
||||
"state": ["ai governance add-on"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Generated
+10
-10
@@ -1743,7 +1743,7 @@ Whether to start an in-memory AI Gateway instance.
|
||||
| YAML | <code>ai_gateway.openai_base_url</code> |
|
||||
| Default | <code>https://api.openai.com/v1/</code> |
|
||||
|
||||
The base URL of the OpenAI API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this option seeds provider configuration at startup only exactly once. It will not be used in service runtime. The base URL of the OpenAI API.
|
||||
|
||||
### --ai-gateway-openai-key
|
||||
|
||||
@@ -1752,7 +1752,7 @@ The base URL of the OpenAI API.
|
||||
| Type | <code>string</code> |
|
||||
| Environment | <code>$CODER_AI_GATEWAY_OPENAI_KEY</code> |
|
||||
|
||||
The key to authenticate against the OpenAI API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this option seeds provider configuration at startup only exactly once. It will not be used in service runtime. The key to authenticate against the OpenAI API.
|
||||
|
||||
### --ai-gateway-anthropic-base-url
|
||||
|
||||
@@ -1763,7 +1763,7 @@ The key to authenticate against the OpenAI API.
|
||||
| YAML | <code>ai_gateway.anthropic_base_url</code> |
|
||||
| Default | <code>https://api.anthropic.com/</code> |
|
||||
|
||||
The base URL of the Anthropic API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this option seeds provider configuration at startup only exactly once. It will not be used in service runtime. The base URL of the Anthropic API.
|
||||
|
||||
### --ai-gateway-anthropic-key
|
||||
|
||||
@@ -1772,7 +1772,7 @@ The base URL of the Anthropic API.
|
||||
| Type | <code>string</code> |
|
||||
| Environment | <code>$CODER_AI_GATEWAY_ANTHROPIC_KEY</code> |
|
||||
|
||||
The key to authenticate against the Anthropic API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this option seeds provider configuration at startup only exactly once. It will not be used in service runtime. The key to authenticate against the Anthropic API.
|
||||
|
||||
### --ai-gateway-bedrock-base-url
|
||||
|
||||
@@ -1782,7 +1782,7 @@ The key to authenticate against the Anthropic API.
|
||||
| Environment | <code>$CODER_AI_GATEWAY_BEDROCK_BASE_URL</code> |
|
||||
| YAML | <code>ai_gateway.bedrock_base_url</code> |
|
||||
|
||||
The base URL to use for the AWS Bedrock API. Use this setting to specify an exact URL to use. Takes precedence over CODER_AI_GATEWAY_BEDROCK_REGION.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this option seeds provider configuration at startup only exactly once. It will not be used in service runtime. The base URL to use for the AWS Bedrock API. Use this setting to specify an exact URL to use. Takes precedence over CODER_AI_GATEWAY_BEDROCK_REGION.
|
||||
|
||||
### --ai-gateway-bedrock-region
|
||||
|
||||
@@ -1792,7 +1792,7 @@ The base URL to use for the AWS Bedrock API. Use this setting to specify an exac
|
||||
| Environment | <code>$CODER_AI_GATEWAY_BEDROCK_REGION</code> |
|
||||
| YAML | <code>ai_gateway.bedrock_region</code> |
|
||||
|
||||
The AWS Bedrock API region to use. Constructs a base URL to use for the AWS Bedrock API in the form of 'https://bedrock-runtime.<region>.amazonaws.com'.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this option seeds provider configuration at startup only exactly once. It will not be used in service runtime. The AWS Bedrock API region to use. Constructs a base URL to use for the AWS Bedrock API in the form of 'https://bedrock-runtime.<region>.amazonaws.com'.
|
||||
|
||||
### --ai-gateway-bedrock-access-key
|
||||
|
||||
@@ -1801,7 +1801,7 @@ The AWS Bedrock API region to use. Constructs a base URL to use for the AWS Bedr
|
||||
| Type | <code>string</code> |
|
||||
| Environment | <code>$CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY</code> |
|
||||
|
||||
The access key to authenticate against the AWS Bedrock API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this option seeds provider configuration at startup only exactly once. It will not be used in service runtime. The access key to authenticate against the AWS Bedrock API.
|
||||
|
||||
### --ai-gateway-bedrock-access-key-secret
|
||||
|
||||
@@ -1810,7 +1810,7 @@ The access key to authenticate against the AWS Bedrock API.
|
||||
| Type | <code>string</code> |
|
||||
| Environment | <code>$CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY_SECRET</code> |
|
||||
|
||||
The access key secret to use with the access key to authenticate against the AWS Bedrock API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this option seeds provider configuration at startup only exactly once. It will not be used in service runtime. The access key secret to use with the access key to authenticate against the AWS Bedrock API.
|
||||
|
||||
### --ai-gateway-bedrock-model
|
||||
|
||||
@@ -1821,7 +1821,7 @@ The access key secret to use with the access key to authenticate against the AWS
|
||||
| YAML | <code>ai_gateway.bedrock_model</code> |
|
||||
| Default | <code>global.anthropic.claude-sonnet-4-5-20250929-v1:0</code> |
|
||||
|
||||
The model to use when making requests to the AWS Bedrock API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this option seeds provider configuration at startup only exactly once. It will not be used in service runtime. The model to use when making requests to the AWS Bedrock API.
|
||||
|
||||
### --ai-gateway-bedrock-small-fastmodel
|
||||
|
||||
@@ -1832,7 +1832,7 @@ The model to use when making requests to the AWS Bedrock API.
|
||||
| YAML | <code>ai_gateway.bedrock_small_fast_model</code> |
|
||||
| Default | <code>global.anthropic.claude-haiku-4-5-20251001-v1:0</code> |
|
||||
|
||||
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.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set, this option seeds provider configuration at startup only exactly once. It will not be used in service runtime. 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.
|
||||
|
||||
### --ai-gateway-retention
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ This starter template lets you connect to your workspace in a few ways:
|
||||
- VS Code Desktop: Loads your workspace into
|
||||
[VS Code Desktop](https://code.visualstudio.com/Download) installed on your
|
||||
local computer.
|
||||
- code-server: Opens [browser-based VS Code](../ides/web-ides.md) with your
|
||||
- code-server: Opens [browser-based VS Code](../user-guides/workspace-access/web-ides.md) with your
|
||||
workspace.
|
||||
- Terminal: Opens a browser-based terminal with a shell in the workspace's
|
||||
Docker instance.
|
||||
@@ -77,7 +77,7 @@ This starter template lets you connect to your workspace in a few ways:
|
||||
|
||||
> [!TIP]
|
||||
> You can edit the template to let developers connect to a workspace in
|
||||
> [a few more ways](../ides.md).
|
||||
> [a few more ways](../user-guides/workspace-access/index.md).
|
||||
|
||||
When you're done, you can stop the workspace. -->
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ repository.
|
||||
|
||||
## Content
|
||||
|
||||
Defer to our [Contributing/Documentation](../contributing/documentation.md) page
|
||||
Defer to our [Contributing/Documentation](../about/contributing/documentation.md) page
|
||||
for rules on technical writing.
|
||||
|
||||
### Adding Photos
|
||||
|
||||
@@ -277,7 +277,6 @@ data "coder_external_auth" "github" {
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
data "coder_task" "me" {}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"cluster" : "dogfood-v2"
|
||||
@@ -991,10 +990,6 @@ resource "coder_metadata" "container_info" {
|
||||
key = "region"
|
||||
value = data.coder_parameter.region.option[index(data.coder_parameter.region.option.*.value, data.coder_parameter.region.value)].name
|
||||
}
|
||||
item {
|
||||
key = "ai_task"
|
||||
value = data.coder_task.me.enabled ? "yes" : "no"
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_script" "boundary_config_setup" {
|
||||
|
||||
@@ -204,7 +204,6 @@ data "coder_external_auth" "github" {
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
data "coder_task" "me" {}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"cluster" : "dogfood-v2"
|
||||
@@ -541,99 +540,28 @@ resource "coder_metadata" "container_info" {
|
||||
key = "region"
|
||||
value = data.coder_parameter.region.option[index(data.coder_parameter.region.option.*.value, data.coder_parameter.region.value)].name
|
||||
}
|
||||
item {
|
||||
key = "ai_task"
|
||||
value = data.coder_task.me.enabled ? "yes" : "no"
|
||||
}
|
||||
}
|
||||
|
||||
# --- AI task support ---
|
||||
|
||||
locals {
|
||||
claude_system_prompt = <<-EOT
|
||||
-- Framing --
|
||||
You are a helpful coding assistant working on the coder/vscode-coder
|
||||
VS Code extension. Aim to autonomously investigate and solve issues
|
||||
the user gives you and test your work, whenever possible.
|
||||
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask
|
||||
the user but opt for autonomy.
|
||||
|
||||
-- Tool Selection --
|
||||
- Built-in tools for everything:
|
||||
(file operations, git commands, builds & installs, one-off shell commands)
|
||||
|
||||
-- Testing --
|
||||
Integration tests launch a real VS Code instance and require a
|
||||
virtual framebuffer. Run them headlessly with:
|
||||
xvfb-run -a pnpm test:integration
|
||||
This matches how CI runs them. Unit tests do not need xvfb-run:
|
||||
pnpm test
|
||||
|
||||
-- Workflow --
|
||||
When starting new work:
|
||||
1. If given a GitHub issue URL, use the `gh` CLI to read the full
|
||||
issue details with `gh issue view <issue-number>`.
|
||||
2. Create a feature branch for the work using a descriptive name
|
||||
based on the issue or task.
|
||||
Example: `git checkout -b fix/issue-123-ssh-retry`
|
||||
3. Proceed with implementation following the AGENTS.md guidelines.
|
||||
|
||||
-- Context --
|
||||
This is the coder/vscode-coder VS Code extension. It is a real-world
|
||||
production extension used by developers to connect to Coder workspaces.
|
||||
Be sure to read AGENTS.md before making any changes.
|
||||
EOT
|
||||
}
|
||||
|
||||
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.9.2"
|
||||
enable_boundary = true
|
||||
agent_id = coder_agent.dev.id
|
||||
workdir = local.repo_dir
|
||||
claude_code_version = "latest"
|
||||
model = "opus"
|
||||
order = 999
|
||||
claude_api_key = data.coder_parameter.use_ai_bridge.value ? data.coder_workspace_owner.me.session_token : var.anthropic_api_key
|
||||
agentapi_version = "latest"
|
||||
system_prompt = local.claude_system_prompt
|
||||
ai_prompt = data.coder_task.me.prompt
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "dev.registry.coder.com/coder/claude-code/coder"
|
||||
version = "5.2.0"
|
||||
enable_ai_gateway = data.coder_parameter.use_ai_bridge.value
|
||||
anthropic_api_key = data.coder_parameter.use_ai_bridge.value ? "" : var.anthropic_api_key
|
||||
agent_id = coder_agent.dev.id
|
||||
workdir = local.repo_dir
|
||||
}
|
||||
|
||||
resource "coder_ai_task" "task" {
|
||||
count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0
|
||||
app_id = module.claude-code[count.index].task_app_id
|
||||
}
|
||||
|
||||
resource "coder_app" "watch" {
|
||||
count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0
|
||||
resource "coder_app" "claude" {
|
||||
agent_id = coder_agent.dev.id
|
||||
slug = "watch"
|
||||
display_name = "pnpm watch"
|
||||
icon = "${data.coder_workspace.me.access_url}/icon/code.svg"
|
||||
command = "screen -x pnpm_watch"
|
||||
share = "authenticated"
|
||||
open_in = "tab"
|
||||
order = 0
|
||||
}
|
||||
|
||||
resource "coder_script" "watch" {
|
||||
count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0
|
||||
display_name = "pnpm watch"
|
||||
agent_id = coder_agent.dev.id
|
||||
run_on_start = true
|
||||
start_blocks_login = false
|
||||
icon = "${data.coder_workspace.me.access_url}/icon/code.svg"
|
||||
script = <<-EOT
|
||||
#!/usr/bin/env bash
|
||||
set -eux -o pipefail
|
||||
|
||||
trap 'coder exp sync complete pnpm-watch' EXIT
|
||||
coder exp sync want pnpm-watch install-deps
|
||||
coder exp sync start pnpm-watch
|
||||
|
||||
cd "${local.repo_dir}" && screen -dmS pnpm_watch /bin/sh -c 'while true; do pnpm watch; echo "pnpm watch exited with code $? restarting in 10s"; sleep 10; done'
|
||||
slug = "claude"
|
||||
display_name = "Claude Code"
|
||||
icon = "/icon/claude.svg"
|
||||
open_in = "slim-window"
|
||||
command = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
cd "${local.repo_dir}"
|
||||
exec tmux new-session -A -s claude claude
|
||||
EOT
|
||||
}
|
||||
|
||||
@@ -1076,9 +1076,11 @@ func (s *Server) handleResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *htt
|
||||
|
||||
switch {
|
||||
case resp.StatusCode >= http.StatusInternalServerError:
|
||||
logger.Error(s.ctx, "received error response from aibridged")
|
||||
logger.Error(s.ctx, "received error response from aibridged",
|
||||
slog.F("response_body", s.readErrorBodyForLog(resp, logger)))
|
||||
case resp.StatusCode >= http.StatusBadRequest:
|
||||
logger.Warn(s.ctx, "received error response from aibridged")
|
||||
logger.Warn(s.ctx, "received error response from aibridged",
|
||||
slog.F("response_body", s.readErrorBodyForLog(resp, logger)))
|
||||
default:
|
||||
logger.Debug(s.ctx, "received response from aibridged")
|
||||
}
|
||||
@@ -1101,6 +1103,35 @@ func (s *Server) handleResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *htt
|
||||
return resp
|
||||
}
|
||||
|
||||
// maxLoggedErrorBodyBytes bounds how much of an aibridged error response
|
||||
// body is rendered into a log line, so a large upstream error payload
|
||||
// cannot blow up log volume.
|
||||
const maxLoggedErrorBodyBytes = 16 << 10 // 16 KiB
|
||||
|
||||
// readErrorBodyForLog reads resp.Body for diagnostic logging and restores
|
||||
// it with an equivalent reader, so the proxy still forwards the body
|
||||
// downstream and the response dumper can read it again. The returned
|
||||
// string is truncated to maxLoggedErrorBodyBytes; the restored body is
|
||||
// always complete.
|
||||
func (s *Server) readErrorBodyForLog(resp *http.Response, logger slog.Logger) string {
|
||||
if resp.Body == nil {
|
||||
return ""
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
// Restore the full body even on a read error: the proxy and dumper
|
||||
// downstream still expect a readable body, and a partial body is
|
||||
// better than a nil one.
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
logger.Warn(s.ctx, "failed to read aibridged error response body", slog.Error(err))
|
||||
}
|
||||
if len(body) > maxLoggedErrorBodyBytes {
|
||||
return string(body[:maxLoggedErrorBodyBytes]) + "...(truncated)"
|
||||
}
|
||||
return string(body)
|
||||
}
|
||||
|
||||
// Handler returns an HTTP handler for the AI Bridge Proxy's HTTP endpoints.
|
||||
// This is separate from the proxy server itself and is used by coderd to
|
||||
// serve endpoints like the CA certificate.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package aibridgeproxyd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog/v3/sloggers/slogtest"
|
||||
)
|
||||
|
||||
// TestReadErrorBodyForLog verifies that reading an aibridged error
|
||||
// response body for logging leaves the body intact for downstream
|
||||
// consumers (the proxy forwards it, and the response dumper reads it
|
||||
// again), and that the logged rendering is capped.
|
||||
func TestReadErrorBodyForLog(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
newResponse := func(body string) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("ReturnsBodyAndRestores", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := &Server{ctx: t.Context(), logger: slogtest.Make(t, nil)}
|
||||
resp := newResponse(`{"error":"bad request"}`)
|
||||
|
||||
got := s.readErrorBodyForLog(resp, s.logger)
|
||||
require.Equal(t, `{"error":"bad request"}`, got)
|
||||
|
||||
// The body must still be readable in full for the proxy and the
|
||||
// response dumper.
|
||||
restored, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `{"error":"bad request"}`, string(restored))
|
||||
})
|
||||
|
||||
t.Run("TruncatesLargeBodyButRestoresFull", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := &Server{ctx: t.Context(), logger: slogtest.Make(t, nil)}
|
||||
full := bytes.Repeat([]byte("a"), maxLoggedErrorBodyBytes+512)
|
||||
resp := newResponse(string(full))
|
||||
|
||||
got := s.readErrorBodyForLog(resp, s.logger)
|
||||
require.Len(t, got, maxLoggedErrorBodyBytes+len("...(truncated)"))
|
||||
require.True(t, strings.HasSuffix(got, "...(truncated)"))
|
||||
|
||||
// Truncation only affects the log string; the restored body is
|
||||
// the complete payload.
|
||||
restored, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, full, restored)
|
||||
})
|
||||
|
||||
t.Run("NilBody", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := &Server{ctx: t.Context(), logger: slogtest.Make(t, nil)}
|
||||
resp := &http.Response{StatusCode: http.StatusInternalServerError, Body: nil}
|
||||
|
||||
require.Equal(t, "", s.readErrorBodyForLog(resp, s.logger))
|
||||
})
|
||||
}
|
||||
+44
-18
@@ -125,35 +125,55 @@ AI GATEWAY OPTIONS:
|
||||
disabled, only centralized key authentication is permitted.
|
||||
|
||||
--ai-gateway-anthropic-base-url string, $CODER_AI_GATEWAY_ANTHROPIC_BASE_URL (default: https://api.anthropic.com/)
|
||||
The base URL of the Anthropic API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The base URL of the Anthropic
|
||||
API.
|
||||
|
||||
--ai-gateway-anthropic-key string, $CODER_AI_GATEWAY_ANTHROPIC_KEY
|
||||
The key to authenticate against the Anthropic API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The key to authenticate
|
||||
against the Anthropic API.
|
||||
|
||||
--ai-gateway-bedrock-access-key string, $CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY
|
||||
The access key to authenticate against the AWS Bedrock API.
|
||||
|
||||
--ai-gateway-bedrock-access-key-secret string, $CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY_SECRET
|
||||
The access key secret to use with the access key to authenticate
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The access key to authenticate
|
||||
against the AWS Bedrock API.
|
||||
|
||||
--ai-gateway-bedrock-access-key-secret string, $CODER_AI_GATEWAY_BEDROCK_ACCESS_KEY_SECRET
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The access key secret to use
|
||||
with the access key to authenticate against the AWS Bedrock API.
|
||||
|
||||
--ai-gateway-bedrock-base-url string, $CODER_AI_GATEWAY_BEDROCK_BASE_URL
|
||||
The base URL to use for the AWS Bedrock API. Use this setting to
|
||||
specify an exact URL to use. Takes precedence over
|
||||
CODER_AI_GATEWAY_BEDROCK_REGION.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The base URL to use for the
|
||||
AWS Bedrock API. Use this setting to specify an exact URL to use.
|
||||
Takes precedence over CODER_AI_GATEWAY_BEDROCK_REGION.
|
||||
|
||||
--ai-gateway-bedrock-model string, $CODER_AI_GATEWAY_BEDROCK_MODEL (default: global.anthropic.claude-sonnet-4-5-20250929-v1:0)
|
||||
The model to use when making requests to the AWS Bedrock API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The model to use when making
|
||||
requests to the AWS Bedrock API.
|
||||
|
||||
--ai-gateway-bedrock-region string, $CODER_AI_GATEWAY_BEDROCK_REGION
|
||||
The AWS Bedrock API region to use. Constructs a base URL to use for
|
||||
the AWS Bedrock API in the form of
|
||||
'https://bedrock-runtime.<region>.amazonaws.com'.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The AWS Bedrock API region to
|
||||
use. Constructs a base URL to use for the AWS Bedrock API in the form
|
||||
of 'https://bedrock-runtime.<region>.amazonaws.com'.
|
||||
|
||||
--ai-gateway-bedrock-small-fastmodel string, $CODER_AI_GATEWAY_BEDROCK_SMALL_FAST_MODEL (default: global.anthropic.claude-haiku-4-5-20251001-v1:0)
|
||||
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
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. 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.
|
||||
|
||||
--ai-gateway-circuit-breaker-enabled bool, $CODER_AI_GATEWAY_CIRCUIT_BREAKER_ENABLED (default: false)
|
||||
@@ -172,10 +192,16 @@ AI GATEWAY OPTIONS:
|
||||
to disable (unlimited).
|
||||
|
||||
--ai-gateway-openai-base-url string, $CODER_AI_GATEWAY_OPENAI_BASE_URL (default: https://api.openai.com/v1/)
|
||||
The base URL of the OpenAI API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The base URL of the OpenAI
|
||||
API.
|
||||
|
||||
--ai-gateway-openai-key string, $CODER_AI_GATEWAY_OPENAI_KEY
|
||||
The key to authenticate against the OpenAI API.
|
||||
Deprecated: manage AI Providers from the Coder UI or HTTP API. If set,
|
||||
this option seeds provider configuration at startup only exactly once.
|
||||
It will not be used in service runtime. The key to authenticate
|
||||
against the OpenAI API.
|
||||
|
||||
--ai-gateway-rate-limit int, $CODER_AI_GATEWAY_RATE_LIMIT (default: 0)
|
||||
Maximum number of AI Gateway requests per second per replica. Set to 0
|
||||
|
||||
@@ -61,6 +61,30 @@
|
||||
inherit nodejs; # Ensure it points to the above nodejs version
|
||||
};
|
||||
|
||||
mise = pkgs.stdenvNoCC.mkDerivation rec {
|
||||
pname = "mise";
|
||||
version = "2026.5.12";
|
||||
target = {
|
||||
x86_64-linux = "linux-x64";
|
||||
aarch64-linux = "linux-arm64";
|
||||
x86_64-darwin = "macos-x64";
|
||||
aarch64-darwin = "macos-arm64";
|
||||
}.${system};
|
||||
src = pkgs.fetchurl {
|
||||
url = "https://github.com/jdx/mise/releases/download/v${version}/mise-v${version}-${target}";
|
||||
hash = {
|
||||
x86_64-linux = "sha256-ojiXKjFi1xC4WyjDJDculspOS0hsgf54aVAA2fvHfEg=";
|
||||
aarch64-linux = "sha256-/S1SJ6itCx41nHBSeoNFqa2nIHf43LtVk3FlPD2VRk8=";
|
||||
x86_64-darwin = "sha256-3lfo3IK72ICmnJvIruBrncxXgYSz5c+G/O+AY11qkLQ=";
|
||||
aarch64-darwin = "sha256-53cHBUD/4iz4srn4iu2ItGHQiH2UDE8cGpc1lGPN5uE=";
|
||||
}.${system};
|
||||
};
|
||||
dontUnpack = true;
|
||||
installPhase = ''
|
||||
install -Dm755 "$src" "$out/bin/mise"
|
||||
'';
|
||||
};
|
||||
|
||||
# Check in https://search.nixos.org/packages to find new packages.
|
||||
# Use `nix --extra-experimental-features nix-command --extra-experimental-features flakes flake update`
|
||||
# to update the lock file if packages are out-of-date.
|
||||
@@ -109,6 +133,21 @@
|
||||
vendorHash = "sha256-4Cb15MhKyhRvYVKfMqBwuC3WBBIJE6AinJt02+TSMVY=";
|
||||
};
|
||||
|
||||
paralleltestctx = unstablePkgs.buildGo126Module {
|
||||
pname = "paralleltestctx";
|
||||
version = "0.0.2";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "coder";
|
||||
repo = "paralleltestctx";
|
||||
rev = "v0.0.2";
|
||||
sha256 = "sha256-qFQ4LZR2IwqscypD0URSZKXTlhUcz/axDb8NTH5CxLw=";
|
||||
};
|
||||
|
||||
subPackages = [ "cmd/paralleltestctx" ];
|
||||
vendorHash = "sha256-OuQWmZmofdJKq1hvk43RPkILQwAuFzqhmB22Xf6Z3lA=";
|
||||
};
|
||||
|
||||
# Keep Terraform aligned with provisioner/terraform/testdata/version.txt
|
||||
# so `make gen` remains deterministic in Nix shells.
|
||||
terraform_1_15_5 =
|
||||
@@ -188,6 +227,7 @@
|
||||
lazydocker
|
||||
lazygit
|
||||
less
|
||||
mise
|
||||
unstablePkgs.mockgen
|
||||
moreutils
|
||||
nfpm
|
||||
@@ -195,6 +235,7 @@
|
||||
nodejs
|
||||
openssh
|
||||
openssl
|
||||
paralleltestctx
|
||||
pango
|
||||
pixman
|
||||
pkg-config
|
||||
|
||||
@@ -571,7 +571,6 @@ require (
|
||||
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
||||
github.com/coder/paralleltestctx v0.0.2 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
@@ -618,7 +617,6 @@ require (
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.5 // indirect
|
||||
github.com/lestrrat-go/jwx/v3 v3.1.1 // indirect
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect
|
||||
github.com/moby/moby/api v1.54.0 // indirect
|
||||
github.com/moby/moby/client v0.3.0 // indirect
|
||||
@@ -632,7 +630,6 @@ require (
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/rhysd/actionlint v1.7.10 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
@@ -664,9 +661,7 @@ require (
|
||||
)
|
||||
|
||||
tool (
|
||||
github.com/coder/paralleltestctx/cmd/paralleltestctx
|
||||
github.com/daixiang0/gci
|
||||
github.com/rhysd/actionlint/cmd/actionlint
|
||||
github.com/swaggo/swag/cmd/swag
|
||||
go.uber.org/mock/mockgen
|
||||
golang.org/x/tools/cmd/goimports
|
||||
|
||||
@@ -334,8 +334,6 @@ github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136 h1:0RgB61LcNs
|
||||
github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136/go.mod h1:VkD1P761nykiq75dz+4iFqIQIZka189tx1BQLOp0Skc=
|
||||
github.com/coder/guts v1.7.0 h1:TaZ/PR9wgN8dlbcckaWV1MxkkuEFZRwSRwBBEm8dYXs=
|
||||
github.com/coder/guts v1.7.0/go.mod h1:30SShdvpmsauNlsNjECRB5AppScjYk08rf2ZVpH3MFg=
|
||||
github.com/coder/paralleltestctx v0.0.2 h1:0akzA1oSV0LOl7loR8Mmoq/mu7qGDaFV8DpojotmXiE=
|
||||
github.com/coder/paralleltestctx v0.0.2/go.mod h1:q/wi6cmlBOhrJKjUtouTn4J9xZlRhK0MbgHvJNdGW3w=
|
||||
github.com/coder/pq v1.10.5-0.20250807075151-6ad9b0a25151 h1:YAxwg3lraGNRwoQ18H7R7n+wsCqNve7Brdvj0F1rDnU=
|
||||
github.com/coder/pq v1.10.5-0.20250807075151-6ad9b0a25151/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
|
||||
@@ -881,8 +879,6 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||
@@ -1063,8 +1059,6 @@ github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuM
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rhysd/actionlint v1.7.10 h1:FL3XIEs72G4/++168vlv5FKOWMSWvWIQw1kBCadyOcM=
|
||||
github.com/rhysd/actionlint v1.7.10/go.mod h1:ZHX/hrmknlsJN73InPTKsKdXpAv9wVdrJy8h8HAwFHg=
|
||||
github.com/riandyrn/otelchi v0.5.1 h1:0/45omeqpP7f/cvdL16GddQBfAEmZvUyl2QzLSE6uYo=
|
||||
github.com/riandyrn/otelchi v0.5.1/go.mod h1:ZxVxNEl+jQ9uHseRYIxKWRb3OY8YXFEu+EkNiiSNUEA=
|
||||
github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 h1:4+LEVOB87y175cLJC/mbsgKmoDOjrBldtXvioEy96WY=
|
||||
|
||||
@@ -1,5 +1,53 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools.actionlint]]
|
||||
version = "1.7.10"
|
||||
backend = "aqua:rhysd/actionlint"
|
||||
|
||||
[tools.actionlint."platforms.linux-arm64"]
|
||||
checksum = "sha256:cd3dfe5f66887ec6b987752d8d9614e59fd22f39415c5ad9f28374623f41773a"
|
||||
url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_linux_arm64.tar.gz"
|
||||
|
||||
[tools.actionlint."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:cd3dfe5f66887ec6b987752d8d9614e59fd22f39415c5ad9f28374623f41773a"
|
||||
url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_linux_arm64.tar.gz"
|
||||
|
||||
[tools.actionlint."platforms.linux-x64"]
|
||||
checksum = "sha256:f4c76b71db5755a713e6055cbb0857ed07e103e028bda117817660ebadb4386f"
|
||||
url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_linux_amd64.tar.gz"
|
||||
|
||||
[tools.actionlint."platforms.linux-x64-baseline"]
|
||||
checksum = "sha256:f4c76b71db5755a713e6055cbb0857ed07e103e028bda117817660ebadb4386f"
|
||||
url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_linux_amd64.tar.gz"
|
||||
|
||||
[tools.actionlint."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:f4c76b71db5755a713e6055cbb0857ed07e103e028bda117817660ebadb4386f"
|
||||
url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_linux_amd64.tar.gz"
|
||||
|
||||
[tools.actionlint."platforms.linux-x64-musl-baseline"]
|
||||
checksum = "sha256:f4c76b71db5755a713e6055cbb0857ed07e103e028bda117817660ebadb4386f"
|
||||
url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_linux_amd64.tar.gz"
|
||||
|
||||
[tools.actionlint."platforms.macos-arm64"]
|
||||
checksum = "sha256:004ca87b367b37f4d75c55ab6cf80f9b8c043adbfbd440f31c604d417939c442"
|
||||
url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_darwin_arm64.tar.gz"
|
||||
|
||||
[tools.actionlint."platforms.macos-x64"]
|
||||
checksum = "sha256:16782c41f2af264db80f855ee5d09164ca98fc78edf3bcd0f46eecff279682ba"
|
||||
url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_darwin_amd64.tar.gz"
|
||||
|
||||
[tools.actionlint."platforms.macos-x64-baseline"]
|
||||
checksum = "sha256:16782c41f2af264db80f855ee5d09164ca98fc78edf3bcd0f46eecff279682ba"
|
||||
url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_darwin_amd64.tar.gz"
|
||||
|
||||
[tools.actionlint."platforms.windows-x64"]
|
||||
checksum = "sha256:283467f9d6202a8cb8c00ad8dd0ee4e685b71fb86a6a56c68fcbb9ae8ed91237"
|
||||
url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_windows_amd64.zip"
|
||||
|
||||
[tools.actionlint."platforms.windows-x64-baseline"]
|
||||
checksum = "sha256:283467f9d6202a8cb8c00ad8dd0ee4e685b71fb86a6a56c68fcbb9ae8ed91237"
|
||||
url = "https://github.com/rhysd/actionlint/releases/download/v1.7.10/actionlint_1.7.10_windows_amd64.zip"
|
||||
|
||||
[[tools."aqua:ahmetb/kubectx/kubens"]]
|
||||
version = "0.9.4"
|
||||
backend = "aqua:ahmetb/kubectx/kubens"
|
||||
@@ -432,14 +480,26 @@ url = "https://dl.google.com/go/go1.26.2.windows-amd64.zip"
|
||||
checksum = "sha256:98eb3570bade15cb826b0909338df6cc6d2cf590bc39c471142002db3832b708"
|
||||
url = "https://dl.google.com/go/go1.26.2.windows-amd64.zip"
|
||||
|
||||
[[tools."go:github.com/coder/paralleltestctx/cmd/paralleltestctx"]]
|
||||
version = "0.0.2"
|
||||
backend = "go:github.com/coder/paralleltestctx/cmd/paralleltestctx"
|
||||
|
||||
[[tools."go:github.com/coder/sqlc/cmd/sqlc"]]
|
||||
version = "337309bfb9524f38466a5090e310040fc7af0203"
|
||||
backend = "go:github.com/coder/sqlc/cmd/sqlc"
|
||||
|
||||
[[tools."go:github.com/coder/whichtests"]]
|
||||
version = "ec33bab1ec04cd86beb7a61a069db4463dba63f5"
|
||||
backend = "go:github.com/coder/whichtests"
|
||||
|
||||
[[tools."go:github.com/golang-migrate/migrate/v4/cmd/migrate"]]
|
||||
version = "v4.19.0"
|
||||
backend = "go:github.com/golang-migrate/migrate/v4/cmd/migrate"
|
||||
|
||||
[[tools."go:github.com/golangci/golangci-lint/cmd/golangci-lint"]]
|
||||
version = "1.64.8"
|
||||
backend = "go:github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||
|
||||
[[tools."go:github.com/goreleaser/nfpm/v2/cmd/nfpm"]]
|
||||
version = "v2.35.1"
|
||||
backend = "go:github.com/goreleaser/nfpm/v2/cmd/nfpm"
|
||||
@@ -452,10 +512,18 @@ backend = "go:github.com/mikefarah/yq/v4"
|
||||
version = "v0.3.13"
|
||||
backend = "go:github.com/quasilyte/go-ruleguard/cmd/ruleguard"
|
||||
|
||||
[[tools."go:github.com/slsyy/mtimehash/cmd/mtimehash"]]
|
||||
version = "1.0.0"
|
||||
backend = "go:github.com/slsyy/mtimehash/cmd/mtimehash"
|
||||
|
||||
[[tools."go:github.com/swaggo/swag/cmd/swag"]]
|
||||
version = "v1.16.2"
|
||||
backend = "go:github.com/swaggo/swag/cmd/swag"
|
||||
|
||||
[[tools."go:github.com/tc-hib/go-winres"]]
|
||||
version = "0.3.3"
|
||||
backend = "go:github.com/tc-hib/go-winres"
|
||||
|
||||
[[tools."go:go.uber.org/mock/mockgen"]]
|
||||
version = "v0.6.0"
|
||||
backend = "go:go.uber.org/mock/mockgen"
|
||||
@@ -480,54 +548,6 @@ backend = "go:mvdan.cc/sh/v3/cmd/shfmt"
|
||||
version = "v0.0.34"
|
||||
backend = "go:storj.io/drpc/cmd/protoc-gen-go-drpc"
|
||||
|
||||
[[tools.golangci-lint]]
|
||||
version = "1.64.8"
|
||||
backend = "aqua:golangci/golangci-lint"
|
||||
|
||||
[tools.golangci-lint."platforms.linux-arm64"]
|
||||
checksum = "sha256:a6ab58ebcb1c48572622146cdaec2956f56871038a54ed1149f1386e287789a5"
|
||||
url = "https://github.com/golangci/golangci-lint/releases/download/v1.64.8/golangci-lint-1.64.8-linux-arm64.tar.gz"
|
||||
|
||||
[tools.golangci-lint."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:a6ab58ebcb1c48572622146cdaec2956f56871038a54ed1149f1386e287789a5"
|
||||
url = "https://github.com/golangci/golangci-lint/releases/download/v1.64.8/golangci-lint-1.64.8-linux-arm64.tar.gz"
|
||||
|
||||
[tools.golangci-lint."platforms.linux-x64"]
|
||||
checksum = "sha256:b6270687afb143d019f387c791cd2a6f1cb383be9b3124d241ca11bd3ce2e54e"
|
||||
url = "https://github.com/golangci/golangci-lint/releases/download/v1.64.8/golangci-lint-1.64.8-linux-amd64.tar.gz"
|
||||
|
||||
[tools.golangci-lint."platforms.linux-x64-baseline"]
|
||||
checksum = "sha256:b6270687afb143d019f387c791cd2a6f1cb383be9b3124d241ca11bd3ce2e54e"
|
||||
url = "https://github.com/golangci/golangci-lint/releases/download/v1.64.8/golangci-lint-1.64.8-linux-amd64.tar.gz"
|
||||
|
||||
[tools.golangci-lint."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:b6270687afb143d019f387c791cd2a6f1cb383be9b3124d241ca11bd3ce2e54e"
|
||||
url = "https://github.com/golangci/golangci-lint/releases/download/v1.64.8/golangci-lint-1.64.8-linux-amd64.tar.gz"
|
||||
|
||||
[tools.golangci-lint."platforms.linux-x64-musl-baseline"]
|
||||
checksum = "sha256:b6270687afb143d019f387c791cd2a6f1cb383be9b3124d241ca11bd3ce2e54e"
|
||||
url = "https://github.com/golangci/golangci-lint/releases/download/v1.64.8/golangci-lint-1.64.8-linux-amd64.tar.gz"
|
||||
|
||||
[tools.golangci-lint."platforms.macos-arm64"]
|
||||
checksum = "sha256:70543d21e5b02a94079be8aa11267a5b060865583e337fe768d39b5d3e2faf1f"
|
||||
url = "https://github.com/golangci/golangci-lint/releases/download/v1.64.8/golangci-lint-1.64.8-darwin-arm64.tar.gz"
|
||||
|
||||
[tools.golangci-lint."platforms.macos-x64"]
|
||||
checksum = "sha256:b52aebb8cb51e00bfd5976099083fbe2c43ef556cef9c87e58a8ae656e740444"
|
||||
url = "https://github.com/golangci/golangci-lint/releases/download/v1.64.8/golangci-lint-1.64.8-darwin-amd64.tar.gz"
|
||||
|
||||
[tools.golangci-lint."platforms.macos-x64-baseline"]
|
||||
checksum = "sha256:b52aebb8cb51e00bfd5976099083fbe2c43ef556cef9c87e58a8ae656e740444"
|
||||
url = "https://github.com/golangci/golangci-lint/releases/download/v1.64.8/golangci-lint-1.64.8-darwin-amd64.tar.gz"
|
||||
|
||||
[tools.golangci-lint."platforms.windows-x64"]
|
||||
checksum = "sha256:54c2ed3a6b4f2f5da1056fb6e83d6b73b592e06684b65a5999174fabbb251a8f"
|
||||
url = "https://github.com/golangci/golangci-lint/releases/download/v1.64.8/golangci-lint-1.64.8-windows-amd64.zip"
|
||||
|
||||
[tools.golangci-lint."platforms.windows-x64-baseline"]
|
||||
checksum = "sha256:54c2ed3a6b4f2f5da1056fb6e83d6b73b592e06684b65a5999174fabbb251a8f"
|
||||
url = "https://github.com/golangci/golangci-lint/releases/download/v1.64.8/golangci-lint-1.64.8-windows-amd64.zip"
|
||||
|
||||
[[tools.helm]]
|
||||
version = "3.21.0"
|
||||
backend = "aqua:helm/helm"
|
||||
@@ -723,6 +743,10 @@ url = "https://nodejs.org/dist/v22.19.0/node-v22.19.0-win-x64.zip"
|
||||
version = "0.87.0"
|
||||
backend = "npm:@devcontainers/cli"
|
||||
|
||||
[[tools."npm:@puppeteer/browsers"]]
|
||||
version = "2.13.0"
|
||||
backend = "npm:@puppeteer/browsers"
|
||||
|
||||
[[tools.pnpm]]
|
||||
version = "10.33.2"
|
||||
backend = "aqua:pnpm/pnpm"
|
||||
@@ -848,52 +872,52 @@ url = "https://github.com/protocolbuffers/protobuf-go/releases/download/v1.30.0/
|
||||
url = "https://github.com/protocolbuffers/protobuf-go/releases/download/v1.30.0/protoc-gen-go.v1.30.0.windows.amd64.zip"
|
||||
|
||||
[[tools.syft]]
|
||||
version = "1.20.0"
|
||||
version = "1.26.1"
|
||||
backend = "aqua:anchore/syft"
|
||||
|
||||
[tools.syft."platforms.linux-arm64"]
|
||||
checksum = "sha256:53f76737ddbf425c89240d5b0be0990b1a71e66890b44f19743221b17e6ee635"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.20.0/syft_1.20.0_linux_arm64.tar.gz"
|
||||
checksum = "sha256:ed3915cbc9c039f0501cb49d4485125befbd729acc263e767f70a18de3fec10d"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.26.1/syft_1.26.1_linux_arm64.tar.gz"
|
||||
|
||||
[tools.syft."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:53f76737ddbf425c89240d5b0be0990b1a71e66890b44f19743221b17e6ee635"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.20.0/syft_1.20.0_linux_arm64.tar.gz"
|
||||
checksum = "sha256:ed3915cbc9c039f0501cb49d4485125befbd729acc263e767f70a18de3fec10d"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.26.1/syft_1.26.1_linux_arm64.tar.gz"
|
||||
|
||||
[tools.syft."platforms.linux-x64"]
|
||||
checksum = "sha256:689e12c5cbf67521ce61b9c126068f9eaabe1223e77971b2fede50033ff6b5cc"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.20.0/syft_1.20.0_linux_amd64.tar.gz"
|
||||
checksum = "sha256:4f3e84f9467080c876deb0fa968da54309c6d21fb8c00fd3a4e547eb9f006835"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.26.1/syft_1.26.1_linux_amd64.tar.gz"
|
||||
|
||||
[tools.syft."platforms.linux-x64-baseline"]
|
||||
checksum = "sha256:689e12c5cbf67521ce61b9c126068f9eaabe1223e77971b2fede50033ff6b5cc"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.20.0/syft_1.20.0_linux_amd64.tar.gz"
|
||||
checksum = "sha256:4f3e84f9467080c876deb0fa968da54309c6d21fb8c00fd3a4e547eb9f006835"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.26.1/syft_1.26.1_linux_amd64.tar.gz"
|
||||
|
||||
[tools.syft."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:689e12c5cbf67521ce61b9c126068f9eaabe1223e77971b2fede50033ff6b5cc"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.20.0/syft_1.20.0_linux_amd64.tar.gz"
|
||||
checksum = "sha256:4f3e84f9467080c876deb0fa968da54309c6d21fb8c00fd3a4e547eb9f006835"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.26.1/syft_1.26.1_linux_amd64.tar.gz"
|
||||
|
||||
[tools.syft."platforms.linux-x64-musl-baseline"]
|
||||
checksum = "sha256:689e12c5cbf67521ce61b9c126068f9eaabe1223e77971b2fede50033ff6b5cc"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.20.0/syft_1.20.0_linux_amd64.tar.gz"
|
||||
checksum = "sha256:4f3e84f9467080c876deb0fa968da54309c6d21fb8c00fd3a4e547eb9f006835"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.26.1/syft_1.26.1_linux_amd64.tar.gz"
|
||||
|
||||
[tools.syft."platforms.macos-arm64"]
|
||||
checksum = "sha256:91365712a06af0c0dcd06f5e87fc8791c4332831b3dd6f5474acaaf803d71d82"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.20.0/syft_1.20.0_darwin_arm64.tar.gz"
|
||||
checksum = "sha256:00435a3fe2ae940203708ee2eae9976d1719982c628d30b2b78aacd36133ec6b"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.26.1/syft_1.26.1_darwin_arm64.tar.gz"
|
||||
|
||||
[tools.syft."platforms.macos-x64"]
|
||||
checksum = "sha256:5fdf7afd0f1bfdbb2a1a575eacef8e10edfcb4783631baaa7572a9f4a4d86441"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.20.0/syft_1.20.0_darwin_amd64.tar.gz"
|
||||
checksum = "sha256:2eae0b76a208c5916cf02847b94e861024c7a5a6c1e2e606f5436f97747b1f76"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.26.1/syft_1.26.1_darwin_amd64.tar.gz"
|
||||
|
||||
[tools.syft."platforms.macos-x64-baseline"]
|
||||
checksum = "sha256:5fdf7afd0f1bfdbb2a1a575eacef8e10edfcb4783631baaa7572a9f4a4d86441"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.20.0/syft_1.20.0_darwin_amd64.tar.gz"
|
||||
checksum = "sha256:2eae0b76a208c5916cf02847b94e861024c7a5a6c1e2e606f5436f97747b1f76"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.26.1/syft_1.26.1_darwin_amd64.tar.gz"
|
||||
|
||||
[tools.syft."platforms.windows-x64"]
|
||||
checksum = "sha256:b8bfdedb261de2a69768097422a73bc72273ee92136ff676a20c3161e658881f"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.20.0/syft_1.20.0_windows_amd64.zip"
|
||||
checksum = "sha256:7af7acb9f81bdddbc343855cb3a42e1d38ae9a1b044bfcd9b975a118d107849e"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.26.1/syft_1.26.1_windows_amd64.zip"
|
||||
|
||||
[tools.syft."platforms.windows-x64-baseline"]
|
||||
checksum = "sha256:b8bfdedb261de2a69768097422a73bc72273ee92136ff676a20c3161e658881f"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.20.0/syft_1.20.0_windows_amd64.zip"
|
||||
checksum = "sha256:7af7acb9f81bdddbc343855cb3a42e1d38ae9a1b044bfcd9b975a118d107849e"
|
||||
url = "https://github.com/anchore/syft/releases/download/v1.26.1/syft_1.26.1_windows_amd64.zip"
|
||||
|
||||
[[tools.terraform]]
|
||||
version = "1.15.5"
|
||||
@@ -942,3 +966,56 @@ url = "https://releases.hashicorp.com/terraform/1.15.5/terraform_1.15.5_windows_
|
||||
[tools.terraform."platforms.windows-x64-baseline"]
|
||||
checksum = "sha256:2f652dd854af7b7fbb51301afc55b5ef1d3f6e287be7889d4cc3818df891cd38"
|
||||
url = "https://releases.hashicorp.com/terraform/1.15.5/terraform_1.15.5_windows_amd64.zip"
|
||||
|
||||
[[tools.zizmor]]
|
||||
version = "1.11.0"
|
||||
backend = "aqua:zizmorcore/zizmor"
|
||||
|
||||
[tools.zizmor."platforms.linux-arm64"]
|
||||
checksum = "sha256:ce6d71e796b7d3663449151b08cee7c659f89bf36095c432e25169c857f479f0"
|
||||
url = "https://github.com/zizmorcore/zizmor/releases/download/v1.11.0/zizmor-aarch64-unknown-linux-gnu.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.zizmor."platforms.linux-arm64-musl"]
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.zizmor."platforms.linux-x64"]
|
||||
checksum = "sha256:da35e666827cbb1e6ca98b18b7969657b9f186467bfebfa25e730aac527c36f8"
|
||||
url = "https://github.com/zizmorcore/zizmor/releases/download/v1.11.0/zizmor-x86_64-unknown-linux-gnu.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.zizmor."platforms.linux-x64-baseline"]
|
||||
checksum = "sha256:da35e666827cbb1e6ca98b18b7969657b9f186467bfebfa25e730aac527c36f8"
|
||||
url = "https://github.com/zizmorcore/zizmor/releases/download/v1.11.0/zizmor-x86_64-unknown-linux-gnu.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.zizmor."platforms.linux-x64-musl"]
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.zizmor."platforms.linux-x64-musl-baseline"]
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.zizmor."platforms.macos-arm64"]
|
||||
checksum = "sha256:7cf59f08cb50f539ab9ddc6be1d463c81e31f5b189d148fc6f786adf9fc42a5f"
|
||||
url = "https://github.com/zizmorcore/zizmor/releases/download/v1.11.0/zizmor-aarch64-apple-darwin.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.zizmor."platforms.macos-x64"]
|
||||
checksum = "sha256:a1f60dd09527ce546ff86e49ebfa1ab4a6c5d16365662e6932f8d0f46fbb18b2"
|
||||
url = "https://github.com/zizmorcore/zizmor/releases/download/v1.11.0/zizmor-x86_64-apple-darwin.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.zizmor."platforms.macos-x64-baseline"]
|
||||
checksum = "sha256:a1f60dd09527ce546ff86e49ebfa1ab4a6c5d16365662e6932f8d0f46fbb18b2"
|
||||
url = "https://github.com/zizmorcore/zizmor/releases/download/v1.11.0/zizmor-x86_64-apple-darwin.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.zizmor."platforms.windows-x64"]
|
||||
checksum = "sha256:35e038bdbde6fcfdf947c947c7c3fc83c5043e0ded0e5b0d59c30c8eda97fd3a"
|
||||
url = "https://github.com/zizmorcore/zizmor/releases/download/v1.11.0/zizmor-x86_64-pc-windows-msvc.zip"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.zizmor."platforms.windows-x64-baseline"]
|
||||
checksum = "sha256:35e038bdbde6fcfdf947c947c7c3fc83c5043e0ded0e5b0d59c30c8eda97fd3a"
|
||||
url = "https://github.com/zizmorcore/zizmor/releases/download/v1.11.0/zizmor-x86_64-pc-windows-msvc.zip"
|
||||
provenance = "github-attestations"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Keep in lockstep with MISE_VERSION in dogfood/coder/ubuntu-*/Dockerfile.base,
|
||||
# .github/workflows/dogfood.yaml, and scripts/dogfood/mise-oci-wrapper.sh.
|
||||
# Keep in lockstep with .github/actions/setup-mise/action.yml,
|
||||
# .github/actions/setup-mise/checksums.toml, flake.nix,
|
||||
# dogfood/coder/ubuntu-*/Dockerfile.base, and scripts/dogfood/mise-oci-wrapper.sh.
|
||||
min_version = "2026.5.12"
|
||||
|
||||
[settings]
|
||||
@@ -19,8 +20,17 @@ protoc = "23.4"
|
||||
protoc-gen-go = "1.30.0"
|
||||
|
||||
# Go development tools.
|
||||
"go:github.com/coder/paralleltestctx/cmd/paralleltestctx" = "v0.0.2"
|
||||
"go:github.com/coder/whichtests" = "ec33bab1ec04cd86beb7a61a069db4463dba63f5"
|
||||
# Keep golangci-lint on the Go backend while pinned to v1. The upstream
|
||||
# precompiled v1 binary is built with an older Go toolchain and cannot lint
|
||||
# this module's Go version. Upgrading to v2 should let us use the native
|
||||
# golangci-lint mise/aqua backend and GitHub release binaries.
|
||||
"go:github.com/golangci/golangci-lint/cmd/golangci-lint" = "v1.64.8"
|
||||
"go:github.com/golang-migrate/migrate/v4/cmd/migrate" = "v4.19.0"
|
||||
"go:github.com/goreleaser/nfpm/v2/cmd/nfpm" = "v2.35.1"
|
||||
"go:github.com/slsyy/mtimehash/cmd/mtimehash" = "v1.0.0"
|
||||
"go:github.com/tc-hib/go-winres" = "v0.3.3"
|
||||
"go:github.com/mikefarah/yq/v4" = "v4.44.3"
|
||||
"go:github.com/quasilyte/go-ruleguard/cmd/ruleguard" = "v0.3.13"
|
||||
"go:github.com/swaggo/swag/cmd/swag" = "v1.16.2"
|
||||
@@ -30,17 +40,18 @@ protoc-gen-go = "1.30.0"
|
||||
"go:mvdan.cc/sh/v3/cmd/shfmt" = "v3.12.0"
|
||||
|
||||
# Infrastructure, release, and lint CLIs.
|
||||
actionlint = "1.7.10"
|
||||
"aqua:ahmetb/kubectx/kubens" = "0.9.4"
|
||||
cosign = "2.4.3"
|
||||
# crane is the registry client `mise oci push` shells out to. Sourced
|
||||
# here so it travels with the rest of the mise toolset (one source of
|
||||
# truth, deterministic version, no apt drift across CI / wrapper).
|
||||
crane = "0.21.6"
|
||||
golangci-lint = "1.64.8"
|
||||
helm = "3.21.0"
|
||||
kubectx = "0.9.4"
|
||||
syft = "1.20.0"
|
||||
syft = "1.26.1"
|
||||
terraform = "1.15.5"
|
||||
zizmor = "1.11.0"
|
||||
|
||||
# Developer-environment niceties for the dogfood image. Non-dogfood
|
||||
# users who run `mise install` here will pull these too; they are
|
||||
@@ -60,6 +71,9 @@ lazygit = "0.61.1"
|
||||
# Pre-installs the binary so the upstream devcontainers-cli coder
|
||||
# module's `command -v devcontainer` short-circuit fires
|
||||
"npm:@devcontainers/cli" = "0.87.0"
|
||||
# weekly-docs uses this pinned Puppeteer browser installer to install Chrome for
|
||||
# action-linkspector without resolving mutable npm metadata at runtime.
|
||||
"npm:@puppeteer/browsers" = "2.13.0"
|
||||
|
||||
# sqlc (coder fork) bundles sqlite via cgo, so the `go install` build
|
||||
# needs CGO_ENABLED=1. Scope it with `install_env` so it only applies
|
||||
|
||||
@@ -381,7 +381,7 @@ func provisionEnv(
|
||||
"CODER_WORKSPACE_BUILD_ID="+metadata.GetWorkspaceBuildId(),
|
||||
"CODER_TASK_ID="+metadata.GetTaskId(),
|
||||
"CODER_TASK_PROMPT="+metadata.GetTaskPrompt(),
|
||||
"AWS_SDK_UA_APP_ID=APN_1.1/pc_cdfmjwn8i6u8l9fwz8h82e4w3$",
|
||||
awsSDKUserAgentEnv(safeEnvironValue(env, awsSDKUserAgentEnvKey)),
|
||||
)
|
||||
if metadata.GetPrebuiltWorkspaceBuildStage().IsPrebuild() {
|
||||
env = append(env, provider.IsPrebuildEnvironmentVariable()+"=true")
|
||||
|
||||
@@ -53,3 +53,39 @@ func safeEnviron() []string {
|
||||
}
|
||||
return strippedEnv
|
||||
}
|
||||
|
||||
// safeEnvironValue returns the value of the named variable in the given
|
||||
// `KEY=VALUE` environment slice, or an empty string if it is not present.
|
||||
func safeEnvironValue(env []string, name string) string {
|
||||
prefix := name + "="
|
||||
for _, e := range env {
|
||||
if strings.HasPrefix(e, prefix) {
|
||||
return strings.TrimPrefix(e, prefix)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
const (
|
||||
awsSDKUserAgentEnvKey = "AWS_SDK_UA_APP_ID"
|
||||
// awsSDKUserAgentCoder is Coder's AWS Partner Revenue Measurement
|
||||
// User-Agent string. The `APN_1.1/pc_<product-code>$` format and the
|
||||
// space-delimited append behavior below follow AWS's guidance:
|
||||
// https://docs.aws.amazon.com/PRM/latest/aws-prm-onboarding-guide/automated-user-agent.html
|
||||
awsSDKUserAgentCoder = "APN_1.1/pc_cdfmjwn8i6u8l9fwz8h82e4w3$"
|
||||
)
|
||||
|
||||
// awsSDKUserAgentEnv returns the AWS_SDK_UA_APP_ID value to pass to the
|
||||
// Terraform subprocess. If the caller's environment already configures an
|
||||
// Application ID (e.g. an operator who is also an AWS Partner and wants
|
||||
// their own revenue attribution), Coder's value is appended with a space
|
||||
// delimiter so both attributions are preserved. Otherwise Coder's value is
|
||||
// used on its own.
|
||||
//
|
||||
// See: https://docs.aws.amazon.com/PRM/latest/aws-prm-onboarding-guide/automated-user-agent.html
|
||||
func awsSDKUserAgentEnv(existing string) string {
|
||||
if existing == "" {
|
||||
return awsSDKUserAgentEnvKey + "=" + awsSDKUserAgentCoder
|
||||
}
|
||||
return awsSDKUserAgentEnvKey + "=" + existing + " " + awsSDKUserAgentCoder
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSafeEnvironValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := []string{
|
||||
"FOO=bar",
|
||||
"AWS_SDK_UA_APP_ID=my-existing-id",
|
||||
"BAZ=qux",
|
||||
}
|
||||
require.Equal(t, "my-existing-id", safeEnvironValue(env, "AWS_SDK_UA_APP_ID"))
|
||||
require.Equal(t, "bar", safeEnvironValue(env, "FOO"))
|
||||
require.Equal(t, "", safeEnvironValue(env, "MISSING"))
|
||||
}
|
||||
|
||||
func TestAWSSDKUserAgentEnv(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("NoExisting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t,
|
||||
"AWS_SDK_UA_APP_ID=APN_1.1/pc_cdfmjwn8i6u8l9fwz8h82e4w3$",
|
||||
awsSDKUserAgentEnv(""),
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("AppendToExisting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// When the operator is themselves an AWS Partner and has set their own
|
||||
// Application ID, we append Coder's with a space delimiter so both
|
||||
// attributions are preserved. See:
|
||||
// https://docs.aws.amazon.com/PRM/latest/aws-prm-onboarding-guide/automated-user-agent.html
|
||||
require.Equal(t,
|
||||
"AWS_SDK_UA_APP_ID=EXISTING_APP_ID APN_1.1/pc_cdfmjwn8i6u8l9fwz8h82e4w3$",
|
||||
awsSDKUserAgentEnv("EXISTING_APP_ID"),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
# - go.mod
|
||||
# - mise.toml (the dogfood image installs from this manifest)
|
||||
# - flake.nix
|
||||
# - .github/actions/setup-go/action.yml
|
||||
# The version of Go in go.mod is considered the source of truth.
|
||||
|
||||
set -euo pipefail
|
||||
@@ -19,23 +18,17 @@ IGNORE_NIX=${IGNORE_NIX:-false}
|
||||
|
||||
GO_VERSION_GO_MOD=$(grep -Eo 'go [0-9]+\.[0-9]+\.[0-9]+' ./go.mod | cut -d' ' -f2)
|
||||
GO_VERSION_MISE_TOML=$(grep -Eo '^go = "[0-9]+\.[0-9]+\.[0-9]+"' ./mise.toml | sed -E 's/.*"([^"]+)"/\1/')
|
||||
GO_VERSION_SETUP_GO=$(yq '.inputs.version.default' .github/actions/setup-go/action.yaml)
|
||||
GO_VERSION_FLAKE_NIX=$(grep -Eo '\bgo_[0-9]+_[0-9]+\b' ./flake.nix)
|
||||
# Convert to major.minor format.
|
||||
GO_VERSION_FLAKE_NIX_MAJOR_MINOR=$(echo "$GO_VERSION_FLAKE_NIX" | cut -d '_' -f 2-3 | tr '_' '.')
|
||||
log "INFO : go.mod : $GO_VERSION_GO_MOD"
|
||||
log "INFO : mise.toml : $GO_VERSION_MISE_TOML"
|
||||
log "INFO : setup-go/action.yaml : $GO_VERSION_SETUP_GO"
|
||||
log "INFO : flake.nix : $GO_VERSION_FLAKE_NIX_MAJOR_MINOR"
|
||||
|
||||
if [ "$GO_VERSION_GO_MOD" != "$GO_VERSION_MISE_TOML" ]; then
|
||||
error "Go version mismatch between go.mod and mise.toml"
|
||||
fi
|
||||
|
||||
if [ "$GO_VERSION_GO_MOD" != "$GO_VERSION_SETUP_GO" ]; then
|
||||
error "Go version mismatch between go.mod and .github/actions/setup-go/action.yaml"
|
||||
fi
|
||||
|
||||
# At the time of writing, Nix only constrains the major.minor version.
|
||||
# We need to check that specifically.
|
||||
if [ "$IGNORE_NIX" = "false" ]; then
|
||||
|
||||
Executable
+150
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script checks the mise values used by CI and dogfood images:
|
||||
# - mise.toml min_version is the source of truth for the mise version.
|
||||
# - .github/actions/setup-mise/checksums.toml stores pinned binary checksums.
|
||||
# - .github/actions/setup-mise/action.yml
|
||||
# - flake.nix
|
||||
# - scripts/dogfood/mise-oci-wrapper.sh
|
||||
# - dogfood/coder/ubuntu-*/Dockerfile.base
|
||||
|
||||
set -euo pipefail
|
||||
# shellcheck source=scripts/lib.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
||||
cdroot
|
||||
|
||||
check_not_empty() {
|
||||
local label="$1"
|
||||
local value="$2"
|
||||
|
||||
log "INFO : ${label}: ${value}"
|
||||
if [[ -z "${value}" ]]; then
|
||||
error "Missing mise value for ${label}"
|
||||
fi
|
||||
}
|
||||
|
||||
check_equal() {
|
||||
local label="$1"
|
||||
local actual="$2"
|
||||
local expected="$3"
|
||||
|
||||
check_not_empty "${label}" "${actual}"
|
||||
if [[ "${actual}" != "${expected}" ]]; then
|
||||
error "Mise mismatch for ${label}: expected ${expected}, got ${actual}"
|
||||
fi
|
||||
}
|
||||
|
||||
check_sha256_format() {
|
||||
local label="$1"
|
||||
local value="$2"
|
||||
|
||||
if [[ -z "${value}" ]]; then
|
||||
error "Missing mise value for ${label}"
|
||||
fi
|
||||
if [[ ! "${value}" =~ ^[a-f0-9]{64}$ ]]; then
|
||||
error "Expected 64-character lowercase SHA256 for ${label}: ${value}"
|
||||
fi
|
||||
}
|
||||
|
||||
mise_version="$(sed -n 's/^min_version = "\([^"]*\)"/\1/p' mise.toml)"
|
||||
check_not_empty "mise.toml min_version" "${mise_version}"
|
||||
|
||||
action_version="$(
|
||||
awk '
|
||||
$1 == "mise-version:" { in_input = 1; next }
|
||||
in_input && /^ [A-Za-z0-9_-]+:/ { exit }
|
||||
in_input && $1 == "default:" {
|
||||
gsub(/"/, "", $2)
|
||||
print $2
|
||||
exit
|
||||
}
|
||||
' .github/actions/setup-mise/action.yml
|
||||
)"
|
||||
check_equal ".github/actions/setup-mise/action.yml" "${action_version}" "${mise_version}"
|
||||
|
||||
checksum_version="$(
|
||||
awk -v version="${mise_version}" '
|
||||
$0 == "[\"" version "\"]" {
|
||||
print version
|
||||
exit
|
||||
}
|
||||
' .github/actions/setup-mise/checksums.toml
|
||||
)"
|
||||
check_equal ".github/actions/setup-mise/checksums.toml" "${checksum_version}" "${mise_version}"
|
||||
|
||||
declare -A setup_mise_checksums=()
|
||||
for target in linux-x64 linux-arm64 macos-x64 macos-arm64 windows-x64; do
|
||||
checksum="$(./scripts/mise_checksum.sh .github/actions/setup-mise/checksums.toml "${mise_version}" "${target}")"
|
||||
check_not_empty ".github/actions/setup-mise/checksums.toml ${target}" "${checksum}"
|
||||
check_sha256_format ".github/actions/setup-mise/checksums.toml ${target}" "${checksum}"
|
||||
setup_mise_checksums["${target}"]="${checksum}"
|
||||
done
|
||||
linux_x64_checksum="${setup_mise_checksums["linux-x64"]}"
|
||||
|
||||
sri_sha256_to_hex() {
|
||||
local label="$1"
|
||||
local sri="$2"
|
||||
|
||||
if [[ "${sri}" != sha256-* ]]; then
|
||||
error "Expected SRI SHA256 hash for ${label}: ${sri}"
|
||||
fi
|
||||
|
||||
printf '%s' "${sri#sha256-}" | openssl base64 -A -d | od -An -tx1 -v | tr -d ' \n'
|
||||
}
|
||||
|
||||
flake_version="$(
|
||||
awk '
|
||||
/^[[:space:]]*mise = / { in_mise = 1; next }
|
||||
in_mise && /^[[:space:]]*version = / {
|
||||
gsub(/[";]/, "", $3)
|
||||
print $3
|
||||
exit
|
||||
}
|
||||
in_mise && /^[[:space:]]*};/ { exit }
|
||||
' flake.nix
|
||||
)"
|
||||
check_equal "flake.nix" "${flake_version}" "${mise_version}"
|
||||
|
||||
declare -A flake_targets=(
|
||||
["x86_64-linux"]="linux-x64"
|
||||
["aarch64-linux"]="linux-arm64"
|
||||
["x86_64-darwin"]="macos-x64"
|
||||
["aarch64-darwin"]="macos-arm64"
|
||||
)
|
||||
for system in "${!flake_targets[@]}"; do
|
||||
target="${flake_targets[${system}]}"
|
||||
expected_checksum="${setup_mise_checksums[${target}]}"
|
||||
|
||||
flake_hash="$(
|
||||
awk -v nix_system="${system}" '
|
||||
/^[[:space:]]*hash = \{/ { in_hash = 1; next }
|
||||
in_hash && $1 == nix_system {
|
||||
gsub(/[";]/, "", $3)
|
||||
print $3
|
||||
exit
|
||||
}
|
||||
in_hash && /^[[:space:]]*};/ { exit }
|
||||
' flake.nix
|
||||
)"
|
||||
check_not_empty "flake.nix ${system} hash" "${flake_hash}"
|
||||
|
||||
actual_checksum="$(sri_sha256_to_hex "flake.nix ${system}" "${flake_hash}")"
|
||||
check_equal "flake.nix ${system} sha256" "${actual_checksum}" "${expected_checksum}"
|
||||
done
|
||||
|
||||
wrapper_version="$(sed -n 's/^MISE_VERSION="v\([^"]*\)"/\1/p' scripts/dogfood/mise-oci-wrapper.sh)"
|
||||
check_equal "scripts/dogfood/mise-oci-wrapper.sh" "${wrapper_version}" "${mise_version}"
|
||||
wrapper_checksum="$(sed -n 's/^MISE_SHA256="\([a-f0-9]*\)"/\1/p' scripts/dogfood/mise-oci-wrapper.sh)"
|
||||
check_equal "scripts/dogfood/mise-oci-wrapper.sh sha256" "${wrapper_checksum}" "${linux_x64_checksum}"
|
||||
check_sha256_format "scripts/dogfood/mise-oci-wrapper.sh sha256" "${wrapper_checksum}"
|
||||
|
||||
for dockerfile in dogfood/coder/ubuntu-*/Dockerfile.base; do
|
||||
dockerfile_version="$(sed -n 's/.*MISE_VERSION=v\([0-9.]*\).*/\1/p' "${dockerfile}" | head -n 1)"
|
||||
check_equal "${dockerfile}" "${dockerfile_version}" "${mise_version}"
|
||||
|
||||
dockerfile_checksum="$(sed -n 's/.*MISE_SHA256=\([a-f0-9]*\).*/\1/p' "${dockerfile}" | head -n 1)"
|
||||
check_equal "${dockerfile} sha256" "${dockerfile_checksum}" "${linux_x64_checksum}"
|
||||
check_sha256_format "${dockerfile} sha256" "${dockerfile_checksum}"
|
||||
done
|
||||
|
||||
log "Mise version check passed, all versions are ${mise_version}"
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Print the pinned mise SHA256 checksum for a version and release target.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$#" -ne 3 ]]; then
|
||||
echo "usage: $0 <checksums.toml> <mise-version> <target>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
checksums_file="$1"
|
||||
mise_version="$2"
|
||||
target="$3"
|
||||
|
||||
awk -F= -v version="${mise_version}" -v target="${target}" '
|
||||
$0 == "[\"" version "\"]" { in_table = 1; next }
|
||||
/^\[/ { in_table = 0 }
|
||||
in_table {
|
||||
key = $1
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
||||
if (key == target) {
|
||||
value = $2
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
||||
gsub(/^"|"$/, "", value)
|
||||
print value
|
||||
exit
|
||||
}
|
||||
}
|
||||
' "${checksums_file}"
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script determines if a commit in either the main branch or a
|
||||
# `release/x.y` branch should be deployed to dogfood.
|
||||
# This script determines if the current branch should be deployed to dogfood.
|
||||
#
|
||||
# To avoid masking unrelated failures, this script will return 0 in either case,
|
||||
# and will print `DEPLOY` or `NOOP` to stdout.
|
||||
@@ -11,59 +10,16 @@ set -euo pipefail
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
||||
cdroot
|
||||
|
||||
deploy_branch=main
|
||||
|
||||
# Determine the current branch name and check that it is one of the supported
|
||||
# branch names.
|
||||
branch_name=$(git branch --show-current)
|
||||
|
||||
if [[ "$branch_name" != "main" && ! "$branch_name" =~ ^release/[0-9]+\.[0-9]+$ ]]; then
|
||||
error "Current branch '$branch_name' is not a supported branch name for dogfood, must be 'main' or 'release/x.y'"
|
||||
fi
|
||||
log "Current branch '$branch_name'"
|
||||
|
||||
# Determine the remote name
|
||||
remote=$(git remote -v | grep coder/coder | awk '{print $1}' | head -n1)
|
||||
if [[ -z "${remote}" ]]; then
|
||||
error "Could not find remote for coder/coder"
|
||||
fi
|
||||
log "Using remote '$remote'"
|
||||
|
||||
# Step 1: List all release branches and sort them by major/minor so we can find
|
||||
# the latest release branch.
|
||||
release_branches=$(
|
||||
git branch -r --format='%(refname:short)' |
|
||||
grep -E "${remote}/release/[0-9]+\.[0-9]+$" |
|
||||
sed "s|${remote}/||" |
|
||||
sort -V
|
||||
)
|
||||
|
||||
# As a sanity check, release/2.26 should exist.
|
||||
if ! echo "$release_branches" | grep "release/2.26" >/dev/null; then
|
||||
error "Could not find existing release branches. Did you run 'git fetch -ap ${remote}'?"
|
||||
fi
|
||||
|
||||
latest_release_branch=$(echo "$release_branches" | tail -n 1)
|
||||
latest_release_branch_version=${latest_release_branch#release/}
|
||||
log "Latest release branch: $latest_release_branch"
|
||||
log "Latest release branch version: $latest_release_branch_version"
|
||||
|
||||
# Step 2: check if a matching tag `v<x.y>.0` exists. If it does not, we will
|
||||
# use the release branch as the deploy branch.
|
||||
if ! git rev-parse "refs/tags/v${latest_release_branch_version}.0" >/dev/null 2>&1; then
|
||||
log "Tag 'v${latest_release_branch_version}.0' does not exist, using release branch as deploy branch"
|
||||
deploy_branch=$latest_release_branch
|
||||
else
|
||||
log "Matching tag 'v${latest_release_branch_version}.0' exists, using main as deploy branch"
|
||||
fi
|
||||
log "Deploy branch: $deploy_branch"
|
||||
|
||||
# Finally, check if the current branch is the deploy branch.
|
||||
log
|
||||
if [[ "$branch_name" != "$deploy_branch" ]]; then
|
||||
log "VERDICT: DO NOT DEPLOY"
|
||||
echo "NOOP" # stdout
|
||||
else
|
||||
# We no longer deploy release branches to dogfood, and instead test them on the
|
||||
# stable deployment.
|
||||
# TODO: once we're happy with the new deployment process, we can remove this
|
||||
# script and the related GitHub workflow.
|
||||
if [[ "$branch_name" == "main" ]]; then
|
||||
log "VERDICT: DEPLOY"
|
||||
echo "DEPLOY" # stdout
|
||||
else
|
||||
log "VERDICT: NOOP"
|
||||
echo "NOOP" # stdout
|
||||
fi
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Usage: ./zizmor.sh [args...]
|
||||
#
|
||||
# This script is a wrapper around the zizmor Docker image. Zizmor lints GitHub
|
||||
# actions workflows.
|
||||
#
|
||||
# We use Docker to run zizmor since it's written in Rust and is difficult to
|
||||
# install on Ubuntu runners without building it with a Rust toolchain, which
|
||||
# takes a long time.
|
||||
#
|
||||
# The repo is mounted at /repo and the working directory is set to /repo.
|
||||
|
||||
set -euo pipefail
|
||||
# shellcheck source=scripts/lib.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
||||
|
||||
cdroot
|
||||
|
||||
image_tag="ghcr.io/zizmorcore/zizmor:1.11.0"
|
||||
docker_args=(
|
||||
"--rm"
|
||||
"--volume" "$(pwd):/repo"
|
||||
"--workdir" "/repo"
|
||||
"--network" "host"
|
||||
)
|
||||
|
||||
if [[ -t 0 ]]; then
|
||||
docker_args+=("-it")
|
||||
fi
|
||||
|
||||
# If no GH_TOKEN is set, try to get one from `gh auth token`.
|
||||
if [[ "${GH_TOKEN:-}" == "" ]] && command -v gh &>/dev/null; then
|
||||
set +e
|
||||
GH_TOKEN="$(gh auth token)"
|
||||
export GH_TOKEN
|
||||
set -e
|
||||
fi
|
||||
|
||||
# Pass through the GitHub token if it's set, which allows zizmor to scan
|
||||
# imported workflows too.
|
||||
if [[ "${GH_TOKEN:-}" != "" ]]; then
|
||||
docker_args+=("--env" "GH_TOKEN")
|
||||
fi
|
||||
|
||||
logrun exec docker run "${docker_args[@]}" "$image_tag" "$@"
|
||||
Generated
+3
-2
@@ -3240,8 +3240,9 @@ export interface ConvertLoginRequest {
|
||||
/**
|
||||
* CreateAIProviderRequest is the payload for creating a new AI
|
||||
* provider. Name and Type are required. APIKeys carries the plaintext
|
||||
* keys for OpenAI/Anthropic providers; Bedrock providers authenticate
|
||||
* via Settings and must omit APIKeys.
|
||||
* keys for OpenAI/Anthropic providers; Bedrock and Copilot providers
|
||||
* must omit APIKeys (Bedrock authenticates via Settings, Copilot via
|
||||
* request-time GitHub OAuth tokens).
|
||||
*/
|
||||
export interface CreateAIProviderRequest {
|
||||
readonly type: AIProviderType;
|
||||
|
||||
@@ -34,7 +34,7 @@ export const ClickOnDownload: Story = {
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
canvas.getByRole("button", { name: "Download logs" }),
|
||||
canvas.getByRole("button", { name: "Download agent logs" }),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(args.download).toHaveBeenCalledWith(
|
||||
|
||||
@@ -58,7 +58,7 @@ export const DownloadAgentLogsButton: FC<DownloadAgentLogsButtonProps> = ({
|
||||
}}
|
||||
>
|
||||
<DownloadIcon />
|
||||
{isDownloading ? "Downloading..." : "Download logs"}
|
||||
{isDownloading ? "Downloading..." : "Download agent logs"}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ export const DownloadSelectedAgentLogsButton: FC<
|
||||
>
|
||||
<DownloadIcon />
|
||||
<span className="sr-only">
|
||||
{isDownloading ? "Downloading..." : "Download logs"}
|
||||
{isDownloading ? "Downloading..." : "Download agent logs"}
|
||||
</span>
|
||||
<ChevronDownIcon className="size-icon-sm" />
|
||||
</Button>
|
||||
|
||||
@@ -92,6 +92,7 @@ export const MultipleSessions: Story = {
|
||||
...MockSession,
|
||||
id: `session-${i}`,
|
||||
threads: i + 1,
|
||||
providers: i % 2 === 0 ? ["anthropic", "openai"] : ["anthropic"],
|
||||
last_prompt: [
|
||||
"But *can* I really fix it?",
|
||||
"Can you refactor the entire authentication module to use JWT tokens instead of session cookies?",
|
||||
|
||||
@@ -30,6 +30,33 @@ export const Default: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleProvider: Story = {
|
||||
args: {
|
||||
session: {
|
||||
...MockSession,
|
||||
providers: ["anthropic"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleProviders: Story = {
|
||||
args: {
|
||||
session: {
|
||||
...MockSession,
|
||||
providers: ["anthropic", "openai", "copilot"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyProviders: Story = {
|
||||
args: {
|
||||
session: {
|
||||
...MockSession,
|
||||
providers: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NullClient: Story = {
|
||||
args: {
|
||||
session: { ...MockSession, client: null },
|
||||
|
||||
@@ -63,17 +63,23 @@ export const ListSessionsRow: FC<ListSessionsRowProps> = ({
|
||||
</TableCell>
|
||||
<TableCell className="w-40 max-w-40">
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<Badge className="gap-1.5 max-w-full">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<AIBridgeProviderIcon
|
||||
provider={getProviderIconName(session.providers[0])}
|
||||
className="size-icon-xs"
|
||||
/>
|
||||
</div>
|
||||
<span className="truncate min-w-0">
|
||||
{getProviderDisplayName(session.providers[0])}
|
||||
</span>
|
||||
</Badge>
|
||||
{session.providers.length > 1 ? (
|
||||
<Badge className="max-w-full">
|
||||
{session.providers.length} providers
|
||||
</Badge>
|
||||
) : session.providers.length === 1 ? (
|
||||
<Badge className="gap-1.5 max-w-full">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<AIBridgeProviderIcon
|
||||
provider={getProviderIconName(session.providers[0])}
|
||||
className="size-icon-xs"
|
||||
/>
|
||||
</div>
|
||||
<span className="truncate min-w-0">
|
||||
{getProviderDisplayName(session.providers[0])}
|
||||
</span>
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-40 max-w-40">
|
||||
|
||||
+23
@@ -1,4 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { within } from "storybook/test";
|
||||
import { reactRouterParameters } from "storybook-addon-remix-react-router";
|
||||
import { withToaster } from "#/testHelpers/storybook";
|
||||
import { addableProviders } from "../components/addableProviderTypes";
|
||||
@@ -26,16 +27,38 @@ export const AddAnthropic: Story = {
|
||||
args: {
|
||||
provider: addableProviders.find((p) => p.value === "anthropic")!,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText("Add an Anthropic provider");
|
||||
},
|
||||
};
|
||||
|
||||
export const AddOpenAI: Story = {
|
||||
args: {
|
||||
provider: addableProviders.find((p) => p.value === "openai")!,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText("Add an OpenAI provider");
|
||||
},
|
||||
};
|
||||
|
||||
export const AddBedrock: Story = {
|
||||
args: {
|
||||
provider: addableProviders.find((p) => p.value === "bedrock")!,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText("Add an AWS Bedrock provider");
|
||||
},
|
||||
};
|
||||
|
||||
export const AddCopilot: Story = {
|
||||
args: {
|
||||
provider: addableProviders.find((p) => p.value === "copilot")!,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText("Add a GitHub Copilot provider");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,6 +16,9 @@ interface AddProviderPageViewProps {
|
||||
provider: AddableProvider;
|
||||
}
|
||||
|
||||
const indefiniteArticle = (word: string): string =>
|
||||
/^[aeiou]/i.test(word) ? "an" : "a";
|
||||
|
||||
const AddProviderPageView: React.FC<AddProviderPageViewProps> = ({
|
||||
provider,
|
||||
}) => {
|
||||
@@ -38,7 +41,9 @@ const AddProviderPageView: React.FC<AddProviderPageViewProps> = ({
|
||||
size="lg"
|
||||
src={getProviderIcon(provider.value)}
|
||||
/>
|
||||
<SettingsHeaderTitle>{`Add a ${provider.label} provider`}</SettingsHeaderTitle>
|
||||
<SettingsHeaderTitle>{`Add ${indefiniteArticle(
|
||||
provider.label,
|
||||
)} ${provider.label} provider`}</SettingsHeaderTitle>
|
||||
</div>
|
||||
<p className="text-sm text-content-secondary m-0">
|
||||
Configure connection details and credentials.
|
||||
|
||||
+16
@@ -5,6 +5,7 @@ import type { AIProvider } from "#/api/typesGenerated";
|
||||
import {
|
||||
MockAIProviderAnthropic,
|
||||
MockAIProviderBedrock,
|
||||
MockAIProviderCopilot,
|
||||
MockAIProviderOpenAI,
|
||||
} from "#/testHelpers/entities";
|
||||
import { withToaster } from "#/testHelpers/storybook";
|
||||
@@ -53,6 +54,21 @@ export const Bedrock: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
// Copilot has no stored credential, so the edit form renders no API key
|
||||
// field and keeps the immutable name disabled.
|
||||
export const Copilot: Story = {
|
||||
parameters: {
|
||||
reactRouter: routingFor(`/ai/settings/${MockAIProviderCopilot.name}`),
|
||||
...seed(MockAIProviderCopilot),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const name = await canvas.findByLabelText(/^name/i);
|
||||
expect(name).toBeDisabled();
|
||||
expect(canvas.queryByLabelText(/api key/i)).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
// No seeded query: the page renders the loader while useQuery fetches.
|
||||
export const Loading: Story = {
|
||||
parameters: {
|
||||
|
||||
+8
-4
@@ -43,8 +43,12 @@ const UpdateProviderPageView: React.FC = () => {
|
||||
});
|
||||
|
||||
const provider = providerQuery.data;
|
||||
const providerIsOpenAiAnthropic =
|
||||
provider !== undefined && !isBedrockProvider(provider);
|
||||
// Copilot has no stored credential, and Bedrock keeps its secrets in
|
||||
// settings, so only the remaining types surface the api_keys UI.
|
||||
const providerUsesApiKeys =
|
||||
provider !== undefined &&
|
||||
!isBedrockProvider(provider) &&
|
||||
provider.type !== "copilot";
|
||||
|
||||
const updateMutation = useMutation(
|
||||
updateAIProviderMutation(queryClient, providerId ?? ""),
|
||||
@@ -109,8 +113,8 @@ const UpdateProviderPageView: React.FC = () => {
|
||||
}
|
||||
|
||||
const openAiAnthropicSavedApiKey =
|
||||
providerIsOpenAiAnthropic && provider.api_keys.length > 0;
|
||||
const openAiAnthropicMaskedApiKey = providerIsOpenAiAnthropic
|
||||
providerUsesApiKeys && provider.api_keys.length > 0;
|
||||
const openAiAnthropicMaskedApiKey = providerUsesApiKeys
|
||||
? provider.api_keys[0]?.masked
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -64,6 +64,38 @@ export const EditBedrockKeepCredentials: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const AddCopilot: Story = {
|
||||
args: {
|
||||
// The real add flow passes only the type; the form fills name and
|
||||
// endpoint from the copilot defaults.
|
||||
initialValues: { type: "copilot" },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByLabelText(/endpoint/i);
|
||||
expect(canvas.queryByLabelText(/api key/i)).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const EditCopilot: Story = {
|
||||
args: {
|
||||
editing: true,
|
||||
initialValues: {
|
||||
type: "copilot",
|
||||
name: "copilot",
|
||||
displayName: "GitHub Copilot",
|
||||
baseUrl: "https://api.business.githubcopilot.com",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const name = await canvas.findByLabelText(/^name/i);
|
||||
expect(name).toBeDisabled();
|
||||
expect(canvas.queryByLabelText(/api key/i)).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const EditProvider: Story = {
|
||||
args: {
|
||||
editing: true,
|
||||
|
||||
@@ -81,6 +81,10 @@ const providerDefaults: Partial<
|
||||
name: "azure",
|
||||
baseUrl: "https://YOUR-RESOURCE.openai.azure.com/openai/v1",
|
||||
},
|
||||
copilot: {
|
||||
name: "copilot",
|
||||
baseUrl: "https://api.business.githubcopilot.com",
|
||||
},
|
||||
google: {
|
||||
name: "google",
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai/",
|
||||
@@ -164,6 +168,20 @@ const makeBedrockSchema = (editing: boolean) =>
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
const makeCopilotSchema = (editing: boolean) =>
|
||||
Yup.object({
|
||||
type: Yup.string()
|
||||
.oneOf(["copilot"] as const)
|
||||
.required(),
|
||||
name: makeNameSchema(editing),
|
||||
displayName: makeDisplayNameSchema(editing),
|
||||
baseUrl: Yup.string()
|
||||
.url("Endpoint must be a valid URL")
|
||||
.matches(HTTP_SCHEME_REGEX, "Endpoint must use http or https.")
|
||||
.required("Endpoint is required"),
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
const getProviderFormSchema = (editing: boolean) =>
|
||||
Yup.lazy((value: { type?: AIProviderType } | undefined) => {
|
||||
switch (value?.type) {
|
||||
@@ -177,6 +195,8 @@ const getProviderFormSchema = (editing: boolean) =>
|
||||
return makeOpenAiAnthropicSchema(editing);
|
||||
case "bedrock":
|
||||
return makeBedrockSchema(editing);
|
||||
case "copilot":
|
||||
return makeCopilotSchema(editing);
|
||||
default:
|
||||
return Yup.object({
|
||||
type: Yup.string()
|
||||
@@ -185,6 +205,7 @@ const getProviderFormSchema = (editing: boolean) =>
|
||||
"anthropic",
|
||||
"bedrock",
|
||||
"azure",
|
||||
"copilot",
|
||||
"google",
|
||||
"openai-compat",
|
||||
"openrouter",
|
||||
@@ -319,18 +340,38 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
required
|
||||
field={getFieldHelpers("baseUrl")}
|
||||
label="Endpoint"
|
||||
description="The base URL where the provider's API is hosted."
|
||||
description={
|
||||
typeSelectValue === "copilot" ? (
|
||||
<>
|
||||
The base URL for your Copilot tier:{" "}
|
||||
<code>https://api.individual.githubcopilot.com</code>,{" "}
|
||||
<code>https://api.business.githubcopilot.com</code>, or{" "}
|
||||
<code>https://api.enterprise.githubcopilot.com</code>.
|
||||
</>
|
||||
) : (
|
||||
"The base URL where the provider's API is hosted."
|
||||
)
|
||||
}
|
||||
className="w-full"
|
||||
placeholder={baseUrlPlaceholder(form.values.type)}
|
||||
/>
|
||||
<CredentialField
|
||||
required
|
||||
label="API key"
|
||||
helpers={getFieldHelpers("apiKey")}
|
||||
onFocus={() => handleCredentialFocus("apiKey")}
|
||||
autoComplete="new-password"
|
||||
placeholder={apiKeyPlaceholder(form.values.type)}
|
||||
/>
|
||||
{typeSelectValue === "copilot" ? (
|
||||
<p className="text-sm text-content-secondary m-0">
|
||||
Copilot authenticates with each user's GitHub OAuth token at
|
||||
request time, so there is no API key to configure here. This
|
||||
requires a GitHub external authentication provider to be
|
||||
configured.
|
||||
</p>
|
||||
) : (
|
||||
<CredentialField
|
||||
required
|
||||
label="API key"
|
||||
helpers={getFieldHelpers("apiKey")}
|
||||
onFocus={() => handleCredentialFocus("apiKey")}
|
||||
autoComplete="new-password"
|
||||
placeholder={apiKeyPlaceholder(form.values.type)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -409,7 +450,7 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
disabled={isLoading || !form.dirty || !form.isValid}
|
||||
disabled={isLoading || !form.isValid || (editing && !form.dirty)}
|
||||
type="submit"
|
||||
>
|
||||
<Spinner loading={isLoading} />
|
||||
|
||||
@@ -27,6 +27,12 @@ export const Bedrock: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Copilot: Story = {
|
||||
args: {
|
||||
provider: "copilot",
|
||||
},
|
||||
};
|
||||
|
||||
export const Azure: Story = {
|
||||
args: {
|
||||
provider: "azure",
|
||||
|
||||
@@ -15,6 +15,8 @@ export const getProviderIcon = (provider: string): string | undefined => {
|
||||
return "/icon/aws.svg";
|
||||
case "azure":
|
||||
return "/icon/azure.svg";
|
||||
case "copilot":
|
||||
return "/icon/github-copilot.svg";
|
||||
case "google":
|
||||
return "/icon/google.svg";
|
||||
case "vercel":
|
||||
@@ -34,6 +36,8 @@ const getProviderName = (provider: string): string => {
|
||||
return "AWS Bedrock";
|
||||
case "azure":
|
||||
return "Azure OpenAI";
|
||||
case "copilot":
|
||||
return "GitHub Copilot";
|
||||
case "google":
|
||||
return "Google";
|
||||
case "openai-compat":
|
||||
|
||||
@@ -9,6 +9,7 @@ export const addableProviders: readonly AddableProvider[] = [
|
||||
{ value: "anthropic", label: "Anthropic" },
|
||||
{ value: "bedrock", label: "AWS Bedrock" },
|
||||
{ value: "azure", label: "Azure OpenAI" },
|
||||
{ value: "copilot", label: "GitHub Copilot" },
|
||||
{ value: "google", label: "Google" },
|
||||
{ value: "openai", label: "OpenAI" },
|
||||
{ value: "openai-compat", label: "OpenAI-compatible" },
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AIProvider } from "#/api/typesGenerated";
|
||||
import {
|
||||
MockAIProviderAnthropic,
|
||||
MockAIProviderBedrock,
|
||||
MockAIProviderCopilot,
|
||||
MockAIProviderOpenAI,
|
||||
} from "#/testHelpers/entities";
|
||||
import {
|
||||
@@ -45,6 +46,19 @@ const baseBedrockFormValues: ProviderFormValues = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const baseCopilotFormValues: ProviderFormValues = {
|
||||
type: "copilot",
|
||||
name: "copilot",
|
||||
displayName: "GitHub Copilot",
|
||||
baseUrl: "https://api.business.githubcopilot.com",
|
||||
model: "",
|
||||
smallFastModel: "",
|
||||
accessKey: "",
|
||||
accessKeySecret: "",
|
||||
apiKey: "",
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// Cast a plain object to AIProvider's discriminated `settings` shape;
|
||||
// the generated TS interface is empty and the wire form carries the
|
||||
// discriminator keys flattened in alongside the variant fields.
|
||||
@@ -173,6 +187,10 @@ describe("getProviderDisplayType", () => {
|
||||
expect(getProviderDisplayType(MockAIProviderOpenAI)).toBe("openai");
|
||||
});
|
||||
|
||||
it("returns copilot for a Copilot provider", () => {
|
||||
expect(getProviderDisplayType(MockAIProviderCopilot)).toBe("copilot");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["azure", "https://my-resource.openai.azure.com/openai/v1"],
|
||||
["azure", "https://YOUR-RESOURCE.openai.azure.com/openai/v1"],
|
||||
@@ -333,6 +351,31 @@ describe("providerFormValuesToCreate", () => {
|
||||
expect(req.api_keys).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Copilot", () => {
|
||||
it("maps to a distinct wire type with no api_keys", () => {
|
||||
const req = providerFormValuesToCreate(baseCopilotFormValues);
|
||||
expect(req.type).toBe("copilot");
|
||||
expect(req.base_url).toBe("https://api.business.githubcopilot.com");
|
||||
expect(req.api_keys).toBeUndefined();
|
||||
});
|
||||
|
||||
it("never sends api_keys even if the field carries a value", () => {
|
||||
const req = providerFormValuesToCreate({
|
||||
...baseCopilotFormValues,
|
||||
apiKey: "should-be-ignored",
|
||||
});
|
||||
expect(req.api_keys).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits display_name when blank so the server stores NULL", () => {
|
||||
const req = providerFormValuesToCreate({
|
||||
...baseCopilotFormValues,
|
||||
displayName: "",
|
||||
});
|
||||
expect(req.display_name).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("providerFormValuesToUpdate", () => {
|
||||
@@ -460,6 +503,18 @@ describe("providerFormValuesToUpdate", () => {
|
||||
expect(s.access_key_secret).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Copilot", () => {
|
||||
it("patches only the base fields and never sends api_keys", () => {
|
||||
const req = providerFormValuesToUpdate(
|
||||
{ ...baseCopilotFormValues, apiKey: "should-be-ignored" },
|
||||
MockAIProviderCopilot,
|
||||
);
|
||||
expect(req.api_keys).toBeUndefined();
|
||||
expect(req.settings).toBeUndefined();
|
||||
expect(req.base_url).toBe("https://api.business.githubcopilot.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("aiProviderToFormValues", () => {
|
||||
@@ -486,6 +541,14 @@ describe("aiProviderToFormValues", () => {
|
||||
expect(values.accessKeySecret).toBe("");
|
||||
});
|
||||
|
||||
it("seeds Copilot form values without a credential field", () => {
|
||||
const values = aiProviderToFormValues(MockAIProviderCopilot);
|
||||
expect(values.type).toBe("copilot");
|
||||
expect(values.name).toBe(MockAIProviderCopilot.name);
|
||||
expect(values.baseUrl).toBe(MockAIProviderCopilot.base_url);
|
||||
expect(values.apiKey).toBeUndefined();
|
||||
});
|
||||
|
||||
it("falls back to the slug when display_name is empty", () => {
|
||||
const provider: AIProvider = {
|
||||
...MockAIProviderOpenAI,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user