docs(webui): add config import/export design spec

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
m4
2026-08-12 19:01:53 +08:00
parent 3fec24dc2b
commit c9909f58f8
@@ -0,0 +1,224 @@
# 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:
```json
{
"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-..." }
]
}
}
```
```json
{
"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).