Declare Engine/PIP and open TEN-WP-0011 for v0.7 conformance

The security layer model is accepted at v0.7. INTENT.md now declares
Engine / PIP in this repository's own voice (TEN-DEC-2026-001), SCOPE.md
is brought current with shipped APIs, and the intent/scope/implementation
review is history/2026-08-29-security-layer-intent-scope-review.md.

TEN-IN-0003 is absorbed by the declaration. Approvals stay with
approval-engine. TEN-IN-0001 and TEN-IN-0002 promote into TEN-WP-0011.

Assistant: grok
Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
This commit is contained in:
tegwick 2026-08-29 11:57:37 +02:00
parent d9bacfa953
commit 76677bffc5
7 changed files with 780 additions and 86 deletions

228
INTENT.md
View file

@ -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

110
SCOPE.md
View file

@ -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/`

75
decisions/decisions.md Normal file
View file

@ -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.

View file

@ -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"
```

View file

@ -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.

View file

@ -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'
```

View file

@ -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.