Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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 byexpected_revision) andGET/PUT /api/image-generation(ImageGenerationSettings). - Secrets exist server-side in plaintext: registry credentials in the
SQLite
credential_versionstable (store.py:62,resolve_credential()), imageapi_keyin the image-generation settings store (load_image_generation_settings). - Existing PUT semantics already help merge: blank image
api_keykeeps the stored key (http_api.py"blank api_key keeps the stored key"); registry credentials are only touched via explicitcredential_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/exportGET /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 activesecret_valuefromcredential_versions. - Image export: the stored image-generation settings with plaintext
api_keyvalues (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.tssrc/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 asevoscientist-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:
- Input: file picker (
.json) or a paste textarea; either fills the same parse step. - Client validation (pure functions in
src/lib/configTransfer.ts): JSON parseable → envelope fields (kindexact match,format_version: 1) → structural shape ofpayload(providers/models arrays with the required fields per the existingRegistryV4/ImageModelmirrors insrc/lib/modelRegistry.ts/src/lib/imageGeneration.ts). Failures render inline; no request is sent. - 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_keyand no existing stored key). - Mode choice: radio "Replace" / "Merge" (i18n, with one-line explanations).
- Write:
- Replace: the imported payload becomes the PUT body directly —
registry:
PUT /api/model-registrywith the currentexpected_revision(freshly GET-ted at import time) andcredential_writesfrom the imported credentials; image:PUT /api/image-generationwith 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 importedapi_keyis empty sendapi_key: "", which the backend interprets as "keep stored key".
- Replace: the imported payload becomes the PUT body directly —
registry:
- Revision conflict (
expected_revisionmismatch 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:exportscope; 401 without valid delegation; empty-credential registry exports an emptycredentialsarray. Import paths need no new backend tests (existing PUT tests stand). - WebUI (vitest, node env):
configTransfer.tspure 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).