From 2c77cbdcb70ee70927845c5d6435a61a575677dc Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:09:29 -0700 Subject: [PATCH] refactor(hermes_cli/web_models): compact field comments and class spacing; JSON schemas byte-identical --- hermes_cli/web_models.py | 217 +++++++++------------------------------ 1 file changed, 47 insertions(+), 170 deletions(-) diff --git a/hermes_cli/web_models.py b/hermes_cli/web_models.py index 6055d78635..44890ec6ed 100644 --- a/hermes_cli/web_models.py +++ b/hermes_cli/web_models.py @@ -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..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..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..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 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 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