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
7 changes: 5 additions & 2 deletions capd/devspace.local-dev.capd.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
"kind": "dev.inner-loop",
"status": "experimental",
"name": "SourceOS DevSpace — inner-loop dev-environment",
"description": "Nocalhost-style inner-loop over the continuum: an isolated per-user namespace, hot code sync into a running workload, a sidecar dev container, and port-forward + local debug — shortening the modify->result feedback loop. Consumes the control plane; surfaced to humans via the portal and to agents via the MCP ops surface (one governed source, two views).",
"description": "Nocalhost-style inner-loop over the continuum: an isolated per-user namespace (DevSpace), Signadot-style header-routed sandboxes that share a baseline, and the agent-machine as a StatefulSet with a per-replica TopoLVM inception mount. Consumes the control plane; surfaced to humans via the portal and to agents via the MCP ops surface (one governed source, two views).",
"links": {
"engine": "tools/devspace.py",
"portal": "tools/portal_server.py",
"agent_surface": "tools/mcp_ops_server.py",
"reference_pattern": "Nocalhost DevSpace (inner-loop) — met and bettered: sovereign + evidence-emitting"
"inception_storage": "deploy/topolvm/ (TopoLVM StorageClass + runbook)",
"spec_witness": "docs/PATTERN_INTEGRATION.md",
"reference_pattern": "Nocalhost DevSpace + MeshSpace, Signadot sandboxes, StatefulSet + TopoLVM inception mount — met and bettered: sovereign, tenancy-labelled, grant + quota governed, evidence-emitting"
},
"composes_with": {
"control_plane": "caps.infra.paas.continuum-local@0.1.0",
Expand Down
44 changes: 44 additions & 0 deletions deploy/topolvm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# TopoLVM — the agent-machine inception mount

Per the Edge/Fog design, the agent-machine's persistent **inception mount** (`/var/lib/sourceos/inception`)
is backed by **TopoLVM**: topology-aware, LVM-backed, node-local persistent volumes. The DevSpace PVC
and the agent-machine StatefulSet's `volumeClaimTemplates` default to `storageClassName:
topolvm-provisioner`.

## Why TopoLVM (not local-path / NFS)

- **Node-local + fast** — the inception mount is on the node's SSD via LVM, not a network hop.
- **Topology-aware** — `WaitForFirstConsumer` schedules the pod to a node that actually has capacity
to carve the LV, then binds. No "volume provisioned on the wrong node" failures.
- **Capacity-aware + expandable** — TopoLVM tracks free VG space per node and reports it to the
scheduler; `allowVolumeExpansion: true`.

## Install (production cluster)

TopoLVM needs cert-manager (its webhook) and an LVM volume group on each storage node.

```bash
# 1. cert-manager (webhook certs)
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml

# 2. on each node: a volume group named to match the device-class (e.g. an SSD VG)
# sudo vgcreate ssd-vg /dev/nvme1n1 # real disk in prod
# (lab: losetup a sparse file, then vgcreate)

# 3. TopoLVM (Helm) with lvmd pointed at that VG/device-class
helm repo add topolvm https://topolvm.github.io/topolvm
helm install topolvm topolvm/topolvm -n topolvm-system --create-namespace \
--set lvmd.deviceClasses[0].name=ssd \
--set lvmd.deviceClasses[0].volume-group=ssd-vg \
--set lvmd.deviceClasses[0].default=true

# 4. the StorageClass the DevSpace + StatefulSet reference
kubectl apply -f deploy/topolvm/topolvm-storageclass.yaml
```

## Local (kind/podman) note

A rootless-podman `kind` node has no LVM tooling, so TopoLVM won't provision there. For local proofs
the DevSpace is created with `storage_class="standard"` (kind's local-path) — the inception mount
persistence was verified end-to-end that way (write in one pod, read in another). Production is a
one-field swap back to `topolvm-provisioner`.
20 changes: 20 additions & 0 deletions deploy/topolvm/topolvm-storageclass.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# TopoLVM StorageClass for the agent-machine inception mount (Edge/Fog design).
#
# TopoLVM gives topology-aware, LVM-backed, node-local persistent volumes — so the agent-machine's
# inception mount lives on fast local disk and the pod is scheduled to the node that has capacity
# (WaitForFirstConsumer). This is the storage class the DevSpace + agent-machine StatefulSet default
# to (`storage_class="topolvm-provisioner"`).
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: topolvm-provisioner
labels:
sourceos.io/mount: inception
provisioner: topolvm.io
parameters:
# bind to a specific LVM device-class/volume-group configured in lvmd on the nodes.
"csi.storage.k8s.io/fstype": "xfs"
"topolvm.io/device-class": "ssd"
volumeBindingMode: WaitForFirstConsumer # schedule the pod where the LV can be carved
allowVolumeExpansion: true
reclaimPolicy: Delete
52 changes: 52 additions & 0 deletions docs/PATTERN_INTEGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Pattern integration register — demonstrated designs vs. our platform

Every pattern from the reference designs (Nocalhost, Signadot, ShellHub, the Edge/Fog↔Cloud-Twin
K3s design, the 7-layer PaaS, Eclipse/IBM Parallel Environment) mapped to what we have, honestly
graded. `✅` shipped · `◑` partial · `○` gap (named next step).

## Developer inner loop & environments

| Pattern (source) | Our implementation | Status |
|---|---|---|
| **DevSpace** — isolated per-user namespace (Nocalhost) | `devspace.devspace_manifests` — Namespace + ResourceQuota + default-deny NetworkPolicy + inception PVC | ✅ |
| **User → Space → Application** tenancy (Nocalhost) | `DevSpacePlane` (tenant→space→env), tenancy labels; admission quotas per subject | ✅ |
| **MeshSpace / Sandbox** — header-routed fork sharing the baseline (Nocalhost MeshSpace, Signadot) | `devspace.sandbox_manifests` — fork Deployment + Istio VirtualService routing `x-sandbox-routing-key` (≡ Nocalhost `uberctx-trace`) | ✅ |
| **Fast inner loop** — file-sync / port-forward / remote-debug / exec-in-container (Nocalhost Dev Mode) | declared in `caps.dev.devspace-inner-loop`; runtime hot-sync agent | ○ next |
| **KubeConfig connect, any cluster** (Nocalhost) | executor honours an explicit `SOURCEOS_KUBE_CONTEXT` (never the current context) | ✅ |
| **Deploy Helm/Kustomize/YAML** (Nocalhost) | executor emits YAML/JSON manifests; Helm/Kustomize adapters | ◑ |

## Stateful & storage

| Pattern | Our implementation | Status |
|---|---|---|
| **StatefulSet** — stable identity + per-replica storage (k8s framework for stateful apps) | `devspace.agent_machine_statefulset` — headless Service + `volumeClaimTemplates` | ✅ |
| **TopoLVM inception mount** — node-local LVM PV for the agent-machine (Edge/Fog `PersistentVolumes TopoLVM`) | `INCEPTION_MOUNT_PATH` + PVC/volumeClaimTemplates on `topolvm-provisioner`; `deploy/topolvm/`; persistence proven on a real cluster | ✅ |
| **Volumes vs bind-mounts vs tmpfs** (Docker storage) | inception mount = a persistent **volume** (PVC), not a bind/writable-layer — survives pod death | ✅ |
| **Operator pattern** — CRD + controller for stateful lifecycle | `control_loop` is a governed reconciler; a real `AgentMachine` CRD + operator | ○ next |

## Compute fabric & jobs (IBM Parallel Environment / HPC Toolkit)

| Pattern | Our implementation | Status |
|---|---|---|
| **Placement across substrates** (LoadLeveler/SLURM-style) | `compute_plane.place` over local/k8s/hpc-slurm/wasm/p2p/volunteer/blockchain | ✅ |
| **Batch job** | `Job` (k8s, real) / `sbatch` (slurm) | ✅ k8s · ○ slurm submit |
| **Parallel / MPI job (N ranks)** — the POE pattern | Indexed `Job` (k8s) / `--ntasks` MPI + `--array` (slurm) | ○ **next unit** |
| **Real SLURM submission** | today: placement + descriptor only (`DescriptorAdapter`); real `sbatch`/`srun`/MPI adapter | ○ **next unit** |
| **Edge/Fog K3s ↔ Cloud Twin sync** over intermittent links (LAN/WAN/sneakernet) | mesh telemetry + hyperswarm scale-up capability; real twin-sync + S3 export | ◑ |

## Reach, governance, evidence

| Pattern | Our implementation | Status |
|---|---|---|
| **SSH gateway to a device fleet** (ShellHub: Server + Agents on computer/device/container/server) | cloud-shell fog: Edge Gateway + HyperSwarm discovery + Grant-bound attach; a ShellHub-style agent per node | ◑ |
| **7-layer PaaS** (UX→Object Store→Derived→Vendor→Retrieval→Policy→Tool-runtime) | knowledge commons (canonical + derived + provenance), MCP surface (tool runtime), promotion gate (policy) | ◑ |
| **Governed connector calls** (Gemini/OpenAI/Claude Files APIs — materialize→handle→dispatch→result) | the SAME governed dispatch as a compute job: a `connector` effect through grant + executor | ○ **next unit** |
| **Append-only audit ledger** (every diagram) | sealed receipts (`artifacts/`), MCP-A2A ledger conformance | ✅ |

## What this establishes

The compute mesh + grants + admission is the **governance & scale-out substrate**; the DevSpace/
Sandbox/StatefulSet plane is the **environment & stateful substrate**. The two open frontiers that
would make the HPC/connector story first-class are one unit: **parallel/MPI jobs (Indexed Job +
real SLURM) and connector-call-as-dispatch** — unifying "run a job" and "call a connector" under one
`Grant`-gated executor. That is the codification the IBM Parallel Environment pattern is asking for.
180 changes: 180 additions & 0 deletions tools/devspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""DevSpace + Sandbox plane — governed, isolated, ephemeral developer environments.

Synthesizes three demonstrated patterns into one governed plane, emitting real, appliable k8s
manifests:

* Nocalhost DevSpace — an isolated per-user namespace with its own quota and default-deny network
isolation; the home for the fast inner loop (file-sync / port-forward / remote-debug).
* The Nocalhost User/Space tenancy model — Workspace/Tenant -> Space -> Application, each space
isolated and labelled.
* Signadot sandboxes — a request-routed ephemeral FORK of a baseline workload. Instead of
duplicating the whole stack, a sandbox shares the baseline cluster and a routing header
(`x-sandbox-routing-key`) sends only matching requests to the fork; everything else hits
baseline. That's how you test one changed service against the real rest-of-system, cheaply.

Every environment is tenancy-labelled, quota-bounded (composes with `admission`), and Grant-labelled
(composes with `mcp_a2a_grant`) — so who owns it, what it may consume, and under what authority are
all first-class.
"""
from __future__ import annotations

import re


# The agent-machine's persistent writable FS seam ("inception mount"), backed by a PVC. Per the
# Edge/Fog design this is a TopoLVM volume — topology-aware, LVM-backed local storage, so the
# agent-machine's state lives on fast node-local disk and follows the node it's scheduled to.
INCEPTION_MOUNT_PATH = "/var/lib/sourceos/inception"
INCEPTION_PVC = "inception-mount"
DEFAULT_STORAGE_CLASS = "topolvm-provisioner"


def _slug(*parts: str) -> str:
s = "-".join(p for p in parts if p)
s = re.sub(r"[^a-z0-9-]", "-", s.lower()).strip("-")
return re.sub(r"-+", "-", s)[:63]


def devspace_manifests(*, tenant: str, user: str, space: str, quota: dict | None = None,
grant_id: str | None = None, inception_storage: str = "10Gi",
storage_class: str = DEFAULT_STORAGE_CLASS) -> list[dict]:
"""Nocalhost-style DevSpace: an isolated per-user Namespace + ResourceQuota + default-deny
NetworkPolicy + the agent-machine's persistent **inception mount** (a TopoLVM-backed PVC)."""
ns_name = "ds-" + _slug(tenant, user, space)
labels = {"sourceos.io/tenant": tenant, "sourceos.io/user": user, "sourceos.io/space": space,
"sourceos.io/kind": "devspace"}
if grant_id:
labels["sourceos.io/grant-id"] = grant_id
quota = quota or {"pods": "10", "requests.cpu": "4", "requests.memory": "8Gi",
"limits.cpu": "8", "limits.memory": "16Gi"}
return [
{"apiVersion": "v1", "kind": "Namespace", "metadata": {"name": ns_name, "labels": labels}},
{"apiVersion": "v1", "kind": "ResourceQuota",
"metadata": {"name": "devspace-quota", "namespace": ns_name, "labels": labels},
"spec": {"hard": quota}},
{"apiVersion": "networking.k8s.io/v1", "kind": "NetworkPolicy",
"metadata": {"name": "devspace-isolation", "namespace": ns_name, "labels": labels},
"spec": {"podSelector": {}, "policyTypes": ["Ingress"],
"ingress": [{"from": [{"podSelector": {}}]}]}}, # only same-DevSpace ingress
{"apiVersion": "v1", "kind": "PersistentVolumeClaim",
"metadata": {"name": INCEPTION_PVC, "namespace": ns_name,
"labels": {**labels, "sourceos.io/mount": "inception"}},
"spec": {"accessModes": ["ReadWriteOnce"], "storageClassName": storage_class,
"resources": {"requests": {"storage": inception_storage}}}},
]


def sandbox_manifests(*, baseline: str, image: str, routing_key: str, namespace: str,
grant_id: str | None = None, port: int = 8080,
command: str | None = None) -> list[dict]:
"""Signadot-style sandbox: a FORK Deployment of `baseline`, plus an Istio VirtualService that
routes requests carrying `x-sandbox-routing-key: <routing_key>` to the fork and everything else
to the baseline — so the sandbox shares the cluster instead of duplicating the stack."""
fork = _slug(baseline, "sbx", routing_key)
labels = {"sourceos.io/kind": "sandbox", "sourceos.io/baseline": baseline,
"sourceos.io/routing-key": routing_key}
if grant_id:
labels["sourceos.io/grant-id"] = grant_id
container = {"name": "app", "image": image, "ports": [{"containerPort": port}]}
if command:
import shlex
container["command"] = shlex.split(command)
deploy = {
"apiVersion": "apps/v1", "kind": "Deployment",
"metadata": {"name": fork, "namespace": namespace, "labels": labels},
"spec": {"replicas": 1, "selector": {"matchLabels": {"app": fork}},
"template": {"metadata": {"labels": {**labels, "app": fork}},
"spec": {"containers": [container]}}},
}
vs = {
"apiVersion": "networking.istio.io/v1beta1", "kind": "VirtualService",
"metadata": {"name": _slug(baseline, "sandbox-route"), "namespace": namespace, "labels": labels},
"spec": {"hosts": [baseline],
"http": [
{"name": f"sandbox-{routing_key}",
"match": [{"headers": {"x-sandbox-routing-key": {"exact": routing_key}}}],
"route": [{"destination": {"host": fork}}]},
{"name": "baseline", "route": [{"destination": {"host": baseline}}]},
]},
}
return [deploy, vs]


def agent_machine_statefulset(*, name: str, tenant: str, space: str, namespace: str,
image: str = "busybox:1.36", replicas: int = 1,
storage_class: str = DEFAULT_STORAGE_CLASS, storage: str = "10Gi",
command: str | None = None, grant_id: str | None = None,
port: int = 8080) -> list[dict]:
"""The agent-machine as a STATEFUL app — the right k8s primitive for a long-lived workload that
keeps state. A StatefulSet gives each replica a stable identity and, via `volumeClaimTemplates`,
its OWN persistent inception mount (on TopoLVM) that follows it across reschedules; a headless
Service gives stable network identity. This is what Jobs/Deployments can't do."""
sname = _slug(name)
labels = {"sourceos.io/kind": "agent-machine", "sourceos.io/tenant": tenant,
"sourceos.io/space": space, "app": sname}
if grant_id:
labels["sourceos.io/grant-id"] = grant_id
container = {"name": "agent-machine", "image": image,
"ports": [{"containerPort": port}],
"resources": {"requests": {"cpu": "100m", "memory": "128Mi"},
"limits": {"cpu": "100m", "memory": "128Mi"}},
"volumeMounts": [{"name": "inception", "mountPath": INCEPTION_MOUNT_PATH}]}
if command:
import shlex
container["command"] = shlex.split(command)
svc = {"apiVersion": "v1", "kind": "Service",
"metadata": {"name": sname, "namespace": namespace, "labels": labels},
"spec": {"clusterIP": "None", "selector": {"app": sname},
"ports": [{"port": port, "name": "agent"}]}}
sts = {"apiVersion": "apps/v1", "kind": "StatefulSet",
"metadata": {"name": sname, "namespace": namespace, "labels": labels},
"spec": {"serviceName": sname, "replicas": replicas,
"selector": {"matchLabels": {"app": sname}},
"template": {"metadata": {"labels": labels}, "spec": {"containers": [container]}},
"volumeClaimTemplates": [
{"metadata": {"name": "inception", "labels": {"sourceos.io/mount": "inception"}},
"spec": {"accessModes": ["ReadWriteOnce"], "storageClassName": storage_class,
"resources": {"requests": {"storage": storage}}}}]}}
return [svc, sts]


class DevSpacePlane:
"""The User/Space tenancy model: a tenant owns spaces; a space holds devspaces + sandboxes."""

def __init__(self):
self._registry: dict[str, dict] = {}

def provision_devspace(self, *, tenant, user, space, quota=None, grant_id=None) -> dict:
manifests = devspace_manifests(tenant=tenant, user=user, space=space, quota=quota, grant_id=grant_id)
ns = manifests[0]["metadata"]["name"]
rec = {"kind": "devspace", "tenant": tenant, "user": user, "space": space,
"namespace": ns, "manifests": manifests}
self._registry[ns] = rec
return rec

def provision_sandbox(self, *, tenant, space, baseline, image, routing_key, namespace,
grant_id=None, command=None) -> dict:
manifests = sandbox_manifests(baseline=baseline, image=image, routing_key=routing_key,
namespace=namespace, grant_id=grant_id, command=command)
key = f"{namespace}/{baseline}#{routing_key}"
rec = {"kind": "sandbox", "tenant": tenant, "space": space, "baseline": baseline,
"routing_key": routing_key, "namespace": namespace, "manifests": manifests}
self._registry[key] = rec
return rec

def environments(self, *, tenant=None) -> list[dict]:
return [r for r in self._registry.values() if tenant is None or r["tenant"] == tenant]


if __name__ == "__main__":
import json
plane = DevSpacePlane()
ds = plane.provision_devspace(tenant="acme", user="alice", space="feature-x")
sb = plane.provision_sandbox(tenant="acme", space="feature-x", baseline="productpage",
image="acme/productpage:pr-42", routing_key="pr-42",
namespace=ds["namespace"])
print(json.dumps({"devspace_namespace": ds["namespace"],
"devspace_kinds": [m["kind"] for m in ds["manifests"]],
"sandbox_fork": sb["manifests"][0]["metadata"]["name"],
"routing": sb["manifests"][1]["spec"]["http"][0]["match"]}, indent=2))
Loading
Loading