Compare commits

...

5 commits

Author SHA1 Message Date
8b87b17980 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.
2026-08-18 12:39:24 +02:00
77452492ec docs: treat live net-kingdom user-engine manifests as the wrap source
user-engine already runs from net-kingdom/sso-mfa/k8s/user-engine.
The rapp absorbs those manifests (manifest-managed), keeps or
cutovers the dedicated CNPG cluster, and becomes the only apply home.
2026-08-18 12:21:53 +02:00
56cc9099a3 docs: record knative admission and identity-grouping caveats
rail-knative is verified as a runtime but not production-approved.
Keep rapp-user-engine a single-repo pilot; a future grouped
rapp-netkingdom-identity is only legal if members share rollback fate.
2026-08-18 12:20:27 +02:00
f285b2f315 docs: name the three wrap paths in the Railiance guide
Path A (rapp-*) is the target; Path B (create-overlay) is transitional;
Path C (railiance-apps S5) is how user-facing apps still ship. Fabric
is discovery only.
2026-08-18 12:19:52 +02:00
5197aa7425 docs: add Railiance app wrapper best-practice guide
Record how rapp-* repos wrap first-party apps for Railiance, using
user-engine as the pilot. Opens RMGR-WP-0006 (T01 done) and includes
fix-consistency ID writeback for WP-0004/0005/0006.
2026-08-18 12:18:45 +02:00
9 changed files with 1499 additions and 1 deletions

View file

@ -24,6 +24,7 @@ Architecture:
- [Observation and command contracts v0.1](docs/observation-command-contracts_v0.1.md)
- [State Hub extraction inventory v0.1](docs/state-hub-extraction-inventory_v0.1.md)
- [ADR-001 Implementation foundation](docs/adr-001-implementation-foundation.md)
- [Railiance app deployment guide](docs/RailianceAppDeploymentGuide.md) (`RMGR-WP-0006`)
```bash
make install # or: uv pip install -e ".[dev]"
@ -32,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

@ -12,6 +12,8 @@
| workplan | RMGR-WP-0002 | finished | — | workplans/RMGR-WP-0002-retirement-dual-run-strangler.md |
| workplan | RMGR-WP-0003 | finished | — | workplans/RMGR-WP-0003-production-pilot-dual-run.md |
| workplan | RMGR-WP-0004 | proposed | — | workplans/RMGR-WP-0004-repository-standards-conformance.md |
| workplan | RMGR-WP-0005 | proposed | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
| workplan | RMGR-WP-0006 | active | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md |
| task | RMGR-WP-0001-T01 | done | — | workplans/RMGR-WP-0001-foundation.md |
| task | RMGR-WP-0001-T02 | done | — | workplans/RMGR-WP-0001-foundation.md |
| task | RMGR-WP-0001-T03 | done | — | workplans/RMGR-WP-0001-foundation.md |
@ -34,3 +36,20 @@
| task | RMGR-WP-0004-T05 | wait | — | workplans/RMGR-WP-0004-repository-standards-conformance.md |
| task | RMGR-WP-0004-T06 | wait | — | workplans/RMGR-WP-0004-repository-standards-conformance.md |
| task | RMGR-WP-0004-T07 | wait | — | workplans/RMGR-WP-0004-repository-standards-conformance.md |
| task | RMGR-WP-0004-T08 | wait | — | workplans/RMGR-WP-0004-repository-standards-conformance.md |
| task | RMGR-WP-0004-T09 | wait | — | workplans/RMGR-WP-0004-repository-standards-conformance.md |
| task | RMGR-WP-0005-T01 | todo | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
| task | RMGR-WP-0005-T02 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
| task | RMGR-WP-0005-T03 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
| task | RMGR-WP-0005-T04 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
| task | RMGR-WP-0005-T05 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
| task | RMGR-WP-0005-T06 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
| task | RMGR-WP-0005-T07 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
| task | RMGR-WP-0005-T08 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
| task | RMGR-WP-0005-T09 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
| task | RMGR-WP-0005-T10 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
| task | RMGR-WP-0006-T01 | done | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md |
| task | RMGR-WP-0006-T02 | todo | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md |
| task | RMGR-WP-0006-T03 | todo | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md |
| task | RMGR-WP-0006-T04 | todo | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md |
| task | RMGR-WP-0006-T05 | todo | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md |

View file

@ -0,0 +1,944 @@
# Railiance App Deployment Guide
Best-practice guide for wrapping an ordinary application as a Railiance
managed workload (`rapp-*`) so Repo Manager can later scaffold the same
shape for `rapp-some-app`.
This document is a practice and work-structure guide. It does **not**
restate the family schemas. Where a field list and a schema disagree, the
schema wins:
- [`railiance-master/schemas/rapp.schema.json`](../../railiance-master/schemas/rapp.schema.json)
- [`railiance-master/docs/adr/ADR-0007-rapp-declaration-contract.md`](../../railiance-master/docs/adr/ADR-0007-rapp-declaration-contract.md)
- [`railiance-master/docs/repo-family-bootstrap-contract.md`](../../railiance-master/docs/repo-family-bootstrap-contract.md)
Worked example throughout: `user-engine` + the empty stub `rapp-user-engine`.
---
## 1. Purpose
Repo Manager should make it easy to set up a managed Railiance application
wrapper. The product of that setup is a `rapp-<app>` repository: a
packaging and operations home that can deploy, verify, roll back, and bind
the app onto a rail and a reef.
This guide answers:
1. What a managed Railiance application **is**.
2. How rails, reefs, and existing rapps already do it.
3. Which files, declarations, and operator surfaces a new wrapper needs.
4. How `user-engine` should be wrapped as `rapp-user-engine`.
5. What Repo Manager must later automate, in order.
It is the first deliverable of `RMGR-WP-0006`. Implementation of the
scaffolder comes after this shape is accepted.
---
## 2. Vocabulary
Railiance classifies repositories on four complementary axes
(`railiance-master/docs/repository-axes.md`, ADR-0001):
| Prefix | Axis | Question it answers |
| --- | --- | --- |
| `railiance-*` | ownership | Who owns this capability or layer? |
| `rail-*` | execution contract | How does a workload run here? |
| `rapp-*` | managed workload package | What is packaged and operated? |
| `reef-*` | substrate | Where does it run, and what is bound there? |
A `rapp` is **managed packaging, not ownership**. The application repo
keeps domain logic, tests, image build, and schema migrations. The rapp
owns Helm/manifests, rail compatibility, smoke and rollback, secret
*references*, and reef binding. The architecture term is *managed
workload package*; "service wrapper" is informal.
Do not derive a rapp from a Forgejo organization or a State Hub domain.
Rapp context is its own dimension (ADR-0007):
- repos ↔ rapps is **many-to-many**
- deployables ↔ rapps is **one-to-one**
Grouping is legitimate only where members share rollout and rollback fate.
---
## 3. Current inventory (2026-08-18)
### 3.1 Rails
| Rail | Kind | Architecture | Readiness | Use when |
| --- | --- | --- | --- | --- |
| `rail-kubernetes` | base | Kubernetes | verified | Default for platform services **and** managed applications |
| `rail-knative` | derived from `rail-kubernetes` | Knative Serving on Kubernetes | rail.yaml `verified`; reef binding not production-approved | Scale-to-zero, revision traffic, request activation (today: Qonto). Serving 1.22.0 is installed on Railiance01; single-node control plane still blocks production approval. |
Default bias: use `rail-kubernetes` unless the workload truly needs
Knative lifecycle semantics. Do not invent a new rail for an adapter or
values profile.
`rail-kubernetes` also owns the generic staged-promotion contract
`railiance/app.toml` (Stage 1 local, Stage 2 canary, Stage 3 promote) and
the compatibility overlay-repo pattern. That overlay is the migration-era
wrapper. Durable first-class packaging belongs in `rapp-*`.
### 3.2 Reefs
| Reef | Kind | State | What it is |
| --- | --- | --- | --- |
| `reef-railiance` | `server-group` | active | Home compute reef (`Railiance01`). Hosts `rail-kubernetes` and `rail-knative`. Public host ports 80/443 (and 2224 for the provider agent) are granted. |
| `reef-storage` | `object-storage` | planned | Provider-delegated S3 (Scaleway). Hosts no rail. Backup target, not a compute placement. |
A rapp names its placement in `bound_reefs`. The reef's `bound_rapps` is a
**derived projection** of that field, not a hand list.
Topology is not readiness (ADR-0006). Binding a rapp to a reef does not
admit it to production and does not make it public (ADR-0008).
### 3.3 Live and stub rapps
| Repo | Declaration | Package type | Rail | Ownership repo | Readiness | Notes |
| --- | --- | --- | --- | --- | --- | --- |
| `rapp-openbao` | yes | helm-managed-platform-service | kubernetes | `railiance-platform` | verified | Third-party platform service. Proven S3 pattern. |
| `rapp-postgres` | yes | helm-managed-platform-service | kubernetes | `railiance-platform` | verified | Shared CNPG + per-consumer surface. Apps consume this; they do not run their own Postgres. |
| `rapp-qonto` | yes, pre-schema drift | knative-managed-service (implied) | knative | `qonto-assistant` | verified | First-party Knative app. Missing `composition`, `bound_reefs`, `package_type`; `workload_identity.name` is still `rapp-qonto`. |
| `rapp-policy-nexus` | yes | helm-managed-platform-service | kubernetes | `policy-nexus` | declared | Closest first-party Helm app wrapper. Public grant at `policy.coulomb.social`. |
| `rapp-user-engine` | **no** | — | — | — | stub | Empty git repo. Target of this guide. |
| `rapp-secrets-engine` | **no** | — | — | — | stub | README only. Same family as user-engine. |
| `rapp-tenant-engine` | **no** | — | — | — | stub | README only. Same family. |
| `rapp-vergabe-teilnahme` | never built | — | — | — | — | Planned user-facing proof; workload still lives in `railiance-apps`. |
| `rapp-forgejo` | not built | — | — | — | — | Deferred; forge still mixed into `railiance-forge` / `railiance-apps`. |
The platform-service wrapper is proven twice. The first-party application
wrapper is only half-proven (`rapp-policy-nexus` declared; `rapp-qonto`
drifted; `rapp-vergabe-teilnahme` never extracted). `rapp-user-engine` is
the next application-shaped proof, not a substitute for the missing
Vergabe extraction.
### 3.4 Ownership repos that still hold wrappers
`railiance-apps` is still the S5 release surface for workloads that have
not been extracted (`vergabe-teilnahme`, `coulomb-social`, `core-hub`,
`reuse-surface`, `inter-hub`, Forgejo values). Its
`docs/s5-app-onboarding-checklist.md` remains the operator recipe for
Helm, probes, landing pages, and image pins. New first-class wrappers
should **copy those recipes into a `rapp-*` repo**, not add another chart
to `railiance-apps`.
When a rapp already exists, S5 becomes a thin delegate rather than a
second chart. `railiance-apps` `make policy-nexus-deploy` calls
`make -C ../rapp-policy-nexus deploy`. Production *approval* may still
be recorded in `railiance-apps` bindings; the package and apply path
belong to the rapp.
`railiance-platform` retains cross-workload policy, credential custody,
lane approval, and the postgres credential broker.
`railiance-fabric` projects `supports_rail` / `hosts_rail` / `binds_rapp`
from family declarations. That is discovery, not a deploy engine. Do not
treat "register in Fabric" as wrapping the app.
### 3.5 Three wrap paths (only one is the target)
| Path | Where it lives | Use |
| --- | --- | --- |
| **A. `rapp-*` repo** | `declarations/rapp.yaml` + package Makefile | Canonical destination for a managed app or platform service. This guide. |
| **B. Overlay + `railiance/app.toml`** | `bin/railiance create-overlay` in `rail-kubernetes` | Migration-era third-party wrap. Produces `<app>-railiance-overlay/`. Graduate it to Path A; do not start new overlays. |
| **C. S5 chart in `railiance-apps`** | `charts/<app>/` + `helm/<app>-values.yaml` | How user-facing apps actually ship today. Stay here only for an already-extracted-or-not decision; new managed apps should leave as Path A. |
`flex-auth` is a live Path B example (`flex-auth/railiance/app.toml`)
with no `rapp-*` repo. `vergabe-teilnahme` is a live Path C example.
`rapp-policy-nexus` is the only live A←C bridge.
There is no `railiance register-rapp`. A rapp exists when
`declarations/rapp.yaml` is committed and
`validate-family-declarations.py` accepts it.
---
## 4. Decide before creating a rapp
Ask these in order. Stop at the first "no".
1. **Does the workload have a stable identity?** A name that is not the
repo prefix, a package surface already visible in Git, and a reason to
version and roll back on its own.
2. **Is wrapping distinct from ownership?** If the only thing you would
put in the rapp already belongs in `railiance-platform` or
`railiance-apps` as shared policy, do not create a rapp.
3. **Do members share fate?** One rapp per bounded context that deploys
and rolls back together. Independent rollback → separate rapp.
4. **Which rail?** Default `rail-kubernetes`. Choose `rail-knative` only
for request-activation / scale-to-zero / revision-traffic semantics.
5. **Which reef?** New compute workloads bind `reef-railiance`. Object
storage is `reef-storage` and is consumed as a capability, not as a
placement.
6. **What package type?** One of the schema enums (see §6).
7. **What exposure?** Omit the field (private) until a grant exists.
A service that fails (1) or (2) can still be operated from its ownership
repo. It is not blocked; it is just not ready to be a rapp. Keep it on
Path C (S5) or in the ownership repo until those answers are yes.
If the workload is already an S5 chart and is not yet ready for a rapp,
do not generate an overlay (Path B) on top of it. Path B is only a
third-party compatibility wrap.
---
## 5. Ownership split
Use the S3 ownership test from
`railiance-platform/docs/rapp-platform-service-pattern.md`, generalized
to applications:
1. Would this asset survive replacing the product? → ownership / platform
repo.
2. Does any other workload depend on it? → platform or rail.
3. Does it encode *who may approve*, not *how to apply*? → platform /
reef admission.
4. Otherwise it is the package's.
### Applied to a first-party app
| Asset | Home |
| --- | --- |
| Domain code, tests, migrations, Containerfile, OpenAPI | app repo (`user-engine`) |
| Helm chart, values, NetworkPolicy, Ingress, ServiceAccount | rapp |
| `declarations/rapp.yaml`, `railiance/app.toml` | rapp |
| Deploy / dry-run / status / verify / rollback Makefile | rapp |
| Workload-specific smoke outcomes | rapp |
| Image **build** and digest publication | app repo + forge |
| Image **pin** in the release | rapp (digest, never `latest`) |
| Postgres cluster, backup, credential broker | `rapp-postgres` + `railiance-platform` |
| Consumer declaration (`database-per-consumer`) | `rapp-postgres/consumers/<app>.yaml` requested by the rapp |
| Schema migrations | app repo, invoked by the rapp at rollout |
| OIDC / flex-auth / tenant policy | those ownership repos |
| Secret **values** | OpenBao via the platform broker |
| Secret **references** | rapp `secret_references` + `railiance/app.toml` |
| Public hostname grant | rapp `exposure.grant` + reef public surface |
| DNS / TLS / Traefik | reef / `railiance-cluster` |
| Shared S5 recipes (probe Host header, landing pages) | copied into the rapp; source of the lesson stays in `railiance-apps` |
The rapp repo must never become a shadow ownership repo. If a paragraph
in `INTENT.md` starts explaining *why the product exists*, it belongs in
the app repo.
---
## 6. Required repo baseline
Every `rapp-*` starts with the first-wave bootstrap contract:
```text
rapp-<app>/
.repo-classification.yaml
README.md
INTENT.md
SCOPE.md
AGENTS.md
CLAUDE.md
LICENSE
Makefile
declarations/rapp.yaml # family contract (normative schema)
workplans/
workplans/archived/
```
Then add the package surface. For a `rail-kubernetes` first-party app
the proven layout is the union of `rapp-policy-nexus` (Helm + digest
pins + `make render|deploy|verify-live|rollback`) and the
`rail-kubernetes` overlay contract:
```text
railiance/app.toml # staged promotion (rail contract)
helm/<app>/ # chart owned by the rapp
Chart.yaml
values.yaml
templates/
deployment.yaml
service.yaml
serviceaccount.yaml
networkpolicy.yaml
ingress.yaml # only if exposure will become public/operator
consumers/ # only if this rapp provisions units
docs/
runtime-contract.md
retained-handoff.md # if extracting from railiance-apps / platform
tests/ # render / contract tests, no cluster required
tools/ # verify_live.sh, smoke helpers
bindings/ # production binding records when needed
```
Implementation files come after the baseline. An empty `rapp-*` git
directory is not a wrapper; `railiance-master` already treats
`rapp-user-engine` as undeclared.
### Classification
Copy the shape used by live rapps. Domain follows the **workload**, not
the wrapper:
```yaml
repo_classification:
standard: Repo Classification Standard
version: "1.0"
classified_at: "YYYY-MM-DD"
classified_by: agent
category: project
domain: <workload domain>
secondary_domains:
- infotech
capability_tags:
- platform
- operations
- configuration
business_stake:
- technology
- operations
business_mechanics:
- coordination
- operation
```
---
## 7. `declarations/rapp.yaml`
Normative shape: `railiance-master/schemas/rapp.schema.json`.
Validate with:
```bash
# from railiance-master, with sibling family repos checked out
tools/validate-family-declarations.py --root ..
```
### Required meaning, not a second schema
| Field | Practice |
| --- | --- |
| `kind` | always `managed-workload-package` |
| `repo_family` | always `rapp` |
| `rapp_id` / `repo` | `rapp-<app>` for a single-repo rapp |
| `ownership_repo` | the accountable app or layer repo, **never** the rapp itself. Application: `user-engine`. Platform service: `railiance-platform`. |
| `contract_version` | start at `1.0.0` |
| `readiness_state` | `draft` while scaffolding → `declared` once valid → `deployed` / `verified` only with evidence. There is no `production-approved` on a rapp. |
| `workload_identity.name` | the workload (`user-engine`), **not** `rapp-user-engine`. The schema forbids the `rapp-` prefix. |
| `workload_identity.package_type` | one of `helm-managed-platform-service`, `manifest-managed-platform-service`, `knative-managed-service`, `grouped-composition` |
| `data_classification` | highest class the workload handles |
| `criticality` | `low` \| `medium` \| `high` \| `critical` |
| `primary_rail` | must also appear in `supported_rails` |
| `bound_reefs` | `[]` until placed; then `[reef-railiance]` |
| `runtime_dependencies` | capability names, not the resource that satisfies them |
| `composition.purpose` | one sentence; if it needs "and also", split the rapp |
| `composition.member_repos` | one member for a single-repo rapp; each lists `deployables` |
| `composition.upstream_components` | pinned third-party charts/images. Exact version or digest. Never `latest`. |
| `rollout_contract.default_mode` | `helm-upgrade-install` or `kubectl-server-side-apply` (or `revision-canary` on Knative) |
| `smoke_contract.required` | **outcomes** that must be true, not a command diary |
| `rollback_contract.order` | preferred strategies first |
| `source_documents` | repo + path of the docs this declaration was derived from |
Optional, use when true:
- `secret_references` — OpenBao **paths**, never values
- `consumer_contract` / `consumers` — only if this rapp provisions isolated units to others (`rapp-postgres` is the model)
- `exposure` — omit for private. `public` requires a grant, `binding_admission: production-approved`, and a reef that has granted a public surface.
### Package-type choice
The schema has no `helm-managed-application` enum. Until that is added in
`railiance-master`, first-party Helm apps use
`helm-managed-platform-service` (as `rapp-policy-nexus` does). Do not
invent a local type. Record the naming mismatch in `source_documents` /
workplan notes if you want the schema extended later.
### Capability names in use today
There is not yet a controlled vocabulary shared with
`rail.required_substrate_capabilities`. Prefer names already live in
declarations rather than minting near-duplicates:
- `kubernetes-api`
- `helm-apply-path` / `helm-repo-access`
- `traefik-ingress`
- `cert-manager`
- `forgejo-oci-registry`
- `pvc-storage`
- `operator-kubeconfig-access`
- `knative-serving`
- `keycape-oidc` / `key-cape-oidc` (spellings have already drifted — pick one and stay consistent inside a rapp)
- `flex-auth`
- `openbao`
- `openbao-database-secrets-engine`
- `s3-backup-target`
Who provisions the capability is **outside** the rapp:
`resource-control` procures, the reef publishes non-secret attributes,
`railiance-platform` vends the credential.
---
## 8. Service wrapper patterns
Three live patterns. Copy the one that matches the rail and package type.
### 8.1 Helm platform / app service (`rail-kubernetes`)
References: `rapp-openbao`, `rapp-policy-nexus`,
`railiance-apps/docs/s5-app-onboarding-checklist.md`.
Operator surface (minimum Makefile targets):
| Target | Meaning |
| --- | --- |
| `make check` / `make test` / `make lint` | contract and `helm lint` with dummy digests |
| `make render` | write rendered manifests locally |
| `make server-dry-run` | `kubectl apply --dry-run=server` against a representative API |
| `make deploy` | `helm upgrade --install --atomic --wait` |
| `make status` | helm/kubectl status |
| `make verify-live` | named smoke outcomes from `smoke_contract.required` |
| `make rollback REVISION=…` | previous Helm revision |
Pins:
- image by **digest**, not tag (`rapp-policy-nexus` is the model)
- chart version and app version in `workload_identity`
- forge-owned registry path (`forgejo.coulomb.social/...`)
Runtime hardening that should be the default in new charts:
- non-root numeric user
- read-only root filesystem where the app allows it
- dropped capabilities
- NetworkPolicy default-deny plus explicit ingress/egress
- unauthenticated `/healthz` and `/readyz`
- `/metrics` cluster-internal and authorized
- probe `Host` header set when the app validates Host (Django lesson)
### 8.2 Manifest-managed platform service
Reference: `rapp-postgres` (`kubectl-server-side-apply` of CNPG Cluster
and NetworkPolicies; Helm only for the *operator*, which is cluster-owned).
Use this when the workload is a set of reviewed manifests rather than a
chart you upgrade as a release.
### 8.3 Knative managed service
Reference: `rapp-qonto` (`runtime/knative/`, revision-canary,
previous-revision rollback). Only when `rail-knative` is the primary rail.
### 8.4 Overlay compatibility path
`rail-kubernetes/docs/overlay-repo-pattern.md` still describes
`<app>-railiance-overlay/` with `railiance/app.toml`. The existing
scaffold is:
```bash
# from rail-kubernetes; cluster/bootstrap only keep shims
bin/railiance create-overlay --app-id <id> --upstream-url <url>
bin/railiance run <overlay-dir> # Stage 1
bin/railiance deploy --stage 2 <overlay-dir> # Stage 2 canary
bin/railiance promote <overlay-dir> # Stage 3
bin/railiance rollback <overlay-dir>
```
`create-overlay` writes local files only. It does not create
`declarations/rapp.yaml`, a Forgejo remote, or a family classification.
Repo Manager's later `rmgr rapp init` replaces this for first-party apps.
New work should not create overlay repos. If you find one, graduate it
to `rapp-<app>` and keep `railiance/app.toml` inside the rapp.
---
## 9. Platform handoffs every app wrapper needs
### 9.1 Database
Do not run a sidecar Postgres. The default for a new app is a consumer
against `rapp-postgres`:
- boundary unit: `database-per-consumer`
- credential lane: `railiance-platform-credential-broker`
- roles split: owner / migration / runtime
- tenant-keying required even for a single current tenant
The consumer file lives in `rapp-postgres/consumers/<app>.yaml`. The rapp
records the capability (`openbao-database-secrets-engine` or equivalent)
and the OpenBao path in `secret_references`. The app repo keeps
migrations and applies them with the migration role.
If the app consumes a single `DATABASE_URL`, URL-encode the password.
Prefer discrete `POSTGRES_*` env vars when the framework allows it
(`railiance-apps/docs/django-on-railiance.md`).
A **dedicated CNPG Cluster** is allowed only when placement policy
already says so (live `user-engine-pg` is this case). Then the Cluster
manifests belong in the rapp, not in `net-kingdom` or the app repo. Do
not be both a `rapp-postgres` consumer and a dedicated Cluster.
### 9.2 Secrets
The rapp never stores values. Standing secrets go through a CCR and
`secret_references`. Short-lived leases go through the grant catalog and
`rapp_id`. See `railiance-platform/docs/rapp-credential-lane-binding.md`.
In `railiance/app.toml`, declare `secrets.references` with route ids
(`openbao-api-key`, `key-cape-oidc-login`, …) and Kubernetes Secret /
ExternalSecret **names**.
### 9.3 Identity and authorization
Apps that speak OIDC / flex-auth declare those as runtime capabilities
and receive issuer, audience, client id, and token-file paths through
the broker. The rapp does not mint tenants or tokens.
`workload_identity.tenant` is an opaque NetKingdom id when one is
needed; the rapp never parses or authorizes on it.
### 9.4 Images
Build stays in the app repo. The rapp consumes a forge-published digest.
Verify the cluster can pull the image before the first non-smoke deploy
(`railiance-apps/tools/check-oci-image.sh` is the existing helper).
### 9.5 Ingress, TLS, landing pages
Private by default. When a public hostname is later granted:
- name host, namespace, release, ingress, TLS secret in the runbook
- cert-manager owns certificates
- every public `/` has an intentional browser response (S5 landing-page
rule)
- do not break `/healthz`, `/readyz`, API prefixes, or OIDC callbacks
### 9.6 Exposure (ADR-0008)
| Posture | Who reaches the listener |
| --- | --- |
| omitted / `private` | in-cluster DNS, same-reef services |
| `operator` | same, plus a named ops-bridge / SSH tunnel |
| `public` | internet, only with grant + production-approved binding + reef public surface |
`data_classification: public` is **not** exposure. Port `6443` is never
a grantable public surface.
---
## 10. Worked example: `user-engine``rapp-user-engine`
### 10.1 Current facts
`user-engine` is a headless user-domain / identity-domain service with a
production WSGI portal. The **app repo** already has:
- `Containerfile` — Python 3.12 slim, non-root uid `10001`, port `8080`
- `python -m user_engine.runtime`
- `/healthz`, `/readyz`, `/metrics` (metrics require the trusted proxy marker)
- Postgres store + in-tree migrations
- fail-closed production adapters: OIDC, flex-auth, outbox, provisioning,
optional tenant-management and registration verification
- explicit non-ownership of IdP, authorization PDP, secret custody, **and
deployment** (`docs/release.md`)
`rapp-user-engine` is an empty Forgejo clone
(`forgejo-remote:coulomb/rapp-user-engine.git`). No README, no
declaration. ADR-0007 already lists it as undeclared.
**Live packaging is not in the rapp.** It lives in
`net-kingdom/sso-mfa/k8s/user-engine/`:
| File | Role |
| --- | --- |
| `runtime.yaml` | Namespace, dedicated CNPG `user-engine-pg`, Deployment, Service, SA, NetworkPolicies |
| `ingress.yaml` | Traefik Ingress `users.92-205-62-239.nip.io` (README still says no public Ingress until KeyCape PKCE is configured — treat exposure as unresolved) |
| `openbao-runtime.yaml` | ExternalSecrets from `platform/workloads/user-engine/runtime` |
| `backup.yaml`, `restore-drill.yaml` | Logical dump / restore |
| `verify-operability.sh` | Live checks |
Image already published:
`forgejo.coulomb.social/coulomb/user-engine@sha256:e3b5f65bafc1c0260dfdf2567a52766e67506ceb878a51759a2e9a307c4b5eb8`.
flex-auth for this consumer is pinned **inside** `flex-auth`
(`values/user-engine.yaml`, `examples/user-engine/`), not in a rapp.
Closest sibling analogue: `tenant-engine/deploy/` (digest-pinned
manifests, but that app *is* a `rapp-postgres` consumer).
The wrapper job is therefore **absorb and declare**, not invent a second
production target. Same rule as
`qonto-assistant/docs/knative-runtime-and-rapp-handoff.md`: once the
rapp exists, `net-kingdom` keeps IAM/provisioner contracts and stops
being the apply home.
Sister stubs `rapp-secrets-engine` and `rapp-tenant-engine` follow after
this path validates. `rapp-secrets-engine` is likely a grouping mistake
against `rapp-openbao` / S3, not a fourth identity member.
### 10.2 Recommended decisions
| Decision | Value | Why |
| --- | --- | --- |
| Create a rapp? | yes | stable identity, clear app/wrapper split, own rollout fate |
| `ownership_repo` | `user-engine` | application accountability, same pattern as `policy-nexus` / `qonto-assistant` |
| `workload_identity.name` | `user-engine` | schema rule |
| `package_type` | `manifest-managed-platform-service` | live assets are raw YAML/CNPG, not a Helm chart. Do not invent Helm just to match policy-nexus. |
| `primary_rail` | `rail-kubernetes` | long-running portal + durable store; no Knative need |
| `bound_reefs` | `[]` at `declared`; `[reef-railiance]` when placed | empty is legal and means "not yet placed" |
| `data_classification` | `confidential` | identity and account data |
| `criticality` | `high` | shared identity-domain service; not the secrets/postgres critical path |
| `exposure` | omit (private) until the KeyCape PKCE edge is decided | an Ingress object already exists; that is **not** a grant (ADR-0008). Decide hostname + residual-risk owner before `public`. |
| Granularity | single-repo rapp **for the pilot** | one deployable; do not group with tenant-engine or secrets-engine unless they share rollback fate. `rapp-first-wave-candidates.md` still flags a future grouped `rapp-netkingdom-identity` (user-engine + tenant-engine + key-cape + flex-auth). That grouping is only legal if those members cannot roll back independently — they almost certainly can. Treat the grouped name as a later composition question, not as a reason to leave `rapp-user-engine` undeclared. |
### 10.3 Draft declaration (index, not authority)
The live file will be `rapp-user-engine/declarations/rapp.yaml`. This is
the intended fill of the schema, to be validated before it is treated as
declared.
```yaml
kind: managed-workload-package
repo_family: rapp
rapp_id: rapp-user-engine
repo: rapp-user-engine
ownership_repo: user-engine
contract_version: 1.0.0
readiness_state: draft
workload_identity:
name: user-engine
package_type: manifest-managed-platform-service
app_version: 0.1.0
data_classification: confidential
criticality: high
primary_rail: rail-kubernetes
supported_rails:
- rail-kubernetes
bound_reefs: []
runtime_dependencies:
- kubernetes-api
- helm-apply-path
- forgejo-oci-registry
- pvc-storage
- keycape-oidc
- flex-auth
- openbao
- audit-core
- tenant-engine
secret_references:
- platform/workloads/user-engine/runtime
composition:
purpose: Package and operate the user-engine identity-domain service on Railiance.
member_repos:
- repo: rapp-user-engine
role: Kubernetes manifests, smoke, and rollback for the user-engine runtime
deployables:
- user-engine
upstream_components:
- name: user-engine
source: forgejo.coulomb.social/coulomb/user-engine
version: sha256:e3b5f65bafc1c0260dfdf2567a52766e67506ceb878a51759a2e9a307c4b5eb8
rollout_contract:
default_mode: kubectl-server-side-apply
commands:
- make server-dry-run
- make deploy
production_gate: reef-production-approved
smoke_contract:
required:
- healthz-ok
- readyz-ok-when-store-ready
- metrics-requires-proxy-marker
- local-issuer-rejected-in-production
- live-image-digest-match
commands:
- make verify-live
rollback_contract:
order:
- previous-immutable-image-digest
- apply-reviewed-git-revision
commands:
- make rollback DIGEST=<previous-image-digest>
source_documents:
- repo: user-engine
path: INTENT.md
- repo: user-engine
path: docs/configuration.md
- repo: user-engine
path: docs/operability.md
- repo: net-kingdom
path: sso-mfa/k8s/user-engine/README.md
- repo: railiance-master
path: schemas/rapp.schema.json
- repo: repo-manager
path: docs/RailianceAppDeploymentGuide.md
```
Cardinality note: the live deployable name is `user-engine` (the
Deployment). Migrations run in-process before serve, so do **not**
invent a second `user-engine-migrate` deployable unless a separate Job
is added. `user-engine-pg` is a Cluster owned by this package only if
the dedicated-cluster decision stands; if it is inventoried as its own
deployable it still belongs to **this** rapp.
`ownership_repo: user-engine` already names the source repo. Do not also
claim the Deployment from `net-kingdom`. A single-member composition
matches every live rapp; add `user-engine` as a second member only if it
contributes a *different* deployable.
### 10.4 Runtime inputs the chart must accept
From `user-engine` `runtime.py` and `docs/configuration.md`. All values
arrive as secret-backed env or files. The chart names the keys; OpenBao
fills them.
Required for production assembly:
- `USER_ENGINE_DATABASE_URL` (or discrete Postgres vars + a rendered DSN)
- `USER_ENGINE_OIDC_ISSUER`, `USER_ENGINE_OIDC_AUDIENCE`,
`USER_ENGINE_OIDC_CLIENT_ID`, `USER_ENGINE_OIDC_REDIRECT_URI`
- `USER_ENGINE_FLEX_AUTH_URL`, `USER_ENGINE_FLEX_AUTH_TOKEN_FILE`
- `USER_ENGINE_PROXY_SECRET`
- `USER_ENGINE_LOGIN_URL`
- `USER_ENGINE_EVENT_URL`, `USER_ENGINE_EVENT_TOKEN`
- `USER_ENGINE_PROVISIONING_URL`, `USER_ENGINE_PROVISIONING_TOKEN`
Optional / fail-closed when absent:
- `USER_ENGINE_TENANT_MANAGEMENT_URL` + `USER_ENGINE_TENANT_MANAGEMENT_TOKEN`
- `USER_ENGINE_MAIL_URL` + `USER_ENGINE_MAIL_TOKEN`
- public-registration adapter settings
Port: `USER_ENGINE_HTTP_PORT` (default `8080`). Do not use
`USER_ENGINE_PORT` — Kubernetes service-link injection occupies that name.
Logical secret names the app already documents: `database.url`,
`event.signing_key`, `webhook.shared_secret`. Map those to broker routes
in `railiance/app.toml`; do not invent a second naming scheme in the
chart.
### 10.5 Database decision (do not do both)
Live user-engine uses a **dedicated** CNPG Cluster `user-engine-pg`
(postgresql 17.5, db `user_engine`), not a `rapp-postgres` consumer.
tenant-engine and audit-core *are* platform consumers.
Two legal next states:
1. **Keep dedicated.** Move `user-engine-pg` manifests into the rapp.
Declare `pvc-storage` (and later `s3-backup-target` if backup is
attached). Do not add a postgres consumer.
2. **Become a consumer.** Add `rapp-postgres/consumers/user-engine.yaml`
(same shape as `audit-core.yaml`), delete the dedicated Cluster after
a reviewed cutover. Then drop `pvc-storage` from this rapp.
Pick one in T04. The current live state is (1).
### 10.6 Absorb the NetKingdom apply home
T03 should copy
`net-kingdom/sso-mfa/k8s/user-engine/` into
`rapp-user-engine/manifests/` (or `helm/` only if a real chart is
justified), add Makefile `server-dry-run` / `deploy` / `verify-live` /
`rollback DIGEST=…`, and leave a pointer README in NetKingdom so there
is one apply path. Do not Helm-rewrite the live YAML in the same
change.
### 10.6 What stays in `user-engine`
- domain model, adapters, tests, conformance harness
- `Containerfile` and image publish instructions
- SQL migrations
- portal/OIDC behavior
- `docs/operability.md` diagnostics
The app repo should grow a short "consumed by `rapp-user-engine`" note
pointing here. It should not grow Helm charts.
### 10.7 First-wave `railiance/app.toml` for this rapp
Minimum: `schema_version = "railiance.app.v1"`, `app.id = "user-engine"`,
source digest policy `required`, postgres + oidc + flex-auth + openbao
dependencies, secret references by route, `/healthz` and `/readyz`
checks, Stage 1 (`make test` in the app repo + `helm template`), Stage 2
canary requiring approval, Stage 3 promote with a recorded rollback
target. Copy structure from
`rail-kubernetes/examples/railiance/app.toml`.
---
## 11. Operator checklist (new rapp)
Use this when filling a stub or extracting a wrapper from
`railiance-apps`.
### Shape
- [ ] Confirm the workload passes §4.
- [ ] Create or fill `rapp-<app>` with the §6 baseline.
- [ ] Write `INTENT.md` / `SCOPE.md` as a **package** boundary, not a
product manifesto.
- [ ] Author `declarations/rapp.yaml` to the schema. Start
`readiness_state: draft`.
- [ ] Add `railiance/app.toml` if the primary rail is `rail-kubernetes`.
- [ ] Add the Helm chart or manifest set. Pin images by digest.
- [ ] Add Makefile targets from §8.1.
- [ ] Name smoke **outcomes** and rollback **order**.
- [ ] Leave `exposure` unset.
### Handoffs
- [ ] Request a `rapp-postgres` consumer when a durable store is needed.
- [ ] Request credential lanes / CCRs in `railiance-platform`. Commit
paths only.
- [ ] Confirm forge image publish path in the **app** repo.
- [ ] Confirm cluster pull before the first deploy.
- [ ] Record `source_documents`.
### Evidence before `declared``deployed`
- [ ] `tools/validate-family-declarations.py --root ..` passes.
- [ ] `make render` and `make server-dry-run` pass.
- [ ] `make deploy` on `reef-railiance` only after the reef binding is
intended.
- [ ] `make verify-live` proves every `smoke_contract.required` item.
- [ ] Rollback is rehearsed at least once on a non-destructive revision.
- [ ] Deployable names match the live inventory
(`railiance-platform/docs/evidence/reef-railiance-deployables.json`)
when `--inventory` is run.
### Do not
- [ ] Commit credentials, kubeconfigs, or tokenized registry URLs.
- [ ] Add the workload to `reef.yaml` `bound_rapps` by hand.
- [ ] Put Helm charts in the app repo "just for now".
- [ ] Create a new rail.
- [ ] Create a reef because a hostname exists.
- [ ] Group unrelated engines into one rapp.
- [ ] Set `exposure.posture: public` to make a smoke test easier.
---
## 12. What Repo Manager should automate
This is the work structure for `RMGR-WP-0006`. Each step is a later
command or command flag on the existing governed-mutation path
(`RMGR-WP-0004` scaffolding, specialized for the rapp family).
| Phase | Repo Manager does | Still human |
| --- | --- | --- |
| **P0 — this guide** | Record the shape. | Accept or amend the shape. |
| **P1 — bootstrap stub** | Create or complete `rapp-<app>` with §6 baseline files, classification, and a `draft` `rapp.yaml` from a questionnaire (ownership repo, rail, classification, criticality, purpose). | Confirm ownership split and purpose sentence. |
| **P2 — package skeleton** | Generate Helm chart + Makefile targets from app facts: port, health paths, Containerfile user, image repository. Generate `railiance/app.toml` skeleton. | Review NetworkPolicy and resource requests. |
| **P3 — platform bindings** | Draft `rapp-postgres` consumer and `secret_references` from the app's documented logical secret names. | Approve CCR / consumer apply in the owning packages. |
| **P4 — validate** | Run family-declaration validation and `helm lint` / render. Refuse `readiness_state` promotions that skip evidence. | Promote `draft``declared`. |
| **P5 — place** | Set `bound_reefs: [reef-railiance]` only on an explicit place command. | Production gate and any exposure grant. |
Command sketch (not implemented):
```text
rmgr rapp init --app user-engine --ownership-repo user-engine \
--rail rail-kubernetes --classification confidential --criticality high
rmgr rapp skeleton --path ../rapp-user-engine --from-app ../user-engine
rmgr rapp validate --path ../rapp-user-engine --family-root ..
```
Constraints on the scaffolder:
- Files stay authoritative. The hub is not written to except via
`fix-consistency` after the files exist.
- No secret values, ever.
- Do not invent package types, rails, or reefs.
- Idempotent: re-running `init` on an already-declared rapp must refuse
or update in a reviewed diff, not overwrite a live contract.
- The three engine stubs (`user-engine`, `secrets-engine`,
`tenant-engine`) are the first cohort; `user-engine` is the pilot.
---
## 13. Open questions and known drift
Recorded so the scaffolder does not silently pick a side.
1. **`ownership_repo` tightness.** Schema currently allows a layer repo
(`railiance-platform`) or an application repo. For application rapps,
prefer the application repo. (`railiance-master/schemas/README.md`)
2. **No application package_type.** First-party Helm apps reuse
`helm-managed-platform-service`. A future `railiance-master` schema
change may add `helm-managed-application`.
3. **`rapp-qonto` is not a copy target.** It is verified in production
but does not yet conform to ADR-0007 (`composition`, `bound_reefs`,
`package_type`, workload name). Copy `rapp-policy-nexus` +
`rapp-openbao` instead.
4. **Capability vocabulary is free-form.** Spellings already drift
(`keycape-oidc` vs `key-cape-oidc`). The scaffolder should emit a
small allow-list and refuse new names without a note.
5. **`rapp-vergabe-teilnahme` was the planned user-facing proof and was
never built.** Extracting it remains a `railiance-apps` residual. Do
not pretend `rapp-user-engine` closes that ADR-0003 item. Other
planned-but-unbuilt names (`rapp-forgejo`, `rapp-coulomb-social`,
`rapp-netkingdom-identity`) are not this workplan.
6. **Reef `bound_rapps` on `reef-railiance` has been stale.** Never
teach the scaffolder to edit it.
7. **Metrics and Host-validated apps** need extra chart care. The
scaffolder should read probe paths from the app if present
(`/healthz`, `/readyz` for user-engine) and not assume `/health`.
8. **Grouped identity vs single-engine stubs.** Architecture still
mentions folding `rapp-user-engine` into `rapp-netkingdom-identity`.
ADR-0007 only allows that if members share rollout and rollback fate.
`rapp-secrets-engine` is also a suspect duplicate of `rapp-openbao` /
S3 custody, not a fourth identity member. The pilot declares
`rapp-user-engine` alone; a later composition change is a new
declaration, not a rename of the stub.
---
## 14. References
Architecture and contracts:
- `railiance-master/docs/repository-axes.md`
- `railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md`
- `railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md`
- `railiance-master/docs/adr/ADR-0005-derived-rail-composition.md`
- `railiance-master/docs/adr/ADR-0006-reef-production-admission.md`
- `railiance-master/docs/adr/ADR-0007-rapp-declaration-contract.md`
- `railiance-master/docs/adr/ADR-0008-private-by-default-exposure.md`
- `railiance-master/docs/repo-family-bootstrap-contract.md`
- `railiance-master/docs/rapp-first-wave-candidates.md`
- `railiance-master/schemas/README.md`
Rails and reefs:
- `rail-kubernetes/declarations/rail.yaml`
- `rail-kubernetes/docs/deployment-lifecycle.md`
- `rail-kubernetes/docs/app-toml-contract.md`
- `rail-kubernetes/docs/overlay-repo-pattern.md`
- `rail-knative/declarations/rail.yaml`
- `reef-railiance/declarations/reef.yaml`
- `reef-storage/declarations/reef.yaml`
Proven wrappers and operator recipes:
- `railiance-platform/docs/rapp-platform-service-pattern.md`
- `rapp-openbao/declarations/rapp.yaml`
- `rapp-postgres/declarations/rapp.yaml`
- `rapp-policy-nexus/declarations/rapp.yaml`
- `rapp-policy-nexus/docs/runtime-contract.md`
- `railiance-apps/docs/s5-app-onboarding-checklist.md`
- `railiance-apps/docs/django-on-railiance.md`
- `railiance-apps/docs/operator-recipes.md`
- `railiance-apps/Makefile` (`policy-nexus-deploy` delegate)
- `rail-kubernetes/docs/create-overlay-command.md`
- `railiance-fabric/docs/rail-rapp-reef-adaptation.md` (discovery only)
Worked example:
- `user-engine/INTENT.md`
- `user-engine/docs/configuration.md`
- `user-engine/docs/operability.md`
- `user-engine/docs/postgres-durable-store-consumer-requirements.md`
- `user-engine/Containerfile`
- `user-engine/src/user_engine/runtime.py`
- `net-kingdom/sso-mfa/k8s/user-engine/` (current live apply home)
- `tenant-engine/deploy/` (closest sibling manifest package)
- `qonto-assistant/docs/knative-runtime-and-rapp-handoff.md` (one apply home)

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

@ -198,6 +198,7 @@ with compatibility tests. Cover:
id: RMGR-WP-0004-T08
status: wait
priority: high
state_hub_task_id: "fc7395b0-e86d-4231-baac-ea7fa5dc2174"
```
Implement `ADR-007` decision 1 as a repository standard: `PREFIX-WP-NNNN` names
@ -239,6 +240,7 @@ non-unique identifier would manufacture UUID collisions rather than remove them.
id: RMGR-WP-0004-T09
status: wait
priority: high
state_hub_task_id: "3a6ba954-4e35-4b2d-8dff-d3db84ed832c"
```
Three prefixes are shared across seven repositories, each a single number line

View file

@ -16,6 +16,7 @@ related:
- STATE-WP-0080
- STATE-WP-0068
- CFED-WP-0001
state_hub_workstream_id: "7ddb5421-d960-4a3c-94b1-40b6c96abfab"
---
# Registrar consolidation and deterministic hub identifiers
@ -51,6 +52,7 @@ model; a file carrying a hub's private key is the file holding hub state.
id: RMGR-WP-0005-T01
status: todo
priority: high
state_hub_task_id: "b57a6882-280d-4f0a-9c73-899843dfc3d3"
```
Until derivation ships, exactly one instance may write hub identifiers into
@ -73,6 +75,7 @@ removes the need for the trade.
id: RMGR-WP-0005-T02
status: wait
priority: medium
state_hub_task_id: "8e679ddb-9845-457f-8672-1fd4b7455e7b"
```
Records minted by non-registrar instances before T01 need reconciliation. Known
@ -94,6 +97,7 @@ before then.
id: RMGR-WP-0005-T03
status: wait
priority: high
state_hub_task_id: "28067729-498d-4f47-89bd-5b9718e999c7"
```
Replace minted UUIDs with UUIDv5 derived from the globally unique
@ -116,6 +120,7 @@ same UUID for different workplans. Uniqueness must be enforced first.
id: RMGR-WP-0005-T04
status: wait
priority: high
state_hub_task_id: "503a23a9-ede1-4cf1-bd32-e9669b84ce58"
```
One-time pass over the 758 files carrying hub identifiers: compute the derived
@ -135,6 +140,7 @@ value, update the database to match, and write the file.
id: RMGR-WP-0005-T05
status: wait
priority: low
state_hub_task_id: "3946d1fc-2137-4d7b-a400-29b447ca83de"
```
Once derivation is live fleet-wide, remove the single-writer restriction from
@ -150,6 +156,7 @@ byte-identical writeback, and neither creates a duplicate record.
id: RMGR-WP-0005-T07
status: wait
priority: high
state_hub_task_id: "70f83359-0b61-4dc0-83b0-33f289b64e83"
```
Implement `ADR-010` decisions 13: the central hub on railiance is authoritative
@ -171,6 +178,7 @@ portion of the divergence is redundant and needs no merge — only a rebuild.
id: RMGR-WP-0005-T08
status: wait
priority: high
state_hub_task_id: "241cf058-2f3e-4d49-8cc9-5c714be4a1cf"
```
Implement `ADR-010` decision 4. The two kinds need opposite handling:
@ -197,6 +205,7 @@ reduces its scope.
id: RMGR-WP-0005-T09
status: wait
priority: high
state_hub_task_id: "d40cc4a8-4280-4940-ac1d-dc1049f1b678"
```
28 records exist in the local instance with no backing file. They are the only
@ -227,6 +236,7 @@ register collision)` in `repo-manager`, independent corroboration of the
id: RMGR-WP-0005-T10
status: wait
priority: medium
state_hub_task_id: "15f0f167-a8d0-4d5c-8576-3e93b1e8792f"
```
Implement `ADR-010` decision 7. The retirement splits one hub into several, which
@ -245,6 +255,7 @@ Coordinate with the hub-extension architecture in
id: RMGR-WP-0005-T06
status: wait
priority: medium
state_hub_task_id: "d440d59c-f78e-4752-84c7-f3d5fdf7d3c3"
```
Implement `ADR-007` decision 3: an automated normalization pass may report

View file

@ -0,0 +1,147 @@
---
id: RMGR-WP-0006
type: workplan
title: "Railiance managed-app wrapper setup"
domain: infotech
repo: repo-manager
status: active
owner: grok
topic_slug: infotech
created: "2026-08-18"
updated: "2026-08-18"
parent_project: prj-state-hub-retirement
related:
- RMGR-WP-0004
state_hub_workstream_id: "0d6d2ebf-4b74-4908-995c-17b8987ede05"
---
# Railiance managed-app wrapper setup
## Goal
Give Repo Manager a governed path to set up `rapp-<app>` repositories so
ordinary applications (starting with `user-engine`) can run on Railiance
as managed workloads: declared, packaged, bound to a rail and reef, and
operable without copying charts into `railiance-apps`.
This specializes `RMGR-WP-0004` scaffolding for the `rapp-*` family. It
does not move family schemas out of `railiance-master`, and it does not
operate the cluster.
Practice and remaining work are structured in
[`docs/RailianceAppDeploymentGuide.md`](../docs/RailianceAppDeploymentGuide.md).
## Record the wrapper best-practice guide
```task
id: RMGR-WP-0006-T01
status: done
priority: high
state_hub_task_id: "cb1e2e4e-9aef-4669-89f6-86ea8512b7d2"
```
Survey live rails (`rail-kubernetes`, `rail-knative`), reefs
(`reef-railiance`, `reef-storage`), existing rapps, and the `user-engine`
/ `rapp-user-engine` pair. Write
`docs/RailianceAppDeploymentGuide.md` as the best-practice shape and the
work breakdown for later scaffolder phases.
**Result (2026-08-18):** guide written. Pilot target is
`rapp-user-engine` (empty stub). Copy targets are `rapp-policy-nexus` and
`rapp-openbao`, not drifted `rapp-qonto`.
## Bootstrap the user-engine wrapper to `declared`
```task
id: RMGR-WP-0006-T02
status: done
priority: high
state_hub_task_id: "f0e53954-403a-4e2c-b39f-0bab00eed3cc"
```
Fill `rapp-user-engine` with the bootstrap baseline and a schema-valid
`declarations/rapp.yaml` at `readiness_state: declared` (or `draft` if
image/consumer pins are still missing). Follow the worked example in the
guide. Validate with
`railiance-master/tools/validate-family-declarations.py --root ..`.
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: done
priority: high
state_hub_task_id: "3ba6828e-7514-4f4d-a1d3-54134e039f55"
```
Absorb `net-kingdom/sso-mfa/k8s/user-engine/` into
`rapp-user-engine/manifests/` (or a Helm chart only if a rewrite is
justified separately). Add Makefile `server-dry-run` / `deploy` /
`verify-live` / `rollback DIGEST=…` and a runtime-contract doc. Leave a
pointer in NetKingdom so there is one apply path.
`make server-dry-run` must pass. No production apply in this task.
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: done
priority: medium
state_hub_task_id: "0c2846d9-5db2-4b97-becd-0367b5737188"
```
Draft the `rapp-postgres` consumer and secret-reference list for
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: done
priority: high
state_hub_task_id: "6cf89776-ff31-4dd1-aa9a-39474fc387aa"
```
Turn P1P4 from the guide into Repo Manager commands (`rapp init`,
`rapp skeleton`, `rapp validate`) that are idempotent, secret-free, and
refuse to invent rails, reefs, or package types. Pilot by regenerating
the user-engine wrapper (or the next engine stub) without hand-editing
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;
wrap them only after the user-engine path validates.
- `rapp-vergabe-teilnahme` extraction stays a `railiance-apps` residual
(ADR-0003). This workplan does not close that item.
- Schema enum `helm-managed-application` would be a `railiance-master`
change if the platform-service name proves misleading.