tenant-engine/decisions/decisions.md
tegwick 536dedbc80 chore: commit generated work-record index and hub id writeback after rename handoffs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 101414@bnt-lap001
Assistant-Session: 60b5a221-d435-42b9-b778-983c90c7eed6
2026-09-30 19:08:40 +02:00

240 lines
11 KiB
Markdown

# 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`.
---
## TEN-DEC-2026-003 — net-kingdom's combined runtime.yaml reference copy of tenant-engine is retired in favour of pointers
```yaml
id: TEN-DEC-2026-003
kind: decision
title: "net-kingdom's combined runtime.yaml reference copy of tenant-engine is retired in favour of pointers"
status: resolved
disposition: approved
origin: cross-repo
origin_ref: NK-WP-0039-T04
owner: tenant-engine
affects:
- tenant-engine
- net-kingdom
requested_by: net-kingdom
created: "2026-09-30"
updated: "2026-09-30"
decided_by: tenant-engine (the owner of its own deployment declaration)
decided_at: "2026-09-30"
state_hub_decision_id: "666f7804-3e2d-4149-83ae-b0dd7500d34e"
```
**Question (net-kingdom message 2c637dc9 / 9fc740da):** does net-kingdom keep a
reference copy of tenant-engine's objects in `sso-mfa/k8s/tenant-engine/runtime.yaml`,
or retire it?
**Decision: retire it.** The authoritative declaration is this repository:
`deploy/base/tenant-engine.yaml` (applied by `kubectl apply -k deploy/`, TEN-WP-0008),
with the staged-promotion contract in `railiance/app.toml`. net-kingdom's copy
already differs from live in image digest, storage, strategy, environment and egress
(its own 2026-09-23 `kubectl diff`), and no script or workflow consumes it (net-kingdom
searched; the file carries a DO-NOT-APPLY header). A second hand-kept copy can only
drift further. ADR-0015 of net-kingdom already prefers pointers to owner declarations,
and net-kingdom applied that to the flex-auth objects on 2026-09-28.
**What tenant-engine asks of net-kingdom:** replace the five remaining tenant-engine
objects (Namespace, PVC, Deployment, Service, NetworkPolicy) with links to
`tenant-engine/deploy/base/tenant-engine.yaml` and `tenant-engine/railiance/app.toml`,
keeping the DO-NOT-APPLY guard until the file holds no objects. Nothing in this repo
depends on the copy; no cluster change follows from the retirement.
**Not changed:** `deploy/`, `railiance/`, and all runtime names.