#!/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 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 = 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") 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 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|authentication|oauth|token.*expired)", 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, ...] @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()