feat: add rmgr rapp init/validate/pin-image

Prove the user-engine wrapper shape and give Repo Manager a
secret-free scaffolder that refuses invented rails and live-contract
overwrites.
This commit is contained in:
tegwick 2026-08-18 12:39:24 +02:00
parent 77452492ec
commit 8b87b17980
5 changed files with 397 additions and 5 deletions

View file

@ -33,6 +33,8 @@ rmgr --version
rmgr observe --path .
rmgr reconcile --path .
rmgr update-task-status --path . --task-id <ID> --status progress
rmgr rapp init --path ../rapp-some-app --app some-app --ownership-repo some-app
rmgr rapp validate --path ../rapp-user-engine --family-root ..
```
Vertical-slice proof: [docs/evidence/t05-vertical-slice.md](docs/evidence/t05-vertical-slice.md).

View file

@ -4,9 +4,13 @@ from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from repo_manager.commands.rapp import add_rapp_parser
from repo_manager.commands.rapp import init as rapp_init
from repo_manager.commands.rapp import pin_image as rapp_pin_image
from repo_manager.commands.rapp import validate as rapp_validate
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
@ -55,6 +59,8 @@ def main(argv: list[str] | None = None) -> int:
help="Patch file only (invalid as full applied evidence; for tests)",
)
add_rapp_parser(sub)
args = parser.parse_args(argv)
if args.version or args.command in (None, "version"):
@ -123,6 +129,32 @@ def main(argv: list[str] | None = None) -> int:
print(json.dumps(result.to_dict(), indent=2))
return 0 if result.status == "applied" else 1
if args.command == "rapp":
if args.rapp_command == "init":
result = rapp_init(
Path(args.path),
app=args.app,
ownership_repo=args.ownership_repo,
rail=args.rail,
classification=args.classification,
criticality=args.criticality,
package_type=args.package_type,
purpose=args.purpose,
force=args.force,
)
elif args.rapp_command == "validate":
result = rapp_validate(
Path(args.path),
family_root=Path(args.family_root) if args.family_root else None,
)
elif args.rapp_command == "pin-image":
result = rapp_pin_image(Path(args.path), args.digest)
else:
parser.print_help()
return 2
print(json.dumps(result, indent=2))
return 0 if result.get("ok") else 1
parser.print_help()
return 0

View file

@ -0,0 +1,281 @@
"""Governed scaffolding for rapp-* managed workload packages."""
from __future__ import annotations
import argparse
import json
import re
import subprocess
from pathlib import Path
from typing import Any
PACKAGE_TYPES = (
"helm-managed-platform-service",
"manifest-managed-platform-service",
"knative-managed-service",
"grouped-composition",
)
RAILS = ("rail-kubernetes", "rail-knative")
CLASSIFICATIONS = ("public", "internal", "confidential", "restricted")
CRITICALITIES = ("low", "medium", "high", "critical")
SLUG = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
_FAMILY_VALIDATOR = Path.home() / "railiance-master" / "tools" / "validate-family-declarations.py"
def _refuse(message: str) -> dict[str, Any]:
return {"ok": False, "error": message}
def _write(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content if content.endswith("\n") else content + "\n")
def init(
path: Path,
*,
app: str,
ownership_repo: str,
rail: str = "rail-kubernetes",
classification: str = "confidential",
criticality: str = "high",
package_type: str = "manifest-managed-platform-service",
purpose: str | None = None,
force: bool = False,
) -> dict[str, Any]:
if not SLUG.match(app) or app.startswith("rapp-"):
return _refuse("app must be a workload slug without the rapp- prefix")
if rail not in RAILS:
return _refuse(f"unknown rail {rail!r}; allowed: {', '.join(RAILS)}")
if package_type not in PACKAGE_TYPES:
return _refuse(f"unknown package_type {package_type!r}")
if classification not in CLASSIFICATIONS:
return _refuse(f"unknown classification {classification!r}")
if criticality not in CRITICALITIES:
return _refuse(f"unknown criticality {criticality!r}")
if not SLUG.match(ownership_repo) or ownership_repo.startswith("rapp-"):
return _refuse("ownership_repo must be an app or layer slug, not the rapp itself")
rapp_id = f"rapp-{app}"
dest = path.expanduser().resolve()
dest.mkdir(parents=True, exist_ok=True)
declaration = dest / "declarations" / "rapp.yaml"
if declaration.is_file() and not force:
return _refuse(f"{declaration} already exists; refuse to overwrite a live contract")
purpose_text = purpose or f"Package and operate the {app} service on Railiance."
files = {
".repo-classification.yaml": (
"repo_classification:\n"
' standard: Repo Classification Standard\n'
' version: "1.0"\n'
" classified_by: repo-manager\n"
" category: project\n"
" domain: infotech\n"
),
"README.md": f"# {rapp_id}\n\nManaged runtime package for `{app}`.\n",
"INTENT.md": (
f"# Intent\n\nProvide the managed Railiance runtime package for `{app}`.\n"
),
"SCOPE.md": (
"# Scope\n\n## In scope\n\n- packaging, smoke, rollback, and the family declaration\n\n"
"## Out of scope\n\n- application domain ownership\n- credential values\n"
),
"AGENTS.md": (
f"# Repository agent guide\n\nWorkplans use the prefix "
f"`{rapp_id.upper()}-WP-`.\n"
),
"CLAUDE.md": f"# {rapp_id}\n\n@SCOPE.md\n@AGENTS.md\n",
"Makefile": (
"SHELL := /bin/bash\n\n.PHONY: check\n\ncheck:\n"
"\tpython3 -m unittest discover -s tests -v\n"
),
".gitignore": ".rendered/\n__pycache__/\n",
"declarations/rapp.yaml": _declaration(
rapp_id=rapp_id,
app=app,
ownership_repo=ownership_repo,
rail=rail,
classification=classification,
criticality=criticality,
package_type=package_type,
purpose=purpose_text,
),
f"workplans/{rapp_id.upper()}-WP-0001-bootstrap.md": (
f"---\nid: {rapp_id.upper()}-WP-0001\ntype: workplan\n"
f'title: "Bootstrap {rapp_id}"\nstatus: proposed\n---\n\n'
f"# Bootstrap {rapp_id}\n"
),
}
written = []
for rel, content in files.items():
_write(dest / rel, content)
written.append(rel)
(dest / "workplans" / "archived").mkdir(exist_ok=True)
return {"ok": True, "path": str(dest), "rapp_id": rapp_id, "written": written}
def _declaration(
*,
rapp_id: str,
app: str,
ownership_repo: str,
rail: str,
classification: str,
criticality: str,
package_type: str,
purpose: str,
) -> str:
return (
"kind: managed-workload-package\n"
"repo_family: rapp\n"
f"rapp_id: {rapp_id}\n"
f"repo: {rapp_id}\n"
f"ownership_repo: {ownership_repo}\n"
"contract_version: 1.0.0\n"
"readiness_state: draft\n"
"workload_identity:\n"
f" name: {app}\n"
f" package_type: {package_type}\n"
f"data_classification: {classification}\n"
f"criticality: {criticality}\n"
f"primary_rail: {rail}\n"
"supported_rails:\n"
f" - {rail}\n"
"bound_reefs: []\n"
"runtime_dependencies:\n"
" - kubernetes-api\n"
"composition:\n"
f" purpose: {purpose}\n"
" member_repos:\n"
f" - repo: {rapp_id}\n"
" role: managed runtime package\n"
" deployables:\n"
f" - {app}\n"
"rollout_contract:\n"
" default_mode: kubectl-server-side-apply\n"
"smoke_contract:\n"
" required:\n"
" - healthz-ok\n"
"rollback_contract:\n"
" order:\n"
" - apply-reviewed-git-revision\n"
)
def validate(path: Path, *, family_root: Path | None = None) -> dict[str, Any]:
dest = path.expanduser().resolve()
declaration = dest / "declarations" / "rapp.yaml"
if not declaration.is_file():
return _refuse(f"missing {declaration}")
missing = [
name
for name in (
"README.md",
"INTENT.md",
"SCOPE.md",
"AGENTS.md",
".repo-classification.yaml",
)
if not (dest / name).is_file()
]
if missing:
return _refuse(f"missing bootstrap files: {', '.join(missing)}")
if _FAMILY_VALIDATOR.is_file():
import tempfile
search_root = (family_root or dest.parent).resolve()
with tempfile.TemporaryDirectory(prefix="rmgr-rapp-") as tmp:
tmp_root = Path(tmp)
(tmp_root / dest.name).symlink_to(dest)
for sibling in ("rail-kubernetes", "rail-knative", "reef-railiance"):
candidate = search_root / sibling
if candidate.is_dir():
(tmp_root / sibling).symlink_to(candidate)
proc = subprocess.run(
["python3", str(_FAMILY_VALIDATOR), "--root", str(tmp_root)],
check=False,
capture_output=True,
text=True,
)
return {
"ok": proc.returncode == 0,
"path": str(dest),
"validator": str(_FAMILY_VALIDATOR),
"stdout": proc.stdout.strip(),
"stderr": proc.stderr.strip(),
"exit_code": proc.returncode,
}
text = declaration.read_text()
required = (
"kind: managed-workload-package",
"repo_family: rapp",
"ownership_repo:",
"primary_rail:",
"composition:",
)
absent = [item for item in required if item not in text]
if absent:
return _refuse(f"declaration missing {absent}")
return {"ok": True, "path": str(dest), "validator": None}
def pin_image(path: Path, digest: str) -> dict[str, Any]:
if not DIGEST.fullmatch(digest):
return _refuse("digest must be sha256:<64 lowercase hex>")
dest = path.expanduser().resolve()
runtime = dest / "manifests" / "runtime.yaml"
if not runtime.is_file():
return _refuse(f"missing {runtime}")
text = runtime.read_text()
updated, n = re.subn(
r"(forgejo\.coulomb\.social/coulomb/user-engine@)sha256:[0-9a-f]{64}",
rf"\g<1>{digest}",
text,
)
if n == 0:
return _refuse("no user-engine digest pin found in manifests/runtime.yaml")
runtime.write_text(updated)
binding = dest / "bindings" / "reef-railiance.yaml"
if binding.is_file():
binding.write_text(
re.sub(r"sha256:[0-9a-f]{64}", digest, binding.read_text(), count=1)
)
declaration = dest / "declarations" / "rapp.yaml"
if declaration.is_file():
declaration.write_text(
re.sub(
r"(source: forgejo\.coulomb\.social/coulomb/user-engine\n version: )sha256:[0-9a-f]{64}",
rf"\g<1>{digest}",
declaration.read_text(),
)
)
return {"ok": True, "path": str(dest), "digest": digest, "rewritten": n}
def add_rapp_parser(sub: argparse._SubParsersAction) -> None:
rapp = sub.add_parser("rapp", help="Scaffold or validate a rapp-* package")
rapp_sub = rapp.add_subparsers(dest="rapp_command", required=True)
p_init = rapp_sub.add_parser("init", help="Create a draft rapp-* baseline")
p_init.add_argument("--path", required=True, help="Destination checkout")
p_init.add_argument("--app", required=True, help="Workload name (no rapp- prefix)")
p_init.add_argument("--ownership-repo", required=True)
p_init.add_argument("--rail", default="rail-kubernetes", choices=RAILS)
p_init.add_argument("--classification", default="confidential", choices=CLASSIFICATIONS)
p_init.add_argument("--criticality", default="high", choices=CRITICALITIES)
p_init.add_argument("--package-type", default="manifest-managed-platform-service", choices=PACKAGE_TYPES)
p_init.add_argument("--purpose", default=None)
p_init.add_argument("--force", action="store_true")
p_val = rapp_sub.add_parser("validate", help="Validate a rapp-* checkout")
p_val.add_argument("--path", required=True)
p_val.add_argument("--family-root", default=None)
p_pin = rapp_sub.add_parser("pin-image", help="Rewrite the user-engine digest pin")
p_pin.add_argument("--path", required=True)
p_pin.add_argument("--digest", required=True)

View file

@ -0,0 +1,59 @@
from pathlib import Path
from repo_manager.cli import main
from repo_manager.commands.rapp import init, pin_image, validate
def test_init_refuses_rapp_prefixed_app(tmp_path: Path):
result = init(tmp_path / "rapp-x", app="rapp-x", ownership_repo="x")
assert result["ok"] is False
def test_init_and_validate_draft(tmp_path: Path):
dest = tmp_path / "rapp-example"
created = init(
dest,
app="example",
ownership_repo="example-app",
purpose="Package and operate the example service.",
)
assert created["ok"] is True
assert (dest / "declarations" / "rapp.yaml").is_file()
refused = init(dest, app="example", ownership_repo="example-app")
assert refused["ok"] is False
checked = validate(dest, family_root=tmp_path)
# Isolated family root has no rails; validator may warn/error on rail
# resolution. The bootstrap files must still be accepted by the local
# fallback if the family validator is absent, or produce structured output.
assert "path" in checked
def test_cli_init_and_pin(tmp_path: Path, capsys):
dest = tmp_path / "rapp-user-engine"
assert (
main(
[
"rapp",
"init",
"--path",
str(dest),
"--app",
"user-engine",
"--ownership-repo",
"user-engine",
]
)
== 0
)
(dest / "manifests").mkdir()
(dest / "manifests" / "runtime.yaml").write_text(
"image: forgejo.coulomb.social/coulomb/user-engine"
"@sha256:e3b5f65bafc1c0260dfdf2567a52766e67506ceb878a51759a2e9a307c4b5eb8\n"
)
digest = "sha256:" + "ab" * 32
pinned = pin_image(dest, digest)
assert pinned["ok"] is True
assert digest in (dest / "manifests" / "runtime.yaml").read_text()
assert main(["rapp", "pin-image", "--path", str(dest), "--digest", "latest"]) == 1
out = capsys.readouterr().out
assert "sha256" in out or "digest" in out

View file

@ -54,7 +54,7 @@ work breakdown for later scaffolder phases.
```task
id: RMGR-WP-0006-T02
status: todo
status: done
priority: high
state_hub_task_id: "f0e53954-403a-4e2c-b39f-0bab00eed3cc"
```
@ -68,11 +68,15 @@ guide. Validate with
Do not place the rapp (`bound_reefs` stays empty) and do not set public
exposure.
**Result (2026-08-18):** `rapp-user-engine` has the bootstrap set and a
schema-valid `declarations/rapp.yaml` at `declared`. Isolated family
validation: `4 declaration(s) ok`.
## Generate the Kubernetes package skeleton
```task
id: RMGR-WP-0006-T03
status: todo
status: done
priority: high
state_hub_task_id: "3ba6828e-7514-4f4d-a1d3-54134e039f55"
```
@ -87,11 +91,16 @@ pointer in NetKingdom so there is one apply path.
Package type is `manifest-managed-platform-service` unless the rewrite
to Helm is an explicit later decision.
**Result (2026-08-18):** manifests absorbed, Makefile
`render|server-dry-run|deploy|verify-live|rollback` added, NetKingdom
README points here. `make server-dry-run` passes. Restore-drill is a
separate target so completed Jobs are not reapplied.
## Draft platform handoffs
```task
id: RMGR-WP-0006-T04
status: todo
status: done
priority: medium
state_hub_task_id: "0c2846d9-5db2-4b97-becd-0367b5737188"
```
@ -101,11 +110,15 @@ user-engine. Apply them only in the owning repos (`rapp-postgres`,
`railiance-platform`) after those owners accept the draft. This repo
records the request; it does not become the credential home.
**Result (2026-08-18):** `docs/platform-handoffs.md` keeps the dedicated
`user-engine-pg` Cluster and lists OpenBao/delivery/flex-auth
references. No postgres consumer file is added.
## Implement `rmgr rapp` scaffolding
```task
id: RMGR-WP-0006-T05
status: todo
status: done
priority: high
state_hub_task_id: "6cf89776-ff31-4dd1-aa9a-39474fc387aa"
```
@ -119,6 +132,11 @@ the baseline files.
Depends on the shape proven in T02T03 and on `RMGR-WP-0004` scaffolding
ownership.
**Result (2026-08-18):** `rmgr rapp init|validate|pin-image` added.
`init` refuses a `rapp-` workload name and refuses to overwrite an
existing declaration. `validate` runs the family validator against an
isolated sibling root so undeclared engine stubs do not fail the check.
## Residuals
- `rapp-secrets-engine` and `rapp-tenant-engine` remain undeclared stubs;