Files
EvoScientist-WebUI/docs/superpowers/specs/2026-08-12-config-import-export-design.md
T
2026-08-12 19:01:53 +08:00

9.1 KiB

Model & Image Config Import/Export — Design

Date: 2026-08-12 Status: Approved (brainstorming 2026-08-12, approach A)

Goal

Let an admin move the two whole-document configurations — the model registry (大模型参数) and the image-generation settings (图像参数) — between deployments via JSON files: export to a file, import from a file or pasted JSON, with replace-or-merge semantics.

Decisions locked in during brainstorming:

  • Approach A: backend adds export endpoints only; import reuses the existing, already-validated PUT write paths, with merge computed on the client.
  • Secrets included. Export files contain plaintext credentials. This deliberately breaks the backend's "APIs never return secrets" invariant — contained to two new admin-scoped export routes.
  • Import modes: both replace and merge, chosen by the user per import.
  • No remote/URL import. Local file upload + paste-JSON only; no server-side fetching, no SSRF surface.
  • Two separate files, exported/imported from their respective dialogs — no combined whole-instance bundle.

Feasibility baseline (verified)

  • Both configs are whole-document GET/PUT: GET/PUT /api/model-registry (RegistryV4, optimistic-locked by expected_revision) and GET/PUT /api/image-generation (ImageGenerationSettings).
  • Secrets exist server-side in plaintext: registry credentials in the SQLite credential_versions table (store.py:62, resolve_credential()), image api_key in the image-generation settings store (load_image_generation_settings).
  • Existing PUT semantics already help merge: blank image api_key keeps the stored key (http_api.py "blank api_key keeps the stored key"); registry credentials are only touched via explicit credential_writes.
  • Both backend config families live in one Config API module (EvoScientist/EvoScientist/model_registry/http_api.py).

File format

Two independent .json files with an envelope:

{
  "kind": "evoscientist.model-registry",
  "format_version": 1,
  "exported_at": "2026-08-12T09:00:00Z",
  "payload": {
    "registry": { "...": "RegistryV4 document" },
    "credentials": [
      { "credential_id": "openai-main", "secret_value": "sk-..." }
    ]
  }
}
{
  "kind": "evoscientist.image-generation",
  "format_version": 1,
  "exported_at": "2026-08-12T09:00:00Z",
  "payload": {
    "default_model": "dall-e-3",
    "timeout_seconds": 60,
    "models": [
      { "id": "...", "provider": "openai", "api_key": "sk-...", "...": "..." }
    ]
  }
}

kind acts as the file-type guard: importing a file whose kind does not match the dialog's expected kind is rejected before any further validation. format_version allows future evolution; only 1 is accepted.

Components

1. Backend export routes (EvoScientist, Python)

In model_registry/http_api.py, two new routes on the existing Config API:

  • GET /api/model-registry/export
  • GET /api/image-generation/export

Both require a new scope config:export (delegation JWT scope check, same mechanism as model_config:read etc.; missing scope → 403 via the existing section 9.5 error payload).

  • Registry export: current registry document plus, for every credential id referenced by credential_status, the latest active secret_value from credential_versions.
  • Image export: the stored image-generation settings with plaintext api_key values (the store holds them; only the public GET strips them).

Response body is the envelope JSON described above. Filenames and download mechanics are a client concern.

2. BFF proxy routes (WebUI, Next.js)

  • src/app/api/model-registry/export/route.ts
  • src/app/api/image-generation/export/route.ts

Each mirrors the existing sibling routes exactly: isCrossOrigin guard → requireActor → requireAdmin → configApiFetch(actor, path) → passthrough JSON with NO_STORE.

src/lib/server/delegation.ts: add SCOPE_CONFIG_EXPORT = "config:export" to ADMIN_SCOPES only (regular users never receive it).

No new BFF routes for import — the existing PUT routes are the import write path.

3. Frontend export

In the model-config dialog (RegistryEditor) and the image-models dialog (ImageModelsEditor), an "Export" button in the header area:

  • fetch("/api/model-registry/export") (or image equivalent) → Blob → anchor download as evoscientist-model-registry-<date>.json / evoscientist-image-generation-<date>.json.
  • Static warning text next to the button (i18n): the file contains plaintext secrets.

4. Frontend import

One shared ConfigImportDialog component, parameterized by kind:

  1. Input: file picker (.json) or a paste textarea; either fills the same parse step.
  2. Client validation (pure functions in src/lib/configTransfer.ts): JSON parseable → envelope fields (kind exact match, format_version: 1) → structural shape of payload (providers/models arrays with the required fields per the existing RegistryV4 / ImageModel mirrors in src/lib/modelRegistry.ts / src/lib/imageGeneration.ts). Failures render inline; no request is sent.
  3. Preview: counts (N providers / M models, or M image models) and a per-entry warning list for entries missing secrets (imported file has no credential for a provider that requires one; image model with empty api_key and no existing stored key).
  4. Mode choice: radio "Replace" / "Merge" (i18n, with one-line explanations).
  5. Write:
    • Replace: the imported payload becomes the PUT body directly — registry: PUT /api/model-registry with the current expected_revision (freshly GET-ted at import time) and credential_writes from the imported credentials; image: PUT /api/image-generation with the imported models as-is.
    • Merge: client GETs the current config, merges by id — an imported provider/model replaces the same-id entry wholesale, new ids are appended, unmentioned entries are kept — then PUTs the merged document. Credentials: imported secrets go to credential_writes; providers kept but absent from the import keep their existing credentials untouched. Image models whose imported api_key is empty send api_key: "", which the backend interprets as "keep stored key".
  6. Revision conflict (expected_revision mismatch on PUT): show "configuration changed by someone else" and offer reload — no silent retry.

Merge logic lives in src/lib/configTransfer.ts as pure, unit-tested functions: mergeRegistry(current, imported), mergeImageSettings(current, imported), parseRegistryExport(text), parseImageExport(text).

5. i18n

New keys under a configTransfer namespace in both zh/en catalogs (updated in lockstep): buttons, warnings, import dialog copy (steps, mode labels, errors, success toasts).

Data flow

Export:  UI button → BFF GET /api/*/export (admin, config:export JWT)
         → backend reads store + secrets → envelope JSON → file download

Import:  file/paste → client parse+validate → preview → mode choice
         → [replace] payload + fresh expected_revision → existing PUT
         → [merge]   GET current → merge in client → existing PUT
         → backend section 9.2 verification + rollback on failure (unchanged)

Error handling

  • Malformed/wrong-kind/unsupported-version file: inline dialog error, no network call.
  • Backend PUT validation failure: surface the backend's section 9.5 message in the dialog.
  • Export endpoint for non-admin: 403 (scope enforced at backend; BFF also gates with requireAdmin).
  • Secrets-in-file warning shown at both export (static text) and import (preview notes which secrets will be written).

Security posture

  • The plaintext-secrets surface is exactly two GET routes behind a new admin-only scope; no other route changes its never-secrets behavior.
  • Export files are sensitive: the UI says so at the point of download. Storage/sharing hygiene is the operator's responsibility.
  • No remote import → no SSRF. File size: client rejects files over 1 MB (configs are tens of KB in practice).

Testing

  • Backend (pytest): export routes return registry/settings with plaintext secrets; 403 without config:export scope; 401 without valid delegation; empty-credential registry exports an empty credentials array. Import paths need no new backend tests (existing PUT tests stand).
  • WebUI (vitest, node env): configTransfer.ts pure functions — envelope/kind/version validation failures; merge semantics (override by id, append new, keep unmentioned, blank-key preservation); replace-mode body construction. BFF export route tests following the existing route-test pattern (actor/admin gating, proxying).
  • Manual: export → wipe → import (replace) round-trip on a dev deployment; merge behavior; zh/en copy; both dialogs.

Out of scope

  • Remote/URL import or export to a server.
  • A combined whole-instance bundle (registry + image + other config).
  • Secret encryption inside the export file.
  • Import/export for other config areas (MCP, channels, system config).