# Tenant alignment Answer to the `GLAS-WP-0015` production-dependency handoff question, which asked this engine to state its exact tenant values and produce wrong-tenant denial evidence rather than assume a mapping. Related: `KEY-WP-0013-T02`, `APPROVAL-WP-0002-T01`, `SECRETS-WP-0009-T03`, `FLEX-WP-0021-T05`. The handoff's framing is adopted here without reservation: **spelling similarity is not a mapping**, and JWT/store comparison is exact. A tenant "mapping" invented by a consumer is the same fail-open shape gate-house rejected for action vocabularies in `GH-DEC-2026-008` — a translation can be confidently wrong — arriving by a different road. ## The three values, as this repo actually holds them | Value | Where it lives | Owner | | --- | --- | --- | | CheckRequest tenant | `authorization.REQUEST_TENANT` = `tenant:platform` | flex-auth package | | KeyCape JWT tenant | `service_auth.TENANT` = `tenant:coulomb` | key-cape | | Approval store tenant | `platform` (not held here) | approval-engine | ### CheckRequest tenant: `tenant:platform`, and it was missing entirely `secrets-engine.catalog-lane.lifecycle` **v2** reads ```rego known_tenant := "tenant:platform" request_tenant := object.get(input, "tenant", "") ``` and its `wrong_tenant` first-denial branch fires on anything else. `object.get` with a `""` default is deliberate: **an absent tenant is a denial, not an ignored field.** `build_action_request` emitted no `tenant` field at all. Every gated action this engine sent would have been denied `wrong_tenant` by the deployed v2 package — and, worse, the omission also produced a `request_digest` that could match no correctly issued decision, because `tenant` is hashed material. Fixed: the request now carries `REQUEST_TENANT`, an empty tenant is refused at build time, and the constant is pinned against the vendored allow envelopes so a package retenanting surfaces as a test failure rather than a production denial. ### KeyCape JWT tenant: `tenant:coulomb` — and that is the value the package denies This is the uncomfortable part and it is stated plainly rather than smoothed over. `service_auth.TENANT` is `tenant:coulomb`, preflighted on the KeyCape `client_credentials` token for the accepted `secrets-engine-openbao` service identity. `tenant:coulomb` is **exactly** the value flex-auth used to demonstrate a wrong-tenant denial (`decision_wrong_tenant_deny.json`, `matched_rule: wrong_tenant`). Two readings are possible and this engine does not choose between them: 1. They name **two different layers** — an identity/tenancy tenant for the KeyCape client, and a resource-scoping tenant for the policy package — which happen to use one namespace format. 2. One of the two constants is **wrong**. Reading 1 is plausible and is probably right, but "probably right" is not a contract. Both constants stay as they are, deliberately not unified behind one symbol, until an owner-reviewed mapping exists with a decision reference. This engine will consume that mapping; it will not author it. ### Approval store tenant: `platform` Not held in this repo. Noted only because the handoff asked for all three: the comparison between `platform` and `tenant:platform` is a prefix difference, and a prefix difference is precisely the kind of similarity that must not be treated as identity without an owner saying so. ## Wrong-tenant denial evidence From flex-auth's real v2 deny envelope, vendored at `tests/fixtures/flex-auth-replay/decision_wrong_tenant_deny.json`: ```text decision:7d56b7fc274ddfd6 effect: deny reason: wrong_tenant matched_rule: wrong_tenant policy_version: v2 binding.tenant: tenant:coulomb binding.request_digest: sha256:c9c6e6f8...0d20 ``` flex-auth's fixture varies the tenant on an otherwise-valid `rotate`, so a deny proves the tenant alone carried it. `tests/test_decision_replay.py` asserts our consumer refuses it on `effect` before anything else, and separately pins that a deny carries **no** `lifetime` — a consumer checking lifetime before effect would raise a confusing missing-field error on a well-formed denial. ## Why v1 must not be pinned `v1` shipped and was deployed with no reference to `input.tenant` at all. Every fixture carried `tenant:platform`, so the package's own coverage could not notice, and `FLEX-WP-0021-T02`'s "wrong-tenant deny" gate was recorded as met when it was not. A `rotate` under `tenant:coulomb` returned **allow** against the deployed v1 package (`decision:066e629bbf0c0924`). flex-auth superseded v1 rather than amending it, on the principle that **a fail-open correction has to be visible as a version change; a fail-closed one does not.** A consumer still pinned to `v1` would keep receiving allows it should never have had, unable to tell from the version string that the rule moved underneath it. `test_the_superseded_v1_package_is_not_accepted` pins that this engine refuses a v1 decision. Note the ownership point flex-auth recorded with it: the evaluator hashes `tenant` and carries it in the decision record, but nothing in the evaluation path compares it. **Tenant scoping is the policy package's job, and a package that omits it is not scoped to a tenant at all.** Our own omission was the mirror-image defect on the consumer side, and neither side's tests could see it alone. ## Still open - The owner-reviewed JWT/store/CheckRequest mapping, with a decision reference. Until it exists, no live client or policy subject changes here. - A supported owner access path to `flex-auth-secrets-engine` for a workstation CLI. Service DNS is not workstation connectivity, so the tenant fix above is necessary but not sufficient for activation (`FLEX-WP-0021-T05`).