#!/usr/bin/env python3 """Declarative acceptance catalog: baseline, identity, routing, scopes, access. The catalog is data on purpose. A reviewer should be able to read what the release is asserted to satisfy without reading any plumbing, and the harness should be able to prove structural properties over every entry — both vantages represented, no impersonation from outside a pod, and no mutation command of any kind — before it runs a single command. Catalog drift is a NO_GO, not a pass. If a manifest, a field name, or a workload is renamed, the affected checks report ``NOT_RUN`` because the evidence they name is not there, and the release stops until either the platform or the catalog is corrected. """ from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass, field from typing import Any from hermes_handoff_model import ALWAYS, READ, CheckSpec, Step from hermes_handoff_policy import projection OPERATOR = "operator" SELF = "self" SWITCHYARD = "switchyard" NODE = "node" CHAT = "chat" VANTAGE_NAMES = (OPERATOR, SELF, SWITCHYARD, NODE, CHAT) # `namegenerationobservedGenerationsuspendconditions`. FLUX_PROJECTION = projection( ( "jsonpath={range .items[*]}{.metadata.namespace}/{.metadata.name}{'\\t'}" "{.metadata.generation}{'\\t'}{.status.observedGeneration}{'\\t'}{.spec.suspend}{'\\t'}" "{range .status.conditions[*]}{.type}={.status},{end}{'\\n'}{end}" ).replace("'", '"') ) ENV_NAME_PROJECTION = projection( ( "jsonpath={range .spec.template.spec.containers[*]}{range .env[*]}{.name}{'\\n'}{end}{end}" ).replace("'", '"') ) INIT_NAME_PROJECTION = projection( ( "jsonpath={range .spec.template.spec.initContainers[*]}{.name}{'\\n'}{end}" ).replace("'", '"') ) NODE_NAME_PROJECTION = projection( "jsonpath={range .items[*]}{.spec.nodeName}{'\\n'}{end}".replace("'", '"') ) POOL_PROJECTION = projection( ( "jsonpath={range .items[*]}{.metadata.name}{'\\t'}{.spec.nodeName}{'\\t'}" "{.status.phase}{'\\t'}{range .status.conditions[?(@.type==\"Ready\")]}{.status}{end}" "{'\\t'}{range .spec.volumes[*]}{.persistentVolumeClaim.claimName}{','}{end}{'\\n'}{end}" ).replace("'", '"') ) PROVIDER_API_KEY_NAMES = ( "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "AZURE_OPENAI_API_KEY", "CLAUDE_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN", "GEMINI_API_KEY", "GOOGLE_API_KEY", "OPENAI_API_KEY", "OPENAI_BASE_URL", ) FORGE_CREDENTIAL_NAMES = ( "GITEA_PASSWORD", "GITEA_TOKEN", "GITEA_USERNAME", "GIT_PASSWORD", ) CODEX_HEALTH = "/opt/data/provider-health/codex.json" CLAUDE_HEALTH = "/opt/data/provider-health/claude.json" CODEX_HEALTH_FIELDS = "authenticated,transport,state,model,effort,latency_ms,checked_at" CLAUDE_HEALTH_FIELDS = ( "authenticated,api_provider,auth_method,transport,state,subscription_type," "model,effort,latency_ms,checked_at" ) ROUTING_LOG = "/var/lib/switchyard/routing.jsonl" @dataclass(frozen=True) class Targets: """Runtime names and expected values the catalog is written against. Everything a site can legitimately rename lives here so the assertions stay readable and a rename is a configuration change rather than a code change. """ namespace: str = "hermes" agent_deployment: str = "hermes-agent" agent_container: str = "hermes" chat_statefulset: str = "hermes-chat-tenant" chat_container: str = "hermes" chat_ordinal: int = 0 chat_router_deployment: str = "hermes-chat-router" triage_deployment: str = "hermes" triage_container: str = "hermes" switchyard_deployment: str = "hermes-switchyard" switchyard_container: str = "switchyard" node_daemonset: str = "hermes-node-ssh-access" node_container: str = "key-reconciler" pool_statefulset: str = "hermes-execution-worker" pool_replicas: int = 3 pool_selector: str = "app=hermes-execution-worker" coordinator_claims: tuple[str, ...] = ( "hermes-agent-home", "hermes-agent-workspace", ) node_account: str = "hermes-agent" node_account_identity: str = "1200:1200:/home/hermes-agent:/bin/bash" node_count: int = 3 repo: str = "titan/atlas-iac" remote: str = "origin" baseline_commit: str = "ab346f55509d584e457fe26cf90be3078f7a375c" dependency_pull_requests: tuple[int, ...] = (12, 14, 15, 16, 17, 18, 20) dependency_heads: tuple[tuple[int, str], ...] = () remote_main_sha: str = "" reviewed_pr_number: int = 19 reviewed_head_ref: str = "feature/hermes-full-handoff-acceptance" reviewed_head_sha: str = "" agent_image: str = "" build_sha: str = "" deployment_revision: str = "" expected_suspensions: tuple[str, ...] = () harbor_namespace: str = "harbor" harbor_immutability_job: str = "hermes-agent-immutability" builder_namespace: str = "jenkins" builder_serviceaccount: str = "hermes-image-builder" builder_pipeline: str = "ci/Jenkinsfile.hermes-agent-image" builder_capabilities: str = ( 'add: ["CHOWN", "FOWNER", "DAC_OVERRIDE", "SETGID", "SETUID"]' ) agent_init_containers: tuple[str, ...] = ("patch-web-session-activity",) chat_init_containers: tuple[str, ...] = ("patch-api-server-sessions",) chat_config_revision: str = "" pool_worker_env: tuple[str, ...] = ("HERMES_WORKER_NODE", "HERMES_WORKER_ORDINAL") max_evidence_age_seconds: int = 86_400 routing_tail_lines: int = 600 routing_tail_bytes: int = 512 * 1024 listing_bytes: int = 512 * 1024 now: Any = None extra: dict[str, Any] = field(default_factory=dict) def step( key: str, vantage: str, *argv: str, kind: str = READ, optional: bool = False, record: bool = True, max_bytes: int | None = None, ) -> Step: """Return a probe step addressed to one vantage.""" return Step( key=key, vantage=vantage, argv=tuple(argv), kind=kind, optional=optional, record=record, max_bytes=max_bytes, ) def check( identifier: str, title: str, group: str, rule: str, steps: Sequence[Step], expect: dict[str, Any] | None = None, mandatory: bool = True, scope: str = ALWAYS, rationale: str = "", ) -> CheckSpec: """Return one catalog entry.""" return CheckSpec( id=identifier, title=title, group=group, rule=rule, steps=tuple(steps), expect=expect or {}, mandatory=mandatory, scope=scope, rationale=rationale, )