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
189 lines
14 KiB
Markdown
189 lines
14 KiB
Markdown
# 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.
|