secrets-engine.catalog-lane.lifecycle v1 contained no reference to input.tenant — not in well_formed, not in the denial ladder, not in a test. A rotate on lane:glas-primary under tenant:coulomb returned allow against the deployed package (decision:066e629bbf0c0924). Found while answering glas-harness's tenant-alignment request, which had asked for wrong-tenant denial evidence. There was none to return. Three covers failed the same way: every one of the 29 fixtures carried tenant:platform, so the suite could not report on the field; T02's own gate named "wrong-tenant deny" and was recorded done unmet; and the engine hashes tenant into request_digest but never compares it. Four other published packages carry the branch — this one was the outlier. v2 adds wrong_tenant above wrong_system, three Rego tests and three fixtures (28/28, 32/32). The absent-tenant test caught a second defect in the first draft: a bare input.tenant != comparison is undefined on a missing key, so the branch dropped and the ladder reported the wrong rung. request_tenant := object.get(input, "tenant", "") fixes it. v2 supersedes rather than amends v1 because the defect failed open: a consumer pinned to _VERSION=v1 would keep receiving allows with no signal the rule beneath the version string had changed. The earlier dual-control correction stayed at v1 because it denied everything. Replay envelopes regenerated at v2; both request_digest values are byte-identical, so secrets-engine's digest join needs no re-pinning. The sweep this prompted found tenant-engine unscoped on tenant as well — deployed, and verified allowing tenant:coulomb. Not the same fix: its request tenant names the target rather than the caller, so a constant would break it. Recorded and carried by FLEX-WP-0022 rather than patched unilaterally. FLEX-WP-0021 closes at T05; the pin still serves v1 until a redeploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014aQMM1dPXaPiXVn6DwwtLd Assistant: claude-code Assistant-Model: opus Assistant-Process: 715613@bnt-lap001 Assistant-Session: fabd95c1-4c9e-4080-8849-8707ae025f80
3.8 KiB
| id | type | title | domain | repo | status | owner | topic_slug | planning_priority | planning_order | depends_on_workplans | related_workplans | created | updated | |||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| FLEX-WP-0022 | workplan | Tenant scoping is unstated in tenant-engine and untested in two more packages | infotech | flex-auth | proposed | claude | netkingdom | P1 | 220 |
|
|
2026-09-06 | 2026-09-06 |
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: todo
priority: high
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.
2. Encode the relation, or record that there is none
id: FLEX-WP-0022-T02
status: wait
priority: high
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.
3. Vary tenant in the two suites that hold it constant
id: FLEX-WP-0022-T03
status: todo
priority: medium
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.