Skip to content

feat(webui): provider management UI with keep-key semantics and hot-apply wiring - #37

Merged
fengzhi09 merged 3 commits into
mainfrom
feat/provider-management-ui
Sep 26, 2026
Merged

fengzhi09 merged 3 commits into
mainfrom
feat/provider-management-ui

Conversation

@fengzhi09

Copy link
Copy Markdown
Collaborator

What

Ticket 03 of the provider-configuration feature: the management UI, plus one API-hardening contract that the UI depends on.

  • Management panel in settings (new ~770-line provider-management.tsx): provider list (preset/custom badge, enabled toggle, protocol, auth, has-key), editor (auth-type switch, baseURL, per-model rows with thinkingLevels/modalities multi-select), test-connection with structured errors, soft-delete custom providers. 47 bilingual i18n keys.
  • Keep-key contract (server): auth.apiKey empty OR absent means "keep the existing key" (applyKeepKeyConvention, user-layer-only source so env secrets never persist to operator disk); deliberate key-clear is documented as impossible (delete-and-recreate). Masked keys never round-trip into the form.
  • Hot-apply wiring: providers.updated SSE frame → providersRevision in the store → composer re-fetches /api/models — the model selector updates WITHOUT page refresh (live-verified end to end).

Acceptance (independent agent, 2 rounds + hardening confirm)

Round 1 PASS-WITH-CONCERNS — the sentinel had a bypass: a PUT with auth.apiKey field ABSENT skipped convention+validation and silently wiped the stored key and models (live-proven). Round 2 hardening: absent = keep (probe re-verified live: disk key byte-identical), env-layer scoping restricted to the user file (env secrets zero-leak to disk), no-deliberate-clear documented. Pre-existing-failure claims refuted twice in the good direction (full suite 0 fail). Gates: 74/74 focused ×3 · test:webapp 253/253 · full suite 1494/0 fail · typechecks 0 · check:source ✓.

Known non-blocking: editing an env-layer provider's label is shadowed at merge time by the env layer's scalar (pre-existing merge semantics, noted).

Full pnpm verify deferred to CI. Note: this branch and #36 both touch routes/providers.js (different regions); CI verifies the merge.

Ticket 03 of the model-providers workstream. Adds the settings-modal
panel that lists, edits, adds, deletes and test-connects providers,
and wires the 'providers.updated' SSE frame into the store so the
model selector refreshes without a page reload.

Server side
- keep-existing-key convention: an incoming 'auth.apiKey === ""' on
  PUT means 'do not change the existing key for this provider id'.
  Implemented as a pure helper (lib/providers-config.js#
  applyKeepKeyConvention) called from handlePutProviders before
  validation. Existing key is preserved; non-empty value replaces.
  New providers with empty keys fail the normal byok validation.
  Cross-branch API note: convention lives on this branch only.
- 6 new lib tests + 2 new route tests pin the contract end-to-end
  (round-trip through PUT, masked-only responses, etc.).

Webapp side
- /api/providers + /api/providers/test typed wrappers in lib/api.ts.
- 'providers.updated' added to NAMED_EVENTS in lib/sse.ts with a new
  'providers-updated' SseAction. Store carries a providersRevision
  counter that bumps on the frame; composer re-fetches /api/models
  on revision change so the selector picks up new groups without
  reload.
- New 'providers' section in the settings modal (alongside general /
  appearance / connection). Left rail lists providers with
  preset/custom badge, protocol, auth type, has-key state, per-row
  test summary. Right pane edits auth (byok or coding-plan), baseURL,
  models list (id / label / contextLimit / thinking levels multi /
  modalities multi / add / remove). API key field is write-only:
  the masked value is the placeholder, never the controlled value.
- Per-protocol minimal probe inline: success latency, or mapped
  error (INVALID_KEY / BAD_PROTOCOL / PROBE_FAILED / timeout).
- Preset one-click enable degrades gracefully when the sibling
  branch's endpoints are not yet mounted (404 -> hide section).
- Pure helpers in lib/provider-management.ts (validation,
  draftToWire, describeTestOutcome, draftFromView, newDraftProvider).
  Tested in isolation: 37 webapp tests covering keep-existing
  sentinel, sentinel-not-leaked-into-controlled-field, model-row
  validation, enum value pinning, and the SSE frame parser.

Inventory regenerated via 'node scripts/source-inventory.mjs --write'
(reviewed 3 new file paths: provider-management.tsx,
provider-management.ts, provider-management.test.ts).
…ayer scoping)

Acceptance round-2 hardening for ticket 03's PUT convention.

Two gaps closed:

1. ABSENT auth.apiKey. The v2 normaliser coerces a missing apiKey
   to the empty string before validation, so a PUT body that omits
   the field used to silently wipe a stored credential AND drop the
   model array (the malformed branch also failed the inner models
   shape on some paths). The convention now treats absent-field as
   the sentinel — same as the explicit empty string. Test pins
   this end-to-end through handlePutProviders.

2. ENV-LAYER SCOPING. The convention's 'previous key' lookup used to
   read the merged catalogue. That meant editing a provider whose
   key is sourced from the env or cwd layer would materialise the
   deployment secret into the user-level file — once written there,
   the deployment layer can no longer rotate it. The convention now
   reads the user-level file only via a new loadUserLevelProviders()
   helper. The merged view still wins for the engine
   (loadProvidersConfig priority order), so visible behaviour for the
   operator is unchanged: env-defined keys still win at read time
   even after the user edits.

DELIBERATE KEY CLEARING is impossible by design: every wire shape
that lacks an explicit apiKey inherits the stored key. Rotating a
credential means PUTting the new value. Documented in the API note
and in the editor placeholder ('Stored as-is — leave blank (or
omit the field) to keep the existing key. Clearing is not supported.').

Tests (3x confirmed 0 fail):
- providers-config.test.js: 55 (added: absent-field copies key;
  absent-field keeps empty on new; absent-auth entirely; deliberate
  clear remains impossible; loadUserLevelProviders missing /
  user-only / env-invisible).
- providers.check.mjs: 19 (added: absent-field PUT preserves key
  end-to-end; env-layer key NOT materialised to user-level file).
- webapp typecheck + repo typecheck clean; full pnpm test:webapp
  253 pass; full pnpm test 1494 pass / 0 fail.
…h preset routes)

PR #36 added preset provider templates alongside the existing
provider routes, conflicting with this branch's edits to
packages/webui/server/routes/providers.js (the keep-existing-key
convention in handlePutProviders). Resolution:

- routes/providers.js imports: union of both sides. The keep-key
  helpers (applyKeepKeyConvention, loadUserLevelProviders) from this
  branch plus normaliseProvider (used by handleEnablePreset) from
  main are all imported from lib/providers-config.js.
- The new handleGetPresets / handleEnablePreset handlers from main
  coexist with this branch's handlePutProviders / handleTestProvider
  intact — both sides' logic survives.
- routes/providers.js exports: re-export the preset handlers from
  this single module so the Hono app can mount them.

Gates (post-merge):
- app-hono.test.js: OWNED_ROUTES includes both sides; app.get /
  app.put / app.post calls for /api/providers, /api/providers/test,
  /api/providers/presets, /api/providers/preset/:id/enable all
  present (auto-merged clean).
- providers-config + providers.check: 74 pass / 0 fail × 3.
- app-hono: 85 pass / 0 fail.
- full pnpm test:webapp: 253 pass / 0 fail.
- full pnpm test: 1545 pass / 0 fail / 2 skipped (no regressions).
- pnpm webapp:typecheck + pnpm typecheck: clean.
- node scripts/source-inventory.mjs: clean.
@fengzhi09
fengzhi09 merged commit e58136e into main Sep 26, 2026
8 checks passed
@fengzhi09
fengzhi09 deleted the feat/provider-management-ui branch September 26, 2026 08:56
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