tenant-engine/decisions/decisions.md
tegwick d132db064f Name what CheckRequest.tenant denotes; record the access-engine rename intake
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
2026-09-21 02:09:55 +02:00

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

  1. 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-auth until the governed rename) remains the only PDP. The declaration lives in INTENT.md frontmatter (layer: Engine, role: PIP) and in the body's own-voice note.
  2. 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 by access-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 after access-engine has allowed it, and that decision may rest on an approval-engine claim.
  3. 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-0003 is 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-kingdom is 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

  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.