From 1526981fa3b050dfa8d68f5a46251fa2bd4efee8 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:50:09 -0400 Subject: [PATCH] =?UTF-8?q?feat(paas):=20account=20tiers=20(entitlement)?= =?UTF-8?q?=20+=20availability-maturity=20grade=20=E2=80=94=20the=20BlueMi?= =?UTF-8?q?x=20lessons,=20sovereign?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The enterprise-PaaS governance the BlueMix Gen3/Gen4 diagrams model, done light + sovereign. Account tiers (admission.py) — BlueMix entitlement/Free-Paid, as quota + backend entitlement: - TIERS free|pro|enterprise: each sets max_concurrent/gpu_max/cost_budget AND allowed_backends (free -> local/wasm only; pro -> +k8s/hpc/volunteer; enterprise -> all). - AdmissionController(tiers={subject: tier}); quota_for applies the tier (explicit quotas still override); allowed_backends(subject) feeds compute_plane.place() policy. Default tier: pro. - free tier denies GPU work (gpu_max 0), fail-closed. Availability-maturity grade (availability.py) — the DW-DevOps Zero-Downtime legend as a governance artifact (like the commons' reproducible/declared grade): - GRADES: non-managed -> needs-work -> almost-zd -> zero-downtime. - a CapD declares policy.availability; estate_availability() reports per-capability grades + a histogram (default non-managed). Honest grades added: compute-plane needs-work; commons / verification / data-spheres almost-zd. Tests: +5 tier +2 availability = 147 tools tests green. validate REQUIRED + Makefile `availability`. Completes the three PaaS-lesson units (dev-mode #28, data-spheres #29, tiers+availability). --- Makefile | 3 ++ capd/compute-plane.mesh.capd.json | 1 + capd/data-spheres.mesh.capd.json | 1 + capd/knowledge-commons.mesh.capd.json | 1 + ...volunteer-mesh-verification.mesh.capd.json | 1 + tools/admission.py | 30 ++++++++++-- tools/availability.py | 49 +++++++++++++++++++ tools/test_admission.py | 27 +++++++++- tools/test_availability.py | 27 ++++++++++ tools/validate.py | 1 + 10 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 tools/availability.py create mode 100644 tools/test_availability.py diff --git a/Makefile b/Makefile index 08255df..58f586e 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,9 @@ lease: ## pull/lease scheduler demo: workers pull WUs, crash-stop re-lending, or sphere: ## data-sphere demo: immutable dm-verity sphere, construction-tenancy, intent x link x durability cd tools && python3 data_sphere.py +availability: ## report the estate's availability-maturity grades (the Zero-Downtime legend) + cd tools && python3 availability.py + onboard: ## bring up a workstation: local sovereign forge + local cluster + sourceosctl @echo "[continuum] onboard — scaffold: wires Gitea bring-up + kind/k3s + sourceos-devtools/sourceosctl" diff --git a/capd/compute-plane.mesh.capd.json b/capd/compute-plane.mesh.capd.json index fb2ce8b..5f6586c 100644 --- a/capd/compute-plane.mesh.capd.json +++ b/capd/compute-plane.mesh.capd.json @@ -21,6 +21,7 @@ "scales_up_to": "caps.infra.cluster-scaleup.hyperswarm@0.1.0" }, "policy": { + "availability": "needs-work", "fail_closed": true, "evidence_emitting": true, "sensitive_never_untrusted": true, diff --git a/capd/data-spheres.mesh.capd.json b/capd/data-spheres.mesh.capd.json index 31a096d..6fde9d5 100644 --- a/capd/data-spheres.mesh.capd.json +++ b/capd/data-spheres.mesh.capd.json @@ -19,6 +19,7 @@ "scales_up_to": "caps.infra.cluster-scaleup.hyperswarm@0.1.0" }, "policy": { + "availability": "almost-zd", "immutable": true, "integrity_by_construction": true, "tenancy_by_construction": true, diff --git a/capd/knowledge-commons.mesh.capd.json b/capd/knowledge-commons.mesh.capd.json index 12b92ea..e360fdf 100644 --- a/capd/knowledge-commons.mesh.capd.json +++ b/capd/knowledge-commons.mesh.capd.json @@ -19,6 +19,7 @@ "scales_up_to": "caps.infra.cluster-scaleup.hyperswarm@0.1.0" }, "policy": { + "availability": "almost-zd", "content_addressed": true, "citable": true, "reproducibility_gate": true, diff --git a/capd/volunteer-mesh-verification.mesh.capd.json b/capd/volunteer-mesh-verification.mesh.capd.json index 5624359..85df584 100644 --- a/capd/volunteer-mesh-verification.mesh.capd.json +++ b/capd/volunteer-mesh-verification.mesh.capd.json @@ -20,6 +20,7 @@ "scales_up_to": "caps.infra.cluster-scaleup.hyperswarm@0.1.0" }, "policy": { + "availability": "almost-zd", "result_verification": true, "fail_closed": true, "redundant_quorum": true, diff --git a/tools/admission.py b/tools/admission.py index f37f724..3a942c6 100644 --- a/tools/admission.py +++ b/tools/admission.py @@ -19,18 +19,39 @@ DEFAULT_QUOTA = {"max_concurrent": 4, "gpu_max": 2, "cost_budget": 100.0} +# Account tiers (BlueMix entitlement, done sovereign + light): a tier sets the quota AND which +# backends a subject may reach. `allowed_backends: None` means all. +TIERS = { + "free": {"max_concurrent": 1, "gpu_max": 0, "cost_budget": 5.0, + "allowed_backends": ["local", "wasm-edge"]}, + "pro": {"max_concurrent": 4, "gpu_max": 2, "cost_budget": 100.0, + "allowed_backends": ["local", "wasm-edge", "k8s", "hpc-slurm", "p2p-mesh", "volunteer-boinc"]}, + "enterprise": {"max_concurrent": 16, "gpu_max": 8, "cost_budget": 5000.0, + "allowed_backends": None}, +} + class AdmissionController: - """Per-subject (or per-project) quotas + a live consumption ledger. + """Per-subject (or per-project) quotas + account tiers + a live consumption ledger. With a `ledger_path`, consumption persists across processes (so a CLI enforces a real running - budget, not a fresh one each invocation).""" + budget, not a fresh one each invocation). A `tiers` map (subject -> tier name) applies the tier's + quota + backend entitlement, which explicit `quotas` may still override.""" - def __init__(self, quotas: dict[str, dict] | None = None, ledger_path=None): + def __init__(self, quotas: dict[str, dict] | None = None, ledger_path=None, + tiers: dict[str, str] | None = None): self._quotas = quotas or {} + self._tiers = tiers or {} self._ledger_path = Path(ledger_path) if ledger_path else None self._usage: dict[str, dict] = self._load() + def tier(self, key: str) -> str: + return self._tiers.get(key, "pro") + + def allowed_backends(self, key: str) -> list | None: + """The tier's backend entitlement — feed into compute_plane.place() policy. None = all.""" + return TIERS.get(self.tier(key), {}).get("allowed_backends") + def _load(self) -> dict: if self._ledger_path and self._ledger_path.exists(): try: @@ -45,7 +66,8 @@ def _save(self) -> None: self._ledger_path.write_text(json.dumps(self._usage, sort_keys=True)) def quota_for(self, key: str) -> dict: - return {**DEFAULT_QUOTA, **self._quotas.get(key, {})} + tier_q = {k: v for k, v in TIERS.get(self.tier(key), {}).items() if k != "allowed_backends"} + return {**DEFAULT_QUOTA, **tier_q, **self._quotas.get(key, {})} # tier, then explicit override def usage(self, key: str) -> dict: return self._usage.setdefault(key, {"concurrent": 0, "gpu": 0, "cost": 0.0}) diff --git a/tools/availability.py b/tools/availability.py new file mode 100644 index 0000000..3ce4f61 --- /dev/null +++ b/tools/availability.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Availability maturity grade — the BlueMix / DW-DevOps Zero-Downtime legend as a governance artifact. + +Every capability carries an honest availability GRADE (like the commons' reproducible/declared grade). +The ladder is the continuous-availability legend from the Gen4 diagram: + + non-managed -> needs-work -> almost-zd -> zero-downtime + +A CapD declares its grade in `policy.availability` (default: non-managed). This reports the estate's +availability posture at a glance and gives promotion a hook — a capability shouldn't claim a grade it +can't back. Sovereign + light: a legend, not an IBM stack. +""" +from __future__ import annotations + +import json +from pathlib import Path + +GRADES = ("non-managed", "needs-work", "almost-zd", "zero-downtime") +_ROOT = Path(__file__).resolve().parent.parent + + +def grade_rank(grade: str) -> int: + return GRADES.index(grade) if grade in GRADES else 0 + + +def estate_availability(root=None) -> dict: + """Read every CapD's declared availability grade. Returns per-capability grades + a histogram.""" + root = Path(root or _ROOT) + caps = [] + d = root / "capd" + for f in sorted(d.glob("*.capd.json")) if d.is_dir() else []: + try: + data = json.loads(f.read_text()) + except (OSError, json.JSONDecodeError): + continue + grade = data.get("policy", {}).get("availability", "non-managed") + if grade not in GRADES: + grade = "non-managed" + caps.append({"capability_id": data.get("capability_id", f.stem), "availability": grade}) + hist = {g: sum(1 for c in caps if c["availability"] == g) for g in GRADES} + return {"capabilities": sorted(caps, key=lambda c: -grade_rank(c["availability"])), + "histogram": hist, "total": len(caps)} + + +if __name__ == "__main__": + report = estate_availability() + print(json.dumps({"histogram": report["histogram"], "total": report["total"], + "top": [c for c in report["capabilities"] if c["availability"] != "non-managed"]}, + indent=2)) diff --git a/tools/test_admission.py b/tools/test_admission.py index f273931..08ea300 100644 --- a/tools/test_admission.py +++ b/tools/test_admission.py @@ -55,10 +55,35 @@ def test_subjects_have_isolated_budgets(): def test_default_quota_applies_to_unknown_subject(): - ac = adm.AdmissionController() + ac = adm.AdmissionController(tiers={"spiffe://who/dis": "__none__"}) # unknown tier -> DEFAULT_QUOTA assert ac.quota_for("spiffe://who/dis") == adm.DEFAULT_QUOTA +def test_tier_sets_quota_and_backend_entitlement(): + ac = adm.AdmissionController(tiers={"free-user": "free", "ent-user": "enterprise"}) + assert ac.quota_for("free-user")["gpu_max"] == 0 + assert ac.allowed_backends("free-user") == ["local", "wasm-edge"] + assert ac.allowed_backends("ent-user") is None # enterprise = all backends + assert ac.quota_for("ent-user")["max_concurrent"] == 16 + + +def test_default_tier_is_pro(): + ac = adm.AdmissionController() + assert ac.tier("anyone") == "pro" and ac.quota_for("anyone")["gpu_max"] == 2 + + +def test_explicit_quota_overrides_tier(): + ac = adm.AdmissionController(quotas={"u": {"gpu_max": 5}}, tiers={"u": "free"}) + assert ac.quota_for("u")["gpu_max"] == 5 # explicit override wins + assert ac.quota_for("u")["max_concurrent"] == 1 # ...but the rest is the free tier + + +def test_free_tier_denies_gpu_work(): + ac = adm.AdmissionController(tiers={"u": "free"}) + r = ac.admit("u", {"needs_gpu": True}) + assert not r["admitted"] and "gpu_max" in r["exceeded"] + + def test_ledger_persists_consumption_across_controllers(): import pathlib import tempfile diff --git a/tools/test_availability.py b/tools/test_availability.py new file mode 100644 index 0000000..85c7aa3 --- /dev/null +++ b/tools/test_availability.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Tests for the availability maturity grade (the ZD legend over CapDs).""" +import availability as av + + +def test_grade_ladder_is_ordered(): + assert av.GRADES == ("non-managed", "needs-work", "almost-zd", "zero-downtime") + assert av.grade_rank("almost-zd") > av.grade_rank("needs-work") > av.grade_rank("non-managed") + assert av.grade_rank("bogus") == 0 # unknown -> lowest + + +def test_estate_reports_declared_grades_and_defaults_the_rest(): + r = av.estate_availability() + assert r["total"] >= 5 + grades = {c["capability_id"]: c["availability"] for c in r["capabilities"]} + assert all(g in av.GRADES for g in grades.values()) # every grade is valid (default applied) + assert r["histogram"]["almost-zd"] >= 3 # commons, verification, data-spheres + assert any(g == "needs-work" for g in grades.values()) # compute-plane + + +if __name__ == "__main__": + import sys + fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")] + for fn in fns: + fn() + print(f"ok: {len(fns)} availability tests passed") + sys.exit(0) diff --git a/tools/validate.py b/tools/validate.py index 936dbcd..233a1ce 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -41,6 +41,7 @@ "tools/lease_scheduler.py", "tools/devmode.py", "tools/data_sphere.py", + "tools/availability.py", ] CAPD_KEYS = ("capability_id", "kind", "status", "links", "composes_with", "policy") # Every CapD in capd/ must carry the core keys and parse — not just the flagship control-plane one.