atlas-iac/scripts/ops/hermes_handoff_catalog.py

204 lines
6.6 KiB
Python

#!/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)
# `name<TAB>generation<TAB>observedGeneration<TAB>suspend<TAB>conditions`.
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 = "atlas/titan-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
git_askpass: str = "/opt/coordinator/gitea_askpass.sh"
expect_telegram_sessions: bool = True
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,
)