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
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.mdv0.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.mdINTENT.mdandSCOPE.mdas adapted this sessionintakes/intakes.mdUSER-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:
-
No side effect without a decision record, or a recorded stance. Mutations go through
_authorizethen_record_mutationinsidestore.transaction(). On flex-auth transport/parse failure,FlexAuthHTTPAdapterreturnsDENYwith reason"authorization service unavailable"and a locally minteddecision_id(AuthorizationDecision.decision_iddefault factory)._authorizethen 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_idmust be absent unless a decision was rendered. -
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.
-
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.pycomment 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. -
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)
- Stop minting a local
decision_idon engine-unavailable DENY. Record stance application (zone or equivalent scope, failure mode, outcome).decision_idpresent only when flex-auth rendered one. - Publish
pep-stance.yaml(or equivalent) named from the layer declaration. Total, no implicit default. Test: published map equalsFlexAuthHTTPAdapterbehaviour. Ask gate-house to add the row to §13.1. - Give
AuthorizationDecisiona 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)
layer.yamlplus a conformance script/test adapted from kings-guard: Engine/PIP, emptytooling_contacts, listed non-Tooling clients (Postgres own-store, State Hub, env secrets, flex-auth Engine API, key-cape OIDC as claim input).- Confine
LocalAuthorizationCheckPortto tests and explicitly standalone fixtures. Productioncreate_application()already uses flex-auth; make construction of a default-allow adapter in the runtime path fail.
Should add (evidence and PIP contract)
- 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."
- Prove hats /
AccessControlFactare 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-0001as 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 onUSER-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.