Onboard tenant-engine to the staged-promotion contract
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 50s

TEN-WP-0008. railiance/app.toml declares criticality=high, empty secrets,
isolated canary, and the live PostgreSQL digest as previous_stable.
Manifests render through kustomize (deploy/ and deploy/canary/). Stage 1
passed. Stage 2/3 Helm-only CLI gap requested as RAIL-BS-IN-0001 rather
than a dummy chart.

Assistant: grok
Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
This commit is contained in:
tegwick 2026-08-29 14:51:27 +02:00
parent f9f8e0c54f
commit 6644ad8402
19 changed files with 1053 additions and 18 deletions

View file

@ -4,14 +4,15 @@ PIP ?= $(PYTHON) -m pip
PYTEST ?= $(PYTHON) -m pytest PYTEST ?= $(PYTHON) -m pytest
RUFF ?= $(VENV)/bin/ruff RUFF ?= $(VENV)/bin/ruff
.PHONY: help install-dev test lint run verify-pin .PHONY: help install-dev test lint run verify-pin validate-app-toml
help: help:
@echo "make install-dev Create .venv and install runtime + dev dependencies" @echo "make install-dev Create .venv and install runtime + dev dependencies"
@echo "make test Run the test suite" @echo "make test Run the test suite"
@echo "make lint Run syntax and static checks" @echo "make lint Run syntax and static checks"
@echo "make run Start the local API on 127.0.0.1:8090" @echo "make run Start the local API on 127.0.0.1:8090"
@echo "make verify-pin Check production against the repo's pinned digest" @echo "make verify-pin Check production against the repo's pinned digest"
@echo "make validate-app-toml Validate railiance/app.toml against railiance.app.v1"
$(VENV)/bin/python: $(VENV)/bin/python:
python3 -m venv $(VENV) python3 -m venv $(VENV)
@ -35,5 +36,8 @@ run: $(VENV)/.dev-installed
# Needs cluster access. tenant-engine runs on railiance01: # Needs cluster access. tenant-engine runs on railiance01:
# KUBECONFIG=~/.kube/config-railiance01 make verify-pin # KUBECONFIG=~/.kube/config-railiance01 make verify-pin
validate-app-toml: $(VENV)/.dev-installed
$(PYTHON) tests/validate_app_toml.py
verify-pin: verify-pin:
@./deploy/verify-pin.sh @./deploy/verify-pin.sh

View file

@ -98,12 +98,12 @@ not done.
| Domain model + SQLite/in-memory store | yes | `TEN-WP-0002` | | Domain model + SQLite/in-memory store | yes | `TEN-WP-0002` |
| Cache-read / live-lookup / write APIs | yes | `TEN-WP-0002` | | Cache-read / live-lookup / write APIs | yes | `TEN-WP-0002` |
| `flex-auth` `WriteAuthorizer` | yes | Fail-closed; `is_allowed()` returns a bool and **discards the decision envelope** | | `flex-auth` `WriteAuthorizer` | yes | Fail-closed; `is_allowed()` returns a bool and **discards the decision envelope** |
| Production runtime | yes | `TEN-WP-0004`; image pin in `deploy/tenant-engine.yaml` | | Production runtime | yes | `TEN-WP-0004`; image pin in `deploy/base/tenant-engine.yaml` |
| Lifecycle (update / retire / reactivate) | yes | `TEN-WP-0005`; no hard-delete | | Lifecycle (update / retire / reactivate) | yes | `TEN-WP-0005`; no hard-delete |
| Guardrail policy | yes | `TEN-WP-0006` / `0007`; `docs/tenant-guardrail-policy.md` | | Guardrail policy | yes | `TEN-WP-0006` / `0007`; `docs/tenant-guardrail-policy.md` |
| PostgreSQL production store | yes | `TEN-WP-0009`; deploy mounts `TENANT_ENGINE_DATABASE_URL_FILE` | | PostgreSQL production store | yes | `TEN-WP-0009`; deploy mounts `TENANT_ENGINE_DATABASE_URL_FILE` |
| Mutable grouping | yes | `TEN-WP-0010`; identifier segment stays historical | | Mutable grouping | yes | `TEN-WP-0010`; identifier segment stays historical |
| Staged promotion | **no** | `TEN-WP-0008` | | Staged promotion | yes (declared) | `TEN-WP-0008`; `railiance/app.toml`; live pin recorded as `previous_stable`. Helm CLI apply is a railiance-bootstrap gap (`RAIL-BS-IN-0001`). |
| Machine-readable `layer.yaml` | yes | `TEN-WP-0011-T01`; check in `scripts/check_layer_conformance.py` | | Machine-readable `layer.yaml` | yes | `TEN-WP-0011-T01`; check in `scripts/check_layer_conformance.py` |
| Persisted `authorization_decision_id` | yes | `authz_records` plus mutation event payload (`TEN-WP-0011-T02`) | | Persisted `authorization_decision_id` | yes | `authz_records` plus mutation event payload (`TEN-WP-0011-T02`) |
| Published unreachable-engine stance map | yes | `pep-stance.yaml`, fail-closed, tested equal to shipped behaviour | | Published unreachable-engine stance map | yes | `pep-stance.yaml`, fail-closed, tested equal to shipped behaviour |

View file

@ -1,6 +1,11 @@
# tenant-engine production deployment # tenant-engine production deployment
**Apply home moving to `rapp-tenant-engine`.** These files remain the source of the absorbed manifests until that package is the operator apply path. **Apply path:** `kubectl apply -k deploy/` (TEN-WP-0008). `deploy/base/tenant-engine.yaml`
is the document kustomize includes, and the file `make verify-pin` reads
for the intended digest. Isolated canary: `kubectl apply -k deploy/canary/`.
Staged-promotion contract: `railiance/app.toml`. Recorded stable:
`tenant-engine@sha256:a8e8086ffc5b772c1391b166f5e1884b90f7d327b152c205eceae129df555c24`.
The original manifests were recovered from live objects on 2026-08-14. The The original manifests were recovered from live objects on 2026-08-14. The
@ -42,7 +47,7 @@ git push origin main
# 3. Provision the rapp-postgres consumer and credential projections. # 3. Provision the rapp-postgres consumer and credential projections.
# 4. Run the schema migration Job with the migration-role lease. # 4. Run the schema migration Job with the migration-role lease.
# 5. Follow the stopped-write transfer below; only then apply the runtime. # 5. Follow the stopped-write transfer below; only then apply the runtime.
kubectl apply -f deploy/tenant-engine.yaml kubectl apply -k deploy/
kubectl -n tenant-engine rollout status deploy/tenant-engine --timeout=120s kubectl -n tenant-engine rollout status deploy/tenant-engine --timeout=120s
``` ```
@ -92,7 +97,7 @@ and periodically — drift is not an event you get told about.
It compares four things that are supposed to agree: It compares four things that are supposed to agree:
1. the digest this repo intends to run (`deploy/tenant-engine.yaml`); 1. the digest this repo intends to run (`deploy/base/tenant-engine.yaml`);
2. the digest the Deployment's spec asks for; 2. the digest the Deployment's spec asks for;
3. the digest the **running pod** actually resolved — a spec can be correct 3. the digest the **running pod** actually resolved — a spec can be correct
while the pod answering traffic is an older ReplicaSet that never finished while the pod answering traffic is an older ReplicaSet that never finished

View file

@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- tenant-engine.yaml

View file

@ -0,0 +1,46 @@
# Isolated Stage 2 canary. TEN-WP-0009 moved production off a ReadWriteOnce
# PVC onto PostgreSQL with RollingUpdate, so a second pod is storage-feasible.
# Isolation is a distinct Deployment + Service selector, not a second replica
# behind the production Service — user-engine's NetworkPolicy still points at
# app.kubernetes.io/name=tenant-engine and must not start sending traffic here.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- target:
kind: Deployment
name: tenant-engine
patch: |-
- op: replace
path: /metadata/name
value: tenant-engine-canary
- op: replace
path: /spec/selector/matchLabels/app.kubernetes.io~1name
value: tenant-engine-canary
- op: replace
path: /spec/template/metadata/labels/app.kubernetes.io~1name
value: tenant-engine-canary
- target:
kind: Service
name: tenant-engine
patch: |-
- op: replace
path: /metadata/name
value: tenant-engine-canary
- op: replace
path: /spec/selector/app.kubernetes.io~1name
value: tenant-engine-canary
- target:
kind: NetworkPolicy
name: tenant-engine
patch: |-
- op: replace
path: /metadata/name
value: tenant-engine-canary
- op: replace
path: /spec/podSelector/matchLabels/app.kubernetes.io~1name
value: tenant-engine-canary
- op: replace
path: /spec/ingress
value: []

View file

@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- base

View file

@ -4,7 +4,7 @@
# Compares three things that are supposed to agree and silently did not # Compares three things that are supposed to agree and silently did not
# between 2026-08-13 and 2026-08-16: # between 2026-08-13 and 2026-08-16:
# #
# 1. the digest this repo intends to run (deploy/tenant-engine.yaml) # 1. the digest this repo intends to run (deploy/base/tenant-engine.yaml)
# 2. the digest the Deployment asks for (spec.template.spec.containers) # 2. the digest the Deployment asks for (spec.template.spec.containers)
# 3. the digest the running pod actually (status.containerStatuses.imageID) # 3. the digest the running pod actually (status.containerStatuses.imageID)
# resolved # resolved
@ -33,7 +33,7 @@ set -uo pipefail
NAMESPACE="tenant-engine" NAMESPACE="tenant-engine"
DEPLOYMENT="tenant-engine" DEPLOYMENT="tenant-engine"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MANIFEST="$REPO_ROOT/deploy/tenant-engine.yaml" MANIFEST="$REPO_ROOT/deploy/base/tenant-engine.yaml"
QUIET=0 QUIET=0
LOCAL_PORT="${VERIFY_PIN_PORT:-18131}" LOCAL_PORT="${VERIFY_PIN_PORT:-18131}"

View file

@ -0,0 +1,85 @@
{
"actions": [],
"app": {
"criticality": "high",
"id": "tenant-engine",
"name": "tenant-engine",
"owner": "infotech",
"repo": "tenant-engine"
},
"app_dir": "/home/worsch/tenant-engine",
"approval_id": null,
"chart": "/home/worsch/tenant-engine/charts/tenant-engine",
"checks_expected": [
"stage2-accepted",
"rollback-target",
"cluster-health",
"operator-approval"
],
"contract": "/home/worsch/tenant-engine/railiance/app.toml",
"evidence_expected": [
"promotion commit",
"new stable digest in previous_stable and deploy/base/tenant-engine.yaml",
"post-promotion make verify-pin"
],
"generated_at": "2026-08-29T12:49:54Z",
"mode": "plan",
"namespace": "tenant-engine",
"planned_actions": [
{
"action_ref": "stage3.helm-rollback",
"namespace": "tenant-engine",
"release": "tenant-engine",
"revision": null,
"tool": "helm"
}
],
"prechecks": [
{
"name": "app.toml",
"required": true,
"status": "passed"
},
{
"detail": "image-digest",
"name": "rollback-strategy",
"required": true,
"status": "passed"
},
{
"detail": "plan mode does not execute helm",
"name": "helm",
"required": false,
"status": "not_required"
},
{
"name": "approval-id",
"required": false,
"status": "required_before_apply"
}
],
"previous_stable": "tenant-engine@sha256:a8e8086ffc5b772c1391b166f5e1884b90f7d327b152c205eceae129df555c24",
"promotion_mode": "release-replace",
"release": "tenant-engine",
"requires_approval": true,
"revision": null,
"rollback": {
"command_ref": "rollback.command",
"strategy": "image-digest",
"verification": "make verify-pin agrees repo, spec, and running pod; GET /health returns\n200 with store_backend=postgresql; lifecycle routes still present.\nDo not roll back to sha256:7dc9d7bf1fcbb3f3ad7035366f4cd44038322995a126b4423142e446ca5c9cd5\n(SQLite/PVC) after any PostgreSQL write. The TEN-WP-0007 ladder still\nholds for older images: rolling past a pre-lifecycle digest removes\nGET/PATCH/retire/reactivate that user-engine depends on.\n"
},
"schema_version": "railiance.stage3-rollback-result.v1",
"source": {
"artifact": "image",
"digest_policy": "required",
"revision": "git:main"
},
"stage": "stage3",
"status": "planned",
"summary": {
"actions_failed": 0,
"actions_total": 0,
"required_prechecks_failed": 0
},
"values": "/home/worsch/tenant-engine/values/stage3-production.yaml"
}

View file

@ -0,0 +1,116 @@
{
"app": {
"criticality": "high",
"id": "tenant-engine",
"name": "tenant-engine",
"owner": "infotech",
"repo": "tenant-engine"
},
"app_dir": "/home/worsch/tenant-engine",
"checks": [
{
"command_ref": "checks.unit-tests.command",
"duration_seconds": 78.242,
"exit_code": 0,
"id": "unit-tests",
"required": true,
"status": "passed",
"stderr_bytes": 0,
"stdout_bytes": 2453,
"type": "command"
},
{
"command_ref": "checks.contract-schema.command",
"duration_seconds": 1.703,
"exit_code": 0,
"id": "contract-schema",
"required": true,
"status": "passed",
"stderr_bytes": 0,
"stdout_bytes": 144,
"type": "command"
},
{
"command_ref": "checks.kustomize-production.command",
"duration_seconds": 1.021,
"exit_code": 0,
"id": "kustomize-production",
"required": true,
"status": "passed",
"stderr_bytes": 0,
"stdout_bytes": 4028,
"type": "command"
},
{
"command_ref": "checks.kustomize-canary.command",
"duration_seconds": 1.215,
"exit_code": 0,
"id": "kustomize-canary",
"required": true,
"status": "passed",
"stderr_bytes": 0,
"stdout_bytes": 3839,
"type": "command"
}
],
"commands": [
{
"command_ref": "stages.stage1.commands[0]",
"duration_seconds": 69.439,
"exit_code": 0,
"status": "passed",
"stderr_bytes": 0,
"stdout_bytes": 2453
},
{
"command_ref": "stages.stage1.commands[1]",
"duration_seconds": 1.526,
"exit_code": 0,
"status": "passed",
"stderr_bytes": 0,
"stdout_bytes": 144
},
{
"command_ref": "stages.stage1.commands[2]",
"duration_seconds": 1.48,
"exit_code": 0,
"status": "passed",
"stderr_bytes": 0,
"stdout_bytes": 4028
},
{
"command_ref": "stages.stage1.commands[3]",
"duration_seconds": 1.248,
"exit_code": 0,
"status": "passed",
"stderr_bytes": 0,
"stdout_bytes": 3839
}
],
"contract": "/home/worsch/tenant-engine/railiance/app.toml",
"duration_seconds": 155.875,
"evidence_expected": [
"pytest output",
"app.toml schema validation",
"kustomize render of deploy/ and deploy/canary"
],
"finished_at": "2026-08-29T12:49:41Z",
"namespace": "local",
"release": "tenant-engine-local",
"requires_approval": false,
"schema_version": "railiance.run-result.v1",
"source": {
"artifact": "image",
"digest_policy": "required",
"revision": "git:main"
},
"stage": "stage1",
"started_at": "2026-08-29T12:46:51Z",
"status": "passed",
"summary": {
"checks_total": 4,
"commands_failed": 0,
"commands_total": 4,
"required_checks_failed": 0
}
}

View file

@ -0,0 +1,87 @@
{
"actions": [],
"app": {
"criticality": "high",
"id": "tenant-engine",
"name": "tenant-engine",
"owner": "infotech",
"repo": "tenant-engine"
},
"app_dir": "/home/worsch/tenant-engine",
"approval_id": null,
"canary_mode": "isolated",
"chart": "/home/worsch/tenant-engine/charts/tenant-engine",
"checks_expected": [
"server-dry-run",
"canary-ready",
"canary-health",
"operator-approval"
],
"contract": "/home/worsch/tenant-engine/railiance/app.toml",
"evidence_expected": [
"server-side dry-run of isolated canary",
"deploy/tenant-engine-canary Available",
"canary /health 200",
"State Hub approval id"
],
"generated_at": "2026-08-29T12:49:53Z",
"mode": "plan",
"namespace": "tenant-engine",
"observation_minutes": 15,
"planned_actions": [
{
"action_ref": "stage2.helm",
"chart": "/home/worsch/tenant-engine/charts/tenant-engine",
"mode": "plan",
"namespace": "tenant-engine",
"release": "tenant-engine-canary",
"tool": "helm",
"values": "/home/worsch/tenant-engine/values/stage2-canary.yaml"
}
],
"prechecks": [
{
"name": "app.toml",
"required": true,
"status": "passed"
},
{
"detail": "no Stage 2 helm check declared",
"name": "stage2-helm-check",
"required": true,
"status": "failed"
},
{
"detail": "plan mode does not execute helm",
"name": "helm",
"required": false,
"status": "not_required"
},
{
"name": "approval-id",
"required": false,
"status": "required_before_apply"
},
{
"name": "stage1-result",
"required": false,
"status": "recommended_before_apply"
}
],
"release": "tenant-engine-canary",
"requires_approval": true,
"schema_version": "railiance.stage2-deploy-result.v1",
"source": {
"artifact": "image",
"digest_policy": "required",
"revision": "git:main"
},
"stage": "stage2",
"status": "blocked",
"summary": {
"actions_failed": 0,
"actions_total": 0,
"required_prechecks_failed": 1
},
"values": "/home/worsch/tenant-engine/values/stage2-canary.yaml"
}

View file

@ -0,0 +1,92 @@
{
"actions": [],
"app": {
"criticality": "high",
"id": "tenant-engine",
"name": "tenant-engine",
"owner": "infotech",
"repo": "tenant-engine"
},
"app_dir": "/home/worsch/tenant-engine",
"approval_id": null,
"chart": "/home/worsch/tenant-engine/charts/tenant-engine",
"checks_expected": [
"stage2-accepted",
"rollback-target",
"cluster-health",
"operator-approval"
],
"contract": "/home/worsch/tenant-engine/railiance/app.toml",
"evidence_expected": [
"promotion commit",
"new stable digest in previous_stable and deploy/base/tenant-engine.yaml",
"post-promotion make verify-pin"
],
"generated_at": "2026-08-29T12:49:53Z",
"mode": "plan",
"namespace": "tenant-engine",
"planned_actions": [
{
"action_ref": "stage3.helm-promote",
"chart": "/home/worsch/tenant-engine/charts/tenant-engine",
"namespace": "tenant-engine",
"release": "tenant-engine",
"tool": "helm",
"values": "/home/worsch/tenant-engine/values/stage3-production.yaml"
}
],
"prechecks": [
{
"name": "app.toml",
"required": true,
"status": "passed"
},
{
"detail": "/home/worsch/tenant-engine/charts/tenant-engine",
"name": "stage3-chart",
"required": true,
"status": "failed"
},
{
"detail": "/home/worsch/tenant-engine/values/stage3-production.yaml",
"name": "stage3-values",
"required": true,
"status": "failed"
},
{
"detail": "Stage 3 must record the rollback target before promotion",
"name": "previous-stable",
"required": true,
"status": "passed"
},
{
"detail": "plan mode does not execute helm",
"name": "helm",
"required": false,
"status": "not_required"
},
{
"name": "approval-id",
"required": false,
"status": "required_before_apply"
}
],
"previous_stable": "tenant-engine@sha256:a8e8086ffc5b772c1391b166f5e1884b90f7d327b152c205eceae129df555c24",
"promotion_mode": "release-replace",
"release": "tenant-engine",
"requires_approval": true,
"schema_version": "railiance.stage3-promote-result.v1",
"source": {
"artifact": "image",
"digest_policy": "required",
"revision": "git:main"
},
"stage": "stage3",
"status": "blocked",
"summary": {
"actions_failed": 0,
"actions_total": 0,
"required_prechecks_failed": 2
},
"values": "/home/worsch/tenant-engine/values/stage3-production.yaml"
}

View file

@ -25,6 +25,7 @@ dev = [
"pytest>=8.2,<9.0", "pytest>=8.2,<9.0",
"ruff>=0.6,<1.0", "ruff>=0.6,<1.0",
"pyyaml>=6.0,<7.0", "pyyaml>=6.0,<7.0",
"jsonschema>=4.0,<5.0",
] ]
[project.scripts] [project.scripts]

70
railiance/README.md Normal file
View file

@ -0,0 +1,70 @@
# Railiance staged promotion — tenant-engine
TEN-WP-0008 onboards this repo to `railiance.app.v1` without changing the
contract. Production is described, not redeployed.
## T02 decision: kustomize, not Helm, not a dummy chart
The schema accepts any stage `commands` and check `type`s including
`command` and `kubernetes`. This workload ships a multi-document
manifest, now rendered by `deploy/kustomization.yaml` (`kubectl apply -k
deploy/`). That is option 2 in TEN-WP-0008-T02: converting to kustomize is
cheap and matches the small-headless-service precedent
(`qonto-assistant`).
What the schema does **not** require, and what `bin/railiance
deploy|promote --apply` still requires, is a Helm chart plus
`values/stage3-production.yaml`. Plan mode of those commands fails closed
on "no Stage 2 helm check declared" / missing chart. That is a CLI gap,
not a missing field in `app.toml`. Per the workplan we do not paper over
it with a chart we would never apply. Requested of `railiance-bootstrap`
as `RAIL-BS-IN-0001`.
Stage commands in `app.toml` are therefore the kubectl invocations that
actually work against this shape. Stage 1 (`bin/railiance run`) validates
them by rendering both overlays locally.
## Canary
TEN-WP-0009 removed the ReadWriteOnce PVC and switched the Deployment to
`RollingUpdate`, so a second pod is storage-feasible. `canary_mode =
"isolated"` is a distinct Deployment/Service (`tenant-engine-canary`)
whose selector does not match the production Service. It does not take
`user-engine` traffic. Overlay: `deploy/canary/`.
## Recorded stable (T03)
`[stages.stage3].previous_stable` is the contract's copy of "what should
be running":
```
tenant-engine@sha256:a8e8086ffc5b772c1391b166f5e1884b90f7d327b152c205eceae129df555c24
```
That is the TEN-WP-0009 PostgreSQL runtime pin, the same digest
`deploy/base/tenant-engine.yaml` and `make verify-pin` use. Onboarding does
not ship a newer image.
## Rollback ladder
| Digest | Role |
| --- | --- |
| `a8e8086ffc…` | current stable (PostgreSQL) |
| `023ac49f5c…` | previous PostgreSQL image (leased-login ownership defect) |
| `7dc9d7bf1f…` | last SQLite/PVC image — **not** a rollback target after any PostgreSQL write |
Older TEN-WP-0007 pins (`44ca65f3…` / `08be0b1d…` / `2249e8c6…`) remain
the lifecycle/guardrail feature ladder: rolling past a pre-lifecycle
digest removes routes `user-engine` depends on.
## Secrets
`secrets.references = []` is deliberate. Postgres URL and flex-auth
caller token are projected by those platforms from OpenBao. This repo
does not catalogue them.
## Criticality
`high`. Argument is in `[app].description`. Not `critical`: not
internet-facing, holds no application secret, fail-closed on outage.
Not `medium`: flex-auth and key-cape are live consumers.

242
railiance/app.toml Normal file
View file

@ -0,0 +1,242 @@
schema_version = "railiance.app.v1"
# TEN-WP-0008. Modelled on qonto-assistant (small headless service) for
# stage commands, and on flex-auth for an empty secrets list and isolated
# canary. Stage 2/3 *commands* are kubectl against this repo's kustomize
# overlays. `bin/railiance deploy|promote --apply` currently requires a Helm
# chart the schema does not; that gap is requested of railiance-bootstrap
# rather than papered over with a dummy chart. See railiance/README.md.
[app]
id = "tenant-engine"
name = "tenant-engine"
repo = "tenant-engine"
owner = "infotech"
criticality = "high"
description = """
Canonical PIP for tenant-as-an-entity facts. flex-auth consults it live on
high-stakes decisions and key-cape consults it at token issuance. It holds
no application secrets and is not internet-facing, but an outage makes the
PDP fail closed fleet-wide. high, not critical: the PDP itself is high,
this is a data source whose unavailability is deny rather than an
unauthorized allow, and it does not hold a credential of its own.
"""
[source]
revision = "git:main"
artifact = "image"
digest_policy = "required"
[rollback]
strategy = "image-digest"
command = "kubectl -n tenant-engine rollout undo deploy/tenant-engine"
verification = """
make verify-pin agrees repo, spec, and running pod; GET /health returns
200 with store_backend=postgresql; lifecycle routes still present.
Do not roll back to sha256:7dc9d7bf1fcbb3f3ad7035366f4cd44038322995a126b4423142e446ca5c9cd5
(SQLite/PVC) after any PostgreSQL write. The TEN-WP-0007 ladder still
holds for older images: rolling past a pre-lifecycle digest removes
GET/PATCH/retire/reactivate that user-engine depends on.
"""
[[platform.dependencies]]
name = "platform-pg"
kind = "postgres"
required = true
stage = "stage2"
evidence = "rapp-postgres consumer tenant-engine can reach platform-pg:5432; /health reports store_backend=postgresql."
[[platform.dependencies]]
name = "flex-auth-tenant-engine"
kind = "identity"
required = true
stage = "stage2"
evidence = "http://flex-auth-tenant-engine.flex-auth.svc.cluster.local:8080/healthz returns 200."
[[platform.dependencies]]
name = "forgejo-oci"
kind = "other"
required = true
stage = "stage2"
evidence = "Image forgejo.coulomb.social/coulomb/tenant-engine is pullable by digest."
[[platform.dependencies]]
name = "railiance01-k3s"
kind = "network"
required = true
stage = "stage2"
evidence = "kubectl -n tenant-engine get ns reports Active on railiance01."
[secrets]
# Runtime secret custody is OpenBao's. Postgres URL and flex-auth caller
# token are projected files owned by those platforms, not entries this
# repo catalogues. An empty list is the correct answer (TEN-WP-0008-T01).
references = []
[[observability.health_endpoints]]
name = "local-health"
url = "http://127.0.0.1:8090/health"
stage = "stage1"
expected_status = 200
[[observability.health_endpoints]]
name = "cluster-health"
url = "http://tenant-engine.tenant-engine.svc.cluster.local:8090/health"
stage = "stage2"
expected_status = 200
[[observability.health_endpoints]]
name = "canary-health"
url = "http://tenant-engine-canary.tenant-engine.svc.cluster.local:8090/health"
stage = "stage2"
expected_status = 200
[stages.stage1]
enabled = true
namespace = "local"
release = "tenant-engine-local"
commands = [
"make test",
"make validate-app-toml",
"kubectl kustomize deploy",
"kubectl kustomize deploy/canary",
]
checks = ["unit-tests", "contract-schema", "kustomize-production", "kustomize-canary"]
evidence = ["pytest output", "app.toml schema validation", "kustomize render of deploy/ and deploy/canary"]
requires_approval = false
[stages.stage2]
enabled = true
namespace = "tenant-engine"
release = "tenant-engine-canary"
commands = [
"kubectl apply -k deploy/canary --dry-run=server",
"kubectl apply -k deploy/canary",
]
checks = ["server-dry-run", "canary-ready", "canary-health", "operator-approval"]
evidence = ["server-side dry-run of isolated canary", "deploy/tenant-engine-canary Available", "canary /health 200", "State Hub approval id"]
requires_approval = true
canary_mode = "isolated"
observation_minutes = 15
[stages.stage3]
enabled = true
namespace = "tenant-engine"
release = "tenant-engine"
commands = [
"kubectl apply -k deploy --dry-run=server",
"kubectl apply -k deploy",
]
checks = ["stage2-accepted", "rollback-target", "cluster-health", "operator-approval"]
evidence = ["promotion commit", "new stable digest in previous_stable and deploy/base/tenant-engine.yaml", "post-promotion make verify-pin"]
requires_approval = true
promotion_mode = "release-replace"
# TEN-WP-0008-T03: the contract's recorded stable, not only the manifest pin.
# Current production (TEN-WP-0009 PostgreSQL runtime), CI-built from d2a7fe3.
previous_stable = "tenant-engine@sha256:a8e8086ffc5b772c1391b166f5e1884b90f7d327b152c205eceae129df555c24"
[[checks]]
id = "unit-tests"
type = "command"
stage = "stage1"
description = "Run repository unit tests."
required = true
run = "make test"
timeout_seconds = 600
[[checks]]
id = "contract-schema"
type = "command"
stage = "stage1"
description = "Validate railiance/app.toml against railiance.app.v1."
required = true
run = "make validate-app-toml"
timeout_seconds = 60
[[checks]]
id = "kustomize-production"
type = "command"
stage = "stage1"
description = "Render the production kustomization locally, no cluster contact."
required = true
run = "kubectl kustomize deploy"
[[checks]]
id = "kustomize-canary"
type = "command"
stage = "stage1"
description = "Render the isolated canary kustomization locally, no cluster contact."
required = true
run = "kubectl kustomize deploy/canary"
[[checks]]
id = "local-health"
type = "http"
stage = "stage1"
description = "Confirm a locally served /health when one is running."
required = false
url = "http://127.0.0.1:8090/health"
expected_status = 200
timeout_seconds = 10
[[checks]]
id = "server-dry-run"
type = "command"
stage = "stage2"
description = "Server-side dry run of the isolated canary overlay before apply."
required = true
run = "kubectl apply -k deploy/canary --dry-run=server"
[[checks]]
id = "canary-ready"
type = "kubernetes"
stage = "stage2"
description = "Isolated canary Deployment reaches Available."
required = true
namespace = "tenant-engine"
resource = "deploy/tenant-engine-canary"
condition = "Available"
[[checks]]
id = "canary-health"
type = "http"
stage = "stage2"
description = "Isolated canary Service /health returns 200."
required = true
url = "http://tenant-engine-canary.tenant-engine.svc.cluster.local:8090/health"
expected_status = 200
timeout_seconds = 10
[[checks]]
id = "cluster-health"
type = "http"
stage = "stage2"
description = "Production Service /health returns 200."
required = true
url = "http://tenant-engine.tenant-engine.svc.cluster.local:8090/health"
expected_status = 200
timeout_seconds = 10
[[checks]]
id = "operator-approval"
type = "manual"
stage = "stage2"
description = "Human approval recorded before the isolated canary is applied and before the production pin moves."
required = true
evidence_required = "State Hub approval note id, candidate digest, rollback target (previous_stable)."
[[checks]]
id = "stage2-accepted"
type = "manual"
stage = "stage3"
description = "Stage 2 gates passed for the same candidate artifact."
required = true
evidence_required = "State Hub Stage 2 acceptance progress id."
[[checks]]
id = "rollback-target"
type = "manual"
stage = "stage3"
description = "Previous stable digest is recorded in previous_stable before promotion."
required = true
evidence_required = "tenant-engine image digest currently in previous_stable and deploy/base/tenant-engine.yaml."

View file

@ -0,0 +1,67 @@
"""TEN-WP-0008: app.toml contract and kustomize overlays."""
from __future__ import annotations
import subprocess
import sys
import tomllib
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
STABLE = (
"sha256:a8e8086ffc5b772c1391b166f5e1884b90f7d327b152c205eceae129df555c24"
)
def test_app_toml_validates_against_railiance_app_v1() -> None:
pytest.importorskip("jsonschema")
result = subprocess.run(
[sys.executable, str(ROOT / "tests" / "validate_app_toml.py")],
cwd=ROOT,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr + result.stdout
def test_previous_stable_matches_the_manifest_pin() -> None:
contract = tomllib.loads((ROOT / "railiance" / "app.toml").read_text())
assert contract["stages"]["stage3"]["previous_stable"].endswith(STABLE)
manifest = (ROOT / "deploy" / "base" / "tenant-engine.yaml").read_text()
assert STABLE in manifest
assert contract["secrets"]["references"] == []
assert contract["app"]["criticality"] == "high"
assert contract["stages"]["stage2"]["canary_mode"] == "isolated"
def _kustomize(path: str) -> str:
if subprocess.run(["kubectl", "version", "--client"], capture_output=True).returncode != 0:
pytest.skip("kubectl is required to render kustomize overlays")
result = subprocess.run(
["kubectl", "kustomize", path],
cwd=ROOT,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
return result.stdout
def test_production_kustomize_keeps_the_stable_name() -> None:
rendered = _kustomize("deploy")
assert "name: tenant-engine-canary" not in rendered
assert STABLE in rendered
assert "kind: Deployment" in rendered
def test_canary_kustomize_is_isolated_from_production_selector() -> None:
rendered = _kustomize("deploy/canary")
docs = [doc for doc in rendered.split("---") if doc.strip()]
service = next(doc for doc in docs if "kind: Service" in doc and "tenant-engine-canary" in doc)
assert "app.kubernetes.io/name: tenant-engine-canary" in service
deploy = next(doc for doc in docs if "kind: Deployment" in doc)
assert "name: tenant-engine-canary" in deploy
assert "app.kubernetes.io/name: tenant-engine-canary" in deploy
assert STABLE in deploy

View file

@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Validate railiance/app.toml against the railiance.app.v1 schema."""
from __future__ import annotations
import json
import sys
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CONTRACT = ROOT / "railiance" / "app.toml"
SCHEMA_CANDIDATES = [
Path.home() / "railiance-bootstrap" / "schemas" / "railiance-app.schema.json",
Path.home() / "railiance-cluster" / "schemas" / "railiance-app.schema.json",
]
def main() -> int:
schema_path = next((path for path in SCHEMA_CANDIDATES if path.exists()), None)
if schema_path is None:
print(
"railiance-app.schema.json not found in railiance-bootstrap or railiance-cluster",
file=sys.stderr,
)
return 1
try:
import jsonschema
except ImportError:
print("jsonschema is required to validate railiance/app.toml", file=sys.stderr)
return 1
document = tomllib.loads(CONTRACT.read_text())
schema = json.loads(schema_path.read_text())
jsonschema.validate(document, schema)
print(f"valid {CONTRACT.relative_to(ROOT)} against {schema_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

148
uv.lock generated
View file

@ -32,6 +32,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813 }, { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813 },
] ]
[[package]]
name = "attrs"
version = "26.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548 },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.7.22" version = "2026.7.22"
@ -169,6 +178,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 },
] ]
[[package]]
name = "jsonschema"
version = "4.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "jsonschema-specifications" },
{ name = "referencing" },
{ name = "rpds-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630 },
]
[[package]]
name = "jsonschema-specifications"
version = "2025.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "referencing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "26.3" version = "26.3"
@ -431,6 +467,116 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 },
] ]
[[package]]
name = "referencing"
version = "0.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "rpds-py" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 },
]
[[package]]
name = "rpds-py"
version = "2026.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691 },
{ url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542 },
{ url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180 },
{ url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067 },
{ url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509 },
{ url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754 },
{ url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189 },
{ url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750 },
{ url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576 },
{ url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807 },
{ url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187 },
{ url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030 },
{ url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185 },
{ url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394 },
{ url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753 },
{ url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012 },
{ url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203 },
{ url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984 },
{ url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815 },
{ url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545 },
{ url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828 },
{ url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678 },
{ url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811 },
{ url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382 },
{ url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832 },
{ url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011 },
{ url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431 },
{ url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710 },
{ url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454 },
{ url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063 },
{ url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510 },
{ url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495 },
{ url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454 },
{ url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583 },
{ url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919 },
{ url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725 },
{ url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255 },
{ url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060 },
{ url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960 },
{ url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356 },
{ url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319 },
{ url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508 },
{ url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504 },
{ url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380 },
{ url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976 },
{ url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840 },
{ url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282 },
{ url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403 },
{ url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055 },
{ url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419 },
{ url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848 },
{ url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369 },
{ url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673 },
{ url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500 },
{ url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978 },
{ url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350 },
{ url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486 },
{ url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068 },
{ url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600 },
{ url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726 },
{ url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587 },
{ url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585 },
{ url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479 },
{ url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418 },
{ url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123 },
{ url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351 },
{ url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827 },
{ url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966 },
{ url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680 },
{ url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853 },
{ url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715 },
{ url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864 },
{ url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430 },
{ url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877 },
{ url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933 },
{ url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274 },
{ url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763 },
{ url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467 },
{ url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689 },
{ url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340 },
{ url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179 },
{ url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993 },
{ url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909 },
{ url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584 },
{ url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357 },
{ url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533 },
{ url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204 },
{ url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719 },
]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.16.3" version = "0.16.3"
@ -481,6 +627,7 @@ dependencies = [
[package.optional-dependencies] [package.optional-dependencies]
dev = [ dev = [
{ name = "jsonschema" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "ruff" }, { name = "ruff" },
@ -494,6 +641,7 @@ postgres = [
requires-dist = [ requires-dist = [
{ name = "fastapi", specifier = ">=0.115,<1.0" }, { name = "fastapi", specifier = ">=0.115,<1.0" },
{ name = "httpx", specifier = ">=0.27,<1.0" }, { name = "httpx", specifier = ">=0.27,<1.0" },
{ name = "jsonschema", marker = "extra == 'dev'", specifier = ">=4.0,<5.0" },
{ name = "psycopg", extras = ["binary"], marker = "extra == 'postgres'", specifier = ">=3.2,<4.0" }, { name = "psycopg", extras = ["binary"], marker = "extra == 'postgres'", specifier = ">=3.2,<4.0" },
{ name = "psycopg-pool", marker = "extra == 'postgres'", specifier = ">=3.2,<4.0" }, { name = "psycopg-pool", marker = "extra == 'postgres'", specifier = ">=3.2,<4.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.2,<9.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.2,<9.0" },

View file

@ -4,11 +4,11 @@ type: workplan
title: "Bring tenant-engine under the staged-promotion contract" title: "Bring tenant-engine under the staged-promotion contract"
domain: infotech domain: infotech
repo: tenant-engine repo: tenant-engine
status: ready status: finished
owner: claude owner: grok
topic_slug: tenant-guardrails topic_slug: tenant-guardrails
created: "2026-08-16" created: "2026-08-16"
updated: "2026-08-21" updated: "2026-08-29"
depends_on: depends_on:
- TEN-WP-0007 - TEN-WP-0007
unblocks: [] unblocks: []
@ -59,7 +59,7 @@ is a `railiance-bootstrap` decision to request, not one to make here.
```task ```task
id: TEN-WP-0008-T01 id: TEN-WP-0008-T01
status: todo status: done
priority: high priority: high
state_hub_task_id: "0231e4d1-2e1a-4e77-92ba-2cfaf23692ab" state_hub_task_id: "0231e4d1-2e1a-4e77-92ba-2cfaf23692ab"
``` ```
@ -95,11 +95,16 @@ result as non-secret evidence.
Done when the file validates against the schema and stage 1 passes in plan Done when the file validates against the schema and stage 1 passes in plan
mode. mode.
**Done 2026-08-29:** `railiance/app.toml` validates; `bin/railiance run .`
passed (`docs/evidence/TEN-WP-0008-stage1-result.json`). criticality=high
(argument in `[app].description`); `secrets.references = []`; `/health` on
8090; rollback strategy `image-digest`.
## T02 - Reconcile the deployment shape with the contract ## T02 - Reconcile the deployment shape with the contract
```task ```task
id: TEN-WP-0008-T02 id: TEN-WP-0008-T02
status: todo status: done
priority: high priority: high
state_hub_task_id: "5f2ad011-1de8-45df-8e8f-dd6bcf51ece4" state_hub_task_id: "5f2ad011-1de8-45df-8e8f-dd6bcf51ece4"
``` ```
@ -138,11 +143,18 @@ contract still owns the exact canary mode and promotion commands. Current
production digest: production digest:
`sha256:a8e8086ffc5b772c1391b166f5e1884b90f7d327b152c205eceae129df555c24`. `sha256:a8e8086ffc5b772c1391b166f5e1884b90f7d327b152c205eceae129df555c24`.
**Done 2026-08-29:** option 2 — kustomize wrap (`deploy/` production,
`deploy/canary/` isolated). Stage commands are `kubectl apply -k`, verified
by Stage 1 render. `bin/railiance deploy|promote --plan` still requires
Helm; not papered over. Requested as `RAIL-BS-IN-0001`. Evidence:
`docs/evidence/TEN-WP-0008-stage2-plan.json`,
`docs/evidence/TEN-WP-0008-stage3-plan.json`.
## T03 - Record current production as the stable baseline ## T03 - Record current production as the stable baseline
```task ```task
id: TEN-WP-0008-T03 id: TEN-WP-0008-T03
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "c4b79dc1-c280-428e-9dc5-b17c05b084c6" state_hub_task_id: "c4b79dc1-c280-428e-9dc5-b17c05b084c6"
``` ```
@ -156,11 +168,17 @@ copies of the truth rather than one.
Done when the stable digest is recorded through the contract's own mechanism Done when the stable digest is recorded through the contract's own mechanism
rather than only in `deploy/tenant-engine.yaml`. rather than only in `deploy/tenant-engine.yaml`.
**Done 2026-08-29:** `[stages.stage3].previous_stable =
tenant-engine@sha256:a8e8086ffc5b772c1391b166f5e1884b90f7d327b152c205eceae129df555c24`
(current PostgreSQL runtime, same pin `make verify-pin` reads). Stage 3
plan-mode precheck `previous-stable` passed. The T03 draft's `44ca65f3…`
pin was stale after TEN-WP-0009.
## T04 - Hand the pattern back to the fleet ## T04 - Hand the pattern back to the fleet
```task ```task
id: TEN-WP-0008-T04 id: TEN-WP-0008-T04
status: todo status: done
priority: low priority: low
state_hub_task_id: "b816a862-51e0-4abd-a2f3-ee40359526cc" state_hub_task_id: "b816a862-51e0-4abd-a2f3-ee40359526cc"
``` ```
@ -176,11 +194,16 @@ independently.
Done when the note is sent. Done when the note is sent.
**Done 2026-08-29:** note to `flex-auth` (cost of onboarding a non-Helm
workload) and `RAIL-BS-IN-0001` plus a State Hub message to
`railiance-bootstrap`.
## Out of scope ## Out of scope
- **Changing the staged-promotion contract, CLI, or schema.** Request, do not - **Changing the staged-promotion contract, CLI, or schema.** Request, do not
implement. implement.
- **Building a drift-detection service.** The point of this workplan is that - **Building a drift-detection service.** The point of this workplan is that
staged promotion should make one unnecessary. staged promotion should make one unnecessary.
- **Re-deploying.** Production is healthy on `44ca65f3…`; onboarding describes - **Re-deploying.** Production is healthy on `a8e8086ffc…` (TEN-WP-0009);
what is already running, it does not ship anything new. onboarding describes what is already running, it does not ship TEN-WP-0011
or any newer image.