Complete ops-hub Inter-Hub bootstrap and close bootstrap workplans.

Extract bootstrap logic into src/ops_hub/bootstrap.py with dry-run support,
public hub discovery, and unit tests. Update the gate probe to accept public
hub listing (200 or 401), document dev commands and architecture, and archive
finished OPS-WP-0001 and OPS-WP-0002 workplans.
This commit is contained in:
tegwick 2026-07-07 01:49:12 +02:00
parent 0454e126cb
commit 39080333da
13 changed files with 795 additions and 370 deletions

534
src/ops_hub/bootstrap.py Normal file
View file

@ -0,0 +1,534 @@
"""Authenticated Inter-Hub bootstrap client for ops-hub."""
from __future__ import annotations
import json
import os
import stat
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from ops_hub.interhub_gate_probe import probe_interhub_gate
DEFAULT_BASE = "https://hub.coulomb.social"
ROOT = Path(__file__).resolve().parents[2]
MANIFEST_PATH = ROOT / "seeds" / "ops-hub-manifest.draft.json"
WIDGETS_PATH = ROOT / "seeds" / "ops-hub-widgets.seed.json"
class BootstrapError(RuntimeError):
pass
@dataclass(frozen=True)
class BootstrapPlan:
gate_passed: bool
hub: dict[str, Any]
manifest: dict[str, Any]
api_consumer: dict[str, Any]
runtime_key: dict[str, Any]
widgets: dict[str, Any]
event: dict[str, Any] | None
def load_secret(name: str, file_name: str) -> str:
value = os.environ.get(name, "").strip()
if value:
return value
file_path = os.environ.get(file_name, "").strip()
if file_path:
return Path(file_path).read_text(encoding="utf-8").strip()
return ""
def request_json(
base_url: str,
method: str,
path: str,
token: str | None,
body: dict[str, Any] | None,
*,
expected: set[int],
) -> dict[str, Any]:
data = json.dumps(body).encode("utf-8") if body is not None else None
request = urllib.request.Request(base_url + path, data=data, method=method)
request.add_header("Accept", "application/json")
request.add_header("User-Agent", "ops-hub-bootstrap/0.1")
if token:
request.add_header("Authorization", f"Bearer {token}")
if body is not None:
request.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(request, timeout=30) as response:
status = response.status
payload = response.read().decode("utf-8")
except urllib.error.HTTPError as error:
payload = error.read().decode("utf-8", errors="replace")
raise BootstrapError(f"{method} {path} failed with HTTP {error.code}: {payload}") from error
if status not in expected:
raise BootstrapError(f"{method} {path} returned HTTP {status}, expected {sorted(expected)}")
if not payload:
return {}
return json.loads(payload)
def list_items(base_url: str, path: str, token: str | None) -> list[dict[str, Any]]:
response = request_json(base_url, "GET", path, token, None, expected={200})
data = response.get("data", [])
if not isinstance(data, list):
raise BootstrapError(f"expected paginated data array from {path}")
return data
def first_by(items: list[dict[str, Any]], key: str, value: Any) -> dict[str, Any] | None:
return next((item for item in items if item.get(key) == value), None)
def load_manifest(path: Path = MANIFEST_PATH) -> dict[str, Any]:
manifest = json.loads(path.read_text(encoding="utf-8"))
required = [
"hub",
"manifestVersion",
"declaredWidgetTypes",
"declaredEventTypes",
"declaredAnnotationCategories",
"declaredPolicyScopes",
]
missing = [key for key in required if key not in manifest]
if missing:
raise BootstrapError(f"{path} missing required key(s): {', '.join(missing)}")
return manifest
def load_widgets(path: Path = WIDGETS_PATH) -> list[dict[str, Any]]:
widgets = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(widgets, list):
raise BootstrapError(f"{path} must contain a JSON array")
return widgets
def find_hub(base_url: str, operator_key: str | None, slug: str) -> dict[str, Any] | None:
tokens: list[str | None] = [None]
if operator_key:
tokens.append(operator_key)
for token in tokens:
try:
existing = first_by(list_items(base_url, "/api/v2/hubs", token), "slug", slug)
except BootstrapError:
continue
if existing:
return existing
return None
def plan_hub(base_url: str, operator_key: str, manifest: dict[str, Any]) -> dict[str, Any]:
slug = manifest["hub"]["slug"]
existing = find_hub(base_url, operator_key, slug)
if existing:
return {"action": "reuse", "record": existing}
return {"action": "create", "record": manifest["hub"]}
def plan_manifest(
base_url: str,
operator_key: str,
hub_id: str,
manifest: dict[str, Any],
) -> dict[str, Any]:
query = urllib.parse.urlencode({"hubId": hub_id})
manifests = list_items(base_url, f"/api/v2/hub-capability-manifests?{query}", operator_key)
active = first_by(manifests, "status", "active")
if active:
return {"action": "reuse", "record": active, "activate": False}
draft = first_by(manifests, "status", "draft")
if draft:
return {"action": "update", "record": draft, "activate": True}
return {"action": "create", "record": manifest_body(manifest, hub_id), "activate": True}
def plan_api_consumer(base_url: str, operator_key: str) -> dict[str, Any]:
existing = first_by(list_items(base_url, "/api/v2/api-consumers", operator_key), "name", "ops-hub")
if existing:
return {"action": "reuse", "record": existing}
return {"action": "create", "record": {"name": "ops-hub"}}
def plan_runtime_key(api_consumer_id: str) -> dict[str, Any]:
existing_runtime_key = load_secret("OPS_HUB_KEY", "OPS_HUB_KEY_FILE")
if existing_runtime_key:
return {
"action": "reuse",
"apiConsumerId": api_consumer_id,
"keyPrefix": existing_runtime_key[:8],
}
return {"action": "create", "apiConsumerId": api_consumer_id}
def plan_widgets(
base_url: str,
runtime_key: str | None,
hub_id: str,
widget_seeds: list[dict[str, Any]],
) -> dict[str, Any]:
if not runtime_key:
return {
"action": "plan",
"create": [seed.get("capabilityRef") for seed in widget_seeds],
"reuse": [],
}
existing_widgets = list_items(base_url, "/api/v2/widgets", runtime_key)
existing_by_ref = {
widget.get("capabilityRef"): widget
for widget in existing_widgets
if widget.get("hubId") == hub_id and widget.get("capabilityRef")
}
create = [seed for seed in widget_seeds if seed.get("capabilityRef") not in existing_by_ref]
reuse = [existing_by_ref[seed["capabilityRef"]] for seed in widget_seeds if seed.get("capabilityRef") in existing_by_ref]
return {"action": "plan", "create": create, "reuse": reuse}
def plan_event(widgets: dict[str, Any], skip_event: bool) -> dict[str, Any] | None:
if skip_event:
return None
all_widgets = widgets.get("reuse", []) + widgets.get("create", [])
readiness = next(
(widget for widget in all_widgets if widget.get("capabilityRef") == "ops:readiness:gitea-registry"),
None,
)
if readiness:
return {"action": "submit", "widgetId": readiness.get("id"), "eventType": "ops-endpoint-verified"}
return {"action": "submit", "widgetId": None, "eventType": "ops-endpoint-verified"}
def build_plan(
base_url: str,
operator_key: str,
*,
skip_event: bool = False,
manifest: dict[str, Any] | None = None,
widget_seeds: list[dict[str, Any]] | None = None,
) -> BootstrapPlan:
manifest = manifest or load_manifest()
widget_seeds = widget_seeds or load_widgets()
gate = probe_interhub_gate(base_url)
if not gate.passed:
raise BootstrapError("Inter-Hub bootstrap gate is not open")
hub_plan = plan_hub(base_url, operator_key, manifest)
hub_id = hub_plan["record"]["id"] if hub_plan["action"] == "reuse" else "<new>"
manifest_plan = (
plan_manifest(base_url, operator_key, hub_plan["record"]["id"], manifest)
if hub_plan["action"] == "reuse"
else {"action": "create", "record": manifest_body(manifest), "activate": True}
)
consumer_plan = plan_api_consumer(base_url, operator_key)
consumer_id = consumer_plan["record"].get("id", "<new>")
runtime_plan = plan_runtime_key(consumer_id)
widgets_plan = plan_widgets(
base_url,
load_secret("OPS_HUB_KEY", "OPS_HUB_KEY_FILE") or None,
hub_id if hub_plan["action"] == "reuse" else "<new>",
widget_seeds,
)
event_plan = plan_event(widgets_plan, skip_event)
return BootstrapPlan(
gate_passed=gate.passed,
hub=hub_plan,
manifest=manifest_plan,
api_consumer=consumer_plan,
runtime_key=runtime_plan,
widgets=widgets_plan,
event=event_plan,
)
def manifest_body(manifest: dict[str, Any], hub_id: str | None = None) -> dict[str, Any]:
body: dict[str, Any] = {
"manifestVersion": manifest["manifestVersion"],
"declaredWidgetTypes": manifest["declaredWidgetTypes"],
"declaredEventTypes": manifest["declaredEventTypes"],
"declaredAnnotationCategories": manifest["declaredAnnotationCategories"],
"declaredPolicyScopes": manifest["declaredPolicyScopes"],
"capabilityDescription": manifest.get("capabilityDescription", ""),
"contact": manifest.get("contact", "operator"),
}
if hub_id:
body["hubId"] = hub_id
return body
def ensure_hub(base_url: str, operator_key: str, manifest: dict[str, Any]) -> dict[str, Any]:
hub_seed = manifest["hub"]
slug = hub_seed["slug"]
existing = find_hub(base_url, operator_key, slug)
if existing:
return {"record": existing, "created": False}
body = {
"slug": slug,
"name": hub_seed["name"],
"domain": hub_seed["domain"],
"hubKind": hub_seed.get("hubKind", "domain"),
"hubFamily": "vsm",
"vsmFunction": "OPS",
"vsmSystem": "1",
}
record = request_json(base_url, "POST", "/api/v2/hubs", operator_key, body, expected={201})
return {"record": record, "created": True}
def ensure_manifest(base_url: str, operator_key: str, hub_id: str, manifest: dict[str, Any]) -> dict[str, Any]:
query = urllib.parse.urlencode({"hubId": hub_id})
manifests = list_items(base_url, f"/api/v2/hub-capability-manifests?{query}", operator_key)
active = first_by(manifests, "status", "active")
if active:
return {"record": active, "created": False, "activated": False}
draft = first_by(manifests, "status", "draft")
if draft:
record = request_json(
base_url,
"PATCH",
f"/api/v2/hub-capability-manifests/{draft['id']}",
operator_key,
manifest_body(manifest),
expected={200},
)
created = False
else:
record = request_json(
base_url,
"POST",
"/api/v2/hub-capability-manifests",
operator_key,
manifest_body(manifest, hub_id),
expected={201},
)
created = True
activated = request_json(
base_url,
"POST",
f"/api/v2/hub-capability-manifests/{record['id']}/activate",
operator_key,
None,
expected={200},
)
return {"record": activated, "created": created, "activated": True}
def ensure_api_consumer(base_url: str, operator_key: str, manifest_id: str) -> dict[str, Any]:
consumers = list_items(base_url, "/api/v2/api-consumers", operator_key)
existing = first_by(consumers, "name", "ops-hub")
if existing:
return {"record": existing, "created": False}
record = request_json(
base_url,
"POST",
"/api/v2/api-consumers",
operator_key,
{
"name": "ops-hub",
"description": "API consumer for the VSM Operations hub",
"hubCapabilityManifestId": manifest_id,
"rateLimitPerMinute": 120,
"quotaPerDay": 50000,
},
expected={201},
)
return {"record": record, "created": True}
def write_runtime_key(full_key: str, output_path: str | None) -> Path:
if output_path:
path = Path(output_path)
path.write_text(full_key, encoding="utf-8")
else:
fd, raw_path = tempfile.mkstemp(prefix="ops-hub-runtime-key-", text=True)
path = Path(raw_path)
with os.fdopen(fd, "w", encoding="utf-8") as handle:
handle.write(full_key)
path.chmod(stat.S_IRUSR | stat.S_IWUSR)
return path
def ensure_runtime_key(
base_url: str,
operator_key: str,
api_consumer_id: str,
output_path: str | None,
) -> dict[str, Any]:
existing_runtime_key = load_secret("OPS_HUB_KEY", "OPS_HUB_KEY_FILE")
if existing_runtime_key:
return {
"created": False,
"token": existing_runtime_key,
"keyPrefix": existing_runtime_key[:8],
"keyFile": os.environ.get("OPS_HUB_KEY_FILE"),
}
response = request_json(
base_url,
"POST",
f"/api/v2/api-consumers/{api_consumer_id}/api-keys",
operator_key,
{"scopes": "framework:read hub:ops-hub:read hub:ops-hub:write"},
expected={201},
)
full_key = response.get("fullKey")
if not full_key:
raise BootstrapError("api key creation did not return display-once fullKey")
key_file = write_runtime_key(full_key, output_path)
api_key = response.get("apiKey") or {}
return {
"created": True,
"token": full_key,
"keyPrefix": api_key.get("keyPrefix", full_key[:8]),
"keyFile": str(key_file),
}
def ensure_widgets(base_url: str, runtime_key: str, hub_id: str, widget_seeds: list[dict[str, Any]]) -> dict[str, Any]:
existing_widgets = list_items(base_url, "/api/v2/widgets", runtime_key)
existing_by_ref = {
widget.get("capabilityRef"): widget
for widget in existing_widgets
if widget.get("hubId") == hub_id and widget.get("capabilityRef")
}
created: list[dict[str, Any]] = []
reused: list[dict[str, Any]] = []
for seed in widget_seeds:
existing = existing_by_ref.get(seed.get("capabilityRef"))
if existing:
reused.append(existing)
continue
body = {"hubId": hub_id, "status": "active", **seed}
created.append(request_json(base_url, "POST", "/api/v2/widgets", runtime_key, body, expected={201}))
return {"created": created, "reused": reused}
def submit_gitea_event(base_url: str, runtime_key: str, widgets: dict[str, Any]) -> dict[str, Any] | None:
all_widgets = widgets["created"] + widgets["reused"]
readiness = next(
(widget for widget in all_widgets if widget.get("capabilityRef") == "ops:readiness:gitea-registry"),
None,
)
if not readiness:
return None
return request_json(
base_url,
"POST",
"/api/v2/interaction-events",
runtime_key,
{
"widgetId": readiness["id"],
"eventType": "ops-endpoint-verified",
"viewContext": "railiance-apps/workplans/RAIL-AP-WP-0001",
"metadata": {
"vsmFunction": "OPS",
"vsmSystem": "S1",
"endpoint": "https://gitea.coulomb.social/v2/",
"expectedStatus": 401,
"observedHeader": "Docker-Distribution-Api-Version: registry/2.0",
"recordedBy": "ops-hub/scripts/ops-hub-bootstrap-api.py",
"recordedAt": int(time.time()),
},
},
expected={201},
)
def run_bootstrap(
base_url: str,
operator_key: str,
*,
runtime_key_output: str | None = None,
skip_event: bool = False,
manifest: dict[str, Any] | None = None,
widget_seeds: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
gate = probe_interhub_gate(base_url)
if not gate.passed:
raise BootstrapError("Inter-Hub bootstrap gate is not open")
manifest = manifest or load_manifest()
widget_seeds = widget_seeds or load_widgets()
hub = ensure_hub(base_url, operator_key, manifest)
hub_record = hub["record"]
manifest_result = ensure_manifest(base_url, operator_key, hub_record["id"], manifest)
manifest_record = manifest_result["record"]
consumer = ensure_api_consumer(base_url, operator_key, manifest_record["id"])
runtime_key = ensure_runtime_key(base_url, operator_key, consumer["record"]["id"], runtime_key_output)
widgets = ensure_widgets(base_url, runtime_key["token"], hub_record["id"], widget_seeds)
event = None if skip_event else submit_gitea_event(base_url, runtime_key["token"], widgets)
return {
"ok": True,
"dryRun": False,
"base": base_url,
"gate": {"passed": gate.passed, "hubsStatus": gate.hubs_status},
"hub": {"id": hub_record["id"], "slug": hub_record["slug"], "created": hub["created"]},
"manifest": {
"id": manifest_record["id"],
"status": manifest_record["status"],
"created": manifest_result["created"],
"activated": manifest_result["activated"],
},
"apiConsumer": {"id": consumer["record"]["id"], "name": consumer["record"]["name"], "created": consumer["created"]},
"runtimeKey": {
"created": runtime_key["created"],
"keyPrefix": runtime_key["keyPrefix"],
"keyFile": runtime_key["keyFile"],
"storeImmediatelyInOpenBao": "platform/operators/ops-hub/runtime",
"field": "OPS_HUB_KEY",
},
"widgets": {"created": len(widgets["created"]), "reused": len(widgets["reused"])},
"event": None
if event is None
else {"id": event["id"], "eventType": event["eventType"], "widgetId": event["widgetId"]},
}
def dry_run_bootstrap(
base_url: str,
operator_key: str,
*,
skip_event: bool = False,
manifest: dict[str, Any] | None = None,
widget_seeds: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
gate = probe_interhub_gate(base_url)
plan = build_plan(
base_url,
operator_key,
skip_event=skip_event,
manifest=manifest,
widget_seeds=widget_seeds,
)
return {
"ok": True,
"dryRun": True,
"base": base_url,
"gate": {"passed": gate.passed, "hubsStatus": gate.hubs_status},
"plan": {
"hub": plan.hub,
"manifest": plan.manifest,
"apiConsumer": plan.api_consumer,
"runtimeKey": plan.runtime_key,
"widgets": {
"create": len(plan.widgets.get("create", [])),
"reuse": len(plan.widgets.get("reuse", [])),
},
"event": plan.event,
},
}

View file

@ -82,7 +82,7 @@ def probe_interhub_gate(base_url: str, timeout: float = 10.0) -> GateResult:
hubs = observe_status(api_url(normalized, "/api/v2/hubs"), timeout)
openapi_observation, openapi = fetch_json(api_url(normalized, "/api/v2/openapi.json"), timeout)
present, missing = evaluate_openapi_paths(openapi)
passed = hubs.status == 401 and not missing
passed = hubs.status in {200, 401} and not missing
return GateResult(
base_url=normalized.rstrip("/"),
passed=passed,