# Decision records ## TEN-DEC-2026-001 — Declare Engine / PIP; contest approval lifecycle here ```yaml 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 ```yaml 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`.