user-engine/history/2026-08-29-security-layer-scope-intent-assessment.md
tegwick 96430299b7
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Declare Engine/PIP and open USER-WP-0024 for layer conformance
Adopt security-layer-model v0.7 in INTENT.md in this repository's own
voice, restate SCOPE.md from that declaration, and record the
scope-versus-implementation assessment. USER-IN-0001 is answered;
runtime follow-through is USER-WP-0024.

Assistant: grok
Assistant-Session: 01a04cea-f0d6-7ab3-9ffd-881eb6bea6cb
2026-08-29 11:56:06 +02:00

317 lines
16 KiB
Markdown

# Security layer model — scope vs intent vs implementation
**Date:** 2026-08-29
**Repository:** user-engine
**Audience:** user-engine, gate-house, flex-auth, ops-warden
**Status:** current-state review, not a decision record
**Sources:**
- `net-kingdom/SECURITY-COMPANION.md` v0.2 (operative form; start here)
- `net-kingdom/canon/standards/security-layer-model_v0.7.md` (accepted statute)
- `net-kingdom/canon/standards/user-engine-boundary-contract_v0.1.md`
- `INTENT.md` and `SCOPE.md` as adapted this session
- `intakes/intakes.md` `USER-IN-0001`
- gate-house broadcast 2026-08-29 (accepted v0.7; nine undeclared repositories)
- working references: `ops-warden/layer.yaml`, `ops-warden/pep-stance.yaml`,
`kings-guard/layer.yaml`
- implementation: `src/user_engine/service.py`,
`src/user_engine/adapters/flex_auth.py`,
`src/user_engine/adapters/local.py`,
`src/user_engine/domain/models.py`,
`src/user_engine/runtime.py`,
`src/user_engine/projections.py`
The statute governs where the companion and the statute disagree. No
disagreement was found that this review needed to work around.
---
## 1. Executive assessment
user-engine is already the product INTENT described before the layer
model: a headless user-domain service with ports, a PostgreSQL store, a
portal, and a live flex-auth caller. That slice is finished through
`USER-WP-0023`.
The accepted layer model asks for something the product workplans never
carried: an own-voice Engine/PIP declaration, a machine-readable
conformance surface, and the PEP obligations that attach to any runtime
that causes a protected side effect.
Until this session the repository carried only a gate-house review note
on `INTENT.md` that named a layer and admitted the body was unadapted.
That is not a declaration (companion §2, statute §11). `USER-IN-0001`
stayed open.
This review records the declaration, the SCOPE restatement, and the
implementation gaps that must move. It does not contest Engine/PIP.
---
## 2. What we declare, and what we do not contest
| Item | Position |
| --- | --- |
| Layer | **Engine** |
| Role | **PIP** |
| Catalog owns | users, accounts, memberships |
| Boundary contract | unchanged |
| One decision point | `access-engine` / `flex-auth`; we supply claims and enforce results |
| Contest the proposed layer? | **No.** Same inputs, same user-domain result. Not inference, not Tooling, not the PDP. |
**Finding, not a contest.** Statute §4 catalogues user-engine as Engine/PIP
and does not mark it PEP-shaped. Companion §5 and statute §6.4 say a PEP
is any runtime that causes a protected side effect; being PEP-shaped does
not change layer. user-engine's production mutations (account lifecycle,
memberships, invitations, registration, hat selection, catalog and
profile writes) are protected side effects gated on flex-auth. We are
PEP-shaped in fact. §13.1 currently inventories `ops-warden` (published)
and `ops-mason` (missing). user-engine is a third PEP-shaped consumer
with no row. One row was already the finding; a silent third is worse.
Raised for gate-house to inventory. We do not ask to be recatalogued as
Staff.
---
## 3. Intent vs scope after the adaptation
INTENT is now the layer declaration plus the previous product aspiration.
SCOPE is the implemented product plus the layer obligations that are now
in-scope because INTENT adopted them.
| Area | INTENT | SCOPE after this session | Implementation |
| --- | --- | --- | --- |
| Engine / PIP declaration | Required, own voice, frontmatter | Declared; points at companion and statute | Frontmatter present. No `layer.yaml`. |
| Users, accounts, memberships | Own them | Own them | Shipped. |
| Claims, not decisions | Subject context is a PIP input | Explicit; local double excluded from production | Production asks flex-auth. Hats/facts not yet proven under §6.1. Local port can still decide. |
| PEP stance map | Published, tested equal to code, inventoried in §13.1 | Named as remaining product scope | Fail-closed in `FlexAuthHTTPAdapter`; unpublished. |
| Allow lifetime | Every allow has a lifetime | Named as missing | `AuthorizationDecision` has effect, id, reason, obligations. No TTL or binding. |
| Evidence bound | No completeness claim; cadence for load-bearing | Named as partial | Local outbox is transactional with mutations. No classification, no cadence. |
| Tooling contacts | No OpenBao or key-cape admin client | Out of scope | Held. Env-injected secrets. Unused `SecretProvider` port. |
| Observation / actuation | Do not assume either exists | Out of scope | Not implemented. Correct. |
SCOPE is not silently claiming INTENT's new layer obligations as done.
The remaining holes are listed in SCOPE "Current Status" and mapped to
`USER-WP-0024`.
---
## 4. Implementation assessment against the companion
Companion sections are the working checklist. Statute citations in
parentheses.
### 4.1 Declare your layer (companion §2 / statute §11)
**Before this session:** undeclared. Gate-house prose in `INTENT.md` is
explicitly not a declaration.
**After this session:** `INTENT.md` frontmatter `layer: Engine`,
`role: PIP`, plus own-voice body. That is the form companion §2 asks
for.
**Still missing:** a machine-readable declaration file equivalent to
`ops-warden/layer.yaml`. Statute §11 allows frontmatter *or* an
equivalent file; the working references use a file because that is what
a conformance script can fail a commit against. user-engine has no
checker, so a new OpenBao import would not fail the build. Necessity:
add `layer.yaml` and a check adapted from kings-guard (Engine, not Staff;
expect no Tooling client, record non-Tooling clients).
### 4.2 Rules that bind everyone (companion §3 / statute §6, §8, §9.7)
| Rule | Shipped behaviour | Gap |
| --- | --- | --- |
| One decision point | Production `FlexAuthHTTPAdapter` POSTs `/v1/check`. Service `_authorize` denies on non-allow. | `LocalAuthorizationCheckPort` *is* a local PDP (default ALLOW, reason `"local"`). Runtime assembly uses flex-auth only, but nothing prevents a production constructor from passing the local port. |
| Compiled data still deciding | Access-control facts and hats are documented as claim templates (`docs/hats-realms-services-assets-access-profiles.md`). Projections cache profile material only (`projections.py`). | No test that a consumer cannot treat `AccessControlFact` or an active hat as an allow. Tenant capability roles are forwarded, not evaluated — held, documented in the architecture blueprint. |
| Doctrine as a claim | We do not apply zone stance or maturity ourselves. | None for doctrine we author — we author none. |
| Adaptive systems only tighten | We do not raise authority on missing evidence. | Held. Do not plan around actuation; it is at zero. |
| Staff never touches Tooling | We are Engine; §5 does not bind this shape the same way. PostgreSQL is the modeled-concept store, not a §4 Tooling row. | Record PostgreSQL, State Hub, and env-injected secrets as non-Tooling / own-store in `layer.yaml` so the check is total. |
| Every allow has a lifetime | Not modelled. | `AuthorizationDecision` must carry a lifetime (TTL or session/obligation binding). Replay of a stored verdict outside that lifetime is a second decision point. Today we do not cache verdicts, which is the safe subset, but we also cannot state a deadline (statute §9.7.2: a PEP MUST state one). |
### 4.3 If you touch Tooling (companion §4 / statute §5)
Catalogued Tooling is `key-cape` and `OpenBao`.
| Contact | Present? | Shape |
| --- | --- | --- |
| OpenBao HTTP/CLI | No | Blocked-clean on a secrets-engine signing/read surface. Runtime secrets come from the environment / projected files (`runtime.py`). |
| key-cape admin / LDAP | No | Consumes verified IAM Profile claims (OIDC/JWKS). That is Engine input, not a Staff Tooling client. |
| PostgreSQL | Yes — own store | Modeled-concept persistence for a PIP. Not §4 Tooling. List it in the declaration so the check is total. |
| State Hub | Agents writing progress, not the service | Not catalogued. List as non-Tooling. Carve-out sunsets; we do not become a second state plane. |
No §5.1/§5.2/§5.3 Staff contact exists. Conformance state for Tooling
contact is **conforming** (Engine with no catalogued Tooling client),
provided we declare the own-store and non-Tooling clients rather than
leave them silent.
`SecretProvider` in `ports.py` has no implementation. Leave it as a port.
Do not add an OpenBao adapter to close it.
### 4.4 If you cause side effects (companion §5 / statute §6.4)
user-engine is PEP-shaped. Four obligations:
1. **No side effect without a decision record, or a recorded stance.**
Mutations go through `_authorize` then `_record_mutation` inside
`store.transaction()`. On flex-auth transport/parse failure,
`FlexAuthHTTPAdapter` returns `DENY` with reason `"authorization
service unavailable"` and a **locally minted** `decision_id`
(`AuthorizationDecision.decision_id` default factory). `_authorize`
then writes that id onto the deny audit record. That is a fake
decision id, not a recorded stance application. Statute v0.7
obligation 1 is explicit: where the engine is unreachable, record
the application of the stance *in place of* the decision.
`decision_id` must be absent unless a decision was rendered.
2. **Do not replay a verdict outside its binding and lifetime.** No
verdict cache exists in the flex-auth adapter. Good. Negative
caching is not used. Keep it that way unless a cache is declared
with lifetime. The mechanical replay test (canonical request digest
+ lifetime) is not implemented because we do not replay; still need
a lifetime on the decision we *hold for the mutation*.
3. **Publish the unreachable-engine stance map.** Shipped behaviour is
fail-closed for every call — no per-zone map, no published file, no
test that file equals code. `runtime.py` comment says "Production
authorization and delivery are fail-closed HTTP boundaries." That is
a comment, which statute §6.4 forbids as the form of a stance.
Reference shape: `ops-warden/pep-stance.yaml`. user-engine's map can
be simpler (one protected-system, fail-closed total, including
unknown) but it must be total, published, and tested.
4. **Stay reconstructable**, bounded by evidence (below). Deny audits
are written outside the mutation transaction, which is correct
(there is no mutation). They currently look like engine decisions.
### 4.5 Evidence (companion §6 / statute §9.6)
| Kind | What we have | Classification |
| --- | --- | --- |
| Successful user-domain mutation | `append_audit` + `append_outbox` inside the same `store.transaction()` as the save | Accidental omission of the *local* record is prevented. Drain to `audit-core` is after commit (local outbox pattern). |
| Authorization denial | `append_audit` in `_authorize`, not transactional with a mutation | Load-bearing if any control treats absence of a deny as "it was allowed". Today nothing else branches on that record — attributive in practice, load-bearing in intent (denials are the class the statute names). |
| Membership removal / account disable | Mutation path with outbox | Load-bearing for access-review and for PIP consumers who cache facts. |
What we may not claim, and currently do not claim in code:
- the audit record proves it happened;
- there is no record, so it did not happen.
Docs (`docs/evidence-gap-examples.md`) talk about missing *external*
review references, not about this bound. Completeness of the local trail
is easy to over-claim in portal copy and operability docs. Necessity:
classify events, declare attributive vs load-bearing, declare a cadence
or heartbeat for denials and revocations (rate monitoring will not work
at this volume), and never describe the trail as complete.
No production observation exists to cite (companion §10). This review
does not.
### 4.6 Agents (companion §7 / statute §3.4)
The service is an Engine. Agents *working the repository* are Staff
principals. Credential routing is already inlined in `AGENTS.md` via
`warden route`. No standing user-engine credential is vended to agents.
No change to runtime is required; INTENT now states the four agent
rules so a future convenience (a long-lived portal token in an agent
session, treating tool availability as permission) fails the
declaration.
### 4.7 Conformance state (companion §8 / statute §11)
| Question | State |
| --- | --- |
| Tooling contact | **Conforming** — none to catalogued Tooling, once listed |
| Capability we refuse to take locally (secrets-engine API, actuation, observation) | **Blocked-clean** — not worse than conforming |
| PEP stance unpublished; fake decision ids | **Undeclared violation** of §6.4 until the map and recording ship |
| Layer declaration | **In progress** — own-voice frontmatter done; machine-readable file and checker not done |
Blocked-clean on actuation and observation is the correct posture.
Do not open a kings-guard or OpenBao client to look more complete.
### 4.8 Railiance workloads (companion §9)
`rapp-user-engine` is the managed package. Decisions come from
flex-auth; credentials from the platform after deploy-time injection;
evidence toward `audit-core` via the outbox. Axis-to-layer mapping is
unsettled (statute §20.3); this review does not invent one.
### 4.9 Estate zeros (companion §10)
Do not cite "observed in operation." Do not plan automatic containment,
step-up, or isolation as user-engine work.
---
## 5. Necessities for the current implementation
Ranked by whether the accepted standard is already violated in shipped
behaviour, versus missing declaration machinery.
### Must change (shipped behaviour vs v0.7)
1. **Stop minting a local `decision_id` on engine-unavailable DENY.**
Record stance application (zone or equivalent scope, failure mode,
outcome). `decision_id` present only when flex-auth rendered one.
2. **Publish `pep-stance.yaml`** (or equivalent) named from the layer
declaration. Total, no implicit default. Test: published map equals
`FlexAuthHTTPAdapter` behaviour. Ask gate-house to add the row to
§13.1.
3. **Give `AuthorizationDecision` a lifetime** and refuse to treat an
allow with no end as a standing grant. Even without a verdict cache,
the PEP must state one deadline.
### Must add (declaration and checkability)
4. **`layer.yaml`** plus a conformance script/test adapted from
kings-guard: Engine/PIP, empty `tooling_contacts`, listed
non-Tooling clients (Postgres own-store, State Hub, env secrets,
flex-auth Engine API, key-cape OIDC as claim input).
5. **Confine `LocalAuthorizationCheckPort`** to tests and explicitly
standalone fixtures. Production `create_application()` already uses
flex-auth; make construction of a default-allow adapter in the
runtime path fail.
### Should add (evidence and PIP contract)
6. **Classify local audit/outbox events** as load-bearing or
attributive. Declare a heartbeat or reconciliation for denials and
membership/account revocations. Fix docs language so the trail is
never "complete."
7. **Prove hats / `AccessControlFact` are claims.** A test that
selecting a hat or exporting facts never returns an allow/deny, and
a consumer contract sentence in the hats doc and the boundary-facing
docs: compiling those facts into a local decision is §6.1.
### Do not do
- Do not add an OpenBao or key-cape admin client.
- Do not cache flex-auth allows.
- Do not observe production or build containment.
- Do not treat `USER-IN-0001` as closed by the old gate-house note;
it closes when this repository's own files declare, which they now
do for INTENT, with implementation follow-through on `USER-WP-0024`.
---
## 6. Mapping to work
| Necessity | Workplan task |
| --- | --- |
| Own-voice INTENT + SCOPE + this review | `USER-WP-0024-T01` (done this session) |
| `layer.yaml` + conformance check | `USER-WP-0024-T02` |
| PEP stance map, stance recording, lifetime, local-port confinement | `USER-WP-0024-T03` |
| Evidence classification and cadence | `USER-WP-0024-T04` |
| Access-control facts as claims | `USER-WP-0024-T05` |
| Intake close and §13.1 registration request | `USER-WP-0024-T06` |
---
## 7. Adoption note for gate-house
user-engine answers `USER-IN-0001`: **Engine, role PIP**, in our own
voice, in `INTENT.md`. We do not contest the proposed layer. We raise
that we are PEP-shaped for our own mutations and are absent from
statute §13.1. Findings against the accepted text remain welcome; this
review is one.