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 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 63291@bnt-lap001 Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
8.8 KiB
Decision records
TEN-DEC-2026-001 — Declare Engine / PIP; contest approval lifecycle here
id: TEN-DEC-2026-001
kind: decision
title: "Declare Engine / PIP; contest approval lifecycle here"
status: resolved
disposition: approved
origin: cross-repo
origin_ref: TEN-IN-0003
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md
companion: net-kingdom/SECURITY-COMPANION.md
owner: tenant-engine
affects:
- tenant-engine
- gate-house
- net-kingdom
- access-engine
- approval-engine
created: "2026-08-29"
updated: "2026-08-29"
decided_by: tenant-engine (reviewing side)
decided_at: "2026-08-29"
state_hub_decision_id: "34cfa01f-57c8-4576-b8df-6a1b81baaef5"
Context
gate-house asked this repository, via TEN-IN-0003, to declare its layer in
INTENT.md in its own voice, or contest it. Proposed layer: Engine —
tenant-as-an-entity facts. The existing boundary contract was to hold
unchanged. One live question: the reserved guardrail/quota policy was
briefly considered as a home for organizational approval lifecycle before
approval-engine was seeded; if approvals belong nearer tenant governance,
now is the time to say.
The NetKingdom Security Layer Model was accepted at v0.7 on 2026-08-29. The
working companion is net-kingdom/SECURITY-COMPANION.md v0.2. Statute §4
already catalogues this repository as Engine / PIP. Statute §11 says a layer
stated about a repository is not a declaration; only this file, in this
repository's voice, conforms. The previous INTENT.md carried a gate-house
review note naming Engine above a line that admitted the body was unadapted.
Decision
- Declare Engine, role PIP. Same authoritative tenant state yields the
same result. This repository supplies tenant-as-an-entity facts as claims
a decision consumes. It does not render or cache an authorization
decision.
access-engine(flex-authuntil the governed rename) remains the only PDP. The declaration lives inINTENT.mdfrontmatter (layer: Engine,role: PIP) and in the body's own-voice note. - Contest placing approval lifecycle here. A guardrail is a safety
ceiling — a PIP fact. An approval is a durable object owned by
approval-engine(statute §4, §9.4) and consumed as a claim byaccess-engine. Tenant governance will not mint, store, or evaluate approvals. If a tenant mutation should require one, the write stays PEP-shaped: this engine mutates only afteraccess-enginehas allowed it, and that decision may rest on anapproval-engineclaim. - Name the write path PEP-shaped without becoming a PDP. Protected
side effects (create, grant, revoke, plan, lifecycle, grouping,
guardrail mutations) proceed only with a decision record or a recorded
fail-closed unreachable-engine stance. That stance is already the
shipped default; publishing it and persisting decision ids is
implementation, recorded as
TEN-WP-0011, not a change of layer.
Consequences
TEN-IN-0003is closed, absorbed by this decision.- Implementation gaps against v0.7 are not hidden by the declaration.
They are
TEN-WP-0011. - A boundary-contract amendment in
net-kingdomis requested from that 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
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
-
CheckRequest.tenantdenotes 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 sametenant_idonto bothCheckRequest.tenantandresource.id, and that is deliberate, not an artefact. Every call site inapp.pysupplies the target:payload.tenant_idfortenant.create, the{tenant_id}path segment for every other action. -
tenantcannot 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-suppliedactorstring (tenancy.yamlI1, gap I). There is no caller tenant in scope at the pointauthorize()is called. Putting one inCheckRequest.tenantwould mean shipping a value we do not hold. -
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.readandtenant.guardrail.set. The third option is not a distinct semantics in this API; it is the same field seen from the guardrail side. -
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.createin particular has no existing target at check time. Any rule requiring the caller's tenant to equalCheckRequest.tenantwould deny every create.- Authorization on this surface is therefore a service-identity
question over
(subject.id, action).tenantandresource.idcarry 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.
- The write API is a platform-operator admin surface. Its subjects are
platform service identities in
-
tenant.guardrail.readdoes not differ, and must not. flex-auth is the platform PDP and calls it while rendering decisions about arbitrary tenants. A same-tenant rule ontenant.guardrail.readwould break flex-auth itself. Same relation (target tenant record), same unrestricted scope. -
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,
tenantequalsresource.id. A check where they differ did not come from this engine. - Scope: no action in the vocabulary is refused on the relationship
between
subjectandtenant. Varyingtenantacross 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.
- Invariant: on every action tenant-engine sends,
Consequences
docs/flex-auth-integration.mdstates 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.mutatepackage can now carry a stated tenant rule with varying fixtures. PerFLEX-DEC-2026-008that 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.yamlgap 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.