railiance-master/schemas/README.md
codex 4a664533d3 feat: implement RMASTER-WP-0023 private-by-default exposure
Add the exposure contract, additive family schema fields, validator
checks and fixtures, the reef-railiance exception snapshot, and
routed intakes. Enforcement stays in the owning repos.
2026-08-15 20:08:37 +02:00

197 lines
9.5 KiB
Markdown

# Railiance family declaration schemas
Normative schemas for the repo-family declaration files. `docs/repo-family-bootstrap-contract.md`
names the minimum fields; these schemas define their shapes and are the
authority where the two disagree.
| Family | Declaration path | Schema |
|---|---|---|
| `rapp-*` | `declarations/rapp.yaml` | `rapp.schema.json` |
| `rail-*` | `declarations/rail.yaml` | `rail.schema.json` |
| `reef-*` | `declarations/reef.yaml` | `reef.schema.json` |
Written under `RMASTER-WP-0021` from the drift survey routed by
`railiance-platform` under its `RAILIANCE-WP-0015` (State Hub messages
`04c776c4`, `f88f938d`).
## The two cardinalities
These are the load-bearing rules. Everything the validator can usefully check
follows from keeping them distinct.
- **Repos to rapps is many-to-many.** A repo may contribute to more than one
rapp — a shared library, or a service that is a member of one bundle and a
dependency of another.
- **Deployables to rapps is one-to-one.** Every running deployable has exactly
one rapp that owns its rollout.
The second is what makes the coverage question well-formed: *does every live
deployable belong to exactly one rapp?* That single check catches stale reef
bindings, unpackaged live workloads, and the workload-count gap at once. It is
what turns this schema from a one-shot lint into a standing control loop.
Rapp bounded context is therefore its own dimension, derived neither from
Forgejo organizations nor from State Hub domains. A repo lives in exactly one
Forgejo org, so `org:repo` is one-to-many, and a many-to-many grouping cannot
be derived from a one-to-many one. Domains fail in both directions. See
`docs/adr/ADR-0007-rapp-declaration-contract.md` and decision `d07ee5f9`.
## Normative choices, and what they cost
The three live rapp declarations expressed their rollout, smoke and rollback
contracts three mutually unreadable ways. The schema picks one shape. Where it
picks, it says so and it says who pays.
**`default_mode` is kept** — all three declarations already carried it. No cost.
**`supported_commands` becomes `commands`** in all three contracts. Mechanical
rename for `rapp-openbao` and `rapp-postgres`.
**`smoke_contract.required` is required, and `commands` is optional.** Taken
from `rapp-qonto`. This is the one migration with real cost: the platform rapps
declare only commands, so their owners must name the outcomes those commands
establish. That cost is the point. A command list records what was run; a
required list records what must be true. Only the second can be checked by
anything other than the person who ran it.
**`rollback_contract.order` is required.** Taken from `rapp-qonto`. Recoverable
from the command sequences the platform rapps already document, so the cost is
low. `direct_kubernetes_fallback` generalizes to `fallback`.
**`workload_identity.name` is the workload, never the repo**, and the schema
forbids the `rapp-` prefix on it outright. `rapp-qonto` currently sets it to
`rapp-qonto`; the correct value is `qonto`.
**`contract_version`, `readiness_state`, `data_classification` and
`criticality` are required.** All four exist only in `rapp-qonto` today. The
first two are promoted because the rail family has carried them consistently in
both rails and has not drifted — that contrast is the whole argument for
schema. The second two are promoted because a package whose classification and
criticality are unstated cannot be admitted to a reef on evidence.
**`consumer_contract` is schema-defined but optional.** Only `rapp-postgres`
provisions isolated units to other services. Requiring it everywhere would be
false precision.
## Open question: what may `ownership_repo` name?
Currently `rapp-openbao` and `rapp-postgres` name `railiance-platform`, a layer
repo, and `rapp-qonto` names `qonto-assistant`, an application repo.
The schema admits **both**, requiring only that there be exactly one and that
it not be the rapp repo itself. This is deliberately the permissive reading,
taken so the schema does not silently invalidate two of three live declarations
on a question the survey cannot settle.
The tighter alternative is that `ownership_repo` must name the repo whose team
is accountable for the workload in production, which would likely disqualify a
layer repo that owns dozens of unrelated workloads. That is a modelling call
about where accountability sits, not a lint, and it belongs to the architecture
owner. Until it is made, treat the permissive rule as provisional.
## Validator-only constraints
JSON Schema cannot express these; `tools/validate-family-declarations.py` does:
- `primary_rail` must appear in `supported_rails` (rapp) or, when set, in `hosted_rails` (reef). A reef may omit both when it hosts no rail.
- reef `bound_rapps`, if present, must match the projection of `rapp.bound_reefs`
- a rapp `ownership_repo` must not be the rapp repo itself
- declared member repos, named rails, and named reefs must resolve on disk
- a deployable name may belong to at most one rapp
- `exposure.posture: public` on a rapp requires `exposure.grant`,
`binding_admission: production-approved`, and a bound reef that has
granted a public surface (ADR-0008)
- `exposure.posture: public` on a rail requires `exposure.grant` and
`readiness_state: production-approved`
- `exposure.posture: public` on a reef requires `exposure.grants`; a
provider-delegated reef (no rail) cannot be public
- grant port `6443` is never legal
- omitted `exposure` means `private`; the field is additive and must
not be confused with rapp `data_classification: public`
## Rail schema (T04)
`rail.schema.json` codifies the two live rails. It does not tighten the family
beyond current practice.
Required on every rail: `kind`, `rail_id`, `repo`, `ownership_repo`,
`contract_version`, `composition_kind`, `execution_architecture`,
`readiness_state`, `required_substrate_capabilities`, `supported_rollout_modes`.
`composition_kind: derived` additionally requires `base_rail`,
`base_rail_contract`, `inherited_semantics`, `overridden_semantics`, and
`compatibility_constraints`. A `base` rail must not carry those fields.
`required_substrate_capabilities` is the live name. The bootstrap contract's
older `substrate_prerequisites` label is retired; do not reintroduce it.
Rail `readiness_state` uses the composition-contract vocabulary (`declared`,
`installed`, `verified`, `production-approved`, `deprecated`). That is
deliberately not the rapp vocabulary and not the reef `lifecycle_state`.
Validated 2026-08-13 against both live rails with `jsonschema` 4.10.3: both
conform. That is the intended contrast with the rapp family.
## Reef schema (T04)
`reef.schema.json` codifies the live `reef-railiance` declaration, with one
normative change: `bound_rapps` is no longer a hand-maintained required field.
Required on every reef: `kind`, `reef_id`, `repo`, `ownership_repo`,
`substrate_kind`, `lifecycle_state`, `criticality`, `current_members`.
`primary_rail` and `hosted_rails` are required only when the reef hosts
rails. `current_members` may be empty on a planned provider-delegated
reef.
`bound_rapps` is optional. Its source of truth is `rapp.bound_reefs`. The
validator inverts that field. If a reef still lists `bound_rapps`, the list
must match the projection exactly.
**Migration for `reef-railiance`.** The current list is `[rapp-qonto]`. That
is already stale: `rapp-openbao` and `rapp-postgres` are live on Railiance01.
Until those three declarations name `reef-railiance` in `bound_reefs`, the
derived projection is empty and a hand-list of anything fails. The owning
repo should delete `bound_rapps` now, or replace it with the derived set once
the rapps name the reef. Either is a one-line change; leaving the stale list
is the failure mode this check exists to catch.
`rapp.bound_reefs` was added as a required field in the same task. An empty
list is legal and means the package is declared but not yet placed.
Validated 2026-08-13 against the live reef: it conforms as a document. The
`bound_rapps` value does not survive the derivation check, which is the
intended result.
## Runtime dependencies name capabilities, not resources
`runtime_dependencies` is a list of capability names (`s3-backup-target`,
`openbao-database-secrets-engine`). The schema does not, and must not, say
who provisions the thing that satisfies a name.
The postgres backup object store is the worked example. `rapp-postgres`
declares `s3-backup-target` as a consumed capability. The bucket is a
`resource-control` resource (`RESOURCE-WP-0002`). The credential is
`railiance-platform` OpenBao custody (`Secret platform-pg-backup-s3`).
Purchase itself is the escalated human decision `9c21c0e0`. Until
`resource-control` hands an endpoint to `railiance-platform`,
`make postgres-backup-deploy` stays fail-closed on purpose. That is not a
hole in `rapp-postgres` ownership and not a hole in this schema.
## Running the validator
```bash
tools/validate-family-declarations.py
tools/validate-family-declarations.py --root ..
tools/validate-family-declarations.py --inventory path/to/live-deployables.json
tools/validate-family-declarations.py --self-test
```
`--inventory` is the T06 coverage check. This repo does not query a cluster.
The file is produced by an implementation repo and must be a JSON mapping
with a `deployables` list of `{name, namespace?, kind?}` (or bare name
strings). Live names that no rapp claims are reported as wave-2 worklist
items, not suppressed.
The validator is `tools/validate-family-declarations.py` (RMASTER-WP-0021-T05).
It is runnable standalone now. Calling it from `fix-consistency` waits on
`the-custodian` admitting the family prefixes; that sequencing is not ours.