diff --git a/INTENT.md b/INTENT.md index 99769df..b4fc91e 100644 --- a/INTENT.md +++ b/INTENT.md @@ -1,44 +1,96 @@ --- repo: tenant-engine -updated: "2026-07-23" +updated: "2026-08-29" +layer: Engine +role: PIP +standard: netkingdom-security-layer-model +standard_version: "0.7" +companion: net-kingdom/SECURITY-COMPANION.md --- # INTENT -> **NetKingdom layering review — 2026-08-28.** This repository's role was reviewed -> against the NetKingdom IT-security layer model: **Taxonomy → Tooling → Engines → -> Staff**, layered by determinism and by the kind of artifact each layer produces. -> Findings and the argument behind them: -> `gate-house/history/2026-08-28-security-layer-model-and-gate-house-recut.md`. -> The model is `net-kingdom/canon/standards/security-layer-model_v0.1.md` (proposed), -> ratified by `gate-house/decisions/decisions.md` GH-DEC-2026-001. +> This file captures **why this repository exists**, the **direction it is +> moving toward**, and the **kind of system it is meant to become**. +> It is intentionally **aspirational and stable**, not a description of +> current implementation. See `SCOPE.md` for what is implemented today. + +> **Layer: Engine. Role: PIP.** This is the declaration required by +> NetKingdom Security Layer Model v0.7 §11 and the working companion v0.2. +> It is this repository's own voice, not a catalog row or a review note +> written about us. The statute is +> `net-kingdom/canon/standards/security-layer-model_v0.7.md`; the companion +> is `net-kingdom/SECURITY-COMPANION.md`. On disagreement the statute +> governs, and a disagreement is a finding for `gate-house`. > -> The layer rule that binds every repository: **Staff never touches tooling -> directly. It acts only through engine APIs.** +> **The test we pass, and the role we hold.** The same authoritative tenant +> state always yields the same result. That is why we are an Engine, and why +> we are a **PIP**: we supply tenant-as-an-entity facts as claims a decision +> consumes. We are not a PDP. `access-engine` (`flex-auth` until the governed +> rename) is the only policy decision point in the estate. A new engine is a +> PIP unless the statute is amended; we do not become a second one by +> compiling, caching, or resolving anything that determines an outcome. > -> **This repository is Engine — deterministic API for a modeled concept.** Add the layer label. The existing boundary contract (`net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md`) holds unchanged. Same clarification as user-engine: tenancy context is an input to the decision, not a decision. +> **The binding rule, read from this side.** Staff never touches Tooling +> directly. It acts only through Engine APIs. This repository *is* one of +> those APIs. We do not hold a client for a catalogued Tooling-layer system +> (`key-cape`, OpenBao). Persistence we own (PostgreSQL in production, SQLite +> in development) is uncatalogued infrastructure, listed so the check is +> total, not so it can be mistaken for a Tooling contact. > -> *This note records what should change. The body below is not yet adapted.* +> **Writes are PEP-shaped; reads are PIP.** Mutating a tenant, a role grant, +> a plan assignment, a grouping, or a guardrail is a protected side effect. +> It proceeds only with a decision from `access-engine` identifying the +> request, or with a recorded application of a published unreachable-engine +> stance — and that stance, for this engine, is fail-closed. Returning a +> tenant fact is not a side effect; it is the PIP job. We never cache an +> authorization decision, and we never treat a compiled ceiling or a cached +> `tenant_roles` claim as one. +> +> **Approvals are not ours.** The reserved guardrail/quota concern was +> briefly considered as a home for organizational approval lifecycle before +> `approval-engine` was seeded. We contest that placement. A guardrail is a +> safety ceiling — a PIP fact `access-engine` joins with observed +> consumption. An approval is a durable, authenticated, consumable object +> owned by `approval-engine` (§4, §9.4) and consumed as a claim. Tenant +> governance answers *what this tenant is and how far it may go*. It does +> not mint, store, or evaluate approvals. + +--- + +## One-liner + +**Canonical PIP for tenant-as-an-entity facts: existence, onboarding +grouping, capability roles, plan assignment, and guardrail ceilings — +consumed as claims, never as decisions.** + +--- ## Purpose `tenant-engine` exists to be the single, canonical source of truth for what a **tenant** is on the NetKingdom platform — its existence, its onboarding -grouping, its capability roles, and its plan/subscription assignment — -separate from who its *users* are (`user-engine`), who *authenticates* it -(`key-cape`/Keycloak), who *authorizes* actions against it (`flex-auth`), and -what it *pays* for a given plan (`adaptive-pricing`). +grouping, its capability roles, its plan/subscription assignment, and the +safety ceilings that bind it — separate from who its *users* are +(`user-engine`), who *authenticates* it (`key-cape`/Keycloak), who +*authorizes* actions against it (`access-engine` / `flex-auth`), what it +*pays* for a given plan (`adaptive-pricing`), and whether an act has been +*approved* (`approval-engine`). -Full ownership boundary, formally: `net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md`. +Full ownership boundary, formally: +`net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md`. Where +that contract and this file disagree after the layer-model recut, the +statute wins, this file records the intended state, and the contract is +amended — not the other way around. ## Why This Exists -No service in the fleet currently owns tenant-as-an-entity facts as a -queryable resource. `user-engine`'s own boundary contract explicitly scopes -it to *consuming* tenant identifiers and *storing records scoped by* a -tenant — not owning the tenant record itself. The IAM Profile defines the -*shape* of the `tenant` claim, not a live directory of which tenants exist, -what they're allowed to do, or what plan they're on. +No service in the fleet owns tenant-as-an-entity facts as a queryable +resource. `user-engine`'s own boundary contract explicitly scopes it to +*consuming* tenant identifiers and *storing records scoped by* a tenant — +not owning the tenant record itself. The IAM Profile defines the *shape* of +the `tenant` claim, not a live directory of which tenants exist, what they +are allowed to do, or what plan they are on. That gap became concrete during Binky Hedgehog GmbH's onboarding as the platform's first tenant outside `tenant:coulomb` @@ -50,60 +102,85 @@ real governance model (role grants are usually a consequence of a paid plan). See `net-kingdom/docs/adr/ADR-0013-tenant-onboarding-grouping-taxonomy.md` and `ADR-0014-tenant-capability-roles-and-tenant-engine-ownership.md`. +The layer model now names the same gap more sharply. `access-engine` must +consume tenant facts as claims. A stale `VEN` grant surviving a plan +cancellation is not an acceptable input to a money-movement or +credential-vending decision. That is why this engine exists as a live PIP, +not only as a token-issuance cache. + ## Primary Utility `tenant-engine` provides: -- **Tenant records**: existence, identifier, and onboarding grouping - (`trial`/`friendly`/`single`/`small`/`medium`/`large`/`enterprise`/ - `consumer`/`family`/`community`/`association`/`agentic`, per ADR-0013). +- **Tenant records**: existence, identifier, lifecycle, and current + onboarding grouping (`trial`/`friendly`/`single`/`small`/`medium`/ + `large`/`enterprise`/`consumer`/`family`/`community`/`association`/ + `agentic`, per ADR-0013). The identifier's grouping segment is historical; + current grouping is a field, not a parse. - **Capability role grants**: `PLTF`/`IAM`/`VEN`/`CUS`, with an audited grant/revoke trail linking most grants to a plan-assignment change. - **Plan/subscription assignment**: which `adaptive-pricing` plan a tenant currently holds, referenced by id — never a duplicated copy of plan terms. +- **Guardrail/quota policy**: spend, entity-count, and action-count + ceilings. A guardrail is a safety ceiling, not an entitlement, not a + meter, and not a decision. `trial`-grouped tenants default to a **zero** + spend budget. - **A cache-read API** `key-cape` calls at token-issuance time to source the - optional `tenant_roles` token claim (IAM Profile v0.3). -- **A live-lookup API** `flex-auth` calls synchronously for privileged, - high-stakes decisions — the case where a cached claim isn't trustworthy - enough (a stale `VEN` grant surviving a plan cancellation is not an - acceptable risk for money-movement or credential-vending actions). -- Reserved, not yet implemented: **guardrail/quota policy** (spend limits, - entity/action counts) — `trial`-grouped tenants default to a **zero** - spend budget; design is future work. + optional `tenant_roles` token claim (IAM Profile v0.3). The claim is a + point-in-time cache with the token's lifetime; it is never the live fact. +- **A live-lookup API** `access-engine` calls synchronously for privileged, + high-stakes decisions — the case where a cached claim is not trustworthy + enough. Unavailability on this path fails closed at the consumer, never + open. +- **A write API** for grant/revoke/plan/lifecycle/grouping/guardrail + mutations. Writes are PEP-shaped: authorization-gated by `access-engine`, + never self-authorized, never performed without a decision record or a + recorded fail-closed stance. ## Intended Users - `key-cape` / Keycloak, at token issuance time (cache-read API). -- `flex-auth`, at authorization-decision time (live-lookup API, and the - target of write-authorization for grant/revoke/plan mutations). +- `access-engine` (`flex-auth`), at authorization-decision time (live-lookup + API), and as the authorizer of every protected write. - Operators and tenant administrators, indirectly, through whatever admin - surface eventually consumes `tenant-engine`'s APIs (not yet built). + surface consumes these APIs (`user-engine`'s operator UI is the first). - `adaptive-pricing`, as the source `tenant-engine` references for plan *definitions* (not the other way around). +- Staff repositories and agents, only through these Engine APIs — never by + reaching the store, and never by treating a callable tool as permission. ## Strategic Role `tenant-engine` sits **beside** `user-engine`, not inside it — a deliberate bounded-context split, not an oversight. `user-engine`'s aggregate root is the *person*; `tenant-engine`'s is the *tenant*. Consumers that only need a -fast, security-critical tenant-role lookup (`flex-auth` on every privileged -decision, `key-cape` on every token issuance) should never need to depend on -`user-engine`'s much larger surface (registration flows, factor models, -family dataspace onboarding) to get it. This matches the fleet's existing -convention of many small, single-purpose `-engine`/`-core` services rather -than one large one. +fast, security-critical tenant-role lookup (`access-engine` on every +privileged decision, `key-cape` on every token issuance) should never need +to depend on `user-engine`'s much larger surface (registration flows, factor +models, family dataspace onboarding) to get it. + +In the layer model this is the PIP half of that split. Tenancy context is an +**input to the decision, not a decision**. Compiling an effective guardrail, +resolving current grouping, or listing active roles does not decide whether +an actor may act. `access-engine` does that, joining these claims with +identity, zone, approval, maturity, and policy. Request-scoped memoization +inside a single decision is allowed; cross-request caching of live-lookup +results is not — that would recreate the staleness the live path exists to +avoid, and it would be compiled data determining an outcome (§6.1). ## Responsibility Boundary `tenant-engine` owns tenant-as-an-entity facts. It never re-derives or -duplicates facts another system already owns. +duplicates facts another system already owns, and it never renders a +decision another engine is responsible for. ### tenant-engine Owns -- Tenant existence, identifier, and onboarding grouping. +- Tenant existence, identifier, lifecycle, and current onboarding grouping. - Capability role grants and their audit trail. - Plan/subscription assignment (by reference, not by copying plan terms). -- Reserved: guardrail/quota policy, once designed. +- Guardrail/quota policy: the ceiling, not the meter, not the bill, not the + allow. ### Other Systems Own @@ -112,37 +189,72 @@ duplicates facts another system already owns. | OIDC discovery, login, token issuance | `key-cape`, Keycloak | | Tenant identifier format and role vocabulary | NetKingdom (IAM Profile, ADR-0013, ADR-0014) | | Users, profiles, memberships | `user-engine` | -| Authorization decisions | `flex-auth` | +| Authorization decisions | `access-engine` (`flex-auth`) — the only PDP | +| Approval objects | `approval-engine` | | Pricing-model / plan definitions | `adaptive-pricing` | | Runtime secrets | OpenBao / `secrets-engine` | +| Audit event custody | `audit-core` | | Payment processing | Not yet identified — `tenant-engine` records plan assignment, not payment execution | +| Containment / actuation | Unowned and held at zero estate-wide (§9.2). This engine does not acquire it. | ## Non-Goals - Not an identity provider. Never issues, verifies, or refreshes tokens. - Not a policy decision point. Never makes an authorization decision itself - — it answers queries `flex-auth` issues. + — it answers queries `access-engine` issues, and it mutates only when + `access-engine` has allowed the mutation. +- Not an approval engine. Never stores, supersedes, or consumes an approval + object. - Not a billing processor. Records plan assignment, not payment execution. - Not a pricing-model engine. References `adaptive-pricing` plan ids. - Not the user/membership domain. Never stores user accounts or profiles. +- Not a meter. Never observes spend or counts actions. +- Not Tooling. Persistence it owns is infrastructure for its own facts, not + a Tooling-layer service other layers may call. +- Not a place to plan around production observation or automatic + containment. Companion §10: nothing is observed in production, and + nothing can be contained automatically. Neither fact is this engine's to + close. + +## Evidence Bound + +Every mutation emits a domain event. That trail is **attributive** unless a +named control starts branching on an event's presence, in which case that +class becomes **load-bearing** and must emit atomically, locally, and with +reconciliation or a heartbeat (§9.6). + +The archive — wherever it lives — proves that the records it holds were not +altered or truncated after arrival. It does not prove that an event +happened, and absence of a record is not evidence of non-occurrence. Local +events that share a database and runtime credential with the facts they +attest to cannot be described as independent custody. Independent custody +belongs to `audit-core`. ## Governing Principle This repository should stay focused on the purpose above. Work that changes -its authority, ownership boundaries, or operational promises should be -captured in a workplan before implementation, and should stay consistent -with `net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md` — -if the two drift, the canon contract wins and this file should be corrected. +its authority, ownership boundaries, layer, or operational promises should +be captured in a workplan before implementation, and should stay consistent +with the security layer model and +`net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md`. If +the three drift: the statute wins, then the contract, then this file. ## Related -- `docs/flex-auth-integration.md` — how the write API's `WriteAuthorizer` - seam is implemented against a real `flex-auth` -- `net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md` — the - formal ownership contract +- `SCOPE.md` — what is implemented today, and what is not +- `history/2026-08-29-security-layer-intent-scope-review.md` — scope vs + intent vs implementation after v0.7 acceptance +- `workplans/TEN-WP-0011-security-layer-conformance.md` — the evolution + this declaration requires +- `decisions/decisions.md` `TEN-DEC-2026-001` — layer declaration and the + approvals contest +- `docs/flex-auth-integration.md` — write-API `WriteAuthorizer` seam +- `docs/tenant-guardrail-policy.md` — ceiling contract +- `docs/tenant-lifecycle-api.md` — update, retire, reactivate +- `net-kingdom/SECURITY-COMPANION.md` — start here for the layer rules +- `net-kingdom/canon/standards/security-layer-model_v0.7.md` — the statute +- `net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md` - `net-kingdom/canon/standards/iam-profile_v0.3.md` — the `tenant_roles` claim and carrying mechanism this service implements - `net-kingdom/docs/adr/ADR-0013-tenant-onboarding-grouping-taxonomy.md` - `net-kingdom/docs/adr/ADR-0014-tenant-capability-roles-and-tenant-engine-ownership.md` -- `key-cape/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md` — the - first tenant this service will record diff --git a/SCOPE.md b/SCOPE.md index f4cce93..3213608 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -2,65 +2,133 @@ > Lightweight boundary for agents and contributors. Full contract: > `net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md`. +> Aspirational role: `INTENT.md`. Layer declaration: Engine / PIP +> (`INTENT.md` frontmatter; `TEN-DEC-2026-001`). --- ## One-liner -Canonical owner of tenant-as-an-entity facts for NetKingdom: existence, -onboarding grouping, capability roles, and plan/subscription assignment. +Canonical PIP of tenant-as-an-entity facts for NetKingdom: existence, +lifecycle, onboarding grouping, capability roles, plan/subscription +assignment, and guardrail ceilings. --- ## Core Idea -`tenant-engine` answers "what is this tenant, what can it do, and what plan -is it on" for two consumers: `key-cape` (cached, at token issuance) and -`flex-auth` (live, for high-stakes decisions). It never stores users, -issues tokens, or makes authorization decisions. +`tenant-engine` answers "what is this tenant, what roles does it hold, what +plan is it on, and how far may it go" for two consumers: `key-cape` +(cached, at token issuance) and `access-engine` / `flex-auth` (live, for +high-stakes decisions). It never stores users, issues tokens, evaluates +approvals, or makes authorization decisions. Tenancy context is an input to +the decision, not a decision. + +--- + +## Layer + +| Key | Value | +| --- | --- | +| Layer | **Engine** | +| Role | **PIP** | +| Statute | `net-kingdom/canon/standards/security-layer-model_v0.7.md` | +| Companion | `net-kingdom/SECURITY-COMPANION.md` | +| Catalog row | tenant-as-an-entity facts (`§4`) | +| Tooling contacts | none (`key-cape` and OpenBao are not clients of this repo) | +| Non-Tooling clients | PostgreSQL / SQLite (own store); `flex-auth` `POST /v1/check` (Engine API on the write and authorized-read path); State Hub (work records, not a runtime dependency) | +| Write path | PEP-shaped: fail-closed when `access-engine` is unset or unreachable | +| Conformance today | Declaration is in our own voice. Mechanical `layer.yaml` check, persisted decision records, published stance map, and independent evidence custody are **not** yet shipped — see the review and `TEN-WP-0011`. | --- ## In Scope -- Tenant records: identifier, onboarding grouping (ADR-0013). +- Tenant records: identifier, lifecycle (`active` / `retired`), current + onboarding grouping (ADR-0013). Grouping is mutable through an audited + reclassification, not by renaming `tenant_id`. - Capability role grants/revocations: `PLTF`/`IAM`/`VEN`/`CUS` (ADR-0014), audited, mostly plan-linked. - Plan/subscription assignment, referenced by `adaptive-pricing` plan id. +- Guardrail/quota *policy*: spend / entity-count / action-count ceilings, + resolved as a total function of grouping, plan, override, and lifecycle. + Seeded key: `spend.monthly`. `trial` defaults to zero. - Cache-read API for `key-cape` (token-issuance-time `tenant_roles` claim - source). -- Live-lookup API for `flex-auth` (high-stakes, `aal2`-class decisions). -- Write API for grant/revoke/plan mutations, authorization-gated by - `flex-auth`. -- Reserved, not yet implemented: guardrail/quota policy (spend limits, - entity/action counts). + source): `GET /tenants/{id}/roles`. +- Live-lookup API for `access-engine` (high-stakes, `aal2`-class + decisions): `GET /tenants/{id}/roles/live`. Store unavailability returns + `503`, never `200` plus empty roles. +- Write API for create / grant / revoke / plan / metadata / retire / + reactivate / grouping / guardrail mutations, authorization-gated by + `flex-auth`. Unset or unreachable authorizer denies. +- Authoritative tenant read with ETag / If-Match / idempotency on + lifecycle and guardrail mutations. ## Out of Scope - Identity, authentication, MFA, token issuance — `key-cape`/Keycloak. -- Authorization decisions — `flex-auth` is the PDP; `tenant-engine` is a - data source it queries, never the other way around. +- Authorization decisions — `access-engine` is the PDP; `tenant-engine` is + a data source it queries, never the other way around. +- Approval objects and approval lifecycle — `approval-engine`. Contested + in `TEN-DEC-2026-001`; this repo will not grow one. - Users, profiles, memberships — `user-engine`. `tenant_id` is the only key shared between the two services. - Pricing-model / plan term definitions — `adaptive-pricing`. - Payment processing — not yet identified as owned by any repo. - Runtime secret custody — OpenBao / `secrets-engine`. +- Metering / consumption counters — whoever meters the resource writes + them; this service does not observe spend. +- Rate limiting / traffic shaping — gateway concern, not `action_count`. +- Containment / actuation — unowned and held at zero estate-wide. - Irreversible operational decisions without human approval. +- Direct Tooling-layer clients — Staff and agents reach tenant facts + through this Engine, not past it. ## Current State -- Status: bootstrapping. No implementation yet — see - `workplans/TEN-WP-0001-statehub-bootstrap.md` and the first real - implementation workplan it seeds. -- Ownership contract (`tenant-engine-boundary-contract_v0.1.md`) and the two - governing ADRs (0013, 0014) are ratified in `net-kingdom` and predate this - repo's code, by design. +Production service, not a bootstrap. Finished workplans `TEN-WP-0001` +through `TEN-WP-0007`, `TEN-WP-0009`, and `TEN-WP-0010` shipped the domain +model, the three boundary-contract APIs, the `flex-auth` write authorizer, +lifecycle, guardrails, PostgreSQL as the production store, and mutable +grouping. `TEN-WP-0008` (staged-promotion onboarding) is still `ready`, +not done. + +| Surface | Shipped | Notes | +| --- | --- | --- | +| Domain model + SQLite/in-memory store | yes | `TEN-WP-0002` | +| Cache-read / live-lookup / write APIs | yes | `TEN-WP-0002` | +| `flex-auth` `WriteAuthorizer` | yes | Fail-closed; `is_allowed()` returns a bool and **discards the decision envelope** | +| Production runtime | yes | `TEN-WP-0004`; image pin in `deploy/tenant-engine.yaml` | +| Lifecycle (update / retire / reactivate) | yes | `TEN-WP-0005`; no hard-delete | +| Guardrail policy | yes | `TEN-WP-0006` / `0007`; `docs/tenant-guardrail-policy.md` | +| PostgreSQL production store | yes | `TEN-WP-0009`; deploy mounts `TENANT_ENGINE_DATABASE_URL_FILE` | +| Mutable grouping | yes | `TEN-WP-0010`; identifier segment stays historical | +| Staged promotion | **no** | `TEN-WP-0008` | +| Machine-readable `layer.yaml` | **no** | Frontmatter declaration is in `INTENT.md` | +| Persisted `authorization_decision_id` | **no** | Boundary contract requires it; `FlexAuthCheckClient` drops the envelope | +| Published unreachable-engine stance map | **no** | Behaviour is fail-closed in code; companion §5 requires it published and tested equal to shipped behaviour | +| Independent audit-core emission | **no** | Local `events` table only (`TEN-IN-0001`) | +| Bounded event-read interface | **no** | In-process `TenantStore.events()` is unfiltered (`TEN-IN-0002`) | +| Claim freshness / input-class lifetime | **no** | Token lifetime covers the cache-read path by profile convention; live-lookup has no declared deadline | + +The boundary contract still labels guardrail policy "reserved, not +implemented" and still calls this repo "not a policy enforcement point". +Both sentences are stale: guardrails shipped, and the write path is +PEP-shaped even though this repo is not a PDP. Amendment is a +`net-kingdom` change, requested from `TEN-WP-0011`. ## Getting Oriented - Start with: `INTENT.md` +- Working companion: `net-kingdom/SECURITY-COMPANION.md` +- Statute: `net-kingdom/canon/standards/security-layer-model_v0.7.md` - Ownership contract: `net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md` - Claim/carrying mechanism: `net-kingdom/canon/standards/iam-profile_v0.3.md` +- Layer declaration: `decisions/decisions.md` `TEN-DEC-2026-001` +- Review: `history/2026-08-29-security-layer-intent-scope-review.md` +- Evolution workplan: `workplans/TEN-WP-0011-security-layer-conformance.md` - flex-auth write-authorization integration: `docs/flex-auth-integration.md` +- Guardrail contract: `docs/tenant-guardrail-policy.md` +- Lifecycle contract: `docs/tenant-lifecycle-api.md` - Agent instructions: `AGENTS.md`, `CLAUDE.md` - Workplans: `workplans/` diff --git a/decisions/decisions.md b/decisions/decisions.md new file mode 100644 index 0000000..1692472 --- /dev/null +++ b/decisions/decisions.md @@ -0,0 +1,75 @@ +# 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" +``` + +## 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. diff --git a/docs/intakes/residuals.md b/docs/intakes/residuals.md index 95d616a..0d9392f 100644 --- a/docs/intakes/residuals.md +++ b/docs/intakes/residuals.md @@ -12,7 +12,9 @@ id: TEN-IN-0001 kind: intake title: "Externalize tenant-engine audit evidence to audit-core" lane: yellow -status: open +status: closed +outcome: promoted +promoted_to: TEN-WP-0011-T04 priority: high owner: tenant-engine repo: tenant-engine @@ -39,6 +41,11 @@ description: | Promote this intake to a normal workplan before implementation because the delivery semantics and cross-service dependency require design and review. state_hub_intake_id: "01a02176-8ca7-725d-b2c2-4353eb4fd3f5" +promoted_at: "2026-08-29" +notes: + - content: Promoted into TEN-WP-0011-T04 after security-layer-model v0.7 made the evidence bound (statute §9.6) explicit for this PIP. + author: tenant-engine + created: "2026-08-29" ``` --- @@ -50,7 +57,9 @@ id: TEN-IN-0002 kind: intake title: "Remove or authorize the tenant-engine unfiltered event-read interface" lane: red -status: open +status: closed +outcome: promoted +promoted_to: TEN-WP-0011-T05 priority: high owner: tenant-engine repo: tenant-engine @@ -67,4 +76,9 @@ description: | interface. Add cross-tenant negative tests and give risk-nexus the final accessibility and payload reading. state_hub_intake_id: "01a02176-c121-7952-8d52-2cbe282fd883" +promoted_at: "2026-08-29" +notes: + - content: Promoted into TEN-WP-0011-T05. Unfiltered production-protocol event reads remain a reconstructability defect under statute §9.6 even without an HTTP route. + author: tenant-engine + created: "2026-08-29" ``` diff --git a/history/2026-08-29-security-layer-intent-scope-review.md b/history/2026-08-29-security-layer-intent-scope-review.md new file mode 100644 index 0000000..9bfd5b4 --- /dev/null +++ b/history/2026-08-29-security-layer-intent-scope-review.md @@ -0,0 +1,189 @@ +# Review: Security Layer Model v0.7 — INTENT, SCOPE, and implementation + +**Date:** 2026-08-29 +**Author:** grok +**Reviewed:** +- `net-kingdom/SECURITY-COMPANION.md` v0.2 (accepted; start here) +- `net-kingdom/canon/standards/security-layer-model_v0.7.md` (accepted 2026-08-29) +- `net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md` +- this repo's `INTENT.md` / `SCOPE.md` (before and after this session) +- shipped code under `src/tenant_engine/` and finished workplans `TEN-WP-0001`–`0007`, `0009`, `0010` +**Trigger:** gate-house broadcast that v0.7 is accepted; intake `TEN-IN-0003` asking this repository to declare its layer in its own voice, or contest it. Proposed layer: Engine. +**Outcome:** declared Engine / PIP; contested placing approval lifecycle here; remaining implementation gaps captured as `TEN-WP-0011`. + +--- + +## 1. Executive summary + +`tenant-engine` is the catalogued Engine / PIP for tenant-as-an-entity facts. That placement is correct, and this session declares it in `INTENT.md` frontmatter and prose. The gate-house review note that sat above an unadapted body is replaced. + +`SCOPE.md` was the larger documentation failure. It still said the repo was bootstrapping with no implementation, while production already serves tenant records, role grants, plan assignment, lifecycle, guardrails, and a PostgreSQL store, gated on writes (and now reads) through `flex-auth`. + +The implementation is a working PIP with a fail-closed write path. It is **not** yet a conforming PEP-shaped consumer, and it is **not** yet a conforming evidence source. Those are the evolution requirements. + +| Question | Answer | +| --- | --- | +| Is Engine / PIP the right layer? | **Yes.** Same tenant state, same result. Facts as claims, never a decision. | +| Do we contest the proposed layer? | **No.** We contest only the live question about approvals. | +| Do approvals belong here? | **No.** Guardrails are ceilings. Approvals are `approval-engine` objects. | +| Does SCOPE match INTENT? | **After this session, yes** at the documentation layer. | +| Does implementation match INTENT? | **Partial.** PIP reads and fail-closed writes exist. Decision-record persistence, published stance, independent evidence, and claim-lifetime declaration do not. | + +--- + +## 2. What the standard now requires of this repository + +Companion v0.2 is the operative form. The statute governs on disagreement. + +### 2.1 Declare (companion §2, statute §11) + +Machine-readable `layer` in `INTENT.md` frontmatter, plus prose in our own voice. A layer stated *about* us is not a declaration. Engines also carry `role`. Catalog row: Engine / PIP, tenant-as-an-entity facts. + +**Before this session:** a gate-house review note named Engine and admitted the body was unadapted. That is exactly the defect §11 now forbids. + +**After this session:** `layer: Engine`, `role: PIP` in frontmatter; body rewritten. `TEN-IN-0003` closed by `TEN-DEC-2026-001`. + +### 2.2 PIP rules that bind a fact engine + +- One decision point: `access-engine` only. We do not render or cache a decision. +- Compiled data that determines an outcome is still deciding. An effective guardrail, a grouping, or a role set is a **claim**. Joining it with consumption to allow or deny is `access-engine`'s. +- Doctrine and tenancy context arrive as claims, never as a side channel. +- Live-lookup unavailability is input degradation at the consumer: fail to reduced authority is `access-engine`'s when it is reachable; our own read path already returns `503` rather than empty roles. + +### 2.3 PEP-shaped writes (companion §5, statute §6.4 / §9.3) + +Mutating tenant facts is a protected side effect. Being PEP-shaped does not change the layer. Four obligations: + +1. No side effect without a decision record naming the request, **or** a recorded application of the published unreachable-engine stance. +2. Do not replay a verdict outside its binding and lifetime. +3. Publish the unreachable-engine stance map, total, no implicit default, tested equal to shipped behaviour, registered in statute §13.1. +4. Reconstructable, bounded by §9.6. + +Shipped behaviour is fail-closed (`DefaultDenyWriteAuthorizer` when the URL is unset; `FlexAuthCheckClient.is_allowed()` returns `False` on any non-allow, including transport failure). That is the right stance for this engine. It is **not published**, and the decision envelope is **discarded**. + +### 2.4 Evidence (companion §6, statute §9.6) + +Local `events` rows share the production database and runtime credential with the facts they attest to (`TEN-IN-0001`). `TenantStore.events()` returns every tenant's event and payload (`TEN-IN-0002`). Grant/revoke *facts* are live PIP input; the *events* are attributive unless a control starts branching on their presence. Completeness must not be claimed. Independent custody is `audit-core`. + +### 2.5 Two things not to plan around (companion §10) + +Nothing is observed in production. Nothing can be contained automatically. This engine does not acquire actuation, and no workplan here should assume kings-guard is watching tenant mutations in operation. + +--- + +## 3. INTENT success criteria + +INTENT is aspirational. These are the properties the mature system must hold. + +| # | Criterion | Status | Evidence / gap | +| --- | --- | --- | --- | +| 1 | Canonical owner of tenant-as-an-entity facts | **Met** | Domain + store + APIs; boundary contract | +| 2 | Deterministic Engine (same state → same result) | **Met** | Pure domain + total guardrail resolution | +| 3 | PIP, never PDP | **Met in design** | Reads return facts; writes ask `flex-auth`. Guardrail resolution is a ceiling, not an allow. | +| 4 | Cache-read for `key-cape` | **Shipped** | `GET /tenants/{id}/roles` | +| 5 | Live-lookup for `access-engine`, fail-closed on store loss | **Shipped** | `GET /tenants/{id}/roles/live` → `503` on `StoreUnavailableError` | +| 6 | Writes PEP-shaped, never self-authorized | **Partial** | Authorizer seam exists and fails closed. Decision id is not kept. Stance is unpublished. | +| 7 | Guardrails are PIP ceilings, not entitlements or decisions | **Shipped (policy)** | `TEN-WP-0006`; plan-derived layer is empty until `adaptive-pricing` feeds it | +| 8 | Grouping current vs identifier historical | **Shipped** | `TEN-WP-0010` | +| 9 | Approvals not stored or evaluated here | **Met by absence** | Contested in `TEN-DEC-2026-001` so it stays absent | +| 10 | No catalogued Tooling client | **Met** | No OpenBao / `key-cape` client in `src/` | +| 11 | Evidence independently custodied, bound claimed honestly | **Not met** | Local events only; `events()` unfiltered | +| 12 | Claim freshness / lifetime declared per input class | **Not met** | Relies on token TTL for cache-read; live path has no stated deadline | + +**Score: 8 met, 1 partial, 2 not met, 1 met-by-absence.** The two not-met items and the partial are `TEN-WP-0011`. + +--- + +## 4. SCOPE vs INTENT (documentation) + +### Before this session + +| Topic | INTENT (body) | SCOPE | Drift | +| --- | --- | --- | --- | +| Layer | Review note only; body unadapted | Silent | Non-conforming declaration | +| Guardrails | "Reserved, not yet implemented" | Same | Both stale vs `TEN-WP-0006` | +| Current state | Purpose written as if pre-code | "bootstrapping. No implementation yet" | False | +| Approvals | Unasked | Unasked | Live question from `TEN-IN-0003` unanswered | +| Writes | Gated by `flex-auth` | Same | Neither named the PEP shape | + +### After this session + +INTENT states the aspirational Engine / PIP role, the PEP-shaped write path, the evidence bound, and the approvals contest. SCOPE states what is shipped, what is not, and points at `TEN-WP-0011` for the rest. They no longer contradict each other. The remaining documentation debt is the **boundary contract** in `net-kingdom`, which still says guardrails are reserved and that this repo is "not a policy enforcement point" — true of decisions, false of the write-path shape. + +--- + +## 5. SCOPE vs implementation (before the rewrite) + +The pre-session `SCOPE.md` was not a conservative boundary. It was wrong about the current state. + +Shipped and previously in SCOPE as "in scope": tenant records, role grants, plan assignment, cache-read, live-lookup, write API. + +Shipped and previously missing from SCOPE, or marked reserved: + +- Guardrail read/write API (`GET/PUT/DELETE /tenants/{id}/guardrails…`) +- Lifecycle (`PATCH`, retire, reactivate) +- Mutable grouping (`POST /tenants/{id}/grouping`) +- PostgreSQL production store +- ETag / If-Match / idempotency on lifecycle and guardrail mutations +- Authorized reads (`tenant.read`, `tenant.role.read`, `tenant.role.read.live`, `tenant.guardrail.read`) + +Out of SCOPE and correctly absent from the code: identity, PDP, users, pricing terms, payments, secret custody, metering, rate limits, approvals, containment. + +--- + +## 6. Implementation gaps against the accepted model + +Prioritized. "Necessity" means the statute or companion requires it of an Engine / PIP with a PEP-shaped write path. "Should" is the companion's working-reference shape or a pre-existing intake the model now makes sharper. + +| Prio | Gap | Kind | Why it matters | Track | +| --- | --- | --- | --- | --- | +| P0 | Layer declared only as a review note | Necessity | §11; `TEN-IN-0003` | **Closed this session** (`INTENT.md`, `TEN-DEC-2026-001`) | +| P1 | `FlexAuthCheckClient` discards the decision envelope; no `authorization_decision_id` on events | Necessity | §6.4 obligation 1; boundary-contract Audit Correlation Contract already required this field and the code never stored it | `TEN-WP-0011-T02` | +| P1 | Unreachable-engine stance unpublished | Necessity | §6.4 obligation 3 / companion §5. Fail-closed is correct and already coded; a map that lives only in `is_allowed()` / `DefaultDenyWriteAuthorizer` is not published. Must be tested equal to shipped behaviour and offered to the §13.1 register. | `TEN-WP-0011-T02` | +| P1 | Local events share the fact store's credential boundary | Necessity (honesty now; custody next) | §9.6. Attributive today. A database-credential compromise forges fact and evidence together. Independent custody is `audit-core`. | `TEN-WP-0011-T04` ← `TEN-IN-0001` | +| P1 | `TenantStore.events()` is unfiltered | Necessity | RISK-F-0004 / `TEN-IN-0002`. No HTTP route today, but the production protocol still offers a cross-tenant dump. | `TEN-WP-0011-T05` ← `TEN-IN-0002` | +| P2 | No machine-readable `layer.yaml` / conformance test | Should | Companion §2 working reference. Frontmatter satisfies "a `layer:` key". The check that *no new OpenBao client appeared* still wants a file and a test, including the non-Tooling inventory (Postgres, `flex-auth`, State Hub). | `TEN-WP-0011-T01` | +| P2 | No declared freshness / lifetime for PIP claims | Necessity (PDP-side, we owe the input class) | §9.7: a PDP states a deadline **per input class**. Cache-read is bounded by token TTL (IAM Profile). Live-lookup currently forbids cross-request caching in prose only. We must publish the input-class lifetime so `access-engine` can name it. | `TEN-WP-0011-T03` | +| P2 | Live-lookup re-enters `flex-auth` to authorize the read | Confirm | `GET /roles/live` calls `authorizer.authorize(tenant.role.read.live)` which `POST`s `/v1/check`. If that check itself live-looks up tenant roles, the path is cyclic. Likely saved by a static service-identity rule that does not consume tenant-engine claims — **must be proven**, not assumed. | `TEN-WP-0011-T03` | +| P3 | Boundary contract stale on guardrails and "not a PEP" | Docs / canon | Owned by `net-kingdom`. Request an amendment; do not fork the contract here. | `TEN-WP-0011-T06` | +| P3 | Staged promotion still open | Ops, not layer-model | `TEN-WP-0008`. Silent pin rollbacks. Orthogonal to v0.7; remains the other ready workplan. | `TEN-WP-0008` | +| — | Verdict caching | None found | Each write re-checks. Obligation 2 appears held. T02 should lock it with a test. | `TEN-WP-0011-T02` | +| — | Catalogued Tooling contact | None found | No OpenBao / `key-cape` client. Conforming on §5 if the inventory is declared. | `TEN-WP-0011-T01` | +| — | Production observation / automatic containment | Do not plan | Companion §10. No task here invents either. | — | + +--- + +## 7. Approvals contest (the live question on `TEN-IN-0003`) + +gate-house asked whether organizational approval lifecycle belongs nearer tenant governance, given that guardrail/quota policy was once a candidate home before `approval-engine` was seeded. + +**Contest: no.** + +A guardrail answers *how far this tenant may go before something must stop it*. That is a PIP fact, already modelled, already served to `flex-auth` as a ceiling. An approval answers *whether this act has been granted a durable, authenticated, atomically supersedable object*. Statute §4 and §9.4 assign that object to `approval-engine`. Putting it in `tenant-engine` would either mint a second approval store or turn a ceiling engine into a workflow engine. + +If a tenant mutation should require an approval, the shape is: `approval-engine` holds the object; `access-engine` consumes it as a claim on `tenant.*` writes; this engine mutates only after that decision. That is the PEP write path we already owe. It is not a reason to own the object. + +Recorded as `TEN-DEC-2026-001`. + +--- + +## 8. What this session changed, and what it did not + +Changed: + +- `INTENT.md` — own-voice Engine / PIP declaration; body adapted; approvals contested. +- `SCOPE.md` — current state, layer table, guardrails and lifecycle in scope, known gaps named. +- `TEN-DEC-2026-001` — declaration + contest. +- `TEN-IN-0003` — closed, absorbed. +- `TEN-IN-0001` / `TEN-IN-0002` — promoted into `TEN-WP-0011`. +- `TEN-WP-0011` — evolution workplan. + +Did not change: runtime code, deploy pin, `tenancy.yaml`, agent-rule files (`architecture.md` still says guardrails are reserved — residual, not this workplan's job unless an agent trips on it). Did not implement `layer.yaml`, decision-id persistence, stance publication, or audit-core emission. + +--- + +## 9. Suggested next action + +Execute `TEN-WP-0011` in order: T01 (checkable declaration) → T02 (PEP write path) → T03 (PIP freshness, including the live-lookup re-entry proof) → T04/T05 (evidence) → T06 (contract amendment request). Do not wait on `TEN-WP-0008`; it is a different axis (Railiance staged promotion) and both can run. + +Do not add a containment, observation, or approval-object task to this repo on the strength of v0.7. diff --git a/intakes/intakes.md b/intakes/intakes.md index a951e7b..4944487 100644 --- a/intakes/intakes.md +++ b/intakes/intakes.md @@ -7,9 +7,11 @@ id: TEN-IN-0003 kind: intake title: 'Declaration requested: state this repository''s layer in INTENT.md (security layer model §11)' -status: open +status: closed +outcome: absorbed +promoted_to: TEN-DEC-2026-001 origin: cross-repo -origin_ref: net-kingdom security-layer-model_v0.4 §11 +origin_ref: net-kingdom security-layer-model_v0.7 §11 priority: low owner: tenant-engine requested_by: gate-house @@ -28,12 +30,17 @@ description: 'A conformance sweep on 2026-08-28 found this repository has no lay nearer to tenant governance, now is the time to say. Contesting costs nothing — the three repositories that reviewed this model each returned a correction, two of which changed the standard. NOTE for your own records, unrelated to the request: - the State Hub holds two open intakes for tenant-engine created 2026-08-25 (audit - evidence externalization; unfiltered event-read interface) with no intakes/intakes.md + the State Hub holds two open tenant-engine intakes with no intakes/intakes.md in this checkout to back them, which is an ADR-001 file-is-truth inconsistency you may want to reconcile. Standard: net-kingdom/canon/standards/security-layer-model_v0.4.md.' created: '2026-08-28T21:03:13.580775Z' -updated: '2026-08-29T00:45:57.718728Z' +updated: '2026-08-29T12:00:00.000000Z' +closed_at: '2026-08-29' +resolution: 'Declared Engine / PIP in INTENT.md own voice (frontmatter layer/role + plus adapted body). Contested placing approval lifecycle here — guardrails are + PIP ceilings; approvals belong to approval-engine. Implementation gaps against + v0.7 captured as TEN-WP-0011. The withdrawn ADR-001 note in the original intake + remains withdrawn.' notes: - content: CORRECTION from gate-house, 2026-08-29. The closing note in this intake claimed the State Hub holds two open tenant-engine intakes with no backing file, @@ -44,4 +51,9 @@ notes: unchanged. author: gate-house created: '2026-08-29T00:45:57.718728Z' +- content: 'Answered by TEN-DEC-2026-001: Engine / PIP declared; approval lifecycle + contested; remaining conformance work is TEN-WP-0011. Review: + history/2026-08-29-security-layer-intent-scope-review.md.' + author: tenant-engine + created: '2026-08-29T12:00:00.000000Z' ``` diff --git a/workplans/TEN-WP-0011-security-layer-conformance.md b/workplans/TEN-WP-0011-security-layer-conformance.md new file mode 100644 index 0000000..8c81db6 --- /dev/null +++ b/workplans/TEN-WP-0011-security-layer-conformance.md @@ -0,0 +1,224 @@ +--- +id: TEN-WP-0011 +type: workplan +title: "Align tenant-engine with the accepted security layer model" +domain: infotech +repo: tenant-engine +status: ready +owner: grok +topic_slug: netkingdom +created: "2026-08-29" +updated: "2026-08-29" +depends_on: + - TEN-WP-0003 + - TEN-WP-0006 + - TEN-WP-0009 +unblocks: [] +origin: residual +origin_ref: TEN-IN-0003 +--- + +# TEN-WP-0011 — Align tenant-engine with the accepted security layer model + +The NetKingdom Security Layer Model is accepted at v0.7. This repository is +catalogued Engine / PIP for tenant-as-an-entity facts. `INTENT.md` now +declares that in its own voice (`TEN-DEC-2026-001`). The remaining work is +to make the shipped engine match the obligations that declaration takes on. + +Review of record: +`history/2026-08-29-security-layer-intent-scope-review.md`. + +Start here: `net-kingdom/SECURITY-COMPANION.md` v0.2. Statute: +`net-kingdom/canon/standards/security-layer-model_v0.7.md`. On disagreement +the statute governs. + +## Why this is a workplan + +Declaration is documentation. Conformance is mechanical. Companion §2 +offers `layer.yaml` plus a check as the working reference. Companion §5 +and statute §6.4 bind the write path as PEP-shaped. Statute §9.6 and the +already-open intakes `TEN-IN-0001` / `TEN-IN-0002` bind evidence. Statute +§9.7 requires a published lifetime on PIP input classes. + +This is not a layer change (statute §10). We are declaring the layer we +already occupy and closing the gaps between that layer's rules and the +code. + +## Non-goals + +- Not a second PDP. Guardrail resolution stays a ceiling, never an allow. +- Not an approval engine. `TEN-DEC-2026-001` contested that placement. +- Not containment or production observation (companion §10). +- Not staged promotion — that remains `TEN-WP-0008`. +- Not forking the boundary contract. T06 requests an amendment in + `net-kingdom`; it does not copy the contract into this repo. + +## T01 — Machine-readable layer declaration and conformance check + +```task +id: TEN-WP-0011-T01 +status: todo +priority: high +``` + +Add `layer.yaml` modelled on `ops-warden`'s reference and `kings-guard`'s +no-Tooling adaptation: `layer: engine`, `role: pip`, `standard_version: +"0.7"`, empty `tooling_contacts`, and a complete `non_tooling_clients` +inventory (PostgreSQL / SQLite store, `flex-auth` `/v1/check`, State Hub +work records). + +Add a check script and a test so a new catalogued Tooling client +(`key-cape`, OpenBao) fails on the commit that introduces it. Record +non-Tooling clients so the check is total (companion §4). + +Done when: `INTENT.md` frontmatter and `layer.yaml` agree on layer and +role; `make test` covers the check; no OpenBao / `key-cape` client exists +in `src/`. + +## T02 — PEP write-path: decision records and published fail-closed stance + +```task +id: TEN-WP-0011-T02 +status: todo +priority: high +``` + +`FlexAuthCheckClient.is_allowed()` currently returns a bool and drops the +decision envelope. The boundary contract's Audit Correlation Contract +already required `authorization_decision_id` on every mutation; the code +never stored it. + +- Keep the decision record (id, effect, request digest) on the authorizer + seam and persist it on every mutation event. +- Where `access-engine` is unset or unreachable, record the **application + of the stance** in place of a decision (companion §5.1). Do not proceed + silently. +- Publish the unreachable-engine stance as a file named from `layer.yaml` + (fail-closed for every scope this engine serves — this is not + ops-warden's per-zone fail-open map). Assert with a test that the + published map equals shipped behaviour (`DefaultDenyWriteAuthorizer` and + transport-failure deny). +- Lock obligation 2: no verdict cache. Re-check every write. Add a test + that a previous allow cannot authorize a later, different request. +- Offer the map to gate-house for the statute §13.1 register. Do not edit + the statute from this repo. + +Done when: a granted role, a denied grant, and an unreachable-engine +attempt each leave a reconstructable record; `make test` proves stance +file == code; no write path can succeed without one of those two records. + +## T03 — PIP claim freshness, and prove live-lookup is not cyclic + +```task +id: TEN-WP-0011-T03 +status: todo +priority: high +``` + +Statute §9.7: every allow has a lifetime, and a PDP states a deadline per +input class. This engine owes the input-class description. + +- Publish, next to the stance map, the freshness contract for + `tenant_roles` / grouping / plan / guardrail claims: cache-read is + bounded by the IAM Profile token lifetime; live-lookup results MUST NOT + be cached across requests; request-scoped memoization inside one + `access-engine` decision remains allowed (boundary contract). +- Name the consumer-visible failure: store unavailability is `503`, never + empty roles. That is already shipped; make it part of the published + contract so `access-engine` can treat it as input degradation (§9.3). +- **Prove** that `GET /tenants/{id}/roles/live` → `POST /v1/check` + (`tenant.role.read.live`) does not re-enter tenant-engine for the claims + that check needs. If it does, the live path is cyclic and must be + unwound (static service-identity rule, or an authorizer bypass that is + itself a declared exception — prefer the former). Record the proof in + `docs/flex-auth-integration.md`. + +Done when: the freshness contract is published; a test fails if live-lookup +cross-request caching is introduced; the re-entry question is answered with +evidence, not a comment. + +## T04 — Externalize mutation evidence to audit-core + +```task +id: TEN-WP-0011-T04 +status: todo +priority: high +``` + +Promoted from `TEN-IN-0001` (origin: residual of `TEN-WP-0009`). + +The local `events` table shares a database and runtime credential with the +facts it attests to. Statute §9.6: that trail cannot be described as +independent custody. Classify current classes as **attributive** unless a +named control branches on their presence. + +Design and implement a governed tenant-engine → `audit-core` emission path: + +1. Agree envelope, sender registration, and credential custody with + `audit-core` without putting a secret in Git or a work record. Route + credentials through `warden route`, never through a State Hub message. +2. Preserve mutation atomicity (local outbox against crash) while defining + retry, duplicate, and audit-core-unavailable semantics. Unavailable + evidence MUST NOT fail-open a mutation, and SHOULD NOT fail-closed a + mutation either unless a class is later reclassified load-bearing — + declare the trade if emission stays attributive and non-blocking. +3. Prove the external copy cannot be rewritten through tenant-engine's + runtime database credential. +4. Decide whether pre-cutover event classes need a bounded backfill, and + record the decision. +5. Do not claim the archive proves events happened, or that absence proves + they did not. + +Done when: production mutations emit to `audit-core` under the bound above; +the trade (atomic vs non-blocking) is declared; `TEN-IN-0001` can close as +promoted and completed. + +## T05 — Remove or authorize the unfiltered event-read interface + +```task +id: TEN-WP-0011-T05 +status: todo +priority: high +``` + +Promoted from `TEN-IN-0002` (origin: RISK-F-0004). + +`TenantStore.events()` returns every tenant's event and payload. There is +no HTTP event-read route today, but the production protocol still offers +the dump. + +- Remove `events()` from the production protocol, **or** replace it with + an authorized, deliberately scoped export/query that cannot return + another tenant's payload. +- Add cross-tenant negative tests. +- Hand risk-nexus the accessibility and payload reading. + +Done when: a production caller cannot list another tenant's events through +this repo's store protocol; tests say so; `TEN-IN-0002` can close. + +## T06 — Request the boundary-contract amendment + +```task +id: TEN-WP-0011-T06 +status: todo +priority: medium +``` + +`net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md` still +says guardrail/quota policy is "reserved, not yet implemented" and that +`tenant-engine` is "not a policy enforcement point". Both sentences now +mislead: guardrails shipped in `TEN-WP-0006`, and writes are PEP-shaped +even though this repo is not a PDP. + +File an intake on `net-kingdom` (do not edit the canon from here) asking +to: + +- mark guardrail/quota policy as owned and implemented, pointing at + `docs/tenant-guardrail-policy.md`; +- restate the authorization contract: data source / PIP for decisions; + PEP-shaped for its own protected writes; never a second PDP; +- mention Engine / PIP and the v0.7 statute next to the existing IAM + Profile / ADR-0013 / ADR-0014 references. + +Done when: the intake exists on the canon side with a named id, and this +task records it.