docs(webui): add config import/export design spec
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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).
|
||||
Reference in New Issue
Block a user