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

16 KiB

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)

  1. 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).
  2. 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)

  1. 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."
  2. 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.