Phase 3 — BYO API Key
Goal
Let each user supply their own API key per backend, stored in EncryptedKVStore via the user_secrets plugin. Add the settings UI that lets them paste a key, test it, and clear it. Resolution priority at call time: per-user key → instance technical-user key → error.
Preconditions
Phases 1 and 2 shipped. The user_secrets plugin exists in the codebase (see src/codumentor/plugins/user_secrets/plugin.py and src/codumentor/core/secrets/store.py).
Deliverables
auth/byo_key.pyresolving per-user keys fromEncryptedKVStore.plugin.yamlupdated to declaredependencies: [user_secrets].- Resolution logic in
tools/base.py(or wherever phase 1 placed credential resolution) updated to follow the per-user → technical-user → error priority. x-external-agent-settingsweb component, placed underplacements: ["sidebar-panel"]or wherever existing plugin settings render (verify againstPluginSettingsPanel.tsxand theWIDGET_REGISTRY).- FastAPI routes for: list configured backends, set per-user key for a backend, clear per-user key, run an auth probe for a backend. Decide whether to reuse
user_secrets' existing CRUD routes or add plugin-scoped routes. Report the decision and rationale in the handoff. - UI surface in the settings panel: one row per backend showing "key set / not set," a "test" button, a paste-key dialog, a clear button.
- Tests: unit tests for the resolution priority, an E2E test for the settings panel basic flow.
New files
src/codumentor/plugins/external_agent/auth/byo_key.py
ui/plugins/external_agent/settings.tsx
tests/test_external_agent_auth_byo_key.py
ui/plugins/external_agent/e2e/byo_key.spec.ts (or ui/e2e/... per repo convention)
Touched:
src/codumentor/plugins/external_agent/plugin.yaml(add dependency)src/codumentor/plugins/external_agent/plugin.py(wirebyo_keyinto credential resolution)src/codumentor/plugins/external_agent/routes.py(add credential CRUD routes if chosen)ui/plugins/external_agent/manifest.json(declare the settings widget)ui/src/components/settings/PluginSettingsPanel.tsxWIDGET_REGISTRY(register the widget) — see existing entries
Implementation steps
- Decide CRUD route shape first. Read
src/codumentor/plugins/user_secrets/plugin.py. If its routes can be reused with a naming convention likeexternal_agent/<backend>/api_key, prefer that — less surface to maintain. If reuse forces awkward UX (e.g., users seeing raw secret names in a generic UI), expose plugin-scoped routes. Document the decision in the handoff.
auth/byo_key.py: aresolve(user_id, backend) -> Credentials | Nonefunction that hitsEncryptedKVStoreforexternal_agent/<backend>/api_key. ReturnNoneif no per-user key set.
- Resolution priority: in the existing credential resolution path (added in phase 1), call
byo_key.resolvefirst; ifNone, fall back totechnical_user.resolve; if that is alsoNone, raise an error that produces aplugin.external_agent.errorevent with kindauth_requiredpayload (this is a new optional payload field, not a new event kind —erroralready exists).
- Settings UI (
settings.tsx): list each backend configured incodumentor.yaml. For each, show the current state ("system key in use" / "your key set" / "not configured"), with controls to set/clear/test. The test button calls the existingauth/proberoute with the per-user credentials applied.
- Tests: - Unit: priority order, missing key returns
None, key written and read back round-trip. - E2E: navigate to settings, paste a key, click test, see success; clear, see system key state.
Acceptance criteria
make testpasses, including the new E2E (runmake test-pre-committo confirm tier-2 still passes).- Manual probe: user A sets a per-user key, user B does not; A's
claude_codecalls use A's key (visible in usage logs against A's account), B's use the system key. - Clearing the key falls back cleanly to the system key.
Out of scope
- OAuth / browser-based login flow — phase 6.
- Cost attribution dashboards — not in this rollout.
- Per-user override of the binary path or extra_args — keep the per-user surface tight (key only).
Handoff
Fill out the template. Be specific in "Contracts established/changed" about:
- The exact
EncryptedKVStorename/key convention used (e.g.,external_agent/claude-code/api_key). - The new optional payload field on
errorevents (if you addedauth_required). - The route paths you exposed.
- Whether you reused
user_secretsroutes or added plugin-scoped ones.