190 lines
6.2 KiB
Python
190 lines
6.2 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, no mutation outside a server
|
||
|
|
dry run — 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, ATTEMPT, READ, REVIEW, CheckSpec, Step
|
||
|
|
|
||
|
|
OPERATOR = "operator"
|
||
|
|
SELF = "self"
|
||
|
|
SWITCHYARD = "switchyard"
|
||
|
|
NODE = "node"
|
||
|
|
CHAT = "chat"
|
||
|
|
VANTAGE_NAMES = (OPERATOR, SELF, SWITCHYARD, NODE, CHAT)
|
||
|
|
|
||
|
|
# `namespace/name<TAB>suspend<TAB>Type=Status,...` — one compact row per object.
|
||
|
|
FLUX_PROJECTION = (
|
||
|
|
"jsonpath={range .items[*]}{.metadata.namespace}/{.metadata.name}{'\\t'}"
|
||
|
|
"{.spec.suspend}{'\\t'}{range .status.conditions[*]}{.type}={.status},{end}{'\\n'}{end}"
|
||
|
|
).replace("'", '"')
|
||
|
|
|
||
|
|
ENV_NAME_PROJECTION = (
|
||
|
|
"jsonpath={range .spec.template.spec.containers[*]}{range .env[*]}{.name}{'\\n'}{end}{end}"
|
||
|
|
).replace("'", '"')
|
||
|
|
|
||
|
|
INIT_NAME_PROJECTION = (
|
||
|
|
"jsonpath={range .spec.template.spec.initContainers[*]}{.name}{'\\n'}{end}"
|
||
|
|
).replace("'", '"')
|
||
|
|
|
||
|
|
NODE_NAME_PROJECTION = "jsonpath={range .items[*]}{.spec.nodeName}{'\\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 = 1
|
||
|
|
repo: str = "atlas/titan-iac"
|
||
|
|
remote: str = "origin"
|
||
|
|
baseline_commit: str = "ab346f55509d584e457fe26cf90be3078f7a375c"
|
||
|
|
dependency_pull_requests: tuple[int, ...] = (14, 15, 16, 17, 18)
|
||
|
|
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, ...] = ()
|
||
|
|
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 = False
|
||
|
|
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,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def deny(
|
||
|
|
identifier: str,
|
||
|
|
title: str,
|
||
|
|
review: Sequence[str],
|
||
|
|
attempt: Sequence[str],
|
||
|
|
rationale: str,
|
||
|
|
group: str = "access-denied",
|
||
|
|
) -> CheckSpec:
|
||
|
|
"""Return a deny check that pairs a self review with a real refused request."""
|
||
|
|
return check(
|
||
|
|
identifier,
|
||
|
|
title,
|
||
|
|
group,
|
||
|
|
"denied",
|
||
|
|
[
|
||
|
|
step("review", SELF, *review, kind=REVIEW),
|
||
|
|
step("attempt", SELF, *attempt, kind=ATTEMPT),
|
||
|
|
],
|
||
|
|
rationale=rationale,
|
||
|
|
)
|