diff --git a/data/reefs/reef-storage.json b/data/reefs/reef-storage.json new file mode 100644 index 0000000..745d5f8 --- /dev/null +++ b/data/reefs/reef-storage.json @@ -0,0 +1,19 @@ +{ + "schema_version": "0.1", + "reef_id": "reef-storage", + "repo": "reef-storage", + "declaration_ref": "reef:storage/declarations/reef.yaml", + "procuring_entity_id": "entity:railiance", + "financial_entity_id": "entity:railiance", + "role": "storage_substrate", + "resources": [ + {"resource_id": "resource:platform:audit-storage", "role": "object_store"} + ], + "consumers_potential": ["rapp-postgres"], + "consumers_actual": [], + "notes": [ + "Provider-delegated S3 (Scaleway). No rail.", + "Attribute values: reef:storage/substrate/object-stores/platform-audit-storage.yaml", + "Independent of reef-railiance." + ] +} diff --git a/data/resources/platform-audit-storage.proposed.json b/data/resources/platform-audit-storage.json similarity index 50% rename from data/resources/platform-audit-storage.proposed.json rename to data/resources/platform-audit-storage.json index 49dba12..2b3c8b6 100644 --- a/data/resources/platform-audit-storage.proposed.json +++ b/data/resources/platform-audit-storage.json @@ -5,14 +5,44 @@ "financial_entity_id": "entity:railiance", "procuring_entity_id": "entity:railiance", "entity_gap": null, + "description": "Off-host S3-compatible object store for rapp-postgres WAL archive and physical base backups. Procured by Railiance, operated as a Scaleway-delegated substrate on reef-storage, not on reef-railiance.", + "decision": { + "status": "approved", + "chosen": "Scaleway Standard Multi-AZ nl-ams; independent secondary copy on Host Europe Backup Storage or governed Nextcloud (T06)", + "rejected": [ + "Host Europe Cloud Storage as primary (S3 not confirmed orderable)", + "Hetzner Object Storage as primary (no default at-rest encryption)", + "Self-managed Garage as primary (labor and capacity lose at this workload)" + ], + "approved_by": "human-financial-authority", + "approved_on": "2026-08-14", + "ref": "docs/evidence/RESOURCE-WP-0002-primary-selection-2026-08-14.md" + }, + "operational_refs": [ + "reef:storage/declarations/reef.yaml", + "reef:storage/substrate/object-stores/platform-audit-storage.yaml", + "reef:storage/substrate/object-stores/platform-audit-storage.yaml#endpoint", + "reef:storage/substrate/object-stores/platform-audit-storage.yaml#bucket", + "reef:storage/substrate/object-stores/platform-audit-storage.yaml#region", + "reef:storage/substrate/object-stores/platform-audit-storage.yaml#prefix", + "reef:storage/substrate/object-stores/platform-audit-storage.yaml#lifecycle", + "reef:storage/substrate/object-stores/platform-audit-storage.yaml#provider_project_ref" + ], + "credential_handles": [ + "secret:railiance-platform/platform-pg-backup-s3" + ], + "consumers": { + "potential": ["rapp-postgres"], + "actual": [] + }, "resource_class": "storage", - "status": "proposed", + "status": "ordered", "management_model": "provider_managed", "provider": { "name": "Scaleway", - "account_ref": null, + "account_ref": "reef:storage/substrate/object-stores/platform-audit-storage.yaml#provider_project_ref", "product_ref": "scaleway-standard-multi-az", - "provider_resource_id": null + "provider_resource_id": "railiance-platform-pg-backup" }, "service": { "name": "platform audit storage", @@ -23,7 +53,7 @@ "location": { "region": "nl-ams", "country": "NL", - "failure_domains": ["provider:scaleway", "region:nl-ams"], + "failure_domains": ["provider:scaleway", "region:nl-ams", "reef:storage"], "residency": "European Union" }, "capacity": [ @@ -44,13 +74,13 @@ "cost": { "currency": "EUR", "tax_status": "excluded", - "billing_model": "usage-based storage and egress; no commitment", + "billing_model": "usage-based storage and egress; no commitment; Railiance self-use at delivered cost", "commitment_ref": null, "price_evidence": "data/providers/object-storage.json#scaleway-standard-multi-az" }, "lifecycle": { "proposed_on": "2026-08-10", - "ordered_on": null, + "ordered_on": "2026-08-14", "commissioned_on": null, "renews_on": null, "cancel_by": null, @@ -65,6 +95,8 @@ "evidence": [ {"kind": "provider", "ref": "https://www.scaleway.com/en/pricing/storage/", "observed_at": "2026-08-10", "authority": "Scaleway"}, {"kind": "provider", "ref": "https://www.scaleway.com/en/object-storage/", "observed_at": "2026-08-10", "authority": "Scaleway"}, - {"kind": "decision", "ref": "docs/evidence/RESOURCE-WP-0002-provider-due-diligence-2026-08-10.md", "observed_at": "2026-08-10", "authority": "resource-control"} + {"kind": "decision", "ref": "docs/evidence/RESOURCE-WP-0002-provider-due-diligence-2026-08-10.md", "observed_at": "2026-08-10", "authority": "resource-control"}, + {"kind": "decision", "ref": "docs/evidence/RESOURCE-WP-0002-primary-selection-2026-08-14.md", "observed_at": "2026-08-14", "authority": "resource-control"}, + {"kind": "provider", "ref": "reef:storage/substrate/object-stores/platform-audit-storage.yaml", "observed_at": "2026-08-14", "authority": "reef-storage"} ] } diff --git a/docs/evidence/RESOURCE-WP-0002-primary-selection-2026-08-14.md b/docs/evidence/RESOURCE-WP-0002-primary-selection-2026-08-14.md new file mode 100644 index 0000000..026da81 --- /dev/null +++ b/docs/evidence/RESOURCE-WP-0002-primary-selection-2026-08-14.md @@ -0,0 +1,91 @@ +# RESOURCE-WP-0002 T03 — primary object-store selection + +Date: 2026-08-14 +Status: **approved and purchased — cost alert still human** +Resource: `resource:platform:audit-storage` +Procuring / consuming entity: `entity:railiance` (self-use, no 20 % markup) +Operating reef: `reef-storage` (not `reef-railiance`) + +Human financial authority approved the purchase in session 2026-08-14 +(“lets do it”, then “key into OpenBao first”). Bootstrap key is in OpenBao +(`platform/workloads/railiance/scaleway/bootstrap`, KV v2). After the +Object Storage policy attached, list/get succeeded. Private bucket +`railiance-platform-pg-backup` exists in `nl-ams` (created +2026-08-14T16:21:56Z), versioning on, 30-day lifecycle applied, ACL +owner-only. Live attributes are in +`reef-storage/substrate/object-stores/platform-audit-storage.yaml`. +The bootstrap key cannot `write billing_budgets`; founder still sets a +€20 monthly project budget in the Scaleway console. Scoped Barman key is +T04. + +## Recommendation + +| Role | Choice | Why | +| --- | --- | --- | +| **Primary** | Scaleway Object Storage, Standard Multi-AZ, region `nl-ams` (NL, EU) | Only A/B/C candidate that is orderable, S3/SigV4 documented, Multi-AZ, published durability, **managed encryption at rest**, and an independent failure domain from Host Europe `railiance01`. Lowest comparable 320 GB running cost among managed options that meet acceptance. | +| **Independent secondary copy** | Host Europe Backup Storage (SFTP/SCP) or the existing governed Nextcloud lane (T06) | Not S3; not a Barman primary. Keeps a second copy off Scaleway and off the same API credential. | + +Do **not** put the primary bucket on `reef-railiance`. S3 is a +provider-delegated capability; `reef-storage` is the substrate. + +## Ranking (A / B / C) + +Evidence: demand/cost model 2026-08-10, expanded comparison 2026-08-10, +due diligence 2026-08-10. Labor €60/h. Tax excluded. Running totals at +the normalized 320 GB + 5 GB restore-drill point. + +| Criterion | A Host Europe Cloud Storage | B Scaleway Multi-AZ `nl-ams` | C Hetzner Object Storage | +| --- | --- | --- | --- | +| Total cost (320 GB) | unknown (no current S3 quote) | **€65.14**/mo (€5.14 infra + €60 labor) | €96.49/mo (€6.49 min + €90 labor) | +| Compatibility (CNPG/Barman S3) | unknown / not orderable on evidence | documented S3 + SigV4; live preflight still required | documented S3; live preflight still required | +| Resilience | same provider as compute | **different provider**; Multi-AZ; 99.999999999% durability claim | different provider; no quantified storage SLA | +| Sovereignty | DE if it existed | NL / EU | DE / EU | +| Operational effort | unknown | 1 h/mo planned; no rail to run | 1.5 h/mo; SSE-C custody if we accept no default at-rest encryption | +| Exit cost | unknown | egress €0.01/GB after 75 GB free + 4 h labor | inside 1 TB included until quota exceeded | +| Blocking gap | current S3 **not confirmed orderable** | live Barman preflight; contract/tax on the paying account | **no default at-rest encryption** (SSE-C only) | + +Self-managed Garage on 2–3 VMs is €240–€336/mo at 320 GB and fails closed +before month-12 base volume. It is not a primary candidate at this +workload. + +## What we are buying (if approved) + +- Product: Scaleway Standard Multi-AZ Object Storage +- Region: `nl-ams` +- Commitment: **none** (usage-based) +- Payer: Railiance (`entity:railiance`); transfer price = delivered cost +- Public access: disabled +- Identity: narrowest key, bucket/prefix only +- Versioning: on +- Lifecycle: 30-day recovery window (match demand) +- Cost alert: on the Scaleway project +- Owner in inventory: `resource-control` +- Attribute home: `reef-storage/substrate/object-stores/platform-audit-storage.yaml` +- Credential home (after T04): `secret:railiance-platform/platform-pg-backup-s3` +- Consumer potential: `rapp-postgres` +- Consumer actual: none until WAL flows + +## What human financial authority must approve + +1. Create or reuse a Scaleway project paid as Railiance (or GmbH Hauptkonto + until the Railiance account exists). +2. Accept Scaleway’s contract/tax treatment for that account. +3. Accept that Host Europe S3 stays out of the race until written + orderability exists. +4. Accept Hetzner only as a price comparator unless SSE-C custody is + explicitly chosen later. +5. Spend: expected **~€3–€10/mo infrastructure** at current size, plus + ~1 h operator labor; not a committed term. + +After **yes**: create private bucket, scoped key, versioning/lifecycle, +cost alert; fill `reef-storage` attributes (endpoint, bucket, prefix, +project ref); flip inventory `proposed → ordered`; then T04/T05. + +After **no**: write the rejection on this record; do not invent another +primary without a new decision. + +## Authority + +Recommended by: resource-control (this file) +Approved by: human financial authority (Bernd Worsch, chat 2026-08-14) +Approved on: 2026-08-14 diff --git a/schemas/resource-inventory.schema.json b/schemas/resource-inventory.schema.json index 2f3f719..33b16cb 100644 --- a/schemas/resource-inventory.schema.json +++ b/schemas/resource-inventory.schema.json @@ -12,6 +12,37 @@ "financial_entity_id": {"type": ["string", "null"], "pattern": "^entity:[a-z0-9]+$"}, "procuring_entity_id": {"type": ["string", "null"], "pattern": "^entity:[a-z0-9]+$"}, "entity_gap": {"type": ["string", "null"]}, + "description": {"type": "string", "minLength": 1}, + "decision": { + "type": "object", + "additionalProperties": false, + "required": ["status", "ref"], + "properties": { + "status": {"enum": ["recommended", "approved", "rejected"]}, + "chosen": {"type": ["string", "null"]}, + "rejected": {"type": "array", "items": {"type": "string"}}, + "approved_by": {"type": ["string", "null"]}, + "approved_on": {"type": ["string", "null"], "format": "date"}, + "ref": {"type": "string", "minLength": 1} + } + }, + "operational_refs": { + "type": "array", + "items": {"type": "string", "pattern": "^reef:"} + }, + "credential_handles": { + "type": "array", + "items": {"type": "string", "pattern": "^secret:"} + }, + "consumers": { + "type": "object", + "additionalProperties": false, + "required": ["potential", "actual"], + "properties": { + "potential": {"type": "array", "items": {"type": "string"}}, + "actual": {"type": "array", "items": {"type": "string"}} + } + }, "resource_class": {"enum": ["compute_instance", "storage", "network", "kubernetes_capacity", "database", "managed_service", "self_managed_service", "shared_platform_service", "license"]}, "status": {"enum": ["proposed", "ordered", "commissioning", "active", "suspended", "retiring", "retired", "rejected"]}, "management_model": {"enum": ["provider_managed", "self_managed", "shared_capacity"]}, diff --git a/tests/test_portfolio.py b/tests/test_portfolio.py index 2af3c4f..a4c0ae4 100644 --- a/tests/test_portfolio.py +++ b/tests/test_portfolio.py @@ -34,6 +34,19 @@ class PortfolioTest(unittest.TestCase): with self.assertRaisesRegex(ValueError, "shared resources"): validate_record(record) + def test_ordered_resource_requires_approved_decision(self): + record = deepcopy(next( + r for _, r in self.records() if r["id"] == "resource:platform:audit-storage" + )) + record["status"] = "ordered" + record["decision"]["status"] = "draft" + with self.assertRaisesRegex(ValueError, "approved decision"): + validate_record(record) + record["decision"]["status"] = "approved" + record["decision"]["approved_by"] = "human" + record["decision"]["approved_on"] = "2026-08-14" + validate_record(record) + def test_unknown_commission_date_is_preserved(self): record = deepcopy(next(r for _, r in self.records() if r["status"] == "active")) record["lifecycle"]["commissioned_on"] = None @@ -92,6 +105,16 @@ class ReefViewTest(unittest.TestCase): self.assertNotIn("resource:platform:audit-storage", {row["resource_id"] for row in view["resources"]}) self.assertTrue(any("reef-storage" in note for note in view["notes"])) + def test_reef_storage_view_is_delegated_object_store(self): + view = json.loads((ROOT / "data/reefs/reef-storage.json").read_text()) + self.assertEqual("storage_substrate", view["role"]) + self.assertEqual( + ["resource:platform:audit-storage"], + [row["resource_id"] for row in view["resources"]], + ) + self.assertEqual(["rapp-postgres"], view["consumers_potential"]) + self.assertEqual([], view["consumers_actual"]) + if __name__ == "__main__": unittest.main() diff --git a/tools/portfolio.py b/tools/portfolio.py index 0a71f84..e9e39bf 100644 --- a/tools/portfolio.py +++ b/tools/portfolio.py @@ -51,6 +51,18 @@ def validate_record(record: dict) -> None: association_ok(record) + decision = record.get("decision") + if record.get("status") in {"ordered", "commissioning"} and ( + not decision or decision.get("status") != "approved" + ): + raise ValueError("ordered or commissioning resources require an approved decision") + for ref in record.get("operational_refs") or []: + if not str(ref).startswith("reef:"): + raise ValueError(f"operational_refs must be reef: references: {ref}") + for ref in record.get("credential_handles") or []: + if not str(ref).startswith("secret:"): + raise ValueError(f"credential_handles must be secret: references: {ref}") + allocation = record["ownership"]["allocation"] if allocation["mode"] == "unattributed": if allocation["cost_attribution_key"] is not None: diff --git a/workplans/RESOURCE-WP-0002-procure-postgres-backup-storage.md b/workplans/RESOURCE-WP-0002-procure-postgres-backup-storage.md index de50eab..f46ed55 100644 --- a/workplans/RESOURCE-WP-0002-procure-postgres-backup-storage.md +++ b/workplans/RESOURCE-WP-0002-procure-postgres-backup-storage.md @@ -8,7 +8,7 @@ status: active owner: codex topic_slug: railiance created: "2026-08-10" -updated: "2026-08-10" +updated: "2026-08-14" state_hub_workstream_id: "921496a3-280b-4dc8-a3c0-b4ec314142f5" --- @@ -190,7 +190,7 @@ Barman preflight, contract review, and human approval. ```task id: RESOURCE-WP-0002-T03 -status: wait +status: done priority: high state_hub_task_id: "e4184350-dab2-4a0b-bee5-1a641e8a2df3" ``` @@ -207,11 +207,27 @@ Done when the decision is approved and the purchased resource has a non-secret inventory record with provider resource ID, region, service class, contract, renewal/cancellation dates, capacity model, owner, and cost-attribution key. +Progress 2026-08-14: decision record written — +`docs/evidence/RESOURCE-WP-0002-primary-selection-2026-08-14.md`. +Primary: Scaleway Multi-AZ `nl-ams`. Secondary copy: Host Europe Backup +Storage or Nextcloud (T06). **Purchase approved 2026-08-14.** After the +Object Storage policy attached, `scw object bucket list/get` succeeded. +Private bucket `railiance-platform-pg-backup` exists in `nl-ams` +(created 2026-08-14T16:21:56Z), versioning on, 30-day current and +noncurrent lifecycle applied, ACL owner-only. Inventory is +`data/resources/platform-audit-storage.json` (`status: ordered`, +`ordered_on: 2026-08-14`, `provider_resource_id` = bucket name). +Operating facts live on +`reef:storage/substrate/object-stores/platform-audit-storage.yaml`. +Residual: founder must create a €20 monthly Scaleway budget in the +console — this bootstrap key cannot `write billing_budgets`. Scoped +Barman key remains T04. + ## T04 — Establish credential custody and hand off to rapp-postgres ```task id: RESOURCE-WP-0002-T04 -status: wait +status: todo priority: high state_hub_task_id: "a2dc370a-b5e7-44b1-b46a-f3b84815b14a" ```