refactor(hermes_cli/web_models): compact field comments and class spacing; JSON schemas byte-identical

This commit is contained in:
Teknium
2026-09-02 21:09:29 -07:00
parent a05191ac1c
commit 2c77cbdcb7
+47 -170
View File
@@ -16,35 +16,28 @@ class ConfigUpdate(BaseModel):
config: dict
profile: Optional[str] = None
class EnvVarUpdate(BaseModel):
key: str
value: str
profile: Optional[str] = None
# Bearer key for the connectivity probe of a custom/local endpoint
# (``key == "OPENAI_BASE_URL"``): endpoints that gate ``/v1/models`` behind
# auth otherwise look "reachable but empty". Ignored by plain PUT /api/env.
# Bearer key for the connectivity probe when ``key == "OPENAI_BASE_URL"`` (auth-gated
# ``/v1/models`` otherwise looks "reachable but empty"). Ignored by plain PUT /api/env.
api_key: str = ""
class EnvVarDelete(BaseModel):
key: str
profile: Optional[str] = None
class EnvVarReveal(BaseModel):
key: str
profile: Optional[str] = None
class MemoryProviderConfigUpdate(BaseModel):
values: Dict[str, Any] = {}
class MemoryProviderSetupRequest(BaseModel):
values: Dict[str, Any] = {}
class CustomEndpointUpdate(BaseModel):
id: str = ""
name: str
@@ -56,62 +49,50 @@ class CustomEndpointUpdate(BaseModel):
make_default: bool = False
models: Optional[List[str]] = None
class MessagingPlatformUpdate(BaseModel):
enabled: Optional[bool] = None
env: Dict[str, str] = {}
clear_env: List[str] = []
# Explicit body profile beats the switcher's query param (same precedence
# as other scoped writes).
# Explicit body profile beats the switcher's query param (same as other scoped writes).
profile: Optional[str] = None
class TelegramOnboardingStart(BaseModel):
bot_name: Optional[str] = None
class TelegramOnboardingApply(BaseModel):
allowed_user_ids: List[str]
profile: Optional[str] = None
class WhatsAppOnboardingStart(BaseModel):
mode: Optional[str] = "bot"
allowed_users: Optional[str] = ""
profile: Optional[str] = None
class WhatsAppOnboardingApply(BaseModel):
mode: Optional[str] = None
allowed_users: Optional[str] = None
profile: Optional[str] = None
class AudioTranscriptionRequest(BaseModel):
data_url: str
mime_type: Optional[str] = None
class ManagedFileUpload(BaseModel):
path: str
data_url: str
overwrite: bool = True
class ChatImageUpload(BaseModel):
data_url: str
filename: Optional[str] = None
class ManagedDirectoryCreate(BaseModel):
path: str
class ManagedFileDelete(BaseModel):
path: str
recursive: bool = False
class ModelAssignment(BaseModel):
"""Payload for POST /api/model/set — assign a provider/model to a slot.
@@ -124,29 +105,23 @@ class ModelAssignment(BaseModel):
provider: str
model: str
task: str = ""
# OpenAI-compatible endpoint URL for custom/local providers, honored on
# the main slot AND auxiliary slots. The runtime resolvers read
# model.base_url / auxiliary.<task>.base_url from config (they ignore
# OPENAI_BASE_URL), so this is what actually wires a local endpoint.
# Custom/local endpoint URL + key, honored on main AND auxiliary slots: the runtime
# resolvers read model.base_url / auxiliary.<task>.base_url (+ .api_key) from config
# and ignore OPENAI_BASE_URL, so this is what actually wires a local endpoint.
base_url: str = ""
# API key for a custom/local endpoint, persisted to ``model.api_key`` /
# ``auxiliary.<task>.api_key`` where the resolvers read it.
api_key: str = ""
confirm_expensive_model: bool = False
profile: Optional[str] = None
class MoaModelSlot(BaseModel):
provider: str = ""
model: str = ""
# Declared so a GET round-trip doesn't strip it at parse time and wipe it.
# Declared so a GET round-trip doesn't strip and wipe it.
reasoning_effort: Optional[str] = None
enabled: bool = True
class _MoaReferenceControls(BaseModel):
# None = no per-preset override; the fan-out inherits
# auxiliary.moa_reference.timeout (900s default).
# None = no per-preset override; inherits auxiliary.moa_reference.timeout (900s default).
reference_timeout: Optional[float] = None
degraded_reference_policy: Literal["loud", "silent"] = "loud"
@@ -161,29 +136,24 @@ class _MoaReferenceControls(BaseModel):
try:
timeout = float(value)
except (TypeError, ValueError) as exc:
raise ValueError(
"reference_timeout must be a finite positive number"
) from exc
raise ValueError("reference_timeout must be a finite positive number") from exc
if not math.isfinite(timeout) or timeout <= 0:
raise ValueError("reference_timeout must be a finite positive number")
return timeout
class MoaPresetPayload(_MoaReferenceControls):
reference_models: list[MoaModelSlot] = []
aggregator: MoaModelSlot = MoaModelSlot()
# None = temperature omitted from API calls (provider default), matching
# single-model agent behavior.
# None = temperature omitted from API calls (provider default), as for single-model agents.
reference_temperature: Optional[float] = None
aggregator_temperature: Optional[float] = None
max_tokens: int = 4096
# Newer per-preset knobs (see moa_config._normalize_preset): optional so
# older clients keep working, declared so GET round-trips don't erase them.
# Newer per-preset knobs (moa_config._normalize_preset): optional for older clients,
# declared so GET round-trips don't erase them.
reference_max_tokens: Optional[int] = None
fanout: Optional[str] = None
enabled: bool = True
class MoaConfigPayload(_MoaReferenceControls):
default_preset: str = "default"
active_preset: str = ""
@@ -199,39 +169,31 @@ class MoaConfigPayload(_MoaReferenceControls):
enabled: bool = True
profile: Optional[str] = None
class FsWriteText(BaseModel):
path: str
content: str
class GitPathBody(BaseModel):
path: str
class GitFileBody(BaseModel):
path: str
file: Optional[str] = None
class GitPrListBody(BaseModel):
path: str
branches: List[str] = []
# PRs a session recovered from its transcript, which we know by number
# rather than by the branch it came from.
# PRs a session recovered from its transcript — known by number, not branch.
numbers: List[int] = []
class SessionPrScanBody(BaseModel):
ids: List[str] = []
class GitCommitBody(BaseModel):
path: str
message: str
push: bool = False
class GitWorktreeAddBody(BaseModel):
path: str
name: Optional[str] = None
@@ -239,45 +201,35 @@ class GitWorktreeAddBody(BaseModel):
base: Optional[str] = None
existingBranch: Optional[str] = None
class GitWorktreeRemoveBody(BaseModel):
path: str
worktreePath: str
force: bool = False
class GitBranchSwitchBody(BaseModel):
path: str
branch: str
class CuratorPause(BaseModel):
paused: bool
class LearningNodeRef(BaseModel):
id: str
profile: Optional[str] = None
class LearningNodeEdit(BaseModel):
id: str
content: str
profile: Optional[str] = None
class DebugShareRequest(BaseModel):
# Redaction ON by default scrubs credential-shaped tokens before logs leave
# the machine; an operator who knows the logs are clean can opt out.
# Redaction scrubs credential-shaped tokens before logs leave the machine; opt-out only.
redact: bool = True
# Recent log lines included in the summary tail (full logs are separate).
lines: int = 200
lines: int = 200 # recent log lines in the summary tail (full logs are separate)
class TTSSpeakRequest(BaseModel):
text: str
class TTSLeaseRequest(BaseModel):
"""Body for ``POST /api/audio/tts-lease``.
@@ -288,35 +240,26 @@ class TTSLeaseRequest(BaseModel):
lease: str
active: bool = True
class OAuthSubmitBody(BaseModel):
session_id: str
code: str
class BulkDeleteSessions(BaseModel):
ids: List[str]
profile: Optional[str] = None
class SessionImport(BaseModel):
sessions: List[Dict[str, Any]]
profile: Optional[str] = None
class SessionRename(BaseModel):
title: Optional[str] = None
archived: Optional[bool] = None
# Generic visibility flag; also used by cross-profile reconciliation.
hidden: Optional[bool] = None
# Durable "keep" flag (Desktop pins); exempt from the auto_archive sweep.
pinned: Optional[bool] = None
# Read-state watermark (sessions.last_read_at): True = explicitly unread,
# False = read up to now, None = leave alone.
hidden: Optional[bool] = None # also used by cross-profile reconciliation
pinned: Optional[bool] = None # durable "keep" (Desktop pins); exempt from auto_archive
# Read-state watermark (sessions.last_read_at): True = unread, False = read now, None = leave.
unread: Optional[bool] = None
# Mutate a session owned by another profile (opens its state.db).
profile: Optional[str] = None
profile: Optional[str] = None # session owned by another profile (opens its state.db)
class SessionOwnerBackfill(BaseModel):
"""Body for POST /api/sessions/owner-backfill (#94724 legacy migration).
@@ -328,14 +271,13 @@ class SessionOwnerBackfill(BaseModel):
profile: Optional[str] = None
class SessionPrune(BaseModel):
older_than_days: Optional[float] = 90
source: Optional[str] = None
profile: Optional[str] = None
# Extended filters (all optional, AND together — mirrors the CLI flags)
started_before: Optional[float] = None # epoch seconds
started_after: Optional[float] = None # epoch seconds
# Extended filters (all optional, ANDed — mirrors the CLI flags); *_before/after = epoch s
started_before: Optional[float] = None
started_after: Optional[float] = None
title_like: Optional[str] = None
end_reason: Optional[str] = None
cwd_prefix: Optional[str] = None
@@ -356,7 +298,6 @@ class SessionPrune(BaseModel):
include_archived: bool = False
dry_run: bool = False
class CronJobCreate(BaseModel):
prompt: str = ""
schedule: str
@@ -372,62 +313,50 @@ class CronJobCreate(BaseModel):
workdir: Optional[str] = None
no_agent: bool = False
class CronJobUpdate(BaseModel):
updates: dict
class AutomationBlueprintInstantiate(BaseModel):
blueprint: str # blueprint key, e.g. "morning-brief"
values: Dict[str, Any] = {} # filled slot values from the form
blueprint: str # blueprint key, e.g. "morning-brief"
values: Dict[str, Any] = {} # filled slot values from the form
class MCPServerCreate(BaseModel):
name: str
url: Optional[str] = None
command: Optional[str] = None
args: List[str] = []
# env: KEY=VALUE map for stdio servers (API keys, etc.)
env: Dict[str, str] = {}
# auth: "none" | "oauth" | "header" | None
auth: Optional[str] = None
env: Dict[str, str] = {} # KEY=VALUE for stdio servers (API keys, etc.)
auth: Optional[str] = None # "none" | "oauth" | "header" | None
# One-time provisioning input; persisted only to the profile's .env.
bearer_token: Optional[SecretStr] = None
profile: Optional[str] = None
class MCPServersReplace(BaseModel):
# Whole-map replace (name → raw server config) for the GUI mcp.json editor.
# Whole-map replace (name → raw config) for the GUI mcp.json editor.
servers: Dict[str, Dict[str, Any]] = {}
profile: Optional[str] = None
class MCPEnabledToggle(BaseModel):
enabled: bool
profile: Optional[str] = None
class MCPCatalogInstall(BaseModel):
name: str
# env: KEY=VALUE map for catalog entries that declare required env vars.
env: Dict[str, str] = {}
env: Dict[str, str] = {} # KEY=VALUE for entries declaring required env vars
enable: bool = True
profile: Optional[str] = None
class PairingApprove(BaseModel):
platform: str
code: str = ""
request_id: str = ""
profile: Optional[str] = None
class PairingRevoke(BaseModel):
platform: str
user_id: str
profile: Optional[str] = None
class WebhookCreate(BaseModel):
name: str
description: Optional[str] = None
@@ -438,213 +367,161 @@ class WebhookCreate(BaseModel):
deliver: str = "log"
deliver_only: bool = False
deliver_chat_id: Optional[str] = None
# secret: omit to auto-generate
secret: Optional[str] = None
secret: Optional[str] = None # omit to auto-generate
class WebhookEnabledToggle(BaseModel):
enabled: bool
class CredentialPoolAdd(BaseModel):
provider: str
# OAuth pooling stays CLI-only (needs an interactive browser flow).
api_key: str
api_key: str # OAuth pooling stays CLI-only (needs an interactive browser flow)
label: Optional[str] = None
class MemoryProviderSelect(BaseModel):
# "" or "built-in" disables the external provider (built-in only).
provider: str
provider: str # "" or "built-in" disables the external provider
class MemoryReset(BaseModel):
# "all" | "memory" | "user"
target: str = "all"
target: str = "all" # "all" | "memory" | "user"
class BackupRequest(BaseModel):
# Optional output path; defaults to a timestamped zip in the home dir.
output: Optional[str] = None
output: Optional[str] = None # defaults to a timestamped zip in the home dir
class ImportRequest(BaseModel):
archive: str
# Pass --force to `hermes import`: the spawned action runs with
# stdin=DEVNULL, so the CLI's "Continue? [y/N]" overwrite prompt hits EOF
# and aborts whenever the target already has a config (always, when the
# dashboard runs from it). The dashboard confirms in its own modal first.
# --force for `hermes import`: the spawned action has stdin=DEVNULL, so the CLI's
# "Continue? [y/N]" prompt would hit EOF and abort. The dashboard confirms in its own modal.
force: bool = False
class HookCreate(BaseModel):
event: str
command: str
matcher: Optional[str] = None
timeout: Optional[int] = None
# Also write the consent allowlist entry (the authenticated dashboard
# operator is consenting); without it the hook won't fire until approved.
# Also write the consent allowlist entry; without it the hook won't fire until approved.
approve: bool = True
class HookDelete(BaseModel):
event: str
command: str
class SkillInstallRequest(BaseModel):
identifier: str
profile: Optional[str] = None
class SkillUninstallRequest(BaseModel):
name: str
profile: Optional[str] = None
class SkillsUpdateRequest(BaseModel):
profile: Optional[str] = None
class ProfileCreate(BaseModel):
name: str
clone_from: Optional[str] = None
# Backward compatibility for older dashboard/desktop clients. New clients
# send clone_from="default" (or another profile name) explicitly.
clone_from_default: bool = False
clone_from_default: bool = False # legacy clients; new ones send clone_from explicitly
clone_all: bool = False
no_skills: bool = False
description: Optional[str] = None
provider: Optional[str] = None
model: Optional[str] = None
# Profile-builder additions — all applied best-effort AFTER the profile
# directory exists, so a hiccup in any of them never 500s the create.
# Profile-builder additions, applied best-effort AFTER the profile directory exists so a
# hiccup never 500s the create.
mcp_servers: List["MCPServerCreate"] = []
# Skills to KEEP active. Non-empty = "replace" semantics (every seeded
# skill NOT listed is disabled); empty = leave the seeded bundle untouched.
# Skills to KEEP: non-empty = replace semantics (unlisted seeded skills disabled).
keep_skills: List[str] = []
# Skills-hub identifiers, installed async via `hermes -p <name> skills
# install` because skills_hub.SKILLS_DIR is import-time-bound and the
# HERMES_HOME override can't redirect it. PIDs go back for the UI to poll.
# Hub identifiers installed async via `hermes -p <name> skills install` (skills_hub.SKILLS_DIR
# is import-time-bound, so HERMES_HOME can't redirect it); PIDs go back for the UI to poll.
hub_skills: List[str] = []
class ProfileRename(BaseModel):
new_name: str
class ProfileExport(BaseModel):
# Extra root-level files to stage, filename → text (e.g. desktop.json).
extra_files: Dict[str, str] = {}
# Archive path; empty → a staging path under HERMES_HOME.
output: str = ""
extra_files: Dict[str, str] = {} # extra root-level files, filename → text
output: str = "" # archive path; empty → a staging path under HERMES_HOME
class ProfileImport(BaseModel):
# Path to a profile .tar.gz on the backend's filesystem.
archive: str
# Override the profile name inferred from the archive root.
name: Optional[str] = None
archive: str # profile .tar.gz on the backend's filesystem
name: Optional[str] = None # overrides the name inferred from the archive root
class ProfileSoulUpdate(BaseModel):
content: str
class ProfileActiveUpdate(BaseModel):
name: str
class ProfileDescriptionUpdate(BaseModel):
description: str = ""
class ProfileModelUpdate(BaseModel):
provider: str
model: str
class ProfileDescribeAuto(BaseModel):
overwrite: bool = False
class SkillToggle(BaseModel):
name: str
enabled: bool
profile: Optional[str] = None
class SkillCreate(BaseModel):
name: str
content: str
category: Optional[str] = None
profile: Optional[str] = None
class SkillContentUpdate(BaseModel):
name: str
content: str
profile: Optional[str] = None
class ToolsetToggle(BaseModel):
enabled: bool
profile: Optional[str] = None
class ToolsetProviderSelect(BaseModel):
provider: str
# Web-only capability scope: 'search' | 'extract'. Omitted → whole-provider
# selection via the legacy apply_provider_selection path (web.backend).
# Web-only scope 'search' | 'extract'; omitted → whole-provider (legacy web.backend path).
capability: Optional[str] = None
profile: Optional[str] = None
class ToolsetModelSelect(BaseModel):
model: str
provider: Optional[str] = None
profile: Optional[str] = None
class ToolsetEnvUpdate(BaseModel):
env: Dict[str, str]
profile: Optional[str] = None
class ToolsetPostSetup(BaseModel):
key: str
profile: Optional[str] = None
class TerminalBackendSelect(BaseModel):
backend: str
profile: Optional[str] = None
class RawConfigUpdate(BaseModel):
yaml_text: str
profile: Optional[str] = None
class ThemeSetBody(BaseModel):
name: str
class FontSetBody(BaseModel):
font: str
class _AgentPluginInstallBody(BaseModel):
identifier: str
force: bool = False
enable: bool = True
class _PluginProvidersPutBody(BaseModel):
memory_provider: Optional[str] = None
context_engine: Optional[str] = None
class _PluginVisibilityBody(BaseModel):
hidden: bool