# Security Write Branch Inventory

Status: current-main inventory for issue 2249 Tier 2 Item 4.

This document classifies the remaining security and system named-table branches
that are relevant to the records write path. It uses the broad records-write
scope: explicit persistence and recycle-bin write branches that select behavior
by a named security, identity, or metamodel table/object, plus derived
records-write caches and cache invalidations for those branches. The sweep
surface is `persistence_insert.go`, `persistence_bulk_insert.go`,
`persistence_service.go` (including update), `persistence_delete.go`,
`persistence_recyclebin_restore.go`, and `persistence_recyclebin_purge.go`.

The scope is not every generic metadata interpreter read. ValidationRule,
formula, picklist, lookup, default-value, auto-number, default-child-row,
child-clone, audit-row, recycle-bin-row, and record-read-cache machinery is
covered here only when the records write path explicitly branches on a named
security/system table or owns a derived write-path cache for that branch.
Ownership calls use `docs/design/l0-l1-standard.md` as the standard:

- L0 may keep metamodel concepts, engines, and seams.
- Security seams must fail closed and cannot be skippable by omission.
- Behavior expressible through an existing fail-closed metadata or capability
  seam should move behind that seam.

The categories below are operational, not historical:

- `migrate-to-validator`: move the invariant behind source-backed
  ValidationRule metadata or registered `accessPolicy.writeValidation`
  capability declarations.
- `migrate-to-lifecycle/cache seam`: keep the effect in L0, but route the
  branch through metadata-declared lifecycle/cache invalidation hooks or another
  generic substrate seam.
- `accepted L0 security/identity substrate`: keep as engine-resident security
  or identity substrate with one owner and one read path.
- `declared auth-policy write-validation/canonicalization seam`: keep the
  auth-policy grammar engine-owned, but select it through metadata-declared
  write hooks with fail-closed readiness for stale persisted metadata.
- `accepted L0 metadata/tooling substrate`: keep as engine-resident metadata
  write-surface protection with one owner and one read path.
- `source-backed action metadata with transitional L0 enforcement`: the policy
  facts are registered action/flow/mapping content, while the records engine
  owns fail-closed enforcement and cache invalidation until a generic seam names
  the ownership contract directly.
- `source-backed field-mapping metadata with L0 records execution`: field
  mapping facts are source-backed metamodel rows, while the records engine owns
  create-image mutation, cache isolation, and cache invalidation until a generic
  field-mapping execution seam names that contract directly.

## Summary

| Area | Current owner/read path | Classification | Next action |
| --- | --- | --- | --- |
| `SharingRule` shape rules: exactly one `ShareWith*`, criteria rules cannot also set `SharedFrom*`, owner rules require exactly one `SharedFrom*` | Registered `platform.security.write.sharing-rule-shape` capability plus direct `SharingRule` normalization/materialized-clear branches in records persistence paths | registered shape validator is already on the right seam; remaining direct normalization/materialized-clear branches are transitional | Keep shape enforcement behind the declared capability; remove or replace object-specific normalization/materialized-clear branches with a generic final-image canonicalization seam. |
| Pre-flow object-security normalization for `SharingRule` write images | Direct `normalizeObjectSecurityContract` calls on insert, bulk insert, and prepared update | generic final-image canonicalization seam | Remove or replace the pre-flow object-specific normalization path with a generic canonicalization owner if the normalization remains necessary. |
| `ObjectPermissions` dependency rules: parent-controlled child permissions cannot be authored, object-permission booleans stay internally consistent | Registered `platform.security.write.object-permissions` capability | Registered dependency and parent-controlled checks are on the right write-validation seam | Preserve fail-closed declared-capability coverage and keep generic persistence paths free of direct ObjectPermissions validation helper calls. |
| `_System_RecordShare` lock and target guards: share rows cannot mutate locked parent records or retarget existing rows | Direct records persistence branches across create, update, delete, restore, and purge | `accepted L0 security/identity substrate` | Keep as ACL/lock substrate until a generic ACL mutation lifecycle seam exists; preserve one owner/read path for parent-lock checks. |
| `SharingRule` owner references: `SharedFrom*` / `ShareWith*` point at existing role/group rows | Registered write-validation capability | `accepted L0 security/identity substrate` | Keep behind `accessPolicy.writeValidation`; preserve fail-closed required-capability checks. |
| `PermissionSetAssignment` target guard: profile-owned permission sets cannot be assigned or unassigned through generic records writes | Registered write-validation capability | `accepted L0 security/identity substrate` | Keep behind `accessPolicy.writeValidation`; preserve fail-closed required-capability checks. |
| `Group`, `GroupMember`, `UserRole` role-backed group and role hierarchy invariants | Registered write-validation capabilities | `accepted L0 security/identity substrate` | Keep behind `accessPolicy.writeValidation`; these protect platform sharing/identity topology. |
| `Group.UserRoleId` blank-clear canonicalization | Direct `normalizeGroupUserRoleReferenceWriteImage` calls on insert, bulk insert, prepared update, update, and the registered validator image | `accepted L0 security/identity substrate` as transitional support for `platform.security.write.group-user-role-reference` | Keep with the topology validator until a generic optional-reference blank-clear canonicalization seam replaces the inline write-image branch. |
| Owner, queue, and record-type assignment validation | `validateQueueAssignment` reads `_System_Group`, `_System_User`, and `QueueSobject`; `validateRecordTypeId` reads RecordType metadata and effective record-type visibility grants; insert, bulk insert, update, and recycle-bin restore run these validators on the final write image | `accepted L0 security/identity substrate` | Keep as generic ownership and record-type security substrate; preserve post-BeforeSave timing, RLS-bypassing existence checks, queue/object support checks, and the single effective-record-type visibility read path. |
| `Group`, `GroupMember`, `User`, and `UserRole` transitive-membership closure maintenance | Source-aware record transactions mark `_System_GroupMembershipClosure` dirty and rebuild it atomically through `group_membership_closure.go` / `persistence.RebuildGroupMembershipClosure` | `accepted L0 security/identity substrate` | Keep as identity/sharing topology substrate; any future seam must preserve source-aware transaction selection, advisory locking, dirty marking, and atomic rebuild before permissions/RLS reads. |
| System-profile-owned immutability | Metadata-owned `ownerResolution` plus registered write-validation capability | `accepted L0 security/identity substrate` | Keep as L0 security substrate; metadata remains the required-set authority. |
| Permission/sharing/role/session cache invalidation | Registered cache-invalidation capabilities selected by metadata | `accepted L0 security/identity substrate` | Keep through `accessPolicy.cacheInvalidation`; add declarations for any future cache-affecting security object before adding code. |
| `Profile` permission-carrier lifecycle | Metadata-declared `platform.profile-permission-carrier` lifecycle hooks create/refresh/delete profile-backed permission carriers and invalidate permission cache after commit | `accepted L0 security/identity substrate` | Keep through `accessPolicy.lifecycle`; preserve transaction-scoped create/update/delete ordering and post-commit permission-cache invalidation. |
| Protected-record generic CRUD guard for platform-managed rows | Metadata-declared `protectedRecords` policy plus `rejectProtectedGuardFieldWrites` and delete/restore/purge guard reads in records persistence | `accepted L0 security/identity substrate` | Keep as fail-closed protected-record substrate; preserve metadata as the policy owner and keep create, update, delete, restore, and purge coverage aligned. |
| Tooling-only and immutable metadata-table generic CRUD guards | Direct records write-surface guards: `rejectToolingOnlyTableWrites`, `rejectImmutableMetadataFieldWrites`, and `rejectUnbypassableMetadataFieldWrites` | `accepted L0 metadata/tooling substrate` | Keep as metadata/tooling write-surface substrate; add tables only when the replacement Tooling or Metadata API surface is complete, or move the surface policy to a metadata-declared seam first. |
| Pack-owned `_System_EmailTemplate` row mutation guard | Direct records generic CRUD guard `rejectManagedEmailTemplateGenericMutation` reads the existing row `OwnerPack` before update, delete, and purge | `accepted L0 metadata/tooling substrate` | Keep as pack-ownership write-surface substrate until a generic metadata-declared ownership seam can replace the direct table guard without allowing pack-owned rows through generic CRUD. |
| Create-from-source bypass guard and derived registry cache | Source-backed `StandardAction`, `FlowDefinition` / `Flow`, `_System_ObjectFieldMapping`, `_System_Object`, and `_System_Field` metadata derive the owning action/source-FK registry; records insert and bulk insert reject direct creates outside the owning action context and clear the registry cache after source metadata writes | `source-backed action metadata with transitional L0 enforcement` | Keep fail-closed records enforcement until a generic action-owned create/fan-out seam can declare the same ownership contract and cache invalidation explicitly. |
| Insert-inheritance from `_System_ObjectFieldMapping` | `_System_ObjectFieldMapping` rows with `ActionType='insertInheritFromLookup'` are loaded through `cachedInsertInheritanceRules`; single-record insert calls `applyInsertInheritance`; `_System_ObjectFieldMapping` writes clear the insert-inheritance cache | `source-backed field-mapping metadata with L0 records execution` | Keep as records-engine field-mapping execution until a generic metadata-declared create-image mutation or field-mapping seam can preserve the same placement, source-aware reads, FLS behavior, and cache invalidation. |
| `User` password hashing, password write guards, username defaults, deactivation session revocation, active-user cache invalidation, and session revocation on `User` / `Profile` / `UserRole` changes | Direct records-engine branches plus AuthService `UserDeactivationHook` active-user cache invalidation | `accepted L0 security/identity substrate` | Keep as account credential/session substrate unless a generic account-credential or session lifecycle seam is introduced; preserve session and active-user cache invalidation on deactivation. |
| `ConnectedApp` and `ConnectedAppAccess` OAuth-client engine effects: client-secret hashing, session revocation, connected-app cache invalidation | Client-secret hashing runs through the declared ConnectedApp policy canonicalization capability; session revocation and connected-app cache invalidation remain direct records-engine branches | `accepted L0 security/identity substrate` | Keep as OAuth/auth-client substrate unless a generic connected-app/session lifecycle seam is introduced. |
| Compiled `ConnectedApp` policy grammar: closed `ClientType`, token-auth method, grant-type, consent-policy, and session-issuer class values/defaults | Registered `platform.security.write.connected-app-policy-canonicalization` and `platform.security.write.connected-app-policy` capabilities declared on `_System_ConnectedApp`, with startup assertions requiring persisted writeValidation capability reconciliation | declared auth-policy write-validation/canonicalization seam | Preserve fail-closed declarations and readiness coverage; future source-backed policy metadata can replace hardcoded literals without adding generic persistence-path branches. |
| `_System_OAuthCustomScope` public-scope policy cache invalidation | Direct records post-commit mutation signal invalidates the middleware-owned derived OAuth scope policy cache | `accepted L0 security/identity substrate` | Keep as auth-policy cache substrate unless a generic auth-policy cache invalidation seam is introduced. |
| Audit exclusions for non-`User` system tables and `FlowInterview.ContextData` | Direct system-table checks in legacy `generateAuditLogs` and audit v2 wiring skip non-`User` system tables; legacy and v2 update audit also suppress the `FlowInterview.ContextData` payload field | `accepted L0 security/identity substrate` | Keep as audit substrate; preserve legacy/v2 parity and revisit only if audit policy moves to metadata. |

## `SharingRule` Shape And Normalization

Current branches:

- `backend/internal/features/records/security_write_validation_hooks.go`:
  `validateSharingRuleShapeWriteContract` is registered under
  `platform.security.write.sharing-rule-shape` for create, update, and restore.
- `backend/internal/features/records/security_contracts.go`:
  `validateSharingRuleShapeContract` still branches on SharingRule field
  combinations, but the records write path reaches it through the registered
  capability above.
- `backend/internal/features/records/security_contracts.go`:
  `normalizeObjectSecurityContract` branches on `TableSharingRule` to
  canonicalize `Criteria`, `SharedFrom*`, and `ShareWith*` optional fields.
- `backend/internal/features/records/security_contracts.go`:
  `materializeObjectSecurityContractClears` branches on `TableSharingRule` to
  turn deleted optional contract fields into explicit `nil` update diffs when
  the old row carried a value.
- `backend/internal/features/records/persistence_insert.go`,
  `backend/internal/features/records/persistence_bulk_insert.go`,
  `backend/internal/features/records/persistence_service.go`, and
  `backend/internal/features/records/persistence_recyclebin_restore.go`
  directly call those normalization/materialization helpers on pre-flow,
  final-write, prepared-update, bulk, and restore images.

Classification:

- Registered shape validator: already on the declared write-validation seam.
- Direct normalization/materialized-clear branches: transitional; migrate to a
  generic final-image canonicalization seam or remove them when no object-shaped
  security-contract helper remains on the generic records path.

Reasoning:

- Sharing-rule shape enforcement is now a fail-closed registered security
  capability, so the validator itself is not a remaining inline persistence
  branch.
- The remaining normalization and materialized-clear helpers still encode a
  SharingRule field list in generic records code. They protect persisted-image
  parity today, but they are still object-shaped until they move behind a
  generic canonicalization contract or are removed with the helper path.

Next action: keep shape enforcement behind
`platform.security.write.sharing-rule-shape`, then replace or remove the direct
SharingRule normalization/materialized-clear branches as part of the remaining
object-security helper cleanup.

## Pre-Flow Object Security Normalization Path

Current direct branches:

- `backend/internal/features/records/persistence_insert.go`: insert calls
  `normalizeObjectSecurityContract` before schema validation and BeforeSave
  execution.
- `backend/internal/features/records/persistence_bulk_insert.go`: bulk insert
  normalizes `SharingRule` optional fields before chunk validation proceeds.
- `backend/internal/features/records/persistence_service.go`: prepared update
  normalizes the write image and recalculates effective updates before
  returning the prepared update.

Classification: generic final-image canonicalization seam.

Reasoning:

- `SharingRule` pre-flow normalization is image canonicalization, not
  record-validity enforcement. If it remains, it needs a generic owner/read path
  instead of a hardcoded field list in generic records flow.

Next action: remove or replace the pre-flow object-specific normalization path
with a generic canonicalization owner if the normalization remains necessary.

## `ObjectPermissions` Write Contracts

Current owner/read path:

- `backend/internal/features/records/security_write_validation_hooks.go`:
  `validateObjectPermissionsWriteContract` is registered under the
  `platform.security.write.object-permissions` capability for create and
  update.
- `backend/internal/features/records/security_contracts.go`:
  `validateObjectSecurityContract` contains the ObjectPermissions dependency
  helper that the registered capability calls.
- The registered capability also calls
  `RejectParentControlledChildWrite`, so parent-controlled child authoring is
  rejected through the same declared ObjectPermissions write-validation path.

Classification:

- Registered dependency validation: already on the write-validation seam.
- Registered parent-controlled child guard: already on the write-validation
  seam.

Reasoning:

- Object and field permission grants are platform security substrate, so L0 can
  own the engine behavior and permission-service checks.
- Metadata is the declared activation owner for the capability, while L0 still
  owns the generic security substrate and permission-service checks.

Next action: preserve focused tests proving the seam runs for create, update,
preview/prepared update, and bulk create, and remove the separate pre-flow helper
path listed above.

## `_System_RecordShare` Lock And Target Guards

Current direct branches:

- `backend/internal/features/records/persistence_insert.go`: create rejects
  inserting a share row when the parent record is currently locked.
- `backend/internal/features/records/persistence_service.go`: update rejects
  retargeting `ObjectApiName` or `RecordId`, loads the existing share row, and
  rejects changing an existing share row when the parent record is locked.
- `backend/internal/features/records/persistence_delete.go`: delete rejects
  removing a share row when the parent record is locked.
- `backend/internal/features/records/persistence_recyclebin_restore.go`:
  restore rejects reactivating a share row when the parent record is locked.
- `backend/internal/features/records/persistence_recyclebin_purge.go`: purge
  rejects permanently removing a share row when the parent record is locked.

Classification: `accepted L0 security/identity substrate`.

Reasoning:

- `_System_RecordShare` is the platform ACL row that the sharing/RLS engines
  interpret. Mutating it changes a record's effective access surface, so the
  lock gate is security substrate rather than tenant or application policy.
- The parent-lock decision already has one owner/read path through the records
  permission/lock check. The write branches call that owner instead of
  duplicating lock-rule facts.
- Retarget immutability prevents an update from evaluating the old parent while
  persisting a new parent target. That is ACL integrity substrate for the share
  row itself.

Next action: keep the current branches until a generic ACL mutation lifecycle
seam exists. Any future seam must preserve create/update/delete/restore/purge
coverage and the same fail-closed parent-lock read path.

## Sharing, Assignment, Role, And Group Topology

Current registered validators:

- `platform.security.write.sharing-rule-owner-references`
- `platform.security.write.permission-set-assignment-target`
- `platform.security.write.group-user-role-reference`
- `platform.security.write.role-backed-group-member`
- `platform.security.write.user-role-parent`

The validator implementations branch on `SharingRule`,
`PermissionSetAssignment`, `Group`, `GroupMember`, and `UserRole` because the
platform permission and sharing engines interpret those metamodel concepts
directly.

Current inline write-image canonicalization for `Group.UserRoleId`:

- `normalizeGroupUserRoleReferenceWriteImage` converts blank
  `Group.UserRoleId` values to an explicit nil/clear on `Group` write images.
- Single-record insert calls the helper after before-save mutation and
  RecordType/queue validation, before lookup validation, formula compute, and
  the registered write-validation capability.
- Bulk insert calls the helper in both validation-enabled and final
  post-compute paths so per-row create images and the SQL payload agree.
- `PreviewBeforeUpdate` and direct `Update` call the helper on the final
  post-BeforeSave image, then recalculate `effectiveUpdates` for `Group` so a
  blank `UserRoleId` becomes an explicit persisted clear.
- `validateGroupUserRoleReferenceContract` also normalizes the validator image
  before enforcing role-backed-group required/forbidden `UserRoleId` rules.

Classification: `accepted L0 security/identity substrate`.

Reasoning:

- `docs/design/l0-platform-substrate.md` defines profile, permission set, role,
  group identity, object/field grants, transitive group membership, sharing, and
  ownership as platform SSoT facts with permission/RLS read paths.
- These checks protect the internal topology those engines evaluate:
  profile-owned permission assignment targets, role-backed groups, role
  hierarchy cycles, and sharing-rule owner references.
- They are already invoked through metadata-declared write-validation
  capabilities, with fail-closed required-capability guards in
  `runRecordWriteValidators`.
- The direct `Group.UserRoleId` normalization branch is not a separate app
  policy fact. It is write-image canonicalization that keeps the registered
  `platform.security.write.group-user-role-reference` validator, preview
  update, direct update, bulk create, and the eventual SQL diff on the same
  final image.
- Treat the normalization branch as accepted transitional support for the
  topology validator. It becomes a future removal target only if records gains a
  generic optional-reference blank-clear canonicalization seam that can preserve
  the same create, bulk-create, prepared-update, update, and validator-image
  behavior without a `Group` inline branch.

Next action: keep these checks behind `accessPolicy.writeValidation`; keep the
`Group.UserRoleId` normalization branch paired with the validator until a
generic optional-reference canonicalization seam exists, and do not add new
inline persistence branches for adjacent topology rules.

## Owner, Queue, And Record-Type Assignment Validators

Current branches:

- `backend/internal/features/records/persistence_service.go`:
  `validateQueueAssignment` reads `_System_Group` first, then `_System_User`,
  through `cachedFindOne` to validate `OwnerId` existence without RLS filtering.
- `backend/internal/features/records/persistence_service.go`:
  when the owner resolves to a `Group` of type `Queue`, `validateQueueAssignment`
  calls `CheckQueueSupport`, which reads `QueueSobject` for the
  `(QueueId, SobjectType)` support row.
- `backend/internal/features/records/persistence_service.go`:
  `validateRecordTypeId` reads RecordType metadata through
  `metadata.GetRecordTypes`, then checks effective record-type visibility through
  the permission service when explicit visibility grants exist.
- `backend/internal/features/records/persistence_insert.go`,
  `backend/internal/features/records/persistence_bulk_insert.go`,
  `backend/internal/features/records/persistence_service.go`, and
  `backend/internal/features/records/persistence_recyclebin_restore.go` call
  these validators on the final post-BeforeSave write image for create, bulk
  create, update, preview/update completion, and restore reactivation.

Classification: `accepted L0 security/identity substrate`.

Reasoning:

- `OwnerId`, queue ownership, and RecordType availability are Salesforce-modeled
  platform facts interpreted by generic records, permission, and sharing
  engines. They are not CRM pack policy or tenant-specific behavior.
- The owner existence check intentionally bypasses RLS so internal jobs can
  assign an existing owner that the current session cannot otherwise read; a
  missing or soft-deleted owner still fails closed.
- Queue/object support is a generic owner assignment constraint read through the
  `QueueSobject` mapping table. It is not hardcoded to any application object.
- RecordType validity and visibility share one read path through metadata and
  the permission service's effective record-type visibility grants.

Next action: keep this branch family as L0 ownership and record-type security
substrate. Any future seam must preserve the final-image timing, RLS-bypassing
existence semantics, queue/object support check, system-owner exception for
platform writes, and effective record-type visibility read path.

## Group Membership Closure Maintenance

Current write branches:

- `backend/internal/features/records/group_membership_closure.go` detects
  `Group`, `GroupMember`, `User`, and `UserRole` as closure source objects.
- `runGroupMembershipAwareTransactionForObjects` and
  `runGroupMembershipAwarePessimisticTransactionForObjects` select the
  source-aware transaction path, hold the group-membership closure advisory lock,
  and fail closed when a source-table mutation runs inside a plain transaction or
  without raw database access.
- `persistence_insert.go`, `persistence_service.go`, `persistence_delete.go`,
  and `persistence_bulk_insert.go` mark the closure dirty after source-object
  create, update, delete, and bulk create mutations.
- `persistence_recyclebin_restore.go` and `persistence_recyclebin_purge.go`
  route cascade restore/purge transactions through the same source-aware
  transaction wrapper when any tombstone belongs to a closure source object.
- `backend/internal/infrastructure/persistence/group_membership_closure.go`
  owns the full-table rebuild of `_System_GroupMembershipClosure` from active
  `User`, `Group`, `UserRole`, and `GroupMember` rows inside the active SQL
  transaction.

Classification: `accepted L0 security/identity substrate`.

Reasoning:

- `_System_GroupMembershipClosure` is the platform SSoT for transitive group
  membership. Permissions, sharing, RLS, and group-recipient resolution read it
  when expanding group access into user access.
- The branch is not an application record-validity rule that belongs in
  ValidationRule metadata. It is derived identity/sharing topology maintenance
  for the tables the platform permission and sharing engines interpret.
- The current owner/read path is singular: records writes mark source mutations
  dirty in the source-aware transaction, persistence rebuilds the derived closure
  table atomically, and permissions/RLS read `_System_GroupMembershipClosure`.

Next action: keep this as L0 identity/sharing substrate unless a generic
identity-topology lifecycle seam replaces the inline transaction wrapper. Any
future seam must preserve the covered objects and phases, the advisory lock,
dirty marking, atomic rebuild, and the `_System_GroupMembershipClosure` read path
before permissions or sharing checks observe the write.

## System-Profile-Owned Immutability

Current path:

- Managed system table definitions declare `accessPolicy.writeValidation` and
  `ownerResolution`.
- `platform.security.write.system-profile-owned-immutable` is registered as a
  write-validation capability.
- The validator fails closed when managed definitions declare owner resolution
  but persisted metadata is stale.

Classification: `accepted L0 security/identity substrate`.

Reasoning:

- The invariant protects platform-owned security metadata from tenant writes.
- Metadata owns the required-set and resolution paths; the Go branch is the
  fail-closed engine-side enforcement of that metadata.

Next action: keep the required-set tests and metadata reconciliation assertions;
new system-owned objects must declare their owner-resolution path in metadata.

## Profile Permission-Carrier Lifecycle

Current lifecycle path:

- `backend/internal/bootstrap/system_tables.json` declares the
  `platform.profile-permission-carrier` lifecycle capability on `Profile` for
  `after_create_in_tx`, `after_update_in_tx`, and `after_delete_in_tx`.
- `backend/internal/features/records/record_lifecycle_hooks.go` reads
  metadata-declared lifecycle capabilities for the object/phase and fails closed
  when a declared capability has no registered hook.
- `backend/internal/features/records/profile_lifecycle_hooks.go` registers the
  built-in `platform.profile-permission-carrier` handlers.
- After create, the hook ensures the profile-backed permission carrier exists,
  seeds object/field permissions for the new profile, ensures navigation
  visibility, and invalidates the permission cache after commit.
- After update, the hook refreshes navigation visibility for the profile.
- After delete, the hook cleans up the profile-backed permission carrier and
  invalidates the permission cache after commit.

Classification: `accepted L0 security/identity substrate`.

Reasoning:

- A profile-backed permission carrier is platform permission substrate. The
  permission service, not tenant metadata, owns the carrier row lifecycle and the
  derived permission rows that make Profile grants readable through the same
  permission-set paths as other grants.
- The branch is already on the generic `accessPolicy.lifecycle` seam: metadata
  declares the phase/capability, and Go only supplies the engine-owned handler
  for the permission substrate capability.
- Permission cache invalidation belongs with the same lifecycle hook because
  permission reads must not observe stale profile grants after commit.

Next action: keep this lifecycle hook on `accessPolicy.lifecycle`. If future
work extracts more security lifecycle behavior, it should reuse the same
metadata-declared lifecycle seam and preserve transaction-scoped create, update,
and delete ordering plus post-commit permission-cache invalidation.

## Protected-Record Generic CRUD Guard

Current path:

- `backend/internal/bootstrap/system_tables.json` declares `protectedRecords`
  policy on `Profile`: guard field `IsSystem`, `reject_delete`, and the public
  error contract.
- `backend/internal/features/records/persistence_insert.go` and
  `backend/internal/features/records/persistence_bulk_insert.go` call
  `rejectProtectedGuardFieldWrites` before non-SystemContext create writes can
  persist a protected guard value.
- `backend/internal/features/records/persistence_service.go` calls
  `rejectProtectedGuardFieldWrites` on update images before non-SystemContext
  callers can flip the guard field.
- `backend/internal/features/records/persistence_delete.go`,
  `backend/internal/features/records/persistence_recyclebin_restore.go`, and
  `backend/internal/features/records/persistence_recyclebin_purge.go` read the
  same `protectedRecords` policy before delete, restore, and purge.

Classification: `accepted L0 security/identity substrate`.

Reasoning:

- The protected-record fact is source-backed by system-table metadata; the
  records path does not own a separate Profile-specific policy literal.
- The guard protects platform-managed identity rows that the permission engine
  and profile lifecycle hooks rely on. Letting generic CRUD fabricate, clear,
  delete, restore, or purge protected rows would break the platform security
  substrate rather than tenant application policy.
- Create/update guard-field rejection and delete/restore/purge rejection share
  the same policy read path and the same `< SystemContext` bypass threshold, so
  the delete policy cannot be defeated by flipping or pre-setting the guard.

Next action: keep the guard as fail-closed L0 security substrate with metadata
as the policy owner. Any future generic protected-record seam must preserve the
single `protectedRecords` read path and cover create, bulk create, update,
delete, restore, and purge before replacing the inline records calls.

## Tooling, Metadata, And Pack Ownership Generic CRUD Guards

Current path:

- `backend/internal/features/records/persistence_service.go` owns
  `toolingOnlyTables` and `rejectToolingOnlyTableWrites`. The map only includes
  metadata tables with complete replacement Tooling or Metadata API write
  surfaces: `FlowDefinition`, `Flow`, `_System_Object`, and `_System_App`.
- Generic insert, bulk insert, update, delete, recycle-bin restore, and
  recycle-bin purge paths call `rejectToolingOnlyTableWrites` before allowing
  non-bypassing generic CRUD mutation.
- `rejectImmutableMetadataFieldWrites` blocks non-bypassing writes to immutable
  metadata fields on `_System_Object`, `_System_Field`, and
  `_System_InstalledPack`.
- `rejectUnbypassableMetadataFieldWrites` rejects writes to unbypassable
  metadata identity fields on `_System_Object`, `_System_Field`, and
  `_System_EmailTemplate` even for trusted metadata-management callers.
- Insert, bulk insert, and update paths run the immutable/unbypassable field
  guards before persisting metadata-table write images.
- `_System_EmailTemplate` update loads the existing row and calls
  `rejectManagedEmailTemplateGenericMutation` before generic CRUD can mutate a
  pack-owned row.
- `_System_EmailTemplate` delete and recycle-bin purge call the same helper
  before generic CRUD can remove or permanently purge a pack-owned row.

Classification: `accepted L0 metadata/tooling substrate`.

Reasoning:

- These branches select the platform write surface for metamodel rows. They are
  not CRM product policy or tenant configuration; they prevent generic records
  CRUD from bypassing Tooling/Metadata API consistency checks and immutable
  metamodel identity fields.
- The tooling-only guard has an explicit owner/read path: `toolingOnlyTables` is
  the single table list, and each table may enter it only after a complete
  replacement write surface exists.
- Immutable and unbypassable metadata-field guards protect metamodel row
  identity. They belong with L0 metadata storage and admin/tooling APIs, not with
  L1 pack payload.
- The `_System_EmailTemplate` ownership guard protects pack-owned metadata rows
  from generic CRUD mutation. Its current owner/read path is singular: the
  records path loads the existing row, reads `OwnerPack`, and rejects update,
  delete, or purge when a pack owns the row. Generic creation of pack-owned email
  templates is covered by the unbypassable `OwnerPack` field-write guard.

Next action: keep the guards as L0 metadata/tooling substrate. If this surface
policy grows beyond the current explicit table, field, and pack-ownership sets,
move the policy behind a metadata-declared write-surface or ownership seam
before adding parallel owners or a second read path.

## Create-From-Source Action Guard And Cache

Current path:

- `backend/internal/bootstrap/create_from_source_guard.go` derives
  `CreateFromSourceGuardEntry` rows from source-backed `_System_ObjectFieldMapping`,
  `_System_StandardAction`, `FlowDefinition`, `Flow`, `_System_Object`, and
  `_System_Field` metadata. Each entry names one `(TargetObject, SourceFKField)`
  position and the owning standard action whose Flow owns that source-FK fan-out.
- `backend/internal/features/records/persistence_insert.go` calls
  `guardCreateFromSourceContext` after createability and lookup validation, and
  before insert inheritance, so direct single-record creates cannot populate a
  source FK that an owning action must fan out.
- `backend/internal/features/records/persistence_bulk_insert.go` uses the same
  guard body through `guardCreateFromSourceContextWithEntries`, with a lazy
  candidate-FK precheck before loading the full registry for the chunk.
- `guardCreateFromSourceContextWithEntries` allows the write only when the active
  standard-action context matches one of the owning action entries for the same
  source FK. Otherwise it rejects the direct generic create with the neutral
  invalid-operation error.
- `isCreateFromSourceGuardMetadataObject` marks `_System_ObjectFieldMapping`,
  `StandardAction`, `FlowDefinition`, `Flow`, `_System_Object`, and
  `_System_Field` as guard-registry metadata; `invalidateSecurityCachesForChanges`
  clears the derived guard cache when records writes mutate those tables.

Classification:

- Owning action/source-FK fan-out policy: registered source-backed action,
  Flow, and field-mapping metadata.
- Records create rejection and derived registry cache: `source-backed action
  metadata with transitional L0 enforcement`.

Reasoning:

- The action/FK ownership facts are not L0 constants. They are registered
  content read through StandardAction, Flow, and object-field-mapping metadata,
  so deleting or changing the pack metadata changes which direct create is
  rejected.
- The records branch is still engine-resident because the enforcement point must
  fail closed on generic create and bulk create before insert inheritance can
  silently complete an action-owned fan-out. Under the L0/L1 standard's seam
  test, the policy remains source-backed content while enforcement stays in the
  records engine as a transitional shape until a fail-closed action-owned create
  seam exists.
- The cache invalidation branch is part of the same owner/read path: metadata
  writes that can change the derived guard registry clear the registry cache
  before later creates read stale action ownership.

Next action: keep the current guard and cache invalidation until a generic,
metadata-declared action-owned create/fan-out seam can express the same contract.
That seam must preserve the single derived-registry read path, create and bulk
create coverage, owning-action context bypass, and invalidation on all metadata
tables that feed the registry.

## Insert-Inheritance Field-Mapping Execution And Cache

Current path:

- `_System_ObjectFieldMapping.ActionType='insertInheritFromLookup'` is the rule
  source for insert-inheritance. The rule loader calls
  `bootstrap.GetFieldMappingsByTargetContext` with that action type.
- `backend/internal/features/records/persistence_insert.go` calls
  `applyInsertInheritance` on single-record create after
  `guardCreateFromSourceContext` and before validation/default finalization, so
  inherited values participate in ValidationRule evaluation.
- `backend/internal/features/records/insert_inheritance.go` owns
  `cachedInsertInheritanceRules`, keyed by target object and action type.
  Transaction-scoped reads bypass the shared cache so uncommitted
  `_System_ObjectFieldMapping` snapshots do not leak into process-global state.
- `applyInsertInheritance` mutates only blank target fields, skips system,
  formula, non-createable, engine-owned, and blacklisted fields, supports
  source-field and constant-default forms, and retrieves the parent record
  through `ps.Retrieve` as the current user so source FLS is preserved.
- `backend/internal/features/records/security_contracts.go` calls
  `clearInsertInheritanceCache` from `invalidateSecurityCachesForChanges` when
  records writes mutate `_System_ObjectFieldMapping`. Metadata refresh/install
  paths can call `InvalidateInsertInheritanceCache` directly.

Classification:

- Rule facts: source-backed `_System_ObjectFieldMapping` metadata.
- Records execution/cache: `source-backed field-mapping metadata with L0 records
  execution`.

Reasoning:

- This is not a fail-closed security guard; it is included because the inventory
  uses the broad records-write scope and this is an explicit system named-table
  write-path branch.
- The field-population facts are not L0 constants. Changing
  `_System_ObjectFieldMapping` rows changes which create image is mutated.
- The records engine currently owns execution because the mutation must occur
  during insert preparation, before validation sees the final image, and it must
  use the same current-user/FLS source read path as the rest of records create.
- Cache invalidation belongs to the same owner/read path: writes to the mapping
  table must retire derived insert-inheritance rules before later creates read
  stale field mappings.

Next action: keep this as records-engine field-mapping execution until a generic
metadata-declared create-image mutation or field-mapping execution seam can
represent the same contract. That seam must preserve single-record insert
placement, source-aware transaction cache bypass, current-user source retrieval,
FLS behavior, blank-target semantics, and `_System_ObjectFieldMapping`
invalidation.

## Cache And Session Invalidation

Current registered cache capabilities:

- `platform.security.cache.permissions`
- `platform.security.cache.sharing-rules`
- `platform.security.cache.role-hierarchy`
- `platform.security.cache.user-profile-role`
- `platform.security.cache.permission-set-assignment-users`
- `platform.security.cache.permission-set-group-assignment-users`
- `platform.security.cache.permission-set-group-component-users`
- `platform.security.cache.muting-permission-set-users`
- `platform.security.cache.muting-permission-set-permission-users`

The record cache invalidation registry reads metadata-declared
`accessPolicy.cacheInvalidation` capabilities, fails closed for required
security objects, and falls back to global security cache flush only for the
known security cache capabilities.

Classification: `accepted L0 security/identity substrate`.

Reasoning:

- Cache invalidation is engine lifecycle substrate, not application policy.
- Security cache invalidation must be co-located with the write transaction or
  immediate post-commit point so sessions, effective permission snapshots, role
  hierarchy, and sharing reads cannot observe stale grants.

Next action: add future invalidation behavior only by declaring
`accessPolicy.cacheInvalidation` and registering a capability handler.

## User Credential, Connected App, OAuth Scope, And Session Branches

Current direct branches:

- `User` insert, preview, bulk insert, and update paths apply password defaults,
  reject unauthorized password writes, reject current-password reuse, and scrub
  password values from audit payloads.
- `User` update deactivation (`IsActive` from true to false) revokes all
  sessions for that user and calls `UserDeactivationHook.OnUserDeactivated`.
  AuthService implements that hook by invalidating its active-user cache entry
  for the deactivated user.
- `User`, `Profile`, and `UserRole` updates revoke affected sessions so
  `$User`, `$Profile`, `$UserRole`, and permission snapshots are refreshed on
  next login.
- `ConnectedApp` create/update paths hash or preserve client secrets through
  the registered `platform.security.write.connected-app-policy-canonicalization`
  capability, which calls the canonicalization helpers in
  `backend/internal/features/records/connected_app_policy.go`.
- The same registered create/update canonicalizer applies ConnectedApp defaults
  for `ClientType`, `TokenEndpointAuthMethod`, `ConsentPolicy`, and grant-type
  values. The registered `platform.security.write.connected-app-policy`
  validator enforces the closed policy grammar for client type, token-auth
  method, grant type, consent policy, public-scope, redirect, PKCE, and
  session-issuer fields.
- `ConnectedApp` updates revoke sessions for the app when security-sensitive
  app configuration changes, then invalidate the connected-app cache.
- `ConnectedApp` deletes revoke all sessions for the app and invalidate the
  connected-app cache.
- `ConnectedAppAccess` updates and deletes revoke sessions for the parent app so
  access grants cannot remain effective in existing sessions.
- `_System_OAuthCustomScope` writes flow through the records post-commit
  invalidation path in `security_contracts.go`. `PersistenceService` owns the
  mutation signal through `SetOAuthCustomScopeInvalidator`, while HTTP
  middleware owns the derived OAuth scope policy cache. The same public-scope
  policy is also wired into ConnectedApp mutation validation through
  `SetOAuthPublicScopePolicyProvider`, where public ConnectedApp scopes must be
  backed by active public scope policy.

Classification:

- User credential hashing/guards, User deactivation session revocation and
  active-user cache invalidation, ConnectedApp client-secret hashing, session
  revocation, connected-app cache invalidation, and OAuth custom-scope cache
  invalidation: `accepted L0 security/identity substrate`.
- Compiled ConnectedApp policy grammar/defaults and closed allowlists:
  declared auth-policy write-validation/canonicalization seam.

Reasoning:

- User credentials, active-user cache invalidation, client-secret hashing, and
  session revocation are auth substrate, not tenant or application metadata.
- The read path for formula globals and permission snapshots is session-bound,
  so write-site revocation is the causal boundary that keeps the session SSoT
  coherent.
- The active-user cache is part of the same authentication read path: after a
  deactivation write, login/session validation must not continue to read a stale
  active-state decision from AuthService's derived cache.
- Secret hashing must remain engine-owned; metadata may define app rows, but the
  secret write path cannot be delegated to tenant-authored rules.
- ConnectedApp policy grammar now has a declared owner/read path:
  `_System_ConnectedApp.accessPolicy.writeValidation` names the canonicalizer
  and validator capabilities, `security_write_validation_hooks.go` registers
  those capabilities, and bootstrap startup assertions require persisted
  writeValidation capability reconciliation before serving writes on upgraded
  orgs.
- `_System_OAuthCustomScope` controls grantable public OAuth scope policy used
  by middleware route enforcement and ConnectedApp public-scope validation. The
  records layer owns the post-commit mutation signal so both read paths refresh
  after the policy table changes without observing uncommitted rows.

Next action: keep the engine-owned credential/session/cache effects in L0 and
keep ConnectedApp policy grammar behind the declared write-validation and
canonicalization capabilities. Any future source-backed policy metadata must
preserve the same transaction/post-commit ordering, User deactivation session
and active-user cache invalidation, secret hashing behavior, connected-app cache
invalidation, OAuth custom-scope policy invalidation, and fail-closed
writeValidation readiness.

## Audit System-Table Filters

Current direct branches:

- Update writes still call legacy `generateAuditLogs` before `emitUpdateAuditV2`.
  `generateAuditLogs` skips non-`User` system tables while still allowing User
  audit records.
- `generateAuditLogs` also skips `FlowInterview.ContextData` on update so the
  legacy audit path does not write the large per-step execution payload into
  field-history rows.
- Audit v2 wiring carries the same non-`User` system-table skip for create,
  update, delete, and undelete events so the newer audit path stays in parity
  with the legacy update audit helper.
- Audit v2 update wiring mirrors the `FlowInterview.ContextData` suppression so
  the legacy audit row path and the v2 AuditEvent/FieldDiff path have the same
  filtered update image.

Classification: `accepted L0 security/identity substrate`.

Reasoning:

- The branches are audit-surface selection for platform system tables, not
  application policy.
- The exception for User preserves security-relevant identity audit visibility.
- The `FlowInterview.ContextData` branch is a named-table audit-payload filter,
  not tenant behavior. Keeping it aligned across legacy and v2 audit avoids a
  split audit read path where one audit surface records the large execution
  payload and the other suppresses it.

Next action: keep as audit substrate unless audit policy becomes a
metadata-declared surface. Any future audit-policy seam must preserve legacy/v2
filter parity for non-`User` system tables and `FlowInterview.ContextData`.
