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_SINK_TYPE: "null"
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
ACTIVITY_CURATOR_GATE: disabled
---
@@ -58,6 +61,219 @@ data:
Kubernetes projection of the Custodian-owned definition in
`/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
kind: Service
@@ -360,10 +576,16 @@ spec:
- name: external-activity-definitions
mountPath: /etc/activity-core/external-definitions/activity-definitions
readOnly: true
- name: ops-service-inventory
mountPath: /etc/activity-core/ops
readOnly: true
volumes:
- name: external-activity-definitions
configMap:
name: actcore-external-activity-definitions
- name: ops-service-inventory
configMap:
name: actcore-ops-service-inventory
---
apiVersion: apps/v1
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
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
```bash

View File

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

View File

@@ -158,7 +158,11 @@ def _inter_hub_result(sink: dict[str, Any]) -> dict[str, Any]:
missing.append("INTER_HUB_URL")
if not os.environ.get("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")
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:
payload = _payload([])
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
id: ACTIVITY-WP-0007-T04
status: progress
status: done
priority: medium
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
`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
```task
id: ACTIVITY-WP-0007-T05
status: todo
status: done
priority: medium
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
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
```task
@@ -235,6 +248,10 @@ Acceptance criteria:
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`.
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
activity-core should provide this as a bounded probe-and-evidence capability.