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

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

inference: ## sovereign inference demo: models as data spheres, fail-closed sovereign routing
cd tools && python3 inference.py

Expand Down
30 changes: 30 additions & 0 deletions capd/git-push-deploy.mesh.capd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"capability_id": "caps.dev.git-push-deploy@0.1.0",
"kind": "dev.build-deploy",
"status": "experimental",
"name": "Git-push deploy — the Vercel/Heroku ergonomic, sovereign (Cloud Native Buildpacks)",
"description": "Vercel and Heroku are the same move: detect the app from source, build it into a runnable image without a Dockerfile, deploy it, and give a preview environment per branch. This delivers that ergonomic sovereign and open, via Cloud Native Buildpacks / Paketo (`pack build`): source -> detect -> reproducible SBOM'd OCI image (a data sphere, SLSA-attestable) -> a workload the executor dispatches into a DevSpace -> a Signadot-style sandbox as the per-branch preview -> the fail-closed promotion gate as dev->prod. No Docker daemon, no hand-written Dockerfile; the build is reproducible + attestable, the runtime is our mesh (including sovereign GPU inference), and the data stays in residency-fenced data spheres.",
"links": {
"engine": "tools/buildpack.py",
"executor": "tools/executor.py",
"preview": "tools/devspace.py",
"promotion": "tools/promotion_gate.py",
"artifact": "tools/data_sphere.py",
"spec_witness": "docs/BUILD_DEPLOY.md",
"reference_pattern": "Vercel + Heroku (git-push-to-deploy, buildpack auto-detect, per-branch preview, instant rollback) via open Cloud Native Buildpacks / Paketo — met sovereign: reproducible+attestable build, our runtime, our data"
},
"composes_with": {
"inner_loop": "caps.dev.devspace-inner-loop@0.1.0",
"compute_plane": "caps.compute.mesh-plane@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",
"no_dockerfile": true,
"reproducible_image": true,
"per_branch_preview": true,
"promotion_gated": true,
"evidence_emitting": true
}
}
51 changes: 51 additions & 0 deletions docs/BUILD_DEPLOY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Vercel / Heroku, aligned — git-push deploy, sovereign

Vercel and Heroku are the same move (you called it): **detect the app from source, build it into a
runnable image without a Dockerfile, deploy it, and give a preview environment per branch.** That
ergonomic is the gold standard for local→cloud-native developer flow. Here is how it maps onto the
continuum stack — and the one missing piece we just added.

## The mapping

| Vercel / Heroku | Continuum equivalent | Status |
|---|---|---|
| `git push` → it deploys | lifecycle onboard→develop→test→rollout + a push trigger | ◑ (trigger next) |
| **buildpack auto-detect, no Dockerfile** | **`buildpack.py`** — Cloud Native Buildpacks / Paketo `pack build`; detect→build→OCI image | ✅ (this change) |
| slug / build artifact | a reproducible OCI image = a **data sphere** (immutable, provenance, SBOM/SLSA-attestable) | ✅ |
| Procfile / process types (web, worker) | `process_types` on the build → workload `kind` (service=Deployment, worker=Job) | ✅ |
| dyno / serverless runtime | the **executor** adapters (k8s/slurm/wasm/…), scale-to-zero | ✅ |
| **preview deployment per branch** (unique URL) | **Signadot-style sandbox** (`devspace.sandbox_manifests`, header-routed) or a per-user DevSpace | ✅ |
| instant rollback | immutable digest-pinned images + GitOps revision | ✅ |
| pipelines (dev→staging→prod) | the **fail-closed promotion gate** (APPROVE verdict) | ✅ |
| add-ons (Postgres/Redis) | backing services as **data spheres** / a service-broker surface | ◑ |
| account tiers (Free/Pro) | **admission tiers** (entitlement + backend allowlist) | ✅ |

## Why this is the sovereign version, not a re-host

The whole value of Vercel/Heroku is the *build-and-preview ergonomic*, and the whole risk is that
it's a proprietary black box that owns your build, your runtime, and your data. Cloud Native
Buildpacks (buildpacks.io / Paketo — the CNCF standard Heroku itself moved to) gives us the **exact
same ergonomic, open**: `pack build` turns source into a reproducible, SBOM'd OCI image with **no
Docker daemon and no hand-written Dockerfile**, and it runs anywhere. So we get "git push, it
deploys" without surrendering the build (it's reproducible + attestable), the runtime (our executor,
our mesh — including sovereign GPU inference), or the data (data spheres, residency-fenced).

And two things we already had that Vercel/Heroku charge for or don't govern:
- **preview environments** are just our Signadot-style sandboxes / DevSpaces — governed, tenancy-
labelled, quota-bounded, and free.
- the **pipeline** is our fail-closed promotion gate — a preview is promoted to prod only on a sealed
APPROVE verdict, not a dashboard toggle.

## The flow (`buildpack.py`)

```
git push ──> detect(source) ──> pack build (Paketo) ──> reproducible OCI image (a data sphere)
──> deploy_workload() ──> executor dispatch into a DevSpace
──> sandbox = the per-branch PREVIEW (header-routed, shares the baseline)
──> promotion gate ──> prod
```

`build_plan()` is content-addressed (same source → same image → reproducible), fail-closed (no
buildpack match → refuse, don't guess), and its image flows straight into the executor's k8s manifest
(verified in tests). The only remaining piece is the **push trigger** (a webhook that runs this on a
`git push` and opens the preview) — the ergonomic wrapper over machinery that's now all here.
85 changes: 85 additions & 0 deletions tools/buildpack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Buildpack build — the Vercel/Heroku "git push, no Dockerfile, it deploys" ergonomic, sovereign.

Vercel and Heroku are the same move: detect the app's language/framework from source, build it into a
runnable image WITHOUT a Dockerfile, and deploy it — with a preview environment per branch. The open,
sovereign way to do the build half is **Cloud Native Buildpacks** (buildpacks.io / Paketo): `pack
build` runs detect + build phases over source and produces a reproducible, SBOM'd OCI image. No
Docker daemon, no hand-written Dockerfile.

This models the build and wires it into the stack we already have:

source ──detect──> buildpack ──build──> OCI image (reproducible, SBOM/SLSA-attestable)
──> a data sphere / commons record (immutable, provenance)
──> a workload the executor dispatches into a DevSpace
──> a Signadot-style sandbox = the per-branch PREVIEW deployment
──> the promotion gate = the dev->prod pipeline

So Heroku's slug+Procfile+dyno, Vercel's build+preview+rollback, become: CNB image + workload spec +
DevSpace/sandbox — governed, and the preview-env we already ship.
"""
from __future__ import annotations

import hashlib
import json

# detect signal -> Paketo buildpack builder + a default process (Procfile-style "web:" command).
BUILDPACKS = {
"python": {"detect": ["requirements.txt", "pyproject.toml", "Pipfile"],
"builder": "paketobuildpacks/builder-jammy-base", "buildpack": "paketo/python", "web": "python app.py"},
"node": {"detect": ["package.json"], "builder": "paketobuildpacks/builder-jammy-base",
"buildpack": "paketo/nodejs", "web": "npm start"},
"go": {"detect": ["go.mod"], "builder": "paketobuildpacks/builder-jammy-base",
"buildpack": "paketo/go", "web": "./app"},
"rust": {"detect": ["Cargo.toml"], "builder": "paketobuildpacks/builder-jammy-base",
"buildpack": "paketo-community/rust", "web": "./target/release/app"},
"static": {"detect": ["index.html", "public/index.html"], "builder": "paketobuildpacks/builder-jammy-base",
"buildpack": "paketo/web-servers", "web": "serve"},
}


def detect(files: list) -> str | None:
"""Which buildpack matches this source? (No Dockerfile needed.)"""
fs = set(files)
for lang, spec in BUILDPACKS.items():
if any(sig in fs for sig in spec["detect"]):
return lang
return None


def build_plan(*, source_files: list, app_name: str, process: str | None = None,
procfile: dict | None = None) -> dict:
"""CNB-style: detect -> build -> a reproducible OCI image + a Procfile-style process type. The
image digest is content-addressed over the source + buildpack, so the same source builds the same
image (reproducible). The real build is `pack build` with the returned command."""
lang = detect(source_files)
if lang is None:
return {"ok": False, "reason": "no buildpack matched — add a Dockerfile or a known manifest "
"(requirements.txt / package.json / go.mod / Cargo.toml / index.html)"}
spec = BUILDPACKS[lang]
digest = hashlib.sha256(json.dumps({"src": sorted(source_files), "bp": spec["buildpack"]},
sort_keys=True).encode()).hexdigest()
web = (procfile or {}).get("web") or process or spec["web"]
return {"ok": True, "language": lang, "buildpack": spec["buildpack"], "builder": spec["builder"],
"image": f"{app_name}@sha256:{digest[:12]}", "image_digest": "sha256:" + digest,
"process": web, "process_types": procfile or {"web": web},
"pack_command": ["pack", "build", app_name, "--builder", spec["builder"], "--buildpack", spec["buildpack"]]}


def deploy_workload(build: dict, *, kind: str = "service", sensitivity: str = "normal") -> dict | None:
"""The built image -> a workload the executor dispatches (service = long-lived Deployment; worker
= Job). The sandbox is the per-branch preview; the promotion gate is dev->prod."""
if not build.get("ok"):
return None
return {"name": build["image"].split("@")[0], "image": build["image"], "command": build["process"],
"kind": kind, "effect": "compute", "sensitivity": sensitivity, "scalable": True,
"build_digest": build["image_digest"], "provenance": {"buildpack": build["buildpack"]}}


if __name__ == "__main__":
plan = build_plan(source_files=["package.json", "index.js", "README.md"], app_name="my-web-app",
procfile={"web": "node server.js", "worker": "node worker.js"})
print(json.dumps({"language": plan["language"], "image": plan["image"],
"pack": " ".join(plan["pack_command"]),
"workload": deploy_workload(plan)["name"],
"process_types": plan["process_types"]}, indent=2))
58 changes: 58 additions & 0 deletions tools/test_buildpack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Tests for the buildpack build (the Vercel/Heroku 'git push, no Dockerfile' ergonomic via CNB)."""
import buildpack as bp


def test_detect_matches_manifests_and_none_for_unknown():
assert bp.detect(["requirements.txt"]) == "python"
assert bp.detect(["package.json"]) == "node"
assert bp.detect(["go.mod"]) == "go"
assert bp.detect(["Cargo.toml"]) == "rust"
assert bp.detect(["index.html"]) == "static"
assert bp.detect(["README.md"]) is None


def test_build_plan_is_reproducible_and_carries_a_process():
a = bp.build_plan(source_files=["package.json", "index.js"], app_name="app")
b = bp.build_plan(source_files=["index.js", "package.json"], app_name="app") # order-insensitive
assert a["ok"] and a["language"] == "node" and a["buildpack"] == "paketo/nodejs"
assert a["image_digest"] == b["image_digest"] # same source -> same image (reproducible)
assert a["process"] == "npm start" and a["pack_command"][0] == "pack"


def test_procfile_web_overrides_the_default_process():
p = bp.build_plan(source_files=["package.json"], app_name="app",
procfile={"web": "node server.js", "worker": "node w.js"})
assert p["process"] == "node server.js" and p["process_types"]["worker"] == "node w.js"


def test_no_buildpack_match_is_fail_closed():
p = bp.build_plan(source_files=["random.txt"], app_name="app")
assert p["ok"] is False and "no buildpack matched" in p["reason"]
assert bp.deploy_workload(p) is None


def test_built_image_flows_into_an_executor_dispatch():
import executor as ex
import mcp_a2a_grant as g
plan = bp.build_plan(source_files=["go.mod", "main.go"], app_name="svc")
wl = bp.deploy_workload(plan, kind="service")
assert wl["image"] == plan["image"] and wl["build_digest"] == plan["image_digest"]
assert wl["provenance"]["buildpack"] == "paketo/go"
d = {"placement": "scheduled", "backend": "k8s", "backend_trust": "trusted"}
grant = g.issue_grant(
binding={"spiffe_id": "s", "aum_digest": "sha256:" + "ab" * 32, "session_id": "sess_bp1"},
capability={"kind": "mcp_tool", "capability_ref": "c", "capability_digest": "sha256:" + "cd" * 32, "effect": "compute"},
decision=d, attestation=g.attestation_bundle(spiffe_id="s", aum_digest="sha256:" + "ab" * 32, tpm_valid=True, cosign_valid=True),
constraints={"ops_allow": ["x"]}, signer=g.hmac_signer(b"k"))
m = ex.K8sAdapter().manifest(wl, d, grant)
assert m["spec"]["template"]["spec"]["containers"][0]["image"] == plan["image"] # runs the built artifact


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)} buildpack tests passed")
sys.exit(0)
3 changes: 3 additions & 0 deletions tools/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"capd/volunteer-mesh-verification.mesh.capd.json",
"capd/data-spheres.mesh.capd.json",
"capd/sovereign-inference.mesh.capd.json",
"capd/git-push-deploy.mesh.capd.json",
"tools/promotion_gate.py",
"tools/portal_server.py",
"tools/compute_plane.py",
Expand All @@ -44,6 +45,7 @@
"tools/data_sphere.py",
"tools/availability.py",
"tools/inference.py",
"tools/buildpack.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.
Expand All @@ -56,6 +58,7 @@
"capd/volunteer-mesh-verification.mesh.capd.json": "caps.compute.volunteer-mesh-verification",
"capd/data-spheres.mesh.capd.json": "caps.data.spheres",
"capd/sovereign-inference.mesh.capd.json": "caps.inference.sovereign",
"capd/git-push-deploy.mesh.capd.json": "caps.dev.git-push-deploy",
}

errors: list[str] = []
Expand Down
Loading