diff --git a/api/config.py b/api/config.py index 186ce04..99c1c8f 100644 --- a/api/config.py +++ b/api/config.py @@ -12,6 +12,14 @@ class Settings(BaseSettings): database_url: str = "postgresql+asyncpg://custodian:changeme@127.0.0.1:5432/custodian" 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 state_hub_report_dir: str = "reports/recently-on-scope" state_hub_markitect_cli_path: str | None = None diff --git a/api/routers/state.py b/api/routers/state.py index db3d930..319c849 100644 --- a/api/routers/state.py +++ b/api/routers/state.py @@ -7,6 +7,7 @@ from sqlalchemy import func, select, text from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import noload, selectinload +from api.config import settings from api.database import get_session from api.flow_defs import assertion_result_to_dict, load_flow 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: try: 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: return JSONResponse( status_code=503, diff --git a/custodian_cli.py b/custodian_cli.py index 361a1ec..6ac1c8b 100644 --- a/custodian_cli.py +++ b/custodian_cli.py @@ -558,6 +558,10 @@ def cmd_status(_args: argparse.Namespace) -> None: """Quick status: API health + summary totals.""" health = _api_get("/state/health") 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") t = summary["totals"] topics = t.get("topics", {}) diff --git a/deploy/railiance/apps/charts/state-hub/templates/configmap.yaml b/deploy/railiance/apps/charts/state-hub/templates/configmap.yaml index 3d40fb4..8604514 100644 --- a/deploy/railiance/apps/charts/state-hub/templates/configmap.yaml +++ b/deploy/railiance/apps/charts/state-hub/templates/configmap.yaml @@ -9,4 +9,8 @@ data: SBOM_NEXUS_URL: {{ .Values.config.sbomNexusUrl | quote }} SBOM_NEXUS_READ_MODE: {{ .Values.config.sbomNexusReadMode | 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 }} diff --git a/deploy/railiance/apps/charts/state-hub/values.yaml b/deploy/railiance/apps/charts/state-hub/values.yaml index 3f414e3..ce5f05f 100644 --- a/deploy/railiance/apps/charts/state-hub/values.yaml +++ b/deploy/railiance/apps/charts/state-hub/values.yaml @@ -25,6 +25,10 @@ config: sbomNexusUrl: "" sbomNexusReadMode: 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: name: state-hub-env diff --git a/deploy/railiance/apps/helm/state-hub-values.yaml b/deploy/railiance/apps/helm/state-hub-values.yaml index ed07b37..4db2973 100644 --- a/deploy/railiance/apps/helm/state-hub-values.yaml +++ b/deploy/railiance/apps/helm/state-hub-values.yaml @@ -19,6 +19,10 @@ mcp: enabled: true 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" # Reversible T04 read cutover; set back to `legacy` to roll back. sbomNexusReadMode: nexus