#!/usr/bin/env python3 """Shared immutable configuration and result types for Hermes CLI lanes.""" from __future__ import annotations import os import re import threading from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path from typing import Any import yaml import cli_lane_goal DATA_ROOT = Path(os.environ.get("HERMES_HOME", "/opt/data")) SWITCHYARD_URL = os.environ.get( "HERMES_SWITCHYARD_URL", "http://hermes-switchyard.hermes.svc.cluster.local:9005/v1/chat/completions", ) STATE_ROOT = DATA_ROOT / "cli-lanes" CODEX_BIN = DATA_ROOT / "tools/bin/codex" CLAUDE_BIN = Path(os.environ.get("HERMES_CLAUDE_BIN", DATA_ROOT / "tools/bin/claude")) CLAUDE_SETTINGS = DATA_ROOT / "home/.claude/settings.json" RESULT_SCHEMA_PATH = STATE_ROOT / "worker-result.schema.json" EFFORTS = ("low", "medium", "high", "xhigh") CAPABILITIES = ("economy", "balanced", "advanced", "frontier") EXTERNAL_PREFIX = "cli-" DEFAULT_CLAIM_TTL = 7 * 24 * 60 * 60 DEFAULT_MAX_RUNTIME = 12 * 60 * 60 HEARTBEAT_SECONDS = 20 PROVIDER_HEALTH_MAX_AGE_SECONDS = 5 * 60 PROVIDER_AUTH_FAILURE_MAX_AGE_SECONDS = 12 * 60 * 60 QUOTA_MIN_REMAINING_PERCENT_DEFAULT = 15.0 CAPACITY_COOLDOWN_SECONDS_DEFAULT = 300.0 AUTH_COOLDOWN_SECONDS_DEFAULT = 3600.0 QUOTA_METRICS_URL = os.environ.get( "HERMES_CLI_QUOTA_METRICS_URL", "http://127.0.0.1:9010/metrics" ) KANBAN_STORAGE_ATTEMPTS = 5 ARTIFACT_GC_INTERVAL_SECONDS = 5 * 60 ARTIFACT_RETENTION_AGE_SECONDS = 30 * 24 * 60 * 60 ARTIFACT_RETENTION_COUNT = 256 ARTIFACT_RETENTION_BYTES = 128 * 1024 * 1024 MAX_TERMINAL_RECORD_BYTES = 1024 * 1024 QUARANTINE_HASH_BYTES = 64 * 1024 SQLITE_RUN_ID_MAX = 2**63 - 1 PROVIDER_HEALTH_PATHS = { "codex": DATA_ROOT / "provider-health/codex.json", "claude": DATA_ROOT / "provider-health/claude.json", } WORKTREE_LOCK = threading.Lock() BOARD_CORRUPTION_ERRORS: dict[str, str] = {} LAST_ARTIFACT_GC = 0.0 CAPACITY_PATTERN = re.compile( r"(?:rate.?limit|capacity|overload|usage.?limit|quota|credit|exhaust|429|529" r"|authentication|unauthorized|forbidden|oauth|token.*expired|401|403)", re.I, ) NO_CLAUDE_SESSION = "No conversation found with session ID:" CLAUDE_SESSION_COLLISION = "Session ID already in use" NO_CODEX_THREAD = "no rollout found for thread id" RESULT_SCHEMA: dict[str, Any] = { "type": "object", "additionalProperties": False, "required": [ "status", "summary", "changed_files", "tests_run", "artifacts", "findings", "blockers", ], "properties": { "status": { "type": "string", "enum": sorted(cli_lane_goal.RESULT_STATUSES), }, "summary": {"type": "string"}, "changed_files": {"type": "array", "items": {"type": "string"}}, "tests_run": {"type": "array", "items": {"type": "string"}}, "artifacts": {"type": "array", "items": {"type": "string"}}, "findings": { "type": "array", "items": {"type": "string"}, "description": ( "Defects, risks, or observations discovered by a review or diagnosis. " "Findings do not prevent the assigned review or diagnosis from completing." ), }, "blockers": { "type": "array", "items": {"type": "string"}, "description": ( "Concrete obstacles that prevent completion of the assigned task itself. " "This must be empty when status is completed; review findings belong in findings." ), }, }, } @dataclass(frozen=True) class Route: """One provider/model/effort decision for an observable worker boundary.""" provider: str model: str effort: str profile: str classifier: str reason: str latency_ms: int fallback_chain: tuple[str, ...] # Capability is intentionally independent from reasoning effort. Keep the # default for older durable records and positional test fixtures. capability: str = "advanced" @dataclass class ProcessResult: """Captured outcome from one provider CLI invocation.""" returncode: int output: str structured: dict[str, Any] | None capacity_failure: bool class TerminalFinalizationPending(RuntimeError): """An accepted worker result is durable but not committed to Kanban yet.""" def canonical_run_id(value: object) -> int | None: """Return one positive SQLite-safe run ID from an int or canonical decimal.""" if type(value) is int: candidate = value elif type(value) is str: # Bound conversion before ``int`` so an attacker cannot hand Python an # arbitrarily large decimal from an otherwise lexically valid name. if not re.fullmatch(r"[1-9][0-9]{0,18}", value): return None candidate = int(value) else: return None if not 1 <= candidate <= SQLITE_RUN_ID_MAX: return None return candidate @dataclass(frozen=True) class TerminalIdentity: """Replay authority derived only from a journal's directory and filename.""" board: str task_id: str run_id: int state: str def __post_init__(self) -> None: """Reject noncanonical identities before they can reach SQLite.""" if type(self.run_id) is not int or canonical_run_id(self.run_id) is None: raise ValueError("terminal run identity is not a positive SQLite int64") @dataclass class TerminalSnapshot: """One inode-bound, bounded journal read held open across finalization.""" document: dict[str, Any] file_stat: os.stat_result descriptor: int directory_descriptor: int def close(self) -> None: """Release the pinned file and directory descriptors.""" os.close(self.descriptor) os.close(self.directory_descriptor) @dataclass class TerminalRecoverySnapshot: """One raw directory entry pinned before recovery classifies its payload.""" document: dict[str, Any] | None file_stat: os.stat_result descriptor: int | None directory_descriptor: int prefix: bytes invalid_reason: str | None def close(self) -> None: """Release the descriptors retained across classification/quarantine.""" if self.descriptor is not None: os.close(self.descriptor) self.descriptor = None if self.directory_descriptor >= 0: os.close(self.directory_descriptor) self.directory_descriptor = -1 def utc_now() -> str: return datetime.now(timezone.utc).isoformat() def kanban_setting(name: str, default: float) -> float: """Read one numeric routing setting from the deployed kanban config block.""" try: document = yaml.safe_load( (DATA_ROOT / "config.yaml").read_text(encoding="utf-8") ) except (OSError, yaml.YAMLError): return default kanban = document.get("kanban") if isinstance(document, dict) else None value = kanban.get(name) if isinstance(kanban, dict) else None if isinstance(value, bool) or not isinstance(value, (int, float)): return default return float(value)