Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

provision: ## provisioning demo: tenant broker + entitlement tier + BSS meter + /me
cd tools && python3 provisioning.py

deploy: ## git-push-deploy demo: buildpack detect -> reproducible OCI image -> workload (no Dockerfile)
cd tools && python3 buildpack.py

Expand Down
30 changes: 30 additions & 0 deletions capd/provisioning-plane.mesh.capd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"capability_id": "caps.dev.provisioning@0.1.0",
"kind": "dev.control-plane",
"status": "experimental",
"name": "Provisioning plane — the server control plane (broker + entitlement + BSS + /me)",
"description": "The server/control-plane seam the Nocalhost and BlueMix diagrams have and we lacked: log in -> provision my tenant -> get my namespace + quota + endpoint -> see my usage -> be billed. Nocalhost calls it Web+API+Dep; BlueMix calls it SSO+Entitlement+Broker+Token+BSS. This unifies the governed core, composing devspace (isolated tenant namespace) + admission tiers (entitlement: quota + backend allowlist) + grants — as an Open-Service-Broker-shaped surface: provision, bind (namespace + endpoint twin/box + session), deprovision, meter (BSS billing from the usage ledger), and /me (tenant profile). Unlike BlueMix's proprietary Cloud-Foundry stack (Diego/Garden/BOSH, hosted lock-in), this is k8s-native, open, and governed (sealed provisioning receipts).",
"links": {
"engine": "tools/provisioning.py",
"devspace": "tools/devspace.py",
"entitlement": "tools/admission.py",
"portal": "tools/portal_server.py",
"spec_witness": "docs/PAAS_GAP_REGISTER.md",
"reference_pattern": "Nocalhost Web+API+Dep + BlueMix SSO/Entitlement/Broker/Token/BSS — unified, k8s-native, open, governed; the sovereign answer to proprietary Cloud Foundry"
},
"composes_with": {
"inner_loop": "caps.dev.devspace-inner-loop@0.1.0",
"git_push_deploy": "caps.dev.git-push-deploy@0.1.0",
"control_plane": "caps.infra.paas.continuum-local@0.1.0",
"scales_up_to": "caps.infra.cluster-scaleup.hyperswarm@0.1.0"
},
"policy": {
"availability": "needs-work",
"open_service_broker": true,
"entitlement_tiers": true,
"metered_billing": true,
"sealed_provisioning": true,
"k8s_native": true,
"evidence_emitting": true
}
}
112 changes: 112 additions & 0 deletions docs/PAAS_GAP_REGISTER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# PaaS gap register — Nocalhost + BlueMix/Watson, box by box

Every component OCR'd off the four diagrams (Nocalhost *Sidecar* + *How It Works*; BlueMix/Watson
*Gen3* + DW-DevOps *Gen4*), mapped to our stack and graded honestly. `✅` have · `◑` partial · `○`
GAP. The point: we have strong *primitives* but the **server/control-plane seam** that makes those
diagrams feel seamless is mostly missing — that's what this register makes explicit.

## Nocalhost — developer inner loop (client)

| Box (OCR) | Ours | Grade |
|---|---|---|
| Developer → **Dev Mode** | `devmode.py` (patch + sync + port-forward *plan*) | ◑ plan, no live loop / IDE trigger |
| **IDE (Plugin)** — VS Code | — | ○ **GAP: no IDE plugin** (we have a CLI, not an editor extension) |
| **nhctl (CLI)** | `sourceosctl` | ✅ |
| **DevContainer + App** (sidecar dev container) | `devmode_patch` swaps the container | ◑ (no sidecar-alongside-app mode; we replace, not sidecar) |
| Hot Reloading / Local Access / Debugging | sync (`kubectl cp`) + port-forward commands | ◑ (generated, not run; no debugger wiring) |
| **AppA(terminal)** — remote terminal | cloud-shell fog grant-bound attach (*design*) | ○ **GAP: no running remote terminal** |
| namespace-per-user + **ServiceAccount** isolation | `devspace` (Namespace + quota + NetworkPolicy), grant binding | ✅ |

## Nocalhost — server / admin control plane

| Box | Ours | Grade |
|---|---|---|
| **Nocalhost-Web** (admin web console) | portal (read-only dev console) | ◑ (no admin console) |
| **Nocalhost-API** (REST) | MCP ops surface (agent JSON-RPC) | ◑ (no REST admin API) |
| **Nocalhost-Dep** (cluster-side dep, `nocalhost-reserved` ns) | — | ○ **GAP: no cluster-side controller/operator** |
| **Login** / auth | — | ○ **GAP: no login/session/SSO** |
| Admin: **Create User** | — | ○ **GAP** (this change closes it) |
| Admin: **Configure Cluster** | CapDs (static) | ◑ |
| Admin: **Configure Application** | buildpack + workload specs | ◑ |

## BlueMix / Watson Gen3 — multi-tenant SaaS

| Box | Ours | Grade |
|---|---|---|
| **Load Balancer** | — | ○ GAP (no ingress LB in continuum) |
| DataWorks **UI App Server** | portal | ◑ |
| **BlueMix SSO service** | — | ○ **GAP: no SSO** |
| **BlueMix Entitlement service** | admission **tiers** | ◑ (tier data, no entitlement *service*/token) |
| **Watson Token Server** (per-tenant) + **/me API** | grants (session tokens) | ◑ (no token server / user-profile `/me`) — this change adds `/me` |
| **Service Broker** (add tenant / provision new VM) | `devspace` manifests | ○ **GAP** (this change closes it) |
| **Landing Page** / onboarding | — | ○ GAP |
| **Resource Manager** | `compute_plane.place` | ◑ (no persistent RM service) |
| **Liberty Collective Cluster** | k8s via executor | ◑ |
| **DB2** → **Cloudant** (managed state) | sealed ledger + content-addressed commons | ✅ (state is a log, not a monolith DB) |
| **Free/Paid × Interactive/Batch** Spark tiers | admission tiers (free/pro/enterprise) | ◑ (no interactive×batch × free×paid matrix) |

## DW-DevOps Gen4 — ops / HA

| Box | Ours | Grade |
|---|---|---|
| **CDS Load Balancer** (RM Routes → IHS Routes) | — | ○ **GAP: no routing/LB plane** |
| **RM A / RM B** zero-downtime failover | single `place()` decision | ○ **GAP: no HA/failover for the control plane** |
| **Cluster A / Cluster B** (IHS/CC/CM) active-active | one mesh | ○ GAP (no active-active twin clusters) |
| **Metering** | admission cost/usage ledger | ◑ (tracked, not exported) — this change adds a metering record |
| **BSS** (billing / business support) | credits in admission | ○ **GAP: no billing surface** |
| **ELK / Grafana** | prophet-platform OTel/Prometheus config | ◑ (no dashboards in continuum) |
| **Cloudant** managed state | commons + ledger | ✅ |
| **Continuous-Availability legend** | `availability.py` grades | ✅ |
| **Broker / Services Components** | — | ○ (this change) |

## The seam this register exposes

We have the **substrate** (place/grant/admission/executor/devspace/buildpack/inference) but not the
**server control plane** that a developer and an admin actually touch: *log in → provision my tenant
→ get my namespace + quota + endpoint → see my usage → be billed*. Nocalhost calls that Web+API+Dep;
BlueMix calls it SSO+Entitlement+Broker+Token+BSS. It's the same server. `tools/provisioning.py`
(this change) builds the governed core of it — provision/bind/deprovision a tenant (DevSpace + tier +
session), meter usage, and `/me` — and the portal exposes `/api/me` + `/api/provision`.

**Still open after this change** (the honest ranked backlog): IDE plugin · running remote terminal ·
real login/SSO · cluster-side dep operator · load-balancer + RM-A/B failover HA · ELK/Grafana
dashboards · landing page. Named here so nothing hides.

## Cloud Foundry / BlueMix — similar, different, and why ours is best-of-all

**Similar.** Cloud Foundry *invented* the ergonomic everyone copied: `cf push` — git-push-to-deploy
with **buildpacks** (CF's invention; Heroku popularized it, and it's the direct ancestor of the Cloud
Native Buildpacks / Paketo we use in `buildpack.py`), staged apps, service brokers, org/space
multi-tenancy, and marketplace add-ons. BlueMix was IBM's hosted CF plus the Watson/DataWorks
services in the Gen3/Gen4 diagrams. So the *shape* — build, bind, broker, tier, meter — is the shape
we're matching.

**Different, and why the community walked.** Two reasons, and your read is right:
1. **Not k8s-native.** CF is a *parallel, opinionated stack* — its own scheduler (**Diego**, not
k8s), its own container runtime (**Garden**, not OCI/Docker), its own release tool (**BOSH**). It
predated Kubernetes and never aligned with it. When k8s won the orchestration war (~2017), CF's
parallel universe became a liability; `cf-for-k8s`/Korifi arrived too late.
2. **BlueMix buried the open core under proprietary, hosted lock-in.** CF the core was open, but
BlueMix was IBM-hosted + IBM-proprietary services (SSO, Entitlement, Token Server, Watson) on top.
Not the code/cloud-native openness the community wanted.

The community's actual want was **"Heroku's DX on my own Kubernetes, open."** That's why **Porter**
(and Coolify, Dokku, Railway) exist, and why CF's buildpack IP was extracted into the CNCF's
**Cloud Native Buildpacks**.

**Why ours is best-of-all — we took each thing from where it was best:**
- CF's **buildpack / git-push ergonomic** — via open CNB/Paketo (`buildpack.py`), reproducible +
SLSA-attestable, not a proprietary slug.
- Porter's **k8s-native** runtime — the executor dispatches real k8s (proven on a live cluster), no
Diego/Garden/BOSH parallel stack.
- Heroku's **radical simplicity** + BlueMix's **entitlement/broker/metering** — as sovereign, light
primitives (`admission` tiers, `provisioning` broker + BSS meter + `/me`), not a hosted lock-in.
- And what **none** of them have: **zero-trust governance** (grants, Needs firewall, sealed
receipts), a **multi-substrate mesh** (local→k8s→HPC→wasm→p2p→**volunteer**→blockchain — they are
k8s-only), **sovereign GPU inference** (our own LLMs, sensitive data never leaving), **data
spheres** (immutable, residency-fenced), and a **mobile twin/box** front door.

CF/BlueMix answered "how do I push an app to *a* cloud." We answer "how do I run *any* governed
workload — app, batch, MPI, model, volunteer WU — across *my own* sovereign fabric, from my phone,
without a cloud provider." Same push ergonomic on top; a categorically larger, open, governed
substrate underneath.
13 changes: 12 additions & 1 deletion tools/portal_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ def _inference() -> dict:
for m in models]}


def _me() -> dict:
"""The tenant profile (Watson '/me'-style): tier, namespace, quota, live usage, endpoint."""
pv = _sib("provisioning")
adm = _sib("admission")
ac = adm.AdmissionController(ledger_path=_ROOT / "artifacts" / "admission-usage.json",
tiers={"you": "pro"})
b = pv.ServiceBroker(admission=ac)
b.provision(tenant="you", user="dev", tier="pro", app="default")
return b.me("you") or {}


_MANIFEST = json.dumps({
"name": "SourceOS Continuum", "short_name": "Continuum", "start_url": "/", "scope": "/",
"display": "standalone", "background_color": "#0b0d12", "theme_color": "#0b0d12",
Expand Down Expand Up @@ -245,7 +256,7 @@ def route(path: str) -> tuple[int, str, str]:
api = {"/api/capabilities": _capabilities, "/api/lifecycle": _lifecycle,
"/api/evidence": _evidence, "/api/compute": _compute,
"/api/mesh": _mesh, "/api/placements": _placements, "/api/commons": _commons,
"/api/inference": _inference}
"/api/inference": _inference, "/api/me": _me}
if path in api:
return 200, "application/json", json.dumps(api[path](), indent=2, sort_keys=True)
return 404, "text/plain", "not found"
Expand Down
107 changes: 107 additions & 0 deletions tools/provisioning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Provisioning plane — the server/control-plane seam the diagrams have and we lacked.

Nocalhost calls it Web+API+Dep; BlueMix calls it SSO+Entitlement+Broker+Token+BSS. It is the same
server, and a developer/admin actually touches it: log in -> provision my tenant -> get my namespace
+ quota + endpoint -> see my usage -> be billed. This builds the governed core, composing what we
already have (devspace + admission tiers + grants), fail-closed and evidence-emitting — and, unlike
BlueMix's Cloud-Foundry stack, k8s-native and open.

An Open-Service-Broker-shaped surface:
* provision — create a tenant = an isolated DevSpace + an entitlement tier (quota + backend
allowlist), sealed.
* bind — issue access: namespace + endpoint (twin default / box) + a session.
* deprovision— teardown.
* meter — BSS: turn the admission usage ledger into a billable metering record.
* me — the tenant profile (tier, namespace, quota, usage, endpoint).
"""
from __future__ import annotations

import hashlib
import json
from datetime import datetime, timezone

import admission as adm
import devspace as dv


def _seal(body: dict) -> str:
return "sha256:" + hashlib.sha256(
json.dumps(body, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()


def _iso() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


class ServiceBroker:
"""The governed provisioning/entitlement broker. `admission` (an AdmissionController) supplies the
live usage ledger for metering + /me."""

def __init__(self, *, admission=None):
self._tenants: dict[str, dict] = {}
self._admission = admission or adm.AdmissionController()

def provision(self, *, tenant: str, user: str, tier: str = "pro", app: str | None = None,
grant_id: str | None = None) -> dict:
"""Create a tenant: an isolated DevSpace + the tier's entitlement (quota + backend allowlist)."""
tier_spec = adm.TIERS.get(tier, adm.TIERS["pro"])
quota = {k: v for k, v in tier_spec.items() if k != "allowed_backends"}
manifests = dv.devspace_manifests(tenant=tenant, user=user, space=app or "default",
grant_id=grant_id)
rec = {"tenant": tenant, "user": user, "tier": tier,
"namespace": manifests[0]["metadata"]["name"],
"quota": quota, "allowed_backends": tier_spec.get("allowed_backends"),
"manifests": manifests, "state": "provisioned", "provisioned_at": _iso()}
rec["receipt"] = _seal({k: v for k, v in rec.items() if k != "manifests"})
self._tenants[tenant] = rec
return rec

def bind(self, tenant: str, *, endpoint: str = "twin") -> dict | None:
"""Issue access: the connection info (namespace + endpoint + session). twin is the default
rendezvous (always-on); box is the opt-in direct/LAN path."""
t = self._tenants.get(tenant)
if t is None:
return None
session = "sess_" + hashlib.sha256(f"{tenant}:{_iso()}".encode()).hexdigest()[:10]
return {"tenant": tenant, "namespace": t["namespace"], "endpoint": endpoint,
"session_id": session, "quota": t["quota"], "allowed_backends": t["allowed_backends"],
"bound_at": _iso()}

def deprovision(self, tenant: str) -> bool:
return self._tenants.pop(tenant, None) is not None

def me(self, tenant: str) -> dict | None:
"""The tenant profile (Watson '/me'): tier, namespace, quota, live usage, endpoint."""
t = self._tenants.get(tenant)
if t is None:
return None
return {"tenant": tenant, "tier": t["tier"], "namespace": t["namespace"],
"quota": t["quota"], "allowed_backends": t["allowed_backends"],
"usage": self._admission.usage(tenant), "endpoint": "twin"}

def meter(self, tenant: str, *, cost_rate: float = 1.0) -> dict | None:
"""BSS: the admission usage ledger -> a billable metering record."""
t = self._tenants.get(tenant)
if t is None:
return None
usage = self._admission.usage(tenant)
return {"tenant": tenant, "tier": t["tier"], "usage": dict(usage),
"billable_units": round(usage.get("cost", 0.0) * cost_rate, 4), "period": _iso()}

def tenants(self) -> list:
return [{k: v for k, v in t.items() if k != "manifests"} for t in self._tenants.values()]


if __name__ == "__main__":
ac = adm.AdmissionController(tiers={"acme": "free"})
ac.charge("acme", {"needs_gpu": False}, cost=3.0) # some usage
b = ServiceBroker(admission=ac)
prov = b.provision(tenant="acme", user="alice", tier="free", app="feature-x")
print(json.dumps({
"provisioned": {"namespace": prov["namespace"], "tier": prov["tier"],
"allowed_backends": prov["allowed_backends"], "receipt": prov["receipt"][:20] + "…"},
"bind": b.bind("acme"),
"me": b.me("acme"),
"meter": b.meter("acme"),
}, indent=2))
5 changes: 5 additions & 0 deletions tools/test_portal_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ def test_console_is_installable_and_shows_sovereign_inference():
assert "rel=manifest" in html and "Sovereign inference" in html and "epbadge" in html


def test_me_profile_endpoint_returns_tier_namespace_and_endpoint():
d = json.loads(ps.route("/api/me")[2])
assert d.get("tier") == "pro" and d.get("endpoint") == "twin" and "namespace" in d and "quota" in d


def test_inference_api_is_sovereign_first_and_fail_closed_without_endpoints():
with tempfile.TemporaryDirectory() as td:
old, ps._HEARTBEATS = ps._HEARTBEATS, pathlib.Path(td) # no live GPU backend -> no sovereign endpoint
Expand Down
58 changes: 58 additions & 0 deletions tools/test_provisioning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Tests for the provisioning plane (the Nocalhost-API + BlueMix-Broker + Entitlement + BSS + /me seam)."""
import admission as adm
import provisioning as pv


def test_provision_creates_an_isolated_tenant_with_its_tier_entitlement():
b = pv.ServiceBroker()
rec = b.provision(tenant="acme", user="alice", tier="free", app="feat")
assert rec["namespace"] == "ds-acme-alice-feat" and rec["tier"] == "free"
assert rec["quota"]["gpu_max"] == 0 # free tier
assert rec["allowed_backends"] == ["local", "wasm-edge"] # free entitlement
assert rec["receipt"].startswith("sha256:") and rec["state"] == "provisioned"


def test_bind_issues_access_defaulting_to_the_twin():
b = pv.ServiceBroker()
b.provision(tenant="acme", user="alice", tier="pro")
acc = b.bind("acme")
assert acc["endpoint"] == "twin" and acc["session_id"].startswith("sess_")
assert acc["namespace"] == "ds-acme-alice-default"
assert b.bind("nobody") is None # fail-closed on unknown tenant


def test_me_surfaces_the_profile_with_live_usage():
ac = adm.AdmissionController(tiers={"acme": "pro"})
ac.charge("acme", {"needs_gpu": True}, cost=2.0)
b = pv.ServiceBroker(admission=ac)
b.provision(tenant="acme", user="alice", tier="pro")
me = b.me("acme")
assert me["tier"] == "pro" and me["endpoint"] == "twin"
assert me["usage"]["cost"] == 2.0 and me["usage"]["gpu"] == 1
assert b.me("nobody") is None


def test_meter_turns_usage_into_a_billable_record():
ac = adm.AdmissionController()
ac.charge("acme", {}, cost=5.0)
b = pv.ServiceBroker(admission=ac)
b.provision(tenant="acme", user="alice", tier="enterprise")
m = b.meter("acme", cost_rate=2.0)
assert m["billable_units"] == 10.0 and m["tier"] == "enterprise"


def test_deprovision_removes_the_tenant():
b = pv.ServiceBroker()
b.provision(tenant="acme", user="alice")
assert b.deprovision("acme") is True and b.me("acme") is None
assert b.deprovision("acme") is False


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)} provisioning tests passed")
sys.exit(0)
Loading
Loading