State tenant-engine's tenant relation in the write-api package, v3.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 1m24s

tenant-engine named the relation in TEN-DEC-2026-002: `tenant` is the target
tenant record and always equals `resource.id`; the write API is cross-tenant
by design and `tenant.guardrail.read` does not differ. v2 carried no tenant
rule and a constant fixture tenant, so the deliberate scope and an omitted
rule were indistinguishable.

tenant-engine.write-api.mutate v3 (FLEX-DEC-2026-016):
- allowed requires tenant_is_target; a mismatch or absent tenant is denied
  tenant_not_target (object.get, so an absent key names the right cause).
- the cross-tenant scope is stated in the package and quantified by
  test_tenant_never_changes_effect over every action, three subjects and
  four tenants, with guards against passing by denying everything.
- fixtures rotate tenant across four tenants; five cross-tenant allows and
  two tenant_not_target denies added (42 fixtures, 33 tests, all pass).
- user-engine's tenant:platform exclusion is named as a fixed-record rule,
  not a subject/tenant relation, and tested separately.

Closes FLEX-WP-0022 (T01, T02 done). Also records TEN-IN-0004 and
SECRETS-IN-0002 on FLEX-WP-0020 and acknowledges the GH-DEC-2026-017
replies on FLEX-WP-0030-T04.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
This commit is contained in:
tegwick 2026-09-21 07:39:57 +02:00
parent bd3d270531
commit 3081067325
17 changed files with 783 additions and 150 deletions

View file

@ -1891,3 +1891,56 @@ applies there identically: the repository rename answers nothing about it.
**Reversal condition.** Reverses only by a runtime-rename decision that
supersedes `FLEX-DEC-2026-013`'s retain row for policy vocabulary — and that
decision inherits the PDP-first order above.
## FLEX-DEC-2026-016 — `tenant-engine.write-api.mutate` v3 states its tenant relation: target record, invariant enforced, cross-tenant by design
**Date:** 2026-09-21
**Status:** accepted
**Workplan:** `FLEX-WP-0022-T02`
**Answer relied on:** `TEN-DEC-2026-002` (tenant-engine `decisions/decisions.md`,
hub decision `ecaebbb7-fe90-4235-a79f-23457e9727bc`) and
`tenant-engine/docs/flex-auth-integration.md`, both at tenant-engine `d132db0`
**Context.** v2 had no tenant rule and every fixture carried
`tenant:friendly:binky`, so a deliberate cross-tenant scope and an omitted rule
looked identical — the `FLEX-DEC-2026-008` shape. `flex-auth` declined to infer
the relation from `FlexAuthWriteAuthorizer.authorize`. tenant-engine has now
named it in its own record: `tenant` is the target tenant record; it always
equals `resource.id`; no action is refused on the subject/tenant relationship;
`tenant.guardrail.read` does not differ and must not.
**Decision.** v3 encodes both commitments tenant-engine offered, as rules a
reviewer can check rather than as an absence:
1. **Invariant, enforced.** `allowed` now requires `tenant_is_target`
(`object.get(input, "tenant", "") == input.resource.id`, non-empty). A
mismatch or an absent `tenant` is denied `tenant_not_target`, second rung
of the ladder after `wrong_system` — both mean "this check did not come
from tenant-engine". `object.get` is used so an absent key names the right
cause (`FLEX-DEC-2026-008`'s lesson). tenant-engine offered to leave this as
a documented expectation; we enforce it because it is fail-closed, verified
against `authz.py` (one `tenant_id` copied onto both fields), and denies
nothing tenant-engine sends.
2. **Scope, stated and quantified.** The package prose says the write API is
deliberately cross-tenant and why. The embedded test
`test_tenant_never_changes_effect` evaluates every action for three
subjects across four target tenants (including `tenant:platform`) and
requires one effect per pair; `test_cross_tenant_writes_allowed` and
`test_pdp_guardrail_read_is_cross_tenant` stop that passing by denying
everything. A future same-tenant rule fails the suite.
3. **One boundary named rather than hidden.** `user-engine`'s onboarding grant
(NK-WP-0036) excludes the fixed record `tenant:platform`, so for that one
subject varying `tenant` *does* change the effect. That is a fixed-record
exclusion, not a subject/tenant relation, and it is tested separately; it
is reported back to tenant-engine because its commitment (b) reads as
literally unconditional.
**Fixtures.** Every tenant-engine-subject fixture now carries `tenant` equal to
`resource.id`, rotated across four tenants; five cross-tenant allow fixtures
(three `flex-auth` guardrail reads, two creates) and two `tenant_not_target`
denies (mismatch, absent) were added. 42 fixtures, 33 embedded tests, all pass.
**Version.** v2 → v3. The invariant is a new deny, so this is a visible change
of behaviour for any caller that is not tenant-engine; per
`FLEX-DEC-2026-008` it is a version change, not a silent edit. tenant-engine
asked for no notice period.