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
6.3 KiB
| id | type | title | domain | repo | status | flavor | depends_on | owner | topic_slug | planning_priority | planning_order | depends_on_workplans | related_workplans | created | updated | state_hub_workstream_id | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| FLEX-WP-0022 | workplan | Tenant scoping is unstated in tenant-engine and untested in two more packages | infotech | flex-auth | finished | implementation |
|
claude | netkingdom | P1 | 220 |
|
|
2026-09-06 | 2026-09-21 | 804c588c-f47a-50c4-bdd7-51b24bbf9539 |
FLEX-WP-0022 — Tenant scoping is unstated in tenant-engine and untested in two more packages
Opened by the sweep in FLEX-DEC-2026-008, which found
secrets-engine.catalog-lane.lifecycle v1 allowing a foreign tenant and then
asked whether the other packages shared the defect.
What the sweep found
| Package | Fixture tenants | Tenant rule | State |
|---|---|---|---|
secrets-engine |
3 distinct | added at v2 | fixed, FLEX-DEC-2026-008 |
qonto-assistant |
2 distinct | wrong_tenant |
fine |
user-engine |
2 distinct | cross_tenant |
fine |
ops-warden |
constant | wrong_tenant |
rule real, fixtures do not vary it |
railiance-platform |
constant | wrong_tenant |
same |
tenant-engine |
constant | none | deployed, unscoped |
Verified against tenant-engine: an allowed tenant.create re-sent under
tenant: tenant:coulomb returns allow / write_api_policy_matched.
Why tenant-engine is not the same fix
secrets-engine sends its own calling identity's tenant, so a constant
known_tenant is the right rule. tenant-engine is different in kind: its
subjects all sit in tenant:platform while its fixtures send
tenant:friendly:binky, so the request tenant names the target of the
operation. A service whose purpose is creating and retiring tenants acts across
them by design, and a constant would break it on its first real call.
The defect today is therefore not "the rule is missing" but "nobody can tell
whether it is missing": a deliberate cross-tenant scope and an omitted rule
look identical in the artifact. That is the same publishing-shape argument
gate-house made for §12's derived-artifact rule.
1. Get the intended tenant relation from tenant-engine
id: FLEX-WP-0022-T01
status: done
priority: high
state_hub_task_id: "a84dcee5-9426-5b30-8dd3-f4c076d00174"
Owner: flex-auth to ask; tenant-engine owns the answer.
- Ask what the CheckRequest
tenantdenotes on the write API: the caller's tenant, the target tenant record, or the tenant a guardrail applies to. - Ask whether any of the nine write actions must be refused cross-tenant, and
whether
tenant.guardrail.read(whichflex-authitself calls) differs.
Gate: the relation is named by tenant-engine, not inferred here. This is the
FLEX-WP-0021-T01 rule applied to a field rather than to an action list.
2026-09-15: asked tenant-engine to name the relation
(e8ba6a53-0093-4dc0-ad70-01f6b8c8e76b). Their live
FlexAuthWriteAuthorizer.authorize copies tenant_id onto both
CheckRequest.tenant and resource.id. That is observation, not admission.
2026-09-20: re-asked after five days of silence, restating the three questions
and saying plainly that "none" is a complete answer. The gate is unchanged: a
policy rule inferred from reading their code would make flex-auth the author
of their tenancy model, which is the boundary FLEX-WP-0021-T01 exists to hold.
T02 stays wait rather than being guessed forward.
2026-09-21: answered by tenant-engine in its own record, TEN-DEC-2026-002
(hub decision ecaebbb7-fe90-4235-a79f-23457e9727bc) and
docs/flex-auth-integration.md, commit d132db0; message 3d3de8bc. Read
from the committed record, not the message. tenant is the target tenant
record; it always equals resource.id; none of the nine write actions is
refused cross-tenant, deliberately; tenant.guardrail.read does not differ and
must not. Gate met: the relation is named by tenant-engine.
2. Encode the relation, or record that there is none
id: FLEX-WP-0022-T02
status: done
priority: high
state_hub_task_id: "712ac826-845f-54bd-8446-f11258d21eb4"
Owner: flex-auth.
If a relation exists, encode it in tenant-engine.write-api.mutate as a new
version — fail-open corrections are visible as version changes
(FLEX-DEC-2026-008) — with a fixture per side.
If the scope is genuinely unrestricted, say so in the package: a stated "this package is deliberately cross-tenant, because the caller administers tenants" is a rule a reviewer can check. Silence is not.
Either way the fixtures must vary tenant, so the suite reports on the field.
2026-09-21: done as tenant-engine.write-api.mutate v3
(FLEX-DEC-2026-016). Both of tenant-engine's commitments are encoded:
allowed requires tenant_is_target (non-empty tenant equal to
resource.id; otherwise deny / tenant_not_target), and the package states
the cross-tenant scope in prose and quantifies it in
test_tenant_never_changes_effect (every action, three subjects, four
tenants). Fixtures now rotate tenant across tenant:friendly:binky,
tenant:acme:prod, tenant:platform, tenant:trial:demo-company, with five
cross-tenant allows and two tenant_not_target denies added: 42 fixtures and
33 embedded tests pass, go test ./... green. One boundary reported back to
tenant-engine rather than papered over: user-engine's grant excludes the
fixed record tenant:platform, which is target-dependent but not a
subject/tenant relation.
3. Vary tenant in the two suites that hold it constant
id: FLEX-WP-0022-T03
status: done
priority: medium
state_hub_task_id: "3058f171-99d2-526b-a1bb-bd7aed87d10a"
Owner: flex-auth.
ops-warden and railiance-platform have working wrong_tenant rules
exercised only by Rego tests. Add a wrong-tenant deny fixture to each so the
fixture suite covers what the rule claims. No policy change and no version bump:
the behaviour is already correct, only the evidence is thin.
Gate: no package's fixture suite holds tenant constant.
2026-09-15: added fixture:ops-warden-wrong-tenant-deny and
fixture:credential-grant-wrong-tenant-deny plus matching
check_request_deny_wrong_tenant.json files. Both suites now vary tenant.
test-policy and flex-auth check return deny / wrong_tenant. No policy
or version change.