RPF-WP-0018 T01-T06: publish S3 posture, placement policy, quotas, ADR surface

T01 docs/tenancy-posture.md - vector set per service rather than one repo
vector, with the provider-versus-consumer finding routed to net-kingdom.
T02/T03/T06 docs/placement-policy.md - accepts placement ownership scoped
to rule-here/number-there, records a placement owner per workload, reports
the latency-critical + batch co-residency on platform-pg, marks the
connection-ceiling trigger unmonitored pending railiance-telemetry, and
answers the retention floor/ceiling question.
T04 s3-consumer-interfaces 1.1.0 - quota disclosure per SS10.2. Surfaces
that apps-pg has no backup, no resource limits and no tuned parameters.
T05 docs/adr/ created with a mandatory-frontmatter convention and the
first three ADRs. This repo previously held none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
codex 2026-08-17 21:55:11 +02:00
parent b83194741d
commit e7e4e33bb8
9 changed files with 809 additions and 12 deletions

View file

@ -20,6 +20,7 @@
| workplan | RAILIANCE-WP-0016 | finished | — | workplans/RAILIANCE-WP-0016-apps-pg-resource-evidence.md |
| workplan | RAILIANCE-WP-0016 | finished | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
| workplan | RAILIANCE-WP-0017 | finished | — | workplans/RAILIANCE-WP-0017-consumption-mode-enforcement.md |
| workplan | RPF-WP-0018 | proposed | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
| task | RAILIANCE-WP-0005-T01 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
| task | RAILIANCE-WP-0005-T02 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
| task | RAILIANCE-WP-0005-T03 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
@ -80,3 +81,10 @@
| task | RAILIANCE-WP-0016-T04 | done | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
| task | RAILIANCE-WP-0016-T05 | done | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
| task | RAILIANCE-WP-0017-T01 | done | — | workplans/RAILIANCE-WP-0017-consumption-mode-enforcement.md |
| task | RPF-WP-0018-T01 | todo | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
| task | RPF-WP-0018-T02 | todo | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
| task | RPF-WP-0018-T03 | todo | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
| task | RPF-WP-0018-T04 | todo | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
| task | RPF-WP-0018-T05 | todo | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
| task | RPF-WP-0018-T06 | todo | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
| task | RPF-WP-0018-T07 | todo | — | workplans/RPF-WP-0018-policy-surface-alignment.md |

View file

@ -0,0 +1,67 @@
---
title: "ADR-0001 — S3 owns platform services, not the substrate beneath them"
status: accepted
revision: "1.0"
owner: railiance-platform
date: "2026-08-17"
last_reviewed: "2026-08-17"
review_interval: 12m
---
# ADR-0001 — S3 owns platform services, not the substrate beneath them
## Context
`railiance-platform` is S3 on the OAS Stack: the shared services several
applications depend on — PostgreSQL, secrets, cache, object storage. The layers
around it are S1 `railiance-infra` (OS and host concerns), S2
`railiance-cluster` (Kubernetes runtime, ingress), S4 `railiance-enablement`
(tooling and CI), S5 `railiance-apps` (workloads).
This boundary has been stated in `SCOPE.md` and in ADR-003 of `railiance-infra`
since the five-repo split, and it has been tested twice. `RAIL-PL-WP-0001`
existed to extract platform services *out* of S2 subcharts. On 2026-08-17
`POLICY-NEXUS-WP-0001` assigned this repo "the substrate — DNS, TLS, ingress,
hosting" for `policy.coulomb.social`, which would move the boundary back the
other way.
The pressure is predictable and will recur: S3 is the layer that looks like it
owns infrastructure, because it owns things that feel infrastructural. Recording
the rule as an ADR rather than as a line in `SCOPE.md` gives future requests
something to be answered against.
## Decision
**S3 owns shared platform *services* and the interfaces consumers bind to. It
does not own DNS, TLS, Kubernetes ingress, host configuration, or CI.** A
request that would place any of those here is redirected to S1, S2 or S4 rather
than absorbed, even when this repo is the closest available owner and even when
absorbing it would be faster.
Where S3 has a genuine part in such a request — an object-storage bucket, a
credential lane, a database — it supplies that part and co-signs the rest. It
does not become the owner of the whole by being the first repo asked.
## Consequences
- `POLICY-NEXUS-WP-0001` T04 needs splitting: ingress and TLS to S2, DNS to S1
or the reef boundary, storage and credentials here. Routed as `RPF-WP-0018`
finding F1.
- A request this repo declines does not thereby become nobody's. Declining
carries an obligation to name the owner, which is why F1 names two.
- The rule cuts against this repo's convenience in the common case. That is
what makes it a rule rather than a preference.
- `rapp-openbao` and `rapp-postgres` are the same principle applied inward:
the deployable package left, custody and policy stayed. See
`docs/rapp-openbao-boundary.md` and `docs/rapp-postgres-boundary.md`.
## Alternatives considered
**Accept the substrate assignment as written.** Fastest, and the requester had
already resolved it with the operator. Rejected: it re-imports the coupling
`RAIL-PL-WP-0001` spent a workplan removing, and a boundary that yields to
whoever asks most recently is not a boundary.
**Own ingress for S3-adjacent services only.** A narrower version, and it fails
on the first argument about what counts as adjacent. The line has to be drawn
where it can be checked.

View file

@ -0,0 +1,81 @@
---
title: "ADR-0002 — S3 owns the placement rule; the package repo owns the number"
status: proposed
revision: "1.0"
owner: railiance-platform
date: "2026-08-17"
last_reviewed: "2026-08-17"
review_interval: 6m
---
# ADR-0002 — S3 owns the placement rule; the package repo owns the number
## Context
`net-kingdom/canon/standards/tenancy-posture_v0.1.md` §8.2 proposes that
database placement policy — dedicated versus shared, and when that changes —
be owned by `railiance-platform`, co-signed by `adaptive-pricing`.
`tenant-engine` raised the same gap independently on 2026-08-16: both patterns
are live on railiance01, neither is written down, and each new service copies
whichever neighbour it looked at.
The complication is that this repo no longer holds the specs. `RAILIANCE-WP-0012`
and `RAILIANCE-WP-0015` moved the deployable surface to the `rapp-*` repos.
`platform-pg`'s `instances`, `max_connections`, memory limit and retention are
`rapp-postgres`'s cluster CR. Tenancy Posture §19.8 nonetheless asks *this repo*
for `platform-pg`'s declared maximum size — a question one hop from where its
answer lives.
Accepting ownership without stating this would produce either an answer we
cannot substantiate or a quiet non-answer.
## Decision
**Accept placement ownership, split along rule and number.**
*This repo owns the rule:* the default placement level, what overrides it, who
is accountable for each workload's placement, that a shared cluster must publish
a ceiling and an overflow target before its third consumer, that retention has a
floor and a ceiling, and that a consumer's erasure horizon on shared substrate
is disclosed rather than inherited silently.
*The package repo owns the number:* what the ceiling actually is, which resource
binds first, what the retention values are, and the validator that enforces them
where the CR lives.
**Placement ownership per workload sits with the consuming repo; this repo
co-signs.** The consumer knows its latency requirement, erasure horizon and
compliance position; S3 knows the substrate. A placement change needs both.
Putting the decision solely here would recreate the problem §8.2 exists to
avoid — an operations-shaped repo holding a commercial decision alone.
`adaptive-pricing` co-signature is requested, not assumed.
## Consequences
- `docs/placement-policy.md` is the operative document; this ADR is the
decision behind it.
- §19.8 is answered by obligation rather than by number: Rule P-4.1 requires
`platform-pg` and `apps-pg` to declare a ceiling before their third consumer.
Both are at two consumers.
- This repo owes `apps-pg`'s ceiling under its own rule. A policy whose author
is exempt is not a policy.
- §8.1 requires triggers to be *monitored*. This repo has no monitoring plane,
so the connection-and-memory-ceiling trigger is recorded `unmonitored`
pending `railiance-telemetry`. Declaring a ceiling is a partial substitute —
a written threshold is checkable by a human at provisioning, which beats a
limit discovered by an OOM kill.
- If `adaptive-pricing` declines, this ADR is revised to record single
ownership explicitly rather than leaving an unsigned co-signature line.
## Alternatives considered
**Decline ownership; route it to `rapp-postgres`.** They hold the specs and the
operational knowledge. Rejected: placement is a cross-cluster question and
`rapp-postgres` owns one package. A policy owned by one substrate's operator
cannot govern movement between substrates.
**Accept whole, including the numbers.** Rejected: it would either re-import
the deployable surface this repo deliberately gave up, or produce numbers
restated here that drift from the CR — a second source of truth for exactly the
values a consumer must be able to trust.

View file

@ -0,0 +1,67 @@
---
title: "ADR-0003 — Decisions that bind others live in docs/adr, not only in the State Hub"
status: accepted
revision: "1.0"
owner: railiance-platform
date: "2026-08-17"
last_reviewed: "2026-08-17"
review_interval: 12m
---
# ADR-0003 — Decisions that bind others live in `docs/adr`, not only in the State Hub
## Context
This repo recorded decisions with the State Hub's `record_decision()` and wrote
governing content as prose in `docs/` — 24 files on 2026-08-17, none carrying a
status, owner, revision or review date. It held no ADRs at all.
Two things made that a defect rather than a style.
**The hub is a read model.** The estate's standing rule is that local files are
the source of truth and the hub reflects them. A decision that exists only as a
hub record inverts that for the one class of content where it matters most.
**`policy-nexus` publishes canon and ADRs only**, globbing `docs/adr/*.md` from
each repo. A repo with no ADRs is invisible to the estate's publication surface
no matter how much governing content it holds. This repo owns S3 boundary
rules, the credential-lane model and the consumption-mode gate — all binding on
other repos, all unpublishable.
The gap was structural, not an oversight: nothing in the session protocol ever
told anyone to write an ADR here.
## Decision
**A decision that binds anyone outside this repo is recorded as an ADR in
`docs/adr/`, with the frontmatter in `docs/adr/README.md`, before or alongside
any hub record.** The hub record points at the ADR; it does not replace it.
Runbooks and procedures stay as prose in `docs/` and stay unpublished. The test
is not importance — several runbooks are critical — but whether the document
*decides* something for someone else.
## Consequences
- `docs/adr/` exists, with a convention, a template and this repo's first three
ADRs.
- Existing prose is not converted wholesale. Content is promoted when it is
next touched, or when a consumer needs to cite it.
- `last_reviewed` and `review_interval` are mandatory here, which is stricter
than the estate norm — of 69 ADRs across 19 repos on 2026-08-17, 21 had YAML
frontmatter and 2 had any notion of a review date. `POLICY-NEXUS-WP-0001` T05
cannot compute staleness without those fields and must not invent them, since
inventing them would make the publication a second source of truth, which is
that workplan's own top risk. Routed as `RPF-WP-0018` finding F5.
- Session protocol guidance should follow, so the next agent writes an ADR
without being told. Not done in this ADR.
## Alternatives considered
**Keep decisions in the hub and have `policy-nexus` read it.** Rejected on
both sides: it would make a read model authoritative, and it would give the
publication surface a source that no repo can diff or review.
**Add frontmatter to all 24 existing `docs/` files.** Rejected. Most are
runbooks that should not be published, and stamping them with a status would
assert a decision that was never made.

69
docs/adr/README.md Normal file
View file

@ -0,0 +1,69 @@
---
title: "ADR convention for railiance-platform"
status: accepted
revision: "1.0"
owner: railiance-platform
date: "2026-08-17"
last_reviewed: "2026-08-17"
review_interval: 12m
---
# ADRs in this repo
Work record: `RPF-WP-0018` T05.
Until 2026-08-17 this repo held **no ADRs**. Its decisions lived either as
prose in `docs/` without status or ownership, or in the State Hub via
`record_decision()`. Both are wrong homes, for the same reason: the hub is a
read model and prose is not addressable as a decision. `policy-nexus`
publishes canon and ADRs only, so a repo with no ADRs publishes nothing —
regardless of how much governing content it holds. See `ADR-0003`.
## What gets an ADR here
A decision gets an ADR when it **binds someone other than the person who made
it** — another repo, a future maintainer, or a consumer of an S3 interface.
| Content | Home | Published |
| --- | --- | --- |
| A decision binding others | `docs/adr/` | yes |
| A procedure someone follows | `docs/*.md` runbook | no |
| Live interface values | `docs/s3-consumer-interfaces.md` | no |
| Work sequencing | `workplans/` | no |
Runbooks stay prose deliberately. A site that publishes everything publishes
nothing in particular, and `POLICY-NEXUS-WP-0001` T03 already draws that line.
## Frontmatter is mandatory
Every ADR carries the YAML block below. The fields are not decoration: they
are exactly what `POLICY-NEXUS-WP-0001` T02 and T05 consume to render a page
and to mark it stale.
```yaml
---
title: "ADR-NNNN — short imperative statement of the decision"
status: proposed | accepted | superseded | withdrawn
revision: "1.0"
owner: railiance-platform
date: "YYYY-MM-DD" # when decided
last_reviewed: "YYYY-MM-DD"
review_interval: 6m | 12m # after which the page shows as stale
supersedes: ADR-NNNN # optional
superseded_by: ADR-NNNN # optional
---
```
`last_reviewed` and `review_interval` are the two fields the rest of the estate
mostly lacks — of 69 ADRs across 19 repos on 2026-08-17, 2 carried any notion
of a review date. A publication surface cannot compute staleness without them
and must not invent them, so they are required here.
Numbering is `ADR-NNNN`, four digits, never reused. A superseded ADR keeps its
number and its file; it gains `superseded_by` and its status changes. Nothing
is deleted — the published URL must keep resolving.
## Sections
Context · Decision · Consequences · Alternatives considered. Keep the decision
statement to one paragraph a reader can quote.

224
docs/placement-policy.md Normal file
View file

@ -0,0 +1,224 @@
# S3 database placement policy
Work record: `RPF-WP-0018` T02, T03, T06
Effective: 2026-08-17
Framework: `net-kingdom/canon/standards/tenancy-posture_v0.1.md` §4.4, §8
Contract version of this document: `1.0.0`
Status: **provisional** — binding on this repo now; formally conditional on
the framework reaching `accepted` and on `adaptive-pricing` co-signature.
Answers `tenant-engine`'s 2026-08-16 request ("a default and the conditions
that override it, so migrations stop being decided by whichever neighbour was
consulted") and Tenancy Posture §19.2.
## 1. Ownership, and its limit
§8.2 proposes placement ownership to `railiance-platform` co-signed by
`adaptive-pricing`. **Accepted, with the scope stated:**
> **This repo owns the rule. The package repo owns the number.**
We decide *that* a cluster must publish a ceiling, what happens when it is
reached, what the default placement is, and who is accountable for each
workload's placement. We do not decide what `platform-pg`'s `max_connections`
is — that is `rapp-postgres`'s cluster CR, and `RAILIANCE-WP-0012` /
`RAILIANCE-WP-0015` moved that surface there deliberately.
This matters for §19.8, which asks *this repo* for `platform-pg`'s declared
maximum size. That question is misrouted by one hop. `rapp-postgres`'s own
analysis makes the case better than we could: memory probably binds before
connections do, at roughly 10MB per backend against a 1Gi limit, and that is
an observation only the operator of the package can make. Our obligation under
this policy is §4 below — that a ceiling and an overflow target **exist and
are published**. Theirs is what they are.
`adaptive-pricing` co-signature is **requested, not assumed**. The reasoning in
§8.2 is sound: tenancy model selection is commercial as much as technical. If
they decline, this document records single ownership and says so plainly rather
than leaving a co-signature line nobody signed.
## 2. The default, and what overrides it
The ladder is `net-kingdom` canon and is not restated here. Adopted by
reference: P0P4 per Tenancy Posture §4.4.
**Default: P1 — database per consumer, shared cluster** — for platform
services. P3 for client-facing business apps, per
`business-app-service-contract` §1.2. Both already ratified as Decision 4.4.1;
this document adopts rather than re-decides them.
**P0 is not a sanctioned target.** It is the state repos arrive in. A consumer
sharing a database with another consumer is a defect with a migration owed,
not a placement.
**The distinction `tenant-engine` asked us to preserve, preserved:** sharing a
*cluster* is a capacity decision and is cheap to undo — create target, dump,
restore, swap the `secretKeyRef`, restart. Sharing a *database* entangles
schemas and makes relocation a schema merge. This policy says "shared" only
ever in the first sense. Any consumer on an S3-custody cluster owns a whole
database.
**Portability is an expectation, not a courtesy.** A consumer connects by
injected URL only; no cluster, namespace, host or database name in source.
`tenant-engine` adopted this unprompted and it is hereby the rule, because a
placement policy that cannot be executed is a preference.
## 3. Placement owner per workload
§19.2 asks for a recorded owner per workload so that "should this be dedicated"
has an answer that is not archaeology. The **placement owner** decides
placement changes for that workload and is accountable for watching its
triggers. It is not the same role as the operator of the cluster.
| Workload | Substrate | Provided level | Placement owner | Co-signer |
| --- | --- | --- | --- | --- |
| `audit-core` | `platform-pg` | P1 | `audit-core` | this repo |
| `tenant-engine` | `platform-pg` | P1 | `tenant-engine` | this repo |
| `user-engine` | `user-engine-pg` | P2 | `net-kingdom` | this repo |
| `target-revenue` | `target-revenue-pg` | P2 | `target-revenue` | this repo |
| `forgejo` | `forgejo-db` | P2 | this repo | — |
| `vergabe` | `apps-pg` | P1 | `vergabe-teilnahme` | this repo |
| `coulomb_social` | `apps-pg` | P1 | `coulomb-social` | this repo |
**The consuming repo is the placement owner; this repo co-signs.** The
consumer knows its latency requirement, its erasure horizon and its compliance
position; we know the substrate. Neither can decide alone, and putting the
decision solely here would recreate the "ops repo holds a commercial decision"
problem §8.2 exists to avoid. A placement change needs both signatures.
New consumers record a placement owner at provisioning. A provisioning request
without one is incomplete.
## 4. Ceiling and overflow — the §19.8 obligation, generalised
**Rule P-4.1.** Every shared cluster under S3 policy MUST publish a **declared
maximum consumer count** and a **named overflow target**, both before its
third consumer is provisioned.
**Rule P-4.2.** The ceiling is stated as the *binding* constraint with its
number, not as a capacity boast. Where more than one resource could bind
first — connections versus memory is the live case — the declaration names
which is expected to bind and why. A ceiling derived from the resource that
fails *cleanly* while another fails *catastrophically* is a mis-stated ceiling:
connection exhaustion errors and is recoverable, memory pressure OOM-kills and
degrades every co-resident at once.
**Rule P-4.3.** The overflow target is a named substrate, not "we will build
one". "Provision a second cluster" is an answer; "revisit at the time" is not.
**Status against these rules today:**
| Cluster | Ceiling declared | Overflow target | Standing |
| --- | --- | --- | --- |
| `platform-pg` | not yet — `rapp-postgres` owes it | not yet | **2 consumers of ~6 practical; owed before the 3rd** |
| `apps-pg` | not yet — this repo owes it | not yet | 2 consumers; owed before the 3rd |
| `forgejo-db` | n/a — single consumer | n/a | — |
Both live shared clusters are one consumer away from the trigger. This repo
owes `apps-pg`'s ceiling under its own rule, which is the correct asymmetry: a
policy whose author is exempt is not a policy.
## 5. Triggers, and who is watching them
§8.1: triggers MUST be **monitored**, not merely recorded. *"A trigger in a
YAML comment nobody re-reads is documentation, not control."*
This repo has no monitoring plane. `SCOPE.md` records that S3 emits to
`railiance-telemetry` "once the evidence plane exists — seeded 2026-08-11, not
yet implemented". Rather than claim coverage we do not have — the overclaim
§6 prohibits — each trigger below carries either a named watcher or the
literal marker `unmonitored`.
| # | Trigger (§8) | Signal that would fire it | Watcher | Cadence |
| --- | --- | --- | --- | --- |
| 1 | Noisy neighbour on a latency-critical path | Query latency regression on the consumer's own path | **consumer-observed** — the consumer notices, we do not | on incident |
| 2 | Compliance or residency requirement | Consumer declares it | placement owner | at declaration |
| 3 | Plan tier requiring a higher minimum | `adaptive-pricing` tier definition names a P floor | `adaptive-pricing` | on tier change |
| 4 | Erasure horizon no longer fits | Consumer's declared horizon < cluster retention floor | placement owner + this repo | at provisioning, then `unmonitored` |
| 5 | Connection or memory ceiling reached | Backend count vs declared ceiling; memory vs limit | **`unmonitored` — pending `railiance-telemetry`** | — |
**Trigger 5 is the one that matters and it is the one nobody is watching.**
That is the honest position. It is also why Rule P-4.1 forces the ceiling to be
declared *before the third consumer*: a threshold that is written down is at
least checkable by a human at provisioning time, which is strictly better than
a limit discovered by an OOM kill. Declaring the ceiling is a partial substitute
for monitoring it, and it is the part we can do this week.
**Closing trigger 5 belongs to `railiance-telemetry`, not to this workplan.**
Recorded here as a dependency so it is visible rather than assumed.
## 6. Service class co-residency — the §8.3.3 report
§8.3.2 declares service class as a category, never a priority. §8.3.3 requires
the platform to report which classes are co-resident, because "an unenforceable
risk that nobody can see is strictly worse than one that is stated".
| Cluster | Co-resident classes | Mismatch |
| --- | --- | --- |
| `platform-pg` | `latency-critical` (`tenant-engine`) + `batch` (`audit-core`) | **yes — reported** |
| `apps-pg` | `interactive` + `interactive` | no |
| `forgejo-db` | single consumer | n/a |
**`platform-pg` mixes `latency-critical` with `batch` today.** Under §8.3.1
co-residents are equal — community PostgreSQL has no resource governor, so
nothing prioritises `tenant-engine`'s synchronous authorization queries over
`audit-core`'s batch work. `tenant-engine` chose a 5s statement timeout
precisely because it sits on `flex-auth`'s synchronous path.
This is **reported, not remediated**. It may well still be the right call —
§8.3.2 says a recognised mismatch "may still be the right call, but it should
be a decision, not an accident of who was provisioned when". It is now a
decision. The escalation, if the mismatch bites, is P2 for `tenant-engine`, and
the known alternative — gateway-level prioritisation in a connection proxy — is
infrastructure this estate does not run and is not adopted here.
## 7. Retention floor and ceiling — the §19.9 answer
**Answered: yes to both, as a rule; the numbers belong to the package repo.**
**Rule R-7.1 — floor.** A consumer's requested `backupRetentionDays` below the
cluster's declared floor is a **validation error at provisioning**, not a quiet
disappointment. A consumer that asks for 1 day and silently gets 30 has been
told nothing true; a consumer that gets an error learns the actual constraint
at the only moment it can act on it.
**Rule R-7.2 — ceiling.** A maximum exists so no consumer can exhaust the
backup volume. Exceeding it is likewise an error, and the remedy named in the
error is P2 — a dedicated cluster whose retention is the consumer's own.
**Rule R-7.3 — the floor is a placement trigger, and this is the important
one.** Per `rapp-postgres` ADR-0002, physical backup is instance-wide, so on a
shared cluster **a consumer's erasure horizon is the maximum declared across
all co-residents**. A consumer needing a horizon shorter than that floor
**cannot have it at P1** and must move to P2 — for a reason that has nothing to
do with performance or capacity, which is exactly why it must be recorded
rather than assumed.
This makes someone else's retention choice everyone's, which is what
`tenant-engine` warned about on 2026-08-16 and was right to. Rule R-7.4
follows.
**Rule R-7.4 — inherited retention is disclosed, not inherited silently.** A
consumer joining a shared cluster is told its effective erasure horizon at
provisioning, and told again when a co-resident's change moves it. This is
§10.2 applied to retention and is implemented in
`docs/s3-consumer-interfaces.md` 1.1.0.
**Enforcement is the package repo's.** The validator lives where the CR lives.
This document states the rule; `rapp-postgres` and this repo's own `apps-pg`
onboarding enforce it.
## 8. What this policy deliberately does not do
- **It moves no workload.** Declaring where things are is separate from
changing where they are. Nothing above is a migration instruction.
- **It adds no QoS axis.** §19.12 resolved that: we could not enforce one.
- **It does not decide identity-provider placement** (realm-per-tenant versus
Organizations, §19.7). That is `key-cape`'s, on a different substrate.
- **It claims no monitoring.** See §5.
## Review interval
Six months, or on: a new shared-cluster consumer, a co-residency class change,
`railiance-telemetry` going live, or the framework reaching `accepted`. Next
review due **2027-02-17**.

View file

@ -1,8 +1,8 @@
# S3 consumer service interfaces
Work record: `RAILIANCE-WP-0016` item 17
Date: 2026-08-15
Contract version of this index: `1.0.0`
Work record: `RAILIANCE-WP-0016` item 17; `RPF-WP-0018` T04
Date: 2026-08-15, revised 2026-08-17
Contract version of this index: `1.1.0`
Consumers bind to these contracts, not to Helm chart names or pod topology.
Changing an engine behind a versioned interface does not bump the interface
@ -27,6 +27,26 @@ unless the consumer-visible fields change.
`postgresql-ha-pgpool.platform.svc.cluster.local` is **not** an
interface. That stack is retired.
## Quota disclosure
Added in `1.1.0` — additive, so no consumer contract breaks.
Tenancy Posture §10.2: *quotas are disclosed, not discovered.* Every consumer
is told at provisioning what the platform will refuse to do for it, and told
again when that changes. **A consumer learning its statement timeout by hitting
it in production is a disclosure failure, not a consumer bug.**
The obligation this places on S3 is a standing one: a change to any value in
the tables below is announced to bound consumers before it takes effect. The
values themselves are read from the cluster CR and are the package owner's to
set — this index publishes them, it does not choose them.
Retention disclosure carries the additional §7 rule from
`docs/placement-policy.md`: on a shared cluster the **effective erasure
horizon is the maximum declared across co-residents**, not the value the
consumer requested. That number is disclosed, and it moves when a co-resident
changes.
## `apps-pg.v1`
Consumer supplies: app name, namespace, database name, role name. Platform
@ -35,6 +55,30 @@ provisions the role and CNPG `Database`, labels the namespace
RW endpoint: `apps-pg-rw.databases.svc.cluster.local:5432`. Bootstrap
role `apps_admin` is not a consumer credential.
**Quotas and limits** — read from `helm/apps-pg-cluster.yaml`, 2026-08-17:
| Property | Value | Note |
| --- | --- | --- |
| `max_connections` | **PostgreSQL default (100)** | Not tuned in the CR. Not partitioned per consumer — there is no per-consumer connection allowance, so one consumer can exhaust the cluster. |
| `statement_timeout` | **none** | Not set. A runaway query runs until it finishes. |
| `idle_in_transaction_session_timeout` | **none** | Not set. |
| CPU / memory limits | **none declared** | The pod is BestEffort QoS and is the first thing evicted under node pressure. |
| Instances | 1 | No HA. Restart recovery only, per the single-node rail. |
| Storage | 10Gi | |
| Backup | **none configured** | No `barmanObjectStore`, no `retentionPolicy`. |
| Effective erasure horizon | **undefined** | Follows from the line above. |
**Read the backup row before provisioning against this cluster.** `apps-pg`
has no backup at all — not a short retention, none. Recovery from data loss is
not available on this interface today. This is disclosed rather than fixed
because §10.2 requires disclosure now and the fix needs a backup target; it is
recorded as the R-axis gap in `docs/tenancy-posture.md` and is the highest
value item that declaration surfaced.
The unpartitioned connection pool is the same shape of risk `rapp-postgres`
reported for `platform-pg`: the bound is aggregate, so every consumer can be
politely inside its own expectations while the cluster still fails.
## `rapp-postgres.v1`
Consumer is a `consumers[].declaration`. Boundary unit is
@ -42,6 +86,26 @@ Consumer is a `consumers[].declaration`. Boundary unit is
`openbao-dynamic-database-credential` via grant
`rapp-postgres/<consumer>-<role>`. Tenant identifiers are opaque.
**Quotas and limits** — authority is `rapp-postgres`'s
`helm/platform-pg-cluster.yaml`; reproduced here for disclosure, 2026-08-17:
| Property | Value | Note |
| --- | --- | --- |
| `max_connections` | 100 | Aggregate, not per consumer. ~14 per consumer (12 runtime + 2 migration) puts the practical ceiling near 6. |
| Memory limit | 1Gi | Expected to bind before connections do — ~10MB per backend. Memory pressure OOM-kills; connection exhaustion errors cleanly. |
| CPU | 100m request / 1 limit | |
| Instances | 1 | No HA. |
| Storage | 20Gi | |
| Backup retention | 30d | Instance-wide, per `rapp-postgres` ADR-0002. |
| Effective erasure horizon | **30d, or the max across co-residents** | A consumer needing shorter cannot have it at P1. See `docs/placement-policy.md` §7. |
| Statement timeout | consumer-set | `tenant-engine` sets 5s for its own path; not a platform default. |
| Declared ceiling / overflow target | **not yet declared** | Owed by `rapp-postgres` before the third consumer, per `docs/placement-policy.md` Rule P-4.1. |
Service classes currently co-resident on `platform-pg`: `latency-critical`
(`tenant-engine`) and `batch` (`audit-core`). Nothing prioritises between them
— community PostgreSQL has no resource governor. Reported per Tenancy Posture
§8.3.3.
## `ccr-workload-kv.v1` and `credential-grant.v1`
Unchanged semantics from `docs/credential-change-approval.md` and

177
docs/tenancy-posture.md Normal file
View file

@ -0,0 +1,177 @@
# S3 tenancy posture declaration
Work record: `RPF-WP-0018` T01
Declared: 2026-08-17
Framework: `net-kingdom/canon/standards/tenancy-posture_v0.1.md` (draft-5, proposed)
Contract version of this document: `1.0.0`
This is `railiance-platform`'s response to the Tenancy Posture ratification
test (§20.2): *"Each publishes its own posture vector as part of review. The
framework is validated by whether it can describe them accurately."*
It declares under §6 — **conformance is accuracy, not altitude**. Several
levels below are `0`. None of them is an admission of weakness, and §6 is
explicit that a declared low level is conformant while an unevidenced high
level is not.
## Why this is a vector *set* and not a vector
§5 asks a **service** for one level per axis. `railiance-platform` is an OAS
**layer** holding custody of several services with materially different
postures. A single vector for the repo would be an average of things that are
not alike, which is the inaccuracy §6 prohibits.
So: one vector per service, plus an explicit statement of what this repo owns
versus what it merely holds policy for. The distinction matters because
`RAILIANCE-WP-0012` and `RAILIANCE-WP-0015` deliberately moved the deployable
surface to the `rapp-*` repos while custody and policy stayed here.
| Service | This repo's relationship | Deployable owned by |
| --- | --- | --- |
| `apps-pg` | **owns** — cluster CR is `helm/apps-pg-cluster.yaml` | this repo |
| `forgejo-db` | **owns** — cluster CR is `helm/forgejo-db-cluster.yaml` | this repo |
| `openbao` | **custody and policy**; package extracted | `rapp-openbao` |
| `platform-pg` | **policy only**; consumer contract and grants here | `rapp-postgres` |
| Valkey, MinIO | declared capability, no live instance | — |
## The vectors
### `apps-pg` — shared cluster for S5 application databases
```yaml
tenancy:
service: apps-pg
role: storage-provider
current: { I: 0, A: 0, E: 0, P: null, R: 0 }
target: { I: 0, A: 0, E: 0, P: null, R: 2 }
service_class: interactive
reviewed: "2026-08-17"
gap:
R: "No backup is configured on this cluster at all — no barmanObjectStore,
no retentionPolicy, no deletion path. R0 is the accurate reading and
it is worse than it looks: R0 here means no recovery, not merely no
erasure policy. Closing this is the highest-value item this
declaration surfaced. Target R2 requires a backup target first."
notes:
- "I/A/E are 0 by construction, not by neglect. apps-pg holds no tenant
concept, carries no tenant claim, and applies no tenant predicate. The
tenant boundary for data on this cluster sits entirely in the consuming
S5 application. See the provider note below."
- "P is null rather than a number. See §P-axis note."
```
### `forgejo-db` — dedicated cluster for the forge
```yaml
tenancy:
service: forgejo-db
role: storage-provider
current: { I: 0, A: 0, E: 0, P: null, R: 0 }
target: { I: 0, A: 0, E: 0, P: null, R: 2 }
service_class: interactive
reviewed: "2026-08-17"
gap:
R: "Backup exists as a documented procedure (docs/forgejo-backup.md), not
as a declared retention with a published erasure horizon. R2 needs the
horizon published to the consumer, which is the point of the level."
notes:
- "Single-consumer cluster. Provides P2 to forgejo; is not itself at P2."
```
### `openbao` — platform secrets service
```yaml
tenancy:
service: openbao
role: credential-provider
current: { I: 0, A: 2, E: 0, P: 0, R: 0 }
target: { I: 0, A: 2, E: 0, P: 0, R: 1 }
service_class: latency-critical
reviewed: "2026-08-17"
gap:
R: "No retention position on audit device output or on KV version history.
R1 (platform default) is reachable cheaply and is not yet declared."
notes:
- "A2 is claimed on the consumer boundary, not a tenant boundary: policy
per workload path, bound once, centrally, in OpenBao's own policy
engine. It is NOT delegated to flex-auth, so A3 is not claimed and is
not currently a target — a PDP in the credential path would put
flex-auth on OpenBao's availability path and OpenBao on flex-auth's."
- "E is 0 on the TENANT axis and that number is misleading without this
sentence: consumer separation in OpenBao is structural — a workload
token's policy cannot address another workload's path at all, which is
E4-shaped machinery. It scores 0 because the axis measures the tenant
boundary and OpenBao has no tenant dimension. See the provider note."
- "P0 is accurate and deliberate: one instance, every consumer shares it.
Single-node rail; §17 scaling demands apply."
```
### `platform-pg` — policy relationship only
Declared by `rapp-postgres`. This repo does not restate it. What this repo
declares about it is in `docs/placement-policy.md`: the rule, the placement
owner per consumer, and the triggers. The numbers — `instances: 1`,
`max_connections: 100`, `1Gi`, `retentionPolicy: 30d` — are `rapp-postgres`'s
cluster CR and its declaration to make.
## The P-axis note — why two entries are `null`
The P ladder describes **which substrate holds a tenant's data**, from the
point of view of the workload whose data it is. `apps-pg` and `forgejo-db` are
the substrate. Asking a cluster what its own placement level is has no
well-formed answer: `apps-pg` *provides* P1 to each of its consumers, and
`forgejo-db` *provides* P2 to forgejo, but neither *is* at a level.
Declaring `P: 1` for `apps-pg` would read as though the cluster had a
placement, and a later reader would use it to argue about isolation. `null`
plus the provider statement is the accurate form. This is routed as a finding.
## Finding routed to `net-kingdom`
**The five ladders describe a consumer of storage. They do not describe a
provider of it.**
Every service above scores at or near zero on I, A and E, and in each case for
the same structural reason rather than for a weakness: a storage or credential
platform has no tenant dimension of its own. It is exactly as strong or weak as
its consumers make it. The framework anticipates this in §3.3 and in the
"newly absorbed repo" example, but the vector shape gives a provider no way to
say the useful thing, which is not *what level am I at* but **what level do I
make reachable for the workloads I hold**.
The concrete case is the `openbao` E line. `E: 0` is literally correct and
actively misleading — the mechanism in place is credential-scoped structural
separation, the same machinery E4 describes, pointed at a consumer boundary
rather than a tenant one. A reader scanning a table of E values would rank
OpenBao below a service doing per-query filtering in application code, which
inverts the actual security position.
**Proposed amendment, offered as the first correction from review rather than
from research (§Status observes that every correction so far came from research
or relocation):** add a **provider declaration** beside the posture vector — a
platform service states, per axis, the maximum level it makes reachable and
what the consumer must do to reach it. For `apps-pg` that is: E4 unreachable
(shared credential per consumer, no per-tenant credential), E3 reachable once
the GUC contract exists, R2 blocked on a backup target. That is the sentence a
consumer actually needs, and no arrangement of the five consumer ladders
produces it.
This is offered as an addition, not a replacement. The consumer vector is
right for consumers, and this repo is not asking for the ladders to be
renumbered.
## What this repo accepts
- The P0P4 ladder as written, by reference. It describes our estate correctly.
- §8.1's monitoring obligation, with the deferrals stated in
`docs/placement-policy.md` rather than silently.
- §9.1 — no static long-lived database credentials above E0. Already how the
credential broker works; no change needed.
- §10.2 — quota disclosure. Landed in `docs/s3-consumer-interfaces.md` 1.1.0.
- §6 — accuracy over altitude, which is why this document declares four zeros.
## Review interval
Six months, or on any of: a service moving placement level, a backup target
becoming available, or the framework reaching `accepted`. Next review due
**2027-02-17**.

View file

@ -4,7 +4,7 @@ type: workplan
title: "Align S3 with the estate policy surface (Tenancy Posture + policy-nexus)"
domain: financials
repo: railiance-platform
status: proposed
status: active
owner: codex
topic_slug: railiance
created: "2026-08-17"
@ -15,6 +15,7 @@ related:
- RAPP-POSTGRES-WP-0002
origin: routed
origin_ref: "net-kingdom/canon/standards/tenancy-posture_v0.1.md §19.2, §20"
state_hub_workstream_id: "d40827cb-bb48-4cdd-9b41-8dfae116d705"
---
# RPF-WP-0018 — policy surface alignment
@ -65,7 +66,7 @@ monitoring we do not have is the exact overclaim §6 prohibits, so T03
records each trigger with a named monitor or an explicit `unmonitored`
marker, and the marker is the honest answer until telemetry lands.
**Does not fit, and the policy should change.** Three findings, T07.
**Does not fit, and the policy should change.** Five findings, T07.
## Findings against the policy (recommendations, not yet routed)
@ -131,6 +132,38 @@ has custody of — plus a statement of which it owns versus operates. T01
does that and routes the finding back to `net-kingdom` as the framework's
first real correction from review rather than from research.
Refined while writing T01: the sharper form of F4 is that **the five
ladders describe a consumer of storage, not a provider of it.** Every S3
service scores at or near zero on I, A and E for the same structural
reason — a storage or credential platform has no tenant dimension of its
own. The `openbao` E line is the demonstration: `E: 0` is literally correct
and actively misleading, because the mechanism in place is
credential-scoped structural separation, the machinery E4 describes,
pointed at a consumer boundary rather than a tenant one. Proposed remedy is
a **provider declaration** beside the consumer vector, stating per axis the
maximum level the platform makes reachable and what the consumer must do to
reach it. Offered as an addition, not a renumbering.
**F5 — `policy-nexus` T02/T05 cannot be built against the corpus as it
stands.** Measured across the workstation on 2026-08-17: **69 ADRs in 19
repos** — closely matching that workplan's "roughly 68 across 18" — of
which **21 carry YAML frontmatter**, **41 carry any status field**, and
**2 carry any notion of a review date**. T02 specifies a front-matter-driven
renderer taking title, status, revision and review date from the source; T05
requires every page to show status, revision and last-reviewed, with a
staleness marker once a review interval is exceeded. Against this corpus
that renderer has nothing to read for 48 of 69 documents, and the currency
half of the repo's purpose has data for 2.
*Recommend:* T03 of that workplan should treat frontmatter as an **ingestion
precondition**, not a rendering input — a document without the required
fields is reported as non-conformant and left unpublished, rather than
rendered with blanks or with dates the site inferred. Inferring them is the
failure mode that workplan already names as its top risk: the publication
becomes a second source of truth. The corollary is that policy-nexus needs
a conformance report aimed at source repos before it needs a site, and this
repo's `docs/adr/README.md` is one repo's answer to it.
## Boundaries
- No workload moves placement level in this workplan. Declaring where
@ -146,8 +179,9 @@ first real correction from review rather than from research.
```task
id: RPF-WP-0018-T01
status: todo
status: done
priority: high
state_hub_task_id: "ff392a23-e72a-4442-843f-929f218404ae"
```
**Publish the S3 posture vector set.** Write `docs/tenancy-posture.md`:
one vector per service under S3 custody (`openbao`, `apps-pg`, and the
@ -158,8 +192,9 @@ Route F4 to `net-kingdom` with the vector set as the evidence.
```task
id: RPF-WP-0018-T02
status: todo
status: done
priority: high
state_hub_task_id: "962fc494-bb9b-4f67-8e5b-2ed88a670945"
```
**Accept placement ownership, scoped.** Write `docs/placement-policy.md`:
adopt the P0P4 ladder by reference (do not restate it — the canon copy is
@ -171,8 +206,9 @@ either way.
```task
id: RPF-WP-0018-T03
status: todo
status: done
priority: medium
state_hub_task_id: "f6f30233-3f6e-44a0-a644-7268f9d88f3c"
```
**Make triggers monitorable or honestly unmonitored.** For each of the five
§8 triggers, record in `docs/placement-policy.md` what signal would fire it,
@ -184,8 +220,9 @@ report which service classes are co-resident. Today that is
```task
id: RPF-WP-0018-T04
status: todo
status: done
priority: medium
state_hub_task_id: "8a3ce90b-8af2-4dcd-9a35-77025d2123f2"
```
**Disclose quotas to consumers (§10.2).** Extend
`docs/s3-consumer-interfaces.md` to `1.1.0` — additive under its own
@ -196,8 +233,9 @@ these is announced to bound consumers, not discovered by them.
```task
id: RPF-WP-0018-T05
status: todo
status: done
priority: medium
state_hub_task_id: "a6da631d-fa4e-4c2b-acc3-d3cf53cfb57f"
```
**Create the ADR surface (F2).** Create `docs/adr/` with an ADR template
carrying the frontmatter `policy-nexus` T02/T05 consume: title, status,
@ -210,8 +248,9 @@ only home for a decision.
```task
id: RPF-WP-0018-T06
status: todo
status: done
priority: low
state_hub_task_id: "99f2a4d8-b20c-4c43-8c94-0bc469bfcc46"
```
**Answer §19.9 — retention floor and ceiling.** Decide whether
`backupRetentionDays` gets a platform minimum (so a consumer asking for one
@ -223,8 +262,9 @@ maximum declared across co-residents, so a shorter horizon is a P2 trigger.
```task
id: RPF-WP-0018-T07
status: todo
status: progress
priority: high
state_hub_task_id: "b021fda3-23bd-4843-8d1b-983b6ec582b5"
```
**Route the findings.** Reply to `rapp-postgres` and `tenant-engine` with
T01/T02 outcomes and F3/F4. Reply to `policy-nexus` with F1 (substrate