feat(state): let the hub declare what instance it is
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 24s

Answering on a port was the only evidence callers had that they had reached
the authoritative hub. A local cache and central both served port 8000,
separated only by IP family, and every default reached the cache for seven
weeks (ADR-010).

Adds instance_role and instance_label, surfaced on /state/health. The default
is "unknown" on purpose: an instance that has not declared itself is not the
primary. Production values declare primary/railiance01; the chart default
stays unknown because a chart can be installed anywhere.

statehub status now prints which instance answered.

Refs CUST-WP-0067-T03

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
This commit is contained in:
tegwick 2026-08-25 10:36:37 +02:00
parent b28493a676
commit 19fab26746
6 changed files with 32 additions and 1 deletions

View file

@ -12,6 +12,14 @@ class Settings(BaseSettings):
database_url: str = "postgresql+asyncpg://custodian:changeme@127.0.0.1:5432/custodian" database_url: str = "postgresql+asyncpg://custodian:changeme@127.0.0.1:5432/custodian"
api_base: str = "http://127.0.0.1:8000" api_base: str = "http://127.0.0.1:8000"
# What this instance claims to be. Deliberately defaults to "unknown":
# an instance that has not declared itself is not the primary, and callers
# that need the authoritative hub must be able to tell the difference.
# Answering on a port is not evidence of authority — that assumption cost
# seven weeks of onboarding (CUST-WP-0067-T03, ADR-010).
instance_role: Literal["primary", "cache", "unknown"] = "unknown"
# Free-form label to name *which* instance answered, e.g. "railiance01".
instance_label: str | None = None
debug: bool = False debug: bool = False
state_hub_report_dir: str = "reports/recently-on-scope" state_hub_report_dir: str = "reports/recently-on-scope"
state_hub_markitect_cli_path: str | None = None state_hub_markitect_cli_path: str | None = None

View file

@ -7,6 +7,7 @@ from sqlalchemy import func, select, text
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload, selectinload from sqlalchemy.orm import noload, selectinload
from api.config import settings
from api.database import get_session from api.database import get_session
from api.flow_defs import assertion_result_to_dict, load_flow from api.flow_defs import assertion_result_to_dict, load_flow
from api.models.capability_request import CapabilityRequest from api.models.capability_request import CapabilityRequest
@ -1108,7 +1109,13 @@ async def get_next_steps(session: AsyncSession = Depends(get_session)) -> list[N
async def health_check(session: AsyncSession = Depends(get_session)) -> dict: async def health_check(session: AsyncSession = Depends(get_session)) -> dict:
try: try:
await session.execute(text("SELECT 1")) await session.execute(text("SELECT 1"))
return {"status": "ok", "db": "connected"} return {
"status": "ok",
"db": "connected",
# Identity, so a caller can verify it reached the hub it meant to.
"instance_role": settings.instance_role,
"instance_label": settings.instance_label,
}
except Exception as exc: except Exception as exc:
return JSONResponse( return JSONResponse(
status_code=503, status_code=503,

View file

@ -558,6 +558,10 @@ def cmd_status(_args: argparse.Namespace) -> None:
"""Quick status: API health + summary totals.""" """Quick status: API health + summary totals."""
health = _api_get("/state/health") health = _api_get("/state/health")
print(f"API: {health.get('status', '?')} DB: {health.get('db', '?')}") print(f"API: {health.get('status', '?')} DB: {health.get('db', '?')}")
# Which instance answered, not merely that one did.
role = health.get("instance_role") or "undeclared"
label = health.get("instance_label")
print(f"Instance: {role}{f' ({label})' if label else ''}")
summary = _api_get("/state/summary") summary = _api_get("/state/summary")
t = summary["totals"] t = summary["totals"]
topics = t.get("topics", {}) topics = t.get("topics", {})

View file

@ -9,4 +9,8 @@ data:
SBOM_NEXUS_URL: {{ .Values.config.sbomNexusUrl | quote }} SBOM_NEXUS_URL: {{ .Values.config.sbomNexusUrl | quote }}
SBOM_NEXUS_READ_MODE: {{ .Values.config.sbomNexusReadMode | quote }} SBOM_NEXUS_READ_MODE: {{ .Values.config.sbomNexusReadMode | quote }}
SBOM_NEXUS_WRITE_MODE: {{ .Values.config.sbomNexusWriteMode | quote }} SBOM_NEXUS_WRITE_MODE: {{ .Values.config.sbomNexusWriteMode | quote }}
# What this instance claims to be. Callers that need the authoritative hub
# check this rather than assuming whatever answered is central.
STATE_HUB_INSTANCE_ROLE: {{ .Values.config.instanceRole | quote }}
STATE_HUB_INSTANCE_LABEL: {{ .Values.config.instanceLabel | quote }}
{{- end }} {{- end }}

View file

@ -25,6 +25,10 @@ config:
sbomNexusUrl: "" sbomNexusUrl: ""
sbomNexusReadMode: legacy sbomNexusReadMode: legacy
sbomNexusWriteMode: legacy sbomNexusWriteMode: legacy
# CUST-WP-0067-T03. "unknown" is the honest default for a chart that could be
# installed anywhere; the production values set this to primary.
instanceRole: unknown
instanceLabel: ""
secret: secret:
name: state-hub-env name: state-hub-env

View file

@ -19,6 +19,10 @@ mcp:
enabled: true enabled: true
config: config:
# This deployment is the authoritative hub. Callers verify this rather than
# trusting that whatever answered on a port is central (CUST-WP-0067-T03).
instanceRole: primary
instanceLabel: railiance01
sbomNexusUrl: "http://sbom-nexus.sbom-nexus.svc.cluster.local:8010" sbomNexusUrl: "http://sbom-nexus.sbom-nexus.svc.cluster.local:8010"
# Reversible T04 read cutover; set back to `legacy` to roll back. # Reversible T04 read cutover; set back to `legacy` to roll back.
sbomNexusReadMode: nexus sbomNexusReadMode: nexus