Wire ops inventory probes for Railiance

This commit is contained in:
2026-06-05 23:40:25 +02:00
parent 5838077327
commit 4b1b3e1b5f
7 changed files with 467 additions and 4 deletions

View File

@@ -15,6 +15,9 @@ data:
ISSUE_CORE_URL: http://issue-core.issue-core.svc.cluster.local:8010 ISSUE_CORE_URL: http://issue-core.issue-core.svc.cluster.local:8010
ISSUE_SINK_TYPE: "null" ISSUE_SINK_TYPE: "null"
ACTIVITY_DEFINITION_DIRS: /etc/activity-core/external-definitions ACTIVITY_DEFINITION_DIRS: /etc/activity-core/external-definitions
OPS_INVENTORY_PATH: /etc/activity-core/ops/service-inventory.yml
INTER_HUB_URL: ""
OPS_HUB_WIDGET_MAPPING: ""
PROMETHEUS_BIND_ADDR: 0.0.0.0:9090 PROMETHEUS_BIND_ADDR: 0.0.0.0:9090
ACTIVITY_CURATOR_GATE: disabled ACTIVITY_CURATOR_GATE: disabled
--- ---
@@ -58,6 +61,219 @@ data:
Kubernetes projection of the Custodian-owned definition in Kubernetes projection of the Custodian-owned definition in
`/home/worsch/the-custodian/activity-definitions/hourly-recently-on-scope.md`. `/home/worsch/the-custodian/activity-definitions/hourly-recently-on-scope.md`.
ops-service-inventory-probes.md: |
---
id: "40d15a87-7ff6-4d8e-992c-37df15f95110"
name: "Ops Service Inventory Probes"
type: activity-definition
version: "0.1"
enabled: false
owner: custodian
governance: custodian
status: proposed
created: "2026-06-05"
trigger:
type: cron
cron_expression: "15 * * * *"
timezone: Europe/Berlin
misfire_policy: skip
context_sources:
- type: ops-inventory
query: probe_services
required: false
params:
inventory_path: /etc/activity-core/ops/service-inventory.yml
timeout_seconds: 10
include_kinds:
- http
- https
allow_network: true
evidence_sinks:
- type: state-hub-progress
event_type: ops_inventory_probe
author: activity-core
bind_to: context.ops_inventory_probe
---
# ActivityDefinition: Ops Service Inventory Probes
Disabled Railiance projection of the Custodian-owned definition in
`/home/worsch/the-custodian/activity-definitions/ops-service-inventory-probes.md`.
Keep disabled until ops-hub Inter-Hub evidence intake is active.
---
apiVersion: v1
kind: ConfigMap
metadata:
name: actcore-ops-service-inventory
namespace: activity-core
labels:
app.kubernetes.io/name: activity-core
app.kubernetes.io/part-of: activity-core
data:
service-inventory.yml: |
version: 1
last_reviewed: "2026-06-05"
policy:
non_secret_inventory: true
source_of_truth: "/home/worsch/the-custodian/ops/service-inventory.yml"
projection: "Railiance activity-core ConfigMap snapshot for disabled probes"
environments:
- id: local
name: "Local Workstation"
role: "Workstation development and local operations"
lifecycle_state: observed
- id: coulombcore
name: "CoulombCore"
role: "Transitional production-like runtime"
lifecycle_state: observed
- id: railiance01
name: "Railiance01"
role: "First ThreePhoenix foundation node"
lifecycle_state: observed
- id: threephoenix-prod
name: "ThreePhoenix Production"
role: "Target governed production topology"
lifecycle_state: planned
hosts:
- id: local-workstation
environment: local
role: "State Hub and operator workstation runtime"
- id: coulombcore
environment: coulombcore
address: "92.205.130.254"
role: "Current live production-like server"
- id: railiance01
environment: railiance01
address: "92.205.62.239"
role: "First ThreePhoenix foundation node"
clusters:
- id: coulombcore-k3s
environment: coulombcore
host: coulombcore
kind: k3s
lifecycle_state: observed
- id: railiance01-k3s
environment: railiance01
host: railiance01
kind: k3s
lifecycle_state: observed
services:
- id: gitea
name: "Gitea"
kind: application
lifecycle_state: observed
health_status: unknown
environment: coulombcore
owner_repos:
- railiance-apps
runtime:
type: k3s
cluster: coulombcore-k3s
namespace: default
endpoints:
- id: gitea-oci-registry
type: https
url: "https://gitea.coulomb.social/v2/"
expected_status: 401
expected_signal: "OCI registry auth challenge"
widget_ref: "ops:endpoint:gitea-registry"
backing_stores:
- "database:gitea-db"
- "pvc:default/gitea-shared-storage"
access_paths:
- type: k8s
target: "coulombcore-k3s/default"
status: unknown
evidence: []
gaps:
- "Backup and restore evidence for database and shared storage not recorded in ops inventory."
- id: state-hub
name: "State Hub"
kind: coordination-service
lifecycle_state: observed
health_status: observed_ok
environment: local
owner_repos:
- state-hub
- the-custodian
runtime:
type: local-process
host: local-workstation
endpoints:
- id: state-hub-local-api
type: http
url: "http://actcore-state-hub-bridge:8000/state/health"
expected_status: 200
expected_signal: "health response"
backing_stores:
- "postgresql:state-hub"
access_paths:
- type: http
target: "http://actcore-state-hub-bridge:8000"
status: observed_ok
evidence: []
gaps:
- "Future cluster deployment readiness still needs ops evidence."
- id: inter-hub
name: "Inter-Hub"
kind: governance-service
lifecycle_state: observed
health_status: unknown
environment: threephoenix-prod
owner_repos:
- inter-hub
runtime:
type: external
public_endpoint: "https://hub.coulomb.social"
endpoints:
- id: inter-hub-openapi
type: https
url: "https://hub.coulomb.social/api/v2/openapi.json"
expected_status: 200
expected_signal: "OpenAPI document"
- id: inter-hub-ui
type: https
url: "https://hub.coulomb.social/Hubs"
expected_status: 302
expected_signal: "login redirect when unauthenticated"
backing_stores: []
access_paths:
- type: https
target: "https://hub.coulomb.social"
status: unknown
evidence: []
gaps:
- "ops-hub bootstrap requires authenticated UI flow or deployment-side migration."
- id: activity-core
name: "activity-core"
kind: automation-service
lifecycle_state: observed
health_status: observed_ok
environment: railiance01
owner_repos:
- activity-core
- the-custodian
runtime:
type: k3s
cluster: railiance01-k3s
namespace: activity-core
endpoints:
- id: activity-core-api
type: cluster-http
url: "http://actcore-api:8010/health"
expected_status: 200
expected_signal: "db"
backing_stores:
- "postgresql:activity-core"
- "temporal:activity-core"
- "nats:railiance01"
access_paths:
- type: k8s
target: "railiance01-k3s/activity-core"
status: observed_ok
evidence: []
gaps:
- "Add explicit ops inventory probes and evidence events."
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
@@ -360,10 +576,16 @@ spec:
- name: external-activity-definitions - name: external-activity-definitions
mountPath: /etc/activity-core/external-definitions/activity-definitions mountPath: /etc/activity-core/external-definitions/activity-definitions
readOnly: true readOnly: true
- name: ops-service-inventory
mountPath: /etc/activity-core/ops
readOnly: true
volumes: volumes:
- name: external-activity-definitions - name: external-activity-definitions
configMap: configMap:
name: actcore-external-activity-definitions name: actcore-external-activity-definitions
- name: ops-service-inventory
configMap:
name: actcore-ops-service-inventory
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment

View File

@@ -16,6 +16,14 @@ name and access policy.
The runtime image tag is `activity-core:railiance01-prod` and is expected to be The runtime image tag is `activity-core:railiance01-prod` and is expected to be
loaded into the railiance01 K3s containerd image store. loaded into the railiance01 K3s containerd image store.
`20-runtime.yaml` also projects the disabled Custodian-owned
`ops-service-inventory-probes.md` ActivityDefinition and a non-secret
`actcore-ops-service-inventory` ConfigMap snapshot. The source of truth for the
inventory remains `/home/worsch/the-custodian/ops/service-inventory.yml`; update
the ConfigMap projection from that file before enabling the probe schedule.
`OPS_HUB_KEY` is created only as an empty Secret placeholder until the operator
provisions the Inter-Hub ops-hub key.
## Deploy ## Deploy
```bash ```bash

View File

@@ -36,5 +36,6 @@ if ! secret_exists actcore-runtime-secret; then
kubectl -n "$NS" create secret generic actcore-runtime-secret \ kubectl -n "$NS" create secret generic actcore-runtime-secret \
--from-literal=ACTCORE_DB_URL="$ACTCORE_DB_URL" \ --from-literal=ACTCORE_DB_URL="$ACTCORE_DB_URL" \
--from-literal=WEBHOOK_SECRET_GITEA="" \ --from-literal=WEBHOOK_SECRET_GITEA="" \
--from-literal=WEBHOOK_SECRET_GITHUB="" --from-literal=WEBHOOK_SECRET_GITHUB="" \
--from-literal=OPS_HUB_KEY=""
fi fi

View File

@@ -158,7 +158,11 @@ def _inter_hub_result(sink: dict[str, Any]) -> dict[str, Any]:
missing.append("INTER_HUB_URL") missing.append("INTER_HUB_URL")
if not os.environ.get("OPS_HUB_KEY"): if not os.environ.get("OPS_HUB_KEY"):
missing.append("OPS_HUB_KEY") missing.append("OPS_HUB_KEY")
if not (sink.get("widget_mapping") or sink.get("capability_mapping")): if not (
sink.get("widget_mapping")
or sink.get("capability_mapping")
or os.environ.get("OPS_HUB_WIDGET_MAPPING")
):
missing.append("widget_mapping") missing.append("widget_mapping")
if missing: if missing:

View File

@@ -184,6 +184,25 @@ def test_inter_hub_sink_skips_cleanly_when_config_missing(monkeypatch) -> None:
] ]
def test_inter_hub_sink_accepts_widget_mapping_from_env(monkeypatch) -> None:
monkeypatch.delenv("INTER_HUB_URL", raising=False)
monkeypatch.delenv("OPS_HUB_KEY", raising=False)
monkeypatch.setenv("OPS_HUB_WIDGET_MAPPING", "ops:endpoint:gitea-registry")
result = persist_ops_inventory_evidence(
_payload([{"type": "inter-hub-interaction-event"}])
)
assert result == [
{
"type": "inter-hub-interaction-event",
"status": "skipped",
"reason": "missing_inter_hub_config",
"missing": ["INTER_HUB_URL", "OPS_HUB_KEY"],
}
]
def test_no_evidence_sinks_returns_no_results() -> None: def test_no_evidence_sinks_returns_no_results() -> None:
payload = _payload([]) payload = _payload([])
payload["context_sources"][0]["params"] = {} payload["context_sources"][0]["params"] = {}

View File

@@ -0,0 +1,192 @@
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml
import httpx
from activity_core.definition_parser import parse_file
from activity_core.context_resolvers.ops_inventory import OpsInventoryContextResolver
from activity_core.ops_evidence_sinks import persist_ops_inventory_evidence
_REPO_ROOT = Path(__file__).parent.parent
_RUNTIME_PATH = _REPO_ROOT / "k8s" / "railiance" / "20-runtime.yaml"
_BOOTSTRAP_SECRETS_PATH = _REPO_ROOT / "k8s" / "railiance" / "bootstrap-secrets.sh"
def _resources() -> list[dict[str, Any]]:
return [
resource
for resource in yaml.safe_load_all(_RUNTIME_PATH.read_text(encoding="utf-8"))
if isinstance(resource, dict)
]
def _by_kind_name(kind: str, name: str) -> dict[str, Any]:
for resource in _resources():
if resource.get("kind") == kind and resource.get("metadata", {}).get("name") == name:
return resource
raise AssertionError(f"missing {kind}/{name}")
def test_runtime_config_has_ops_inventory_placeholders() -> None:
config = _by_kind_name("ConfigMap", "actcore-runtime-config")
assert config["data"]["OPS_INVENTORY_PATH"] == (
"/etc/activity-core/ops/service-inventory.yml"
)
assert config["data"]["INTER_HUB_URL"] == ""
assert config["data"]["OPS_HUB_WIDGET_MAPPING"] == ""
def test_external_configmap_projects_disabled_ops_probe_definition(tmp_path) -> None:
config = _by_kind_name("ConfigMap", "actcore-external-activity-definitions")
raw_definition = config["data"]["ops-service-inventory-probes.md"]
definition_path = tmp_path / "ops-service-inventory-probes.md"
definition_path.write_text(raw_definition, encoding="utf-8")
definition = parse_file(definition_path)
assert definition.name == "Ops Service Inventory Probes"
assert definition.enabled is False
assert definition.trigger_config["cron_expression"] == "15 * * * *"
assert definition.context_sources == [
{
"type": "ops-inventory",
"query": "probe_services",
"required": False,
"params": {
"inventory_path": "/etc/activity-core/ops/service-inventory.yml",
"timeout_seconds": 10,
"include_kinds": ["http", "https"],
"allow_network": True,
"evidence_sinks": [
{
"type": "state-hub-progress",
"event_type": "ops_inventory_probe",
"author": "activity-core",
}
],
},
"bind_to": "context.ops_inventory_probe",
}
]
def test_ops_inventory_configmap_contains_probeable_inventory() -> None:
config = _by_kind_name("ConfigMap", "actcore-ops-service-inventory")
inventory = yaml.safe_load(config["data"]["service-inventory.yml"])
services = {service["id"]: service for service in inventory["services"]}
assert inventory["policy"]["non_secret_inventory"] is True
assert services["gitea"]["endpoints"][0]["id"] == "gitea-oci-registry"
assert services["state-hub"]["endpoints"][0]["url"] == (
"http://actcore-state-hub-bridge:8000/state/health"
)
assert services["inter-hub"]["endpoints"][0]["id"] == "inter-hub-openapi"
assert services["activity-core"]["endpoints"][0]["id"] == "activity-core-api"
def test_worker_mounts_ops_inventory_configmap() -> None:
deployment = _by_kind_name("Deployment", "actcore-worker")
pod_spec = deployment["spec"]["template"]["spec"]
container = pod_spec["containers"][0]
mounts = {mount["name"]: mount for mount in container["volumeMounts"]}
volumes = {volume["name"]: volume for volume in pod_spec["volumes"]}
assert mounts["ops-service-inventory"]["mountPath"] == "/etc/activity-core/ops"
assert mounts["ops-service-inventory"]["readOnly"] is True
assert volumes["ops-service-inventory"]["configMap"]["name"] == (
"actcore-ops-service-inventory"
)
def test_ops_hub_key_is_secret_only_placeholder() -> None:
runtime_config = _by_kind_name("ConfigMap", "actcore-runtime-config")
bootstrap = _BOOTSTRAP_SECRETS_PATH.read_text(encoding="utf-8")
assert "OPS_HUB_KEY" not in runtime_config["data"]
assert '--from-literal=OPS_HUB_KEY=""' in bootstrap
def test_disabled_ops_probe_definition_can_emit_fixture_evidence(
tmp_path,
monkeypatch,
) -> None:
definition_config = _by_kind_name("ConfigMap", "actcore-external-activity-definitions")
inventory_config = _by_kind_name("ConfigMap", "actcore-ops-service-inventory")
definition_path = tmp_path / "ops-service-inventory-probes.md"
inventory_path = tmp_path / "service-inventory.yml"
definition_path.write_text(
definition_config["data"]["ops-service-inventory-probes.md"],
encoding="utf-8",
)
inventory_path.write_text(
inventory_config["data"]["service-inventory.yml"],
encoding="utf-8",
)
definition = parse_file(definition_path)
source = definition.context_sources[0]
source["params"]["inventory_path"] = str(inventory_path)
def fake_endpoint_get(url: str, **kwargs: Any) -> Any:
if url.endswith("/v2/"):
return _HttpResponse(401, "OCI registry auth challenge")
if url.endswith("/state/health"):
return _HttpResponse(200, "health response")
if url.endswith("/openapi.json"):
return _HttpResponse(200, "OpenAPI document")
if url.endswith("/Hubs"):
return _HttpResponse(302, "login redirect when unauthenticated")
raise AssertionError(f"unexpected endpoint probe {url}")
monkeypatch.setattr(httpx, "get", fake_endpoint_get)
probe = OpsInventoryContextResolver().resolve("probe_services", None, source["params"])
posts: list[dict[str, Any]] = []
def fake_progress_get(url: str, **kwargs: Any) -> _JsonResponse:
return _JsonResponse([])
def fake_progress_post(url: str, **kwargs: Any) -> _JsonResponse:
posts.append({"url": url, **kwargs})
return _JsonResponse({"id": "progress-1"})
monkeypatch.setattr(httpx, "get", fake_progress_get)
monkeypatch.setattr(httpx, "post", fake_progress_post)
result = persist_ops_inventory_evidence(
{
"activity_id": definition.id,
"run_id": "12345678-aaaa-bbbb-cccc-123456789abc",
"scheduled_for": "2026-06-05T10:15:00+00:00",
"version_used": 1,
"context_sources": [source],
"context": {"ops_inventory_probe": probe},
}
)
assert definition.enabled is False
assert result[0]["status"] == "posted"
assert posts[0]["json"]["event_type"] == "ops_inventory_probe"
assert posts[0]["json"]["detail"]["probe"]["summary"]["ok"] == 4
class _HttpResponse:
def __init__(self, status_code: int, text: str) -> None:
self.status_code = status_code
self.text = text
class _JsonResponse:
def __init__(self, payload: Any) -> None:
self.payload = payload
def raise_for_status(self) -> None:
return None
def json(self) -> Any:
return self.payload

View File

@@ -159,7 +159,7 @@ valid and reviewable.
```task ```task
id: ACTIVITY-WP-0007-T04 id: ACTIVITY-WP-0007-T04
status: progress status: done
priority: medium priority: medium
state_hub_task_id: "45132f9f-da3c-44f1-a488-195aa0e46428" state_hub_task_id: "45132f9f-da3c-44f1-a488-195aa0e46428"
``` ```
@@ -184,11 +184,17 @@ it prematurely.
scan a disabled `ops-service-inventory-probes.md` definition carrying an scan a disabled `ops-service-inventory-probes.md` definition carrying an
`ops-inventory` context source and explicit `state-hub-progress` evidence sink. `ops-inventory` context source and explicit `state-hub-progress` evidence sink.
2026-06-05: Completed. The Railiance-projected disabled definition now uses the
`ops-inventory` resolver and explicit `state-hub-progress` evidence sink. Tests
prove the disabled definition can resolve fixture inventory data and emit one
compact `ops_inventory_probe` State Hub progress event without enabling the
production schedule.
## Wire Railiance Runtime Inputs ## Wire Railiance Runtime Inputs
```task ```task
id: ACTIVITY-WP-0007-T05 id: ACTIVITY-WP-0007-T05
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "474564be-a447-4bdf-b995-168f7a93e515" state_hub_task_id: "474564be-a447-4bdf-b995-168f7a93e515"
``` ```
@@ -209,6 +215,13 @@ Scope:
Done when the Railiance worker can see the disabled definition and inventory Done when the Railiance worker can see the disabled definition and inventory
input without leaking secrets or activating the schedule early. input without leaking secrets or activating the schedule early.
2026-06-05: Completed the first production wiring slice. `20-runtime.yaml`
projects the disabled ops probe definition, runtime config placeholders
(`OPS_INVENTORY_PATH`, `INTER_HUB_URL`, `OPS_HUB_WIDGET_MAPPING`), and a
non-secret `actcore-ops-service-inventory` ConfigMap snapshot. The worker mounts
the inventory at `/etc/activity-core/ops`, and `bootstrap-secrets.sh` keeps
`OPS_HUB_KEY` as an empty Secret-only placeholder until operator provisioning.
## Close Safety And Handoff Gates ## Close Safety And Handoff Gates
```task ```task
@@ -235,6 +248,10 @@ Acceptance criteria:
This task waits on the implementation tasks above and, for final Inter-Hub This task waits on the implementation tasks above and, for final Inter-Hub
activation, the operator-gated ops-hub widget/API-key path in `CUST-WP-0047`. activation, the operator-gated ops-hub widget/API-key path in `CUST-WP-0047`.
2026-06-05: The local implementation gates are now satisfied and tested. Live
closure remains waiting on applying the updated Railiance manifests and on the
operator-gated Inter-Hub ops-hub widget/API-key path.
## Review Verdict ## Review Verdict
activity-core should provide this as a bounded probe-and-evidence capability. activity-core should provide this as a bounded probe-and-evidence capability.