Skip to content

feat: add AI providers HTTP CRUD handlers#24894

Merged
dannykopping merged 24 commits into
mainfrom
dk/aibridge-providers-handlers
May 20, 2026
Merged

feat: add AI providers HTTP CRUD handlers#24894
dannykopping merged 24 commits into
mainfrom
dk/aibridge-providers-handlers

Conversation

@dannykopping
Copy link
Copy Markdown
Contributor

@dannykopping dannykopping commented May 1, 2026

Disclaimer: implemented by a Coder Agent using Claude Opus 4.7

Part of the implementation of RFC: Common AI Provider Configs (AIGOV-201). Refined per Scope: AI Governance Settings API design.

What this PR does

Wires the AI provider configuration HTTP handlers under `/api/v2/ai/providers` in the AGPL code path, since Agents will need to use these providers even in OSS.

A provider's API keys are now created and replaced through the provider's own create/update payload. There are no separate `/keys` sub-endpoints: the on-disk split between `ai_providers` and `ai_provider_keys` is an implementation detail and is not surfaced to API consumers.

Endpoints:

Method Path Description
GET /api/v2/ai/providers List all (non-deleted) providers
POST /api/v2/ai/providers Create a new provider
GET /api/v2/ai/providers/{idOrName} Fetch a single provider
PATCH /api/v2/ai/providers/{idOrName} Partial update
DELETE /api/v2/ai/providers/{idOrName} Soft-delete

API shape

`AIProvider` responses include an `api_keys` array of masked strings (via `aibridge/utils.MaskSecret`); plaintext keys never leave the database.

Write payloads accept a plaintext `api_keys` array:

  • `CreateAIProviderRequest.api_keys` (optional): keys to register at create time.
  • `UpdateAIProviderRequest.api_keys` (optional, pointer): when present, replaces all existing keys with the supplied list inside the same transaction. An empty array clears every key. Omitting the field leaves keys untouched.

Bedrock providers authenticate via the encrypted `settings` blob, so any non-empty `api_keys` on a Bedrock provider is rejected with `400`.

PATCH semantics

The update payload uses pointer-and-presence wire conventions throughout, so callers can rotate non-secret fields without resending write-only secrets:

  • Field omitted or `null`: keep existing value. `encoding/json` collapses both forms into a nil pointer; the handler skips the merge.
  • Field present with empty string (`""`): explicit clear. This is how an admin migrates Bedrock from static `AccessKey` / `AccessKeySecret` to IAM role-based auth in a single PATCH.
  • Field present with a value: set / rotate to that value.

For the outer `settings` object: `"settings": null` and omitting the key both preserve the existing blob; `"settings": {}` is rejected with `400` because the discriminator (`_type`) is missing. The settings JSON is opaque to the wrapper and decoded via its `_type` discriminator, leaving room for non-Bedrock provider-specific configuration without breaking the contract.

The `api_keys` field on update is a full replace, not a merge: the supplied list describes the post-patch key set. Use `{ id: existingID }` to keep a key, `{ api_key: plaintext }` to insert a new one; any existing key whose ID is absent from the list is deleted.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

Docs preview

📖 View docs preview for docs/reference/api/aibridge.md

@dannykopping dannykopping force-pushed the dk/aibridge-providers-sdk branch from 73feffa to b401da5 Compare May 1, 2026 18:34
@dannykopping dannykopping force-pushed the dk/aibridge-providers-handlers branch from 6f5778a to 2ce6bfd Compare May 1, 2026 18:34
@dannykopping dannykopping force-pushed the dk/aibridge-providers-sdk branch from b401da5 to 4a69dee Compare May 1, 2026 19:24
@dannykopping dannykopping force-pushed the dk/aibridge-providers-handlers branch from 2ce6bfd to 66e791f Compare May 1, 2026 19:24
@dannykopping dannykopping changed the title feat(enterprise/coderd): add AI Bridge providers HTTP CRUD handlers feat: add AI Bridge providers HTTP CRUD handlers May 1, 2026
@dannykopping dannykopping force-pushed the dk/aibridge-providers-sdk branch 3 times, most recently from b697680 to 5221fc9 Compare May 13, 2026 14:44
@dannykopping dannykopping force-pushed the dk/aibridge-providers-handlers branch from 66e791f to 7270b26 Compare May 13, 2026 14:51
@dannykopping dannykopping force-pushed the dk/aibridge-providers-sdk branch from 5221fc9 to fb7c328 Compare May 13, 2026 15:06
@dannykopping dannykopping force-pushed the dk/aibridge-providers-handlers branch from 7270b26 to 1fe121a Compare May 13, 2026 15:06
@dannykopping dannykopping force-pushed the dk/aibridge-providers-sdk branch from fb7c328 to 9f6b23e Compare May 14, 2026 10:57
@dannykopping dannykopping force-pushed the dk/aibridge-providers-handlers branch from 1fe121a to 7ff75d3 Compare May 14, 2026 10:57
@dannykopping dannykopping force-pushed the dk/aibridge-providers-handlers branch from 7ff75d3 to ae1c48e Compare May 14, 2026 11:23
@dannykopping dannykopping force-pushed the dk/aibridge-providers-sdk branch 2 times, most recently from 42dbe58 to 4f9930f Compare May 14, 2026 11:41
@dannykopping dannykopping force-pushed the dk/aibridge-providers-handlers branch 2 times, most recently from 06a86cd to 75af16f Compare May 14, 2026 13:20
@dannykopping dannykopping force-pushed the dk/aibridge-providers-sdk branch 2 times, most recently from 02883ed to b79d62d Compare May 14, 2026 13:38
@dannykopping dannykopping force-pushed the dk/aibridge-providers-handlers branch from 75af16f to 35194e7 Compare May 14, 2026 13:38
dannykopping and others added 14 commits May 19, 2026 13:46
…provider_keys.go

The keys handlers are a distinct sub-resource; keep them in their
own file to make the providers file easier to navigate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The provider/keys CRUD does not depend on enterprise; move the
handlers, route registration, and tests into the AGPL package so
the feature ships in OSS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ruleguard requires uuid.UUID fields in slog records to use the
"_id" suffix. Rename the per-row log key from "id" to "provider_id".
Regenerate the API reference so the widened ai_provider_type enum
appears in the docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ints

Drop the /keys sub-resource. POST/PATCH /ai/providers now carry the
plaintext api_keys list and replace the provider's keys atomically
inside a named transaction. GET responses return masked keys via
aibridge utils.MaskSecret, so the on-disk split between ai_providers
and ai_provider_keys is no longer surfaced to API consumers. Bedrock
providers continue to reject any non-empty api_keys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the ad hoc strDeref/boolDeref helpers with the existing generic
ptr.NilToDefault. Inline nullStrDeref since its *string -> sql.NullString
conversion doesn't fit the generic shape and it was only used once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…list

The list/get response now returns each API key as {id, masked,
created_at} so clients can reference an existing row without
re-sending its plaintext. The PATCH api_keys field becomes a list of
AIProviderKeyMutation entries with exactly one of id or api_key set:
id keeps the existing key, api_key adds a new plaintext, omitted IDs
are deleted. Unknown IDs and duplicate IDs return 400 without
modifying any rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, a path like /api/v2/ai/providers/Bad_Name returned 404
because the regex check in lookupAIProvider produced sql.ErrNoRows.
Surface a dedicated sentinel and translate it to 400 so integrators
get a hint about the path shape instead of a misleading "not found".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oviders

The runtime dispatches by provider Type, so a Bedrock block on an
OpenAI-typed provider would sit encrypted in the database, unused. The
operator's authentication intent is silently dropped.

Cross-validate on create via codersdk Validate, and on update after the
patch is merged into the existing row's decoded settings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keys are stored verbatim. A value like " sk-real " passes the old
non-empty check but is then sent to OpenAI/Anthropic literally,
producing a 401 the operator has no signal about. Reject at the
validation boundary so the operator gets a clear error instead of a
masked-looking-correct credential.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, mergeAIProviderSettings treated an empty-string AccessKey
or AccessKeySecret as "leave existing value alone." That made it
impossible to migrate from static AWS credentials to IAM role-based
auth in a single PATCH: the only workaround was to clear settings
entirely (which also wiped Region and Model), then re-PATCH the
non-secret fields.

Switch both fields to pointers so the merge can distinguish "omitted"
(nil, keep existing) from "explicitly clear" (non-nil empty string).
Add internal unit tests covering preserve, rotate, and clear paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GetAIProviderKeys was a bare SELECT * over ai_provider_keys, returning
keys for soft-deleted providers too. Each call decrypted every row,
and the API list handler then discarded keys whose provider was gone
— so as soft-deleted providers accumulated, the decrypt work grew
monotonically.

Add an include_deleted parameter to the query. The list handler passes
false (default behaviour now: live providers only); the dbcrypt key
rotation utility passes true so it can still re-encrypt every FK
reference to dbcrypt_keys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… paths

- Replace writeAIProviderLookupError with writeAIProviderError, which
  takes parameterized log/response messages. The update handler now
  surfaces a message about updating, not fetching, when the inner
  tx.UpdateAIProvider or encodeAIProviderSettings call fails.
- Use errAIProviderKeyUnknown.Error() directly in the 400 body so the
  "execute transaction:" wrapper xerrors adds inside InTx does not
  leak to the API consumer.
- Drop the unreachable sql.ErrNoRows branch in the delete handler;
  DeleteAIProviderByID is :exec on a soft-delete UPDATE, which never
  returns that sentinel and is already idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ck secret redaction

The previous test substrings ("bedrock_access_key", "bedrock_access_key_secret")
do not appear in the JSON wire form, which uses access_key and
access_key_secret. The assertions passed vacuously and would not have
caught a redaction regression. Use the real field names with quotes to
avoid matching unrelated substrings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dannykopping dannykopping force-pushed the dk/aibridge-providers-handlers branch from c49b460 to c61758f Compare May 19, 2026 12:52
@dannykopping dannykopping marked this pull request as ready for review May 19, 2026 12:54
@dannykopping dannykopping requested a review from johnstcn May 19, 2026 12:54
@dannykopping dannykopping force-pushed the dk/aibridge-providers-handlers branch from c61758f to 610edd5 Compare May 19, 2026 13:03
@ibetitsmike
Copy link
Copy Markdown
Collaborator

/coder-agents-review

Copy link
Copy Markdown
Contributor

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13 of 15 findings addressed. Strong progress: the Bedrock+keys invariant bypass, type/settings cross-validation, write-only secret merge test, delete handler transaction, dead code removal, whitespace rejection, error messages, lookup validation, vacuous assertions, and credential clearing are all fixed.

Further review is blocked on 2 silent findings:

DEREM-3 (P1) Key rotation is still invisible in the audit log. The thread was marked [Resolved] in the GitHub UI but no code change targeting audit behavior exists on this branch. A commit 282e2c049 ("surface key add/remove/keep counts in audit log") exists in the repo but is not reachable from the PR head 610edd53. If this commit was intended as the fix, it needs to be rebased onto this branch. If the fix is planned for a follow-up PR, a linked ticket is needed.

DEREM-5 (P2) validateAIProviderBaseURL still accepts private/link-local IPs. The author replied "Yes, abstract that logic and reuse" but no code was pushed and no ticket was linked. If this is deferred to a follow-up, please link a ticket.

The panel will review after these findings are addressed or deferred with tickets.

🤖 This review was automatically generated with Coder Agents.

…r include_deleted arg

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dannykopping dannykopping force-pushed the dk/aibridge-providers-handlers branch from 610edd5 to a1a65c8 Compare May 19, 2026 13:22
Copy link
Copy Markdown
Member

@johnstcn johnstcn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some concerns that aren't blocking but would like to talk through them first.

Comment thread codersdk/aiproviders.go
Comment thread enterprise/audit/table.go
Comment on lines 13 to +32
@@ -23,10 +26,10 @@ type AIProviderBedrockSettings struct {
SmallFastModel string `json:"small_fast_model,omitempty"`
// AccessKey is the AWS access key ID used to authenticate against
// Bedrock. Write-only.
AccessKey string `json:"access_key,omitempty"`
AccessKey *string `json:"access_key,omitempty"`
// AccessKeySecret is the AWS secret access key paired with
// AccessKey. Write-only.
AccessKeySecret string `json:"access_key_secret,omitempty"`
AccessKeySecret *string `json:"access_key_secret,omitempty"`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather have separate request/response types where the response types do not include the secret material at all.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to return the masked creds since that'll help operators manage API keys without leaking them.
Objectionable?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair 👍 My only other worry would be the impact of exposing some characters of the secret material.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea here is to share just enough for operators to identify the key by it's {pre,suf}fix. We have comprehensive tests to ensure this behaviour doesn't change unexpectedly, leaking raw creds.

Comment thread codersdk/aiproviders.go
Comment on lines +314 to +316
// and free of leading/trailing whitespace. An empty slice itself is
// permitted: on create it means "no keys yet"; on update it means
// "clear all keys". Keys are stored verbatim; surrounding whitespace
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a potential footgun?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which aspect, specifically? The whitespace or the empty slice semantic?

Copy link
Copy Markdown
Member

@johnstcn johnstcn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving with previous food for thought.

@dannykopping dannykopping merged commit dd32234 into main May 20, 2026
26 of 28 checks passed
@dannykopping dannykopping deleted the dk/aibridge-providers-handlers branch May 20, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants