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,7 +4,7 @@ PIP ?= $(PYTHON) -m pip
PYTEST ?= $(PYTHON) -m pytest
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:
@echo "make install-dev Create .venv and install runtime + dev dependencies"
@ -12,6 +12,7 @@ help:
@echo "make lint Run syntax and static checks"
@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 validate-app-toml Validate railiance/app.toml against railiance.app.v1"
$(VENV)/bin/python:
python3 -m venv $(VENV)
@ -35,5 +36,8 @@ run: $(VENV)/.dev-installed
# Needs cluster access. tenant-engine runs on railiance01:
# KUBECONFIG=~/.kube/config-railiance01 make verify-pin
validate-app-toml: $(VENV)/.dev-installed
$(PYTHON) tests/validate_app_toml.py
verify-pin:
@./deploy/verify-pin.sh

View file

@ -98,12 +98,12 @@ not done.
| Domain model + SQLite/in-memory store | 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** |
| 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 |
| 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` |
| 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` |
| 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 |

View file

@ -1,6 +1,11 @@
# 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
@ -42,7 +47,7 @@ git push origin main
# 3. Provision the rapp-postgres consumer and credential projections.
# 4. Run the schema migration Job with the migration-role lease.
# 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
```
@ -92,7 +97,7 @@ and periodically — drift is not an event you get told about.
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;
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

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
# 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)
# 3. the digest the running pod actually (status.containerStatuses.imageID)
# resolved
@ -33,7 +33,7 @@ set -uo pipefail
NAMESPACE="tenant-engine"
DEPLOYMENT="tenant-engine"
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
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",
"ruff>=0.6,<1.0",
"pyyaml>=6.0,<7.0",
"jsonschema>=4.0,<5.0",
]
[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 },
]
[[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]]
name = "certifi"
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 },
]
[[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]]
name = "packaging"
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 },
]
[[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]]
name = "ruff"
version = "0.16.3"
@ -481,6 +627,7 @@ dependencies = [
[package.optional-dependencies]
dev = [
{ name = "jsonschema" },
{ name = "pytest" },
{ name = "pyyaml" },
{ name = "ruff" },
@ -494,6 +641,7 @@ postgres = [
requires-dist = [
{ name = "fastapi", specifier = ">=0.115,<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-pool", marker = "extra == 'postgres'", specifier = ">=3.2,<4.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"
domain: infotech
repo: tenant-engine
status: ready
owner: claude
status: finished
owner: grok
topic_slug: tenant-guardrails
created: "2026-08-16"
updated: "2026-08-21"
updated: "2026-08-29"
depends_on:
- TEN-WP-0007
unblocks: []
@ -59,7 +59,7 @@ is a `railiance-bootstrap` decision to request, not one to make here.
```task
id: TEN-WP-0008-T01
status: todo
status: done
priority: high
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
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
```task
id: TEN-WP-0008-T02
status: todo
status: done
priority: high
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:
`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
```task
id: TEN-WP-0008-T03
status: todo
status: done
priority: medium
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
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
```task
id: TEN-WP-0008-T04
status: todo
status: done
priority: low
state_hub_task_id: "b816a862-51e0-4abd-a2f3-ee40359526cc"
```
@ -176,11 +194,16 @@ independently.
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
- **Changing the staged-promotion contract, CLI, or schema.** Request, do not
implement.
- **Building a drift-detection service.** The point of this workplan is that
staged promotion should make one unnecessary.
- **Re-deploying.** Production is healthy on `44ca65f3…`; onboarding describes
what is already running, it does not ship anything new.
- **Re-deploying.** Production is healthy on `a8e8086ffc…` (TEN-WP-0009);
onboarding describes what is already running, it does not ship TEN-WP-0011
or any newer image.