feat: add AI providers HTTP CRUD handlers#24894
Conversation
Docs preview📖 View docs preview for |
73feffa to
b401da5
Compare
6f5778a to
2ce6bfd
Compare
b401da5 to
4a69dee
Compare
2ce6bfd to
66e791f
Compare
b697680 to
5221fc9
Compare
66e791f to
7270b26
Compare
5221fc9 to
fb7c328
Compare
7270b26 to
1fe121a
Compare
fb7c328 to
9f6b23e
Compare
1fe121a to
7ff75d3
Compare
7ff75d3 to
ae1c48e
Compare
42dbe58 to
4f9930f
Compare
06a86cd to
75af16f
Compare
02883ed to
b79d62d
Compare
75af16f to
35194e7
Compare
…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>
c49b460 to
c61758f
Compare
c61758f to
610edd5
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
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>
610edd5 to
a1a65c8
Compare
johnstcn
left a comment
There was a problem hiding this comment.
I have some concerns that aren't blocking but would like to talk through them first.
| @@ -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"` | |||
There was a problem hiding this comment.
I'd rather have separate request/response types where the response types do not include the secret material at all.
There was a problem hiding this comment.
I'd like to return the masked creds since that'll help operators manage API keys without leaking them.
Objectionable?
There was a problem hiding this comment.
Fair 👍 My only other worry would be the impact of exposing some characters of the secret material.
There was a problem hiding this comment.
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.
| // 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 |
There was a problem hiding this comment.
This feels like a potential footgun?
There was a problem hiding this comment.
Which aspect, specifically? The whitespace or the empty slice semantic?
johnstcn
left a comment
There was a problem hiding this comment.
Approving with previous food for thought.

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:
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:
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:
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.