From e7e4e33bb8567198668d978dc180fdfb8ecea0ce Mon Sep 17 00:00:00 2001 From: codex Date: Mon, 17 Aug 2026 21:55:11 +0200 Subject: [PATCH] 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 --- WORK-RECORDS.md | 8 + .../ADR-0001-s3-platform-service-boundary.md | 67 ++++++ .../ADR-0002-placement-policy-ownership.md | 81 +++++++ .../ADR-0003-decisions-live-in-the-repo.md | 67 ++++++ docs/adr/README.md | 69 ++++++ docs/placement-policy.md | 224 ++++++++++++++++++ docs/s3-consumer-interfaces.md | 70 +++++- docs/tenancy-posture.md | 177 ++++++++++++++ .../RPF-WP-0018-policy-surface-alignment.md | 58 ++++- 9 files changed, 809 insertions(+), 12 deletions(-) create mode 100644 docs/adr/ADR-0001-s3-platform-service-boundary.md create mode 100644 docs/adr/ADR-0002-placement-policy-ownership.md create mode 100644 docs/adr/ADR-0003-decisions-live-in-the-repo.md create mode 100644 docs/adr/README.md create mode 100644 docs/placement-policy.md create mode 100644 docs/tenancy-posture.md diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index c637b60..1dfa2b6 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -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 | diff --git a/docs/adr/ADR-0001-s3-platform-service-boundary.md b/docs/adr/ADR-0001-s3-platform-service-boundary.md new file mode 100644 index 0000000..53f4460 --- /dev/null +++ b/docs/adr/ADR-0001-s3-platform-service-boundary.md @@ -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. diff --git a/docs/adr/ADR-0002-placement-policy-ownership.md b/docs/adr/ADR-0002-placement-policy-ownership.md new file mode 100644 index 0000000..fe11c39 --- /dev/null +++ b/docs/adr/ADR-0002-placement-policy-ownership.md @@ -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. diff --git a/docs/adr/ADR-0003-decisions-live-in-the-repo.md b/docs/adr/ADR-0003-decisions-live-in-the-repo.md new file mode 100644 index 0000000..187d528 --- /dev/null +++ b/docs/adr/ADR-0003-decisions-live-in-the-repo.md @@ -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. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..591425e --- /dev/null +++ b/docs/adr/README.md @@ -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. diff --git a/docs/placement-policy.md b/docs/placement-policy.md new file mode 100644 index 0000000..b68b28e --- /dev/null +++ b/docs/placement-policy.md @@ -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: P0–P4 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**. diff --git a/docs/s3-consumer-interfaces.md b/docs/s3-consumer-interfaces.md index bb9a596..ce2470a 100644 --- a/docs/s3-consumer-interfaces.md +++ b/docs/s3-consumer-interfaces.md @@ -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/-`. 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 diff --git a/docs/tenancy-posture.md b/docs/tenancy-posture.md new file mode 100644 index 0000000..29fad61 --- /dev/null +++ b/docs/tenancy-posture.md @@ -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 P0–P4 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**. diff --git a/workplans/RPF-WP-0018-policy-surface-alignment.md b/workplans/RPF-WP-0018-policy-surface-alignment.md index ed8deca..faea8ff 100644 --- a/workplans/RPF-WP-0018-policy-surface-alignment.md +++ b/workplans/RPF-WP-0018-policy-surface-alignment.md @@ -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 P0–P4 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