From d132db064fed05e7245c52e27252c147dd4f91a8 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 21 Sep 2026 02:09:55 +0200 Subject: [PATCH] Name what CheckRequest.tenant denotes; record the access-engine rename intake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TEN-DEC-2026-002 answers flex-auth's FLEX-WP-0022-T01, open since 2026-09-15: `tenant` denotes the target tenant record, equals `resource.id` by intent, and the write API is cross-tenant by design — no action is refused on the subject/tenant relationship, and tenant.guardrail.read must not differ because flex-auth itself calls it across tenants. docs/flex-auth-integration.md states the relation in this repo's voice. TEN-IN-0004 is the live record flex-auth asked for on FLEX-WP-0020. Runtime names stay flex-auth (FLEX-DEC-2026-013) and all deploy, cluster and settings coordinates verify as retained; only five documentation repository paths change when the rename lands. Co-Authored-By: Claude Opus 5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 63291@bnt-lap001 Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703 --- WORK-RECORDS.md | 2 + decisions/decisions.md | 118 ++++++++++++++++++++++++++++++++++ docs/flex-auth-integration.md | 44 +++++++++++++ intakes/intakes.md | 57 ++++++++++++++++ 4 files changed, 221 insertions(+) diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 445f64e..2ffd19e 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -79,4 +79,6 @@ | intake | TEN-IN-0001 | closed | yellow | docs/intakes/residuals.md | | intake | TEN-IN-0002 | closed | red | docs/intakes/residuals.md | | intake | TEN-IN-0003 | closed | — | intakes/intakes.md | +| intake | TEN-IN-0004 | open | — | intakes/intakes.md | | decision | TEN-DEC-2026-001 | resolved | — | decisions/decisions.md | +| decision | TEN-DEC-2026-002 | resolved | — | decisions/decisions.md | diff --git a/decisions/decisions.md b/decisions/decisions.md index 23bcb35..f4cccd7 100644 --- a/decisions/decisions.md +++ b/decisions/decisions.md @@ -74,3 +74,121 @@ review note naming Engine above a line that admitted the body was unadapted. workplan: guardrails are no longer reserved, and "not a policy enforcement point" must be restated so it cannot be read as denying the PEP *shape* of our writes. + +--- + +## TEN-DEC-2026-002 — `CheckRequest.tenant` denotes the target tenant record; the write API is cross-tenant by design + +```yaml +id: TEN-DEC-2026-002 +kind: decision +title: "CheckRequest.tenant denotes the target tenant record; the write API is cross-tenant by design" +status: resolved +disposition: approved +origin: cross-repo +origin_ref: FLEX-WP-0022-T01 +owner: tenant-engine +affects: + - tenant-engine + - flex-auth +requested_by: flex-auth +created: "2026-09-21" +updated: "2026-09-21" +decided_by: tenant-engine (the consumer naming its own relation) +decided_at: "2026-09-21" +state_hub_decision_id: "ecaebbb7-fe90-4235-a79f-23457e9727bc" +``` + +## Context + +flex-auth asked, on 2026-09-15 and again on 2026-09-20, for tenant-engine to +*name* the relation rather than infer it from code, and declined to encode a +policy rule from reading `FlexAuthWriteAuthorizer.authorize`. That refusal is +correct: a PDP that infers a consumer's intent and then enforces it has +stopped being a decision point. The custodian's estate-wide review +(`the-custodian/docs/assessments/2026-09-21-layer-declaration-boundaries.md`, +question 8) records this as the only one of nine open estate questions +blocking implementation. + +Three questions were asked. This decision answers all three in +tenant-engine's own words. + +## Decision + +1. **`CheckRequest.tenant` denotes the TARGET tenant record** — the tenant + record the action operates on. Not the caller's tenant. Not, as a separate + meaning, "the tenant a guardrail applies to". + + flex-auth's observation is correct and it *is* the intended relation: + `authorize()` copies the same `tenant_id` onto both `CheckRequest.tenant` + and `resource.id`, and that is deliberate, not an artefact. Every call site + in `app.py` supplies the target: `payload.tenant_id` for `tenant.create`, + the `{tenant_id}` path segment for every other action. + +2. **`tenant` cannot denote the caller's tenant, because tenant-engine does + not know it.** This service verifies no inbound token; the acting identity + arrives as a caller-supplied `actor` string (`tenancy.yaml` I1, gap I). + There is no caller tenant in scope at the point `authorize()` is called. + Putting one in `CheckRequest.tenant` would mean shipping a value we do not + hold. + +3. **For guardrail actions the third reading collapses into the second.** A + guardrail is addressed only through the tenant it constrains, so "the + target tenant record" and "the tenant the guardrail applies to" are the + same tenant on `tenant.guardrail.read` and `tenant.guardrail.set`. The + third option is not a distinct semantics in this API; it is the same field + seen from the guardrail side. + +4. **None of the nine write actions is refused cross-tenant.** This is a + deliberate, unrestricted scope, not an omitted rule: + + - The write API is a platform-operator admin surface. Its subjects are + platform service identities in `tenant:platform`; the targets are + arbitrary tenant records. Creating and retiring tenants is what this + engine is *for*, and it cannot be done from inside the tenant being + created or retired. + - `tenant.create` in particular has no existing target at check time. Any + rule requiring the caller's tenant to equal `CheckRequest.tenant` would + deny every create. + - Authorization on this surface is therefore a **service-identity** + question over `(subject.id, action)`. `tenant` and `resource.id` carry + *which record* is being touched, for the decision record, the audit + trail, and any future per-target scoping — they are not a same-tenant + constraint input today. + +5. **`tenant.guardrail.read` does not differ, and must not.** flex-auth is + the platform PDP and calls it while rendering decisions about arbitrary + tenants. A same-tenant rule on `tenant.guardrail.read` would break + flex-auth itself. Same relation (target tenant record), same unrestricted + scope. + +6. **There is nonetheless a checkable rule, and tenant-engine asks for it to + be stated.** flex-auth is right that an unrestricted scope and an omitted + rule are indistinguishable in a package with no tenant rule and constant + fixtures — that is the defect, and it is ours to remove by saying so. + Two statements tenant-engine commits to, which flex-auth may encode as + rules a reviewer can check: + + - **Invariant:** on every action tenant-engine sends, `tenant` equals + `resource.id`. A check where they differ did not come from this engine. + - **Scope:** no action in the vocabulary is refused on the relationship + between `subject` and `tenant`. Varying `tenant` across fixtures must + not change the effect; that is the assertion we want the suite to make. + + If flex-auth prefers to encode only the second and leave the first as a + documented expectation, that is flex-auth's call as the PDP. + +## Consequences + +- `docs/flex-auth-integration.md` states the relation, the invariant, and the + cross-tenant scope in tenant-engine's voice. That document, not this code, + is the statement of intent. +- flex-auth's `tenant-engine.write-api.mutate` package can now carry a stated + tenant rule with varying fixtures. Per `FLEX-DEC-2026-008` that is a version + bump; tenant-engine expects one and needs no notice period, since the stated + rule matches shipped behaviour and denies nothing that is allowed today. +- Should tenant-engine ever gain a verified inbound identity (`tenancy.yaml` + gap I), the caller's tenant becomes knowable and this decision must be + revisited — but it would be a *new* field, not a change of meaning for this + one. +- Unblocks `FLEX-WP-0022-T02`. diff --git a/docs/flex-auth-integration.md b/docs/flex-auth-integration.md index eecb771..c60fd39 100644 --- a/docs/flex-auth-integration.md +++ b/docs/flex-auth-integration.md @@ -26,6 +26,41 @@ other PIP claim on the check it sends. The check asks whether *this caller* may use *this admin/PIP surface*, not what the tenant is allowed to do on the platform. +## What `tenant` denotes (TEN-DEC-2026-002) + +`tenant` is the **target tenant record** — the tenant the action operates on. +It is *not* the caller's tenant, and on guardrail actions it is not a separate +"tenant the guardrail applies to": a guardrail is addressed only through the +tenant it constrains, so that is the same tenant. + +`tenant` always equals `resource.id`. That is intended, not incidental: one +`tenant_id` is copied onto both fields in `authz.FlexAuthWriteAuthorizer`. +A check where the two differ did not come from this engine. + +`tenant` cannot denote the caller's tenant because this service does not know +it. No inbound token is verified; the acting identity arrives as a +caller-supplied `actor` string (`tenancy.yaml` I1). There is no caller tenant +in scope when the check is built. + +**The write API is cross-tenant by design. No action in the vocabulary is +refused on the relationship between `subject` and `tenant`.** The subjects are +platform service identities in `tenant:platform`; the targets are arbitrary +tenant records. Creating and retiring tenants cannot be done from inside the +tenant concerned, and `tenant.create` has no existing target at check time, so +a same-tenant rule would deny every create. Authorization here is a +service-identity question over `(subject.id, action)`; `tenant` and +`resource.id` say *which record* is being touched, for the decision record and +the audit trail. + +`tenant.guardrail.read` does not differ, and must not: flex-auth calls it while +deciding about arbitrary tenants, so a same-tenant rule on it would break the +PDP itself. + +Varying `tenant` in a conformance fixture must not change the effect. That is +the assertion this engine wants the published package to make — an unrestricted +scope that is *stated* is a rule a reviewer can check; an unrestricted scope +that is merely unwritten is indistinguishable from an omitted one. + Action → resource-type mapping (must match `flex-auth`'s `FLEX-WP-0008-T01` vocabulary exactly — coordinate values, don't diverge): @@ -104,6 +139,15 @@ Tests assert the file equals shipped behaviour. | `TENANT_ENGINE_FLEX_AUTH_TIMEOUT_SECONDS` | `3` | Bounded timeout on the synchronous path — no retries. | | `TENANT_ENGINE_FLEX_AUTH_TOKEN_FILE` | unset | Rotating bearer token file, read on each check. | +## Repository coordinate vs runtime name (TEN-IN-0004) + +The `flex-auth` repository is renaming to `access-engine` (`FLEX-WP-0020`). +**Runtime names stay `flex-auth`** per `FLEX-DEC-2026-013`: the cluster +namespace, the `flex-auth-tenant-engine` Service, the token audience, and this +engine's `TENANT_ENGINE_FLEX_AUTH_*` settings are all unchanged by the rename. +Only the `flex-auth/...` repository paths cited below and above change, and +only once the rename lands. + ## Related - `pep-stance.yaml` — published fail-closed map diff --git a/intakes/intakes.md b/intakes/intakes.md index 8bdf7b7..3a44e5e 100644 --- a/intakes/intakes.md +++ b/intakes/intakes.md @@ -58,3 +58,60 @@ notes: created: '2026-08-29T12:00:00.000000Z' state_hub_intake_id: "01a04cf8-c135-70cb-a24c-33219a9f2985" ``` + +--- + +## TEN-IN-0004 — flex-auth → access-engine repository rename: verify the retained runtime contract + +```yaml +id: TEN-IN-0004 +kind: intake +title: "flex-auth → access-engine repository rename: verify the retained runtime contract" +status: open +origin: cross-repo +origin_ref: FLEX-WP-0020 +priority: low +owner: tenant-engine +requested_by: flex-auth +created: "2026-09-21" +updated: "2026-09-21" +state_hub_intake_id: "01a0c14b-b2f7-78f1-8f6c-e36c952fe004" +description: >- + flex-auth is preparing a repository-coordinate rename to access-engine. + UUID fda8ad85-a7d7-4055-8f21-902a533e59df and Forge ID 42 are unchanged, and + runtime names stay flex-auth per FLEX-DEC-2026-013. tenant-engine was asked to + create a live record and reply with its id, and to verify that documentation + and client configuration continue to use the retained product/runtime contract. +verification: + date: "2026-09-21" + runtime_contract_intact: true + checked: + - "deploy/base/tenant-engine.yaml — TENANT_ENGINE_FLEX_AUTH_URL points at + http://flex-auth-tenant-engine.flex-auth.svc.cluster.local:8080; token + projection audience flex-auth; NetworkPolicy egress selects namespace + flex-auth / app flex-auth-tenant-engine. All runtime names — retained." + - "railiance/app.toml — dependency named flex-auth-tenant-engine with a + healthz evidence URL on the same cluster DNS name. Runtime — retained." + - "src/tenant_engine/flex_auth.py, authz.FlexAuthWriteAuthorizer, the + TENANT_ENGINE_FLEX_AUTH_* settings — module, class and env-var names are + part of tenant-engine's own surface and track the runtime name. Retained." + - "pep-stance.yaml, pip-claims.yaml, layer.yaml — layer.yaml already records + target: 'access-engine (flex-auth)', which is the correct dual form." + followup_on_rename: >- + The only coordinates that change are five repository-path references in + docs/flex-auth-integration.md (flex-auth/schemas/check_request.schema.json, + flex-auth/schemas/decision_envelope.schema.json, + flex-auth/examples/tenant-engine/policy_package.md, + flex-auth/docs/tenancy-posture-review.md, and + flex-auth/workplans/FLEX-WP-0008-tenant-engine-consumer-integration.md). + They are documentation cross-references, not client configuration. This + intake stays open until the rename lands, then they are repointed at + access-engine/ and the intake closes. No runtime change accompanies it. +notes: +- content: >- + This record is not completion evidence for FLEX-WP-0020. It is the live + record flex-auth asked for, and it closes only after the rename lands and + the five documentation coordinates are repointed. + author: tenant-engine + created: "2026-09-21" +```