Skip to content

feat: BYOK provider-key storage for organizations - #78

Merged
man4ish merged 1 commit into
mainfrom
feature/m14-byok-provider-key-storage
Oct 4, 2026
Merged

man4ish merged 1 commit into
mainfrom
feature/m14-byok-provider-key-storage

Conversation

@man4ish

@man4ish man4ish commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

First slice of design-audit gap #4 ("No organisation-scoped BYOK provider-key service exists"). Model routing through the stored key (Claude/OpenAI client calls, token accounting, llm.tokens.* events) and exposing this through the public gateway's own /v1/provider-keys/{provider} path are explicitly follow-up work -- this PR is storage only.

  • PUT/GET/DELETE /orgs/{org_id}/provider-keys(/{provider}), gated on manage_org -- an org-wide infrastructure setting, the same administrative character manage_org already gates elsewhere (GET/PUT /orgs/{org_id} itself), not a self-service literature-API action.
  • Built on organization_config, a table that has existed since the multi-tenant schema migration with no service or route ever reading or writing it -- this is its first real consumer, no migration needed.
  • One provider/key slot per organization (mirrors GlobalConfig's own platform-wide shape in config_service.py) -- setting a new provider replaces whichever one was previously configured.
  • Write-only credentials: the response only ever reports has_key, never the key; crypto.encrypt() (the same module config_service.py already uses) raises a loud 500 rather than silently storing anything in plaintext when CONFIG_ENCRYPTION_KEY isn't configured.
  • Reachable today the same way Studio's own LLM settings page would be, through this org-admin endpoint directly -- not yet through the public /v1 surface.

Test plan

  • pytest tests/test_organization_provider_keys.py tests/test_route_authorization_coverage.py tests/test_config.py tests/test_apikeys.py tests/test_me_api_keys.py -- 59 passed, including permission gating, provider-name validation, the no-plaintext-fallback guarantee, a real encrypt/decrypt round-trip, single-slot replacement, and the updated org-scoped-route-count tripwire (44 -> 47 for the three new routes)
  • Full repo suite -- 1487 passed, 2 skipped, 1 pre-existing flaky MFA concurrency-timing test (confirmed unrelated last milestone, same test)

🤖 Generated with Claude Code

An organization can now store its own encrypted Claude/OpenAI key
(PUT/GET/DELETE /orgs/{org_id}/provider-keys), gated on manage_org --
an org-wide infrastructure setting, not a self-service literature-API
action. Built on the organization_config table, which has existed since
the multi-tenant schema migration with no service or route ever using
it. One provider/key slot per organization, mirroring GlobalConfig's
own platform-wide shape; setting a new provider replaces the previous
one. Credentials are write-only (has_key flags only, never echoed back)
and refused with 500 rather than stored in plaintext when
CONFIG_ENCRYPTION_KEY isn't configured, reusing the same crypto module
config_service.py already established.

Deliberately scoped to storage only: actually routing a
/v1/literature/answers call through the stored provider (token
accounting, llm.tokens.* events), and exposing this through the public
gateway's own /v1/provider-keys/{provider} path, are follow-up work --
this is reachable today the same way Studio's own LLM settings page
would be, through the org-admin endpoint directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@man4ish
man4ish merged commit 7a48d30 into main Oct 4, 2026
1 check passed
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.

1 participant