# App Authority Contract

> **Status:** Normative when merged. This document freezes A1b for the first
> production CRM ownership slice selected by C0. It defines domain authority;
> it does not create storage, a writer, a serving path, or a wire contract.
>
> **Machine-readable companion:**
> [`app-authority-contract.json`](app-authority-contract.json)

## 1. Decision

NexusCRM uses one App authoring model and one future Catalog write path:

```text
publisher-authored canonical AppPackage
  = AppManifest + MetadataBundle
  -> immutable AppRelease digest
  -> Catalog-owned AppInstallation
  -> Catalog-owned HandlerBinding / frontend binding / dependency resolution
  -> fenced Runtime and consumer projections
```

The current CRM `app-pack` is converted once into this model. Generic platform,
tenant, connector, and other non-App packs remain on the neutral
`PackManifest` grammar. No fact is inferred from a similarly named legacy
identifier.

The first concrete App identity and capability declarations are:

```text
PublisherID:            io.nexuscrm
AppID:                  io.nexuscrm.sfcrm
L1 public capability:   io.nexuscrm.sfcrm.ui.record-access.v1
L1 HandlerRef:          io.nexuscrm.sfcrm.handler.ui.record-access.v1
L0 Runtime capability:  runtime.records.evaluate-access.v1
```

The L1 capability identifies the Salesforce-compatible HTTP behavior selected
by C0. The L0 capability identifies a future neutral, bounded record-access
evaluation operation. This ADR names and relates those capabilities but does
not define their semantic request, protobuf, or gRPC method.

## 2. Scope and evidence

This decision consumes:

- C0's selection of
  `POST /services/data/:version/ui-api/record-access` and its one-RPC rule;
- A1a's proof that `PackManifest`, `_System_InstalledPack`, `_System_App`,
  ProductHooks, frontend owner IDs, and Runtime security tables are distinct
  current facts;
- P7a's finite inventory of the independent Runtime metadata closure;
- the existing P6 Runtime security projection and `TrustedCall` admission
  boundary.

The evidence revision is recorded in the companion manifest. Current-tree
anchors are guarded by the backend boundary test. Issues and historical prose
do not override this document or its machine-readable companion.

## 3. Canonical identifiers

### 3.1 Common named-identifier grammar

`PublisherID`, `AppID`, L1 capability IDs, and `HandlerRef` use canonical
lowercase ASCII dot segments:

```text
segment       = [a-z][a-z0-9-]{0,62}, with no trailing '-'
named-id      = segment ('.' segment)+
maximum       = 128 ASCII bytes
```

Inputs must already be canonical. Parsers reject leading or trailing
whitespace, uppercase, Unicode lookalikes, empty segments, and overlength
values; they do not trim, case-fold, or otherwise normalize them. Bytewise
equality is identity equality.

`AppID` must have at least three segments. The extra segment separates a
publisher namespace from an App name without deriving identity from a source
repository or legacy pack name. Capability IDs and handler references append a
terminal major version segment of the form `v[1-9][0-9]*`.

### 3.2 AppID ownership and immutability

The L1 publisher authors `AppID` in the first AppManifest. Catalog's App
identity registry reserves the exact canonical bytes and records the authorized
`PublisherID`. Once reserved, `AppID` is immutable for the lifetime of the
product and is reused by every release and installation of that App.

The CRM conversion uses the explicit constant `io.nexuscrm.sfcrm`. It is not
computed from `salesforce-crm-standard`, `_System_App`, `FrontendAppId`,
`PackInstallId`, a repository URL, or a Git SHA.

A second publisher or package claiming an already reserved AppID fails closed.
The registry never chooses a winner from semver, timestamps, source order, or
repository ownership.

### 3.3 Publisher identity, collision, and transfer

`PublisherID` is a stable logical identity in an operator-owned publisher trust
registry. It is not a URL and is not a signing key. The trust registry binds it
to accepted signing identities/keys and their validity windows. AppManifest
claims are untrusted until a detached publisher attestation is verified against
that registry.

An AppID transfer preserves the AppID and all immutable historical releases.
It requires:

1. a signed transfer statement from the currently authorized publisher;
2. acceptance by the operator-owned trust authority;
3. a monotonically increasing registry ownership epoch; and
4. an append-only audit record naming the old and new PublisherID.

New releases after the transfer must be attested by the new publisher. Old
releases retain their original publisher and attestation. Loss-of-key recovery
is an explicit operator governance event with the same audit and epoch rules;
it is never inferred from a newly uploaded package.

## 4. AppPackage and immutable AppRelease

### 4.1 Closed AppManifest v1

AppManifest v1 has these behavior-affecting fields and no open extension bag:

```text
schema_version
app_id
publisher_id
semantic_version
metadata_bundle_digest
dependencies[]
owned_namespaces[]
required_protocols[]
required_engine_capabilities[]
provided_capabilities[]
frontend_compatibility
```

Semantics:

- `SemanticVersion` is canonical SemVer 2.0.0 without a leading `v` or build
  metadata. Prerelease identifiers are allowed. It is a human/dependency
  selection label covered by the release digest, not the release identity.
- `Dependencies[]` owns App-level dependency truth. Each entry has a unique
  alias, AppID, inclusive minimum semver, exclusive maximum semver, and required
  L1 capability IDs. Catalog resolves it to exact AppRelease digests per
  installation.
- `OwnedNamespaces[]` is the sole App-level namespace claim. Catalog rejects
  overlapping active ownership unless an explicit dependency-owned extension
  point permits it.
- `RequiredProtocols[]` declares protocol ID, required major, and minimum minor.
  A different major is incompatible.
- `RequiredEngineCapabilities[]` declares neutral versioned L0 capabilities.
- `ProvidedCapabilities[]` declares L1 public capabilities, their HandlerRef,
  and the neutral Runtime capabilities each handler needs. A capability is a
  use-case authorization boundary, not a route string or Go interface method.
- `FrontendCompatibility` contains only host-SDK/contribution-schema version
  requirements. It does not identify an active frontend artifact.

The canonical JSON member names and closed v1 shapes are:

```json
{
  "schema_version": "nexuscrm.app-manifest/v1",
  "app_id": "io.nexuscrm.sfcrm",
  "publisher_id": "io.nexuscrm",
  "semantic_version": "1.0.0",
  "metadata_bundle_digest": "sha256:<64 lowercase hex>",
  "dependencies": [
    {
      "alias": "<single canonical segment>",
      "app_id": "<canonical AppID>",
      "minimum_version_inclusive": "<canonical semver>",
      "maximum_version_exclusive": "<canonical semver>",
      "required_capabilities": ["<versioned L1 capability ID>"]
    }
  ],
  "owned_namespaces": ["<canonical named ID>"],
  "required_protocols": [
    {"protocol_id": "<canonical named ID>", "major": 1, "minimum_minor": 0}
  ],
  "required_engine_capabilities": ["<versioned L0 capability ID>"],
  "provided_capabilities": [
    {
      "capability_id": "<versioned L1 capability ID>",
      "kind": "service-handler",
      "handler_ref": "<versioned HandlerRef>",
      "required_runtime_capabilities": ["<versioned L0 capability ID>"]
    }
  ],
  "frontend_compatibility": {
    "mode": "required",
    "host_sdk": {"protocol_id": "<named ID>", "major": 1, "minimum_minor": 0},
    "contribution_schema": {"protocol_id": "<named ID>", "major": 1, "minimum_minor": 0}
  }
}
```

`provided_capabilities.kind` is closed to `metadata` and `service-handler` in
v1. `metadata` requires an absent `handler_ref` and an empty
`required_runtime_capabilities` list. `service-handler` requires both to be
non-empty. `frontend_compatibility` is either exactly `{"mode":"none"}` or the
`required` object above. Protocol major is a positive uint32; minimum minor is
a uint32. Dependency minimum must be lower than maximum.

Canonical set sorting is bytewise by dependency alias, namespace ID, protocol
ID, engine capability ID, and provided capability ID respectively. Nested
capability sets are bytewise sorted. The JSON object order shown is explanatory;
RFC 8785 determines encoded object member order.

The manifest must not contain `AppReleaseDigest`, `ServiceReleaseDigest`,
`FrontendReleaseDigest`, an endpoint URL, an AppInstallationID, a Catalog
revision, or an L2 revision. Those are owned by other lifecycle planes.

Unknown required fields, unknown capability kinds, duplicate IDs, dependency
cycles, invalid ranges, or unsorted/non-canonical sets fail closed.

### 4.2 MetadataBundle v1

MetadataBundle owns only App-authored metadata component facts. The neutral
contract describes whether verified downstream overrides survive replacement;
it does not name repository layers or tenant deployment topology:

```text
schema_version
components[]:
  component_id
  component_kind
  component_format_version
  namespace_id
  business_key
  definition_version
  local_dependencies[]
  external_component_refs[]
  override_policy
  removal_policy
  content_digest
```

Its exact canonical JSON shape is:

```json
{
  "schema_version": "nexuscrm.metadata-bundle/v1",
  "components": [
    {
      "component_id": "sha256:<64 lowercase hex>",
      "component_kind": "<versioned canonical named ID>",
      "component_format_version": 1,
      "namespace_id": "<AppManifest-owned namespace ID>",
      "business_key": "<non-empty canonical UTF-8 key, at most 255 bytes>",
      "definition_version": "<positive canonical uint64 decimal string>",
      "local_dependencies": ["<component_id>"],
      "external_component_refs": [
        {
          "dependency_alias": "<declared App dependency alias>",
          "namespace_id": "<dependency-owned namespace ID>",
          "component_kind": "<versioned canonical named ID>",
          "business_key": "<canonical UTF-8 key>"
        }
      ],
      "override_policy": "preserve-authorized-overrides",
      "removal_policy": "block-if-referenced-or-overridden",
      "content_digest": "sha256:<64 lowercase hex>"
    }
  ]
}
```

`component_id` is derived and verified as SHA-256 over the domain-separated
tuple `(namespace_id, component_kind, business_key)`; it is stable across
definition/content changes and is not another authored name. Business keys must
be valid UTF-8, already canonical for their component kind, contain no control
characters, and have no leading/trailing whitespace.

The closed v1 override policies are `replace` and
`preserve-authorized-overrides`. The latter preserves only overrides already
authorized by the component-kind owner; it does not define an authoring layer
or grant new authority. The closed v1 removal policies are
`remove-if-unreferenced` and `block-if-referenced-or-overridden`; a policy never
silently transfers ownership. Component format version is a positive uint32.

Components sort by `component_id`; local dependencies sort by component ID;
external references sort by
`dependency_alias + namespace_id + component_kind + business_key`. Duplicate
keys or references fail. Canonical component behavior bytes are stored as
content-addressed blobs referenced by `content_digest`; the bundle index cannot
inline a second behavior representation.

The containing AppManifest supplies AppID and App-level dependency aliases.
MetadataBundle cannot repeat AppID, PublisherID, semantic version, App-level
dependency ranges, owned namespaces, protocol requirements, engine
requirements, or provided capabilities.

`LocalDependencies` reference component IDs in the same bundle.
`ExternalComponentRefs` may reference only an alias already declared in
AppManifest plus a component business key; they cannot declare another AppID or
version range. Every business key must fall within an AppManifest-owned
namespace or a dependency's explicit extension point.

Component ownership is therefore `containing AppID + ComponentID`. The bundle
does not repeat `OwnerAppID`. Ownership is write-once. A component-kind owner
separately authorizes which downstream override facts may exist; the component's
`override_policy` only decides whether those already-authorized overrides are
replaced or preserved during release replacement. It grants no override
authority itself.

#### 4.2.1 Layout bundle slot declaration component

MetadataBundle v1 registers the versioned component kind
`io.nexuscrm.layout-bundle-slot.v1` as the signed declaration of one stable
App-owned layout-bundle slot. Its descriptor `business_key` is exactly the
canonical `key256:` plus 64-lowercase-hex `LocalComponentKey`; the descriptor's
`component_format_version` is `1`, `external_component_refs` is empty,
`override_policy` is `replace`, and `removal_policy` is
`block-if-referenced-or-overridden`. Across all namespaces owned by one App, a
MetadataBundle may contain at most one declaration for the same
`LocalComponentKey`.

The component's canonical behavior bytes are RFC 8785 JCS of exactly:

```json
{
  "schema_version": "nexuscrm.layout-bundle-slot/v1",
  "local_component_key": "key256:<64 lowercase hex>",
  "target_component_id": "sha256:<64 lowercase hex>",
  "previous_target_component_id": "sha256:<64 lowercase hex>"
}
```

`local_component_key` must byte-match the descriptor `business_key`.
`definition_version=1` requires `previous_target_component_id` to be omitted,
not `null`; every version above `1` requires it. `target_component_id` must name
a non-slot component in the same MetadataBundle, must differ from the optional
predecessor, and the declaration's `local_dependencies` must be exactly the
singleton array containing that target ID. Catalog rejects a missing, external,
self/slot, duplicated, or otherwise ambiguous target.

The AppRelease verifier's registered-component-kind dispatch must parse this
closed behavior shape, reproduce its exact RFC 8785 bytes, and enforce all
descriptor/target rules above before the release can be registered. Merely
matching arbitrary blob bytes to `content_digest` is not verification of this
kind and fails closed.

The declaration's `ComponentID`, derived from its namespace, kind, and business
key by the existing rule, is stable for the slot lifetime. Catalog records the
immutable `(AppID, LocalComponentKey) -> declaration ComponentID` reservation;
another namespace or declaration cannot claim it. The exact declaration
revision identity is derived without adding a self-referential field:

```text
LayoutBundleSlotRevisionDigest = SHA-256(
  "nexuscrm.layout-bundle-slot-revision.v1\0" ||
  JCS({
    "app_id": AppID,
    "component": ExactCanonicalMetadataBundleComponentDescriptor
  }))
```

The descriptor includes `definition_version`, the singleton local dependency,
closed policies, and `content_digest`; that digest already commits the exact
canonical behavior bytes. Comparing only `ComponentDigest`, target ID, or
behavior digest is insufficient for exact reuse.

Catalog stores a version-indexed global App-slot ledger with these unique facts:

```text
(AppID, LocalComponentKey)
  -> {DeclarationComponentID, NamespaceID}
(AppID, LocalComponentKey, DefinitionVersion)
  -> {LayoutBundleSlotRevisionDigest, TargetComponentID,
      PreviousTargetComponentID}
(AppID, LocalComponentKey)
  -> {HeadDefinitionVersion, HeadRevisionDigest}
(AppID, AppReleaseDigest, LocalComponentKey)
  -> DefinitionVersion
```

The first registered node is version `1` with no predecessor and commits with
an absence CAS. An already registered version is reusable by any AppRelease,
including after newer versions exist, only when its complete revision digest is
identical; a different digest at that version is a fork. A new node must be
exactly `HeadDefinitionVersion+1`, retain the reserved declaration ComponentID
and namespace, name the target stored at `definition_version-1` as its
predecessor, and change the target ComponentID. It commits with a CAS over the
prior head version and revision digest, so concurrent divergent successors
cannot both register. Gaps, overflow, predecessor mismatch, rebinding, or key
reuse fail closed. The predecessor is never chosen from semver, upload order,
activation order, timestamps, or whichever immutable release was observed
most recently.

A target component may change content or format while retaining its stable
ComponentID without advancing the slot declaration. A target identity change
requires a successor. A later successor may intentionally point back to an
older historical target (for example `A -> B -> A`) as long as it names the
immediately preceding version's target; ordinary rollback instead selects the
already registered historical AppRelease/version and appends nothing.

Omission from one immutable AppRelease means only that the release provides no
binding for the slot. It does not mutate, tombstone, or free the global
reservation/lineage and cannot affect another release or installation still
using it. A later release may reuse any exact historical version or append the
next valid successor. Per-installation activation and a Catalog candidate's
required/absent slot set remain separate from global registration. The
descriptor and canonical behavior are transitively covered by
`MetadataBundleDigest` and `AppReleaseDigest`; no open field is added to the
closed MetadataBundle JSON shape.

### 4.3 Canonicalization and digest boundary

Canonical JSON objects use RFC 8785 JSON Canonicalization Scheme. Arrays that
represent sets are sorted by their documented canonical key and reject
duplicates. Ordered behavior arrays remain ordered and are covered in that
order.

Each registered component kind must produce canonical behavior bytes. Unknown
component kinds or a kind without a canonicalizer fail closed.

```text
ComponentID = SHA-256(
  "nexuscrm.app-component-id.v1\0" ||
  NamespaceID || "\0" || ComponentKind || "\0" || BusinessKey)

ComponentDigest = SHA-256(
  "nexuscrm.app-component.v1\0" ||
  ComponentKind || "\0" || ComponentFormatVersion || "\0" ||
  CanonicalBehaviorBytes)

MetadataBundleDigest = SHA-256(
  "nexuscrm.metadata-bundle.v1\0" ||
  JCS(CanonicalBundleIndexIncludingComponentDigests))

AppReleaseDigest = SHA-256(
  "nexuscrm.app-release.v1\0" ||
  JCS(CanonicalAppManifestIncludingMetadataBundleDigest))
```

`AppRelease` is the tuple `(AppID, AppReleaseDigest)`. There is no separate
package identity digest. The AppPackage is a content-addressed object graph:
AppReleaseDigest is its root, AppManifest references MetadataBundleDigest, and
the bundle references every component digest. A registry may have transport-
specific blob/manifest checksums, but they are verified delivery facts and never
a second release identity. The root transitively covers every behavior-affecting
App contract field, every metadata component descriptor, and every canonical
L1 metadata byte.

AppReleaseDigest is not present inside AppManifest, which prevents a circular
self-digest. `(AppID, SemanticVersion)` is an immutable one-to-one registry
mapping to one digest; republishing the same semver with different bytes fails.

### 4.4 Informational metadata and provenance

Descriptions, build logs, repository URLs, Git SHAs, SBOM locations, and other
informational annotations live in a detached publication envelope. Catalog and
Runtime must never branch on them. If a field can affect validation,
installation, metadata interpretation, capability admission, UI behavior, or
execution, it belongs in the canonical core and is digest-covered.

The detached publisher attestation binds at least:

```text
PublisherID
AppID
SemanticVersion
AppReleaseDigest
canonicalization profile
artifact media type
```

Provenance may add source and build evidence, but cannot replace publisher
authorization or change the canonical bytes. Catalog verifies the release
digest, attestation, publisher trust, and AppID reservation before recording an
AppRelease.

## 5. Fact ownership and independent release planes

The companion manifest is the machine authority map. The human summary is:

| Fact | Author | Sole authority/read path |
|---|---|---|
| AppID and AppManifest core | L1 publisher | immutable AppPackage verified and registered by Catalog |
| Publisher trust and AppID transfer | operator trust authority | Catalog publisher/App registry |
| AppRelease digest and semver mapping | derived from canonical AppPackage | Catalog release registry; bytes in artifact registry |
| App dependencies, namespaces, compatibility, provided capabilities | L1 publisher | AppManifest only |
| Component identity, graph, policies, content hash | L1 publisher | MetadataBundle only |
| AppInstallation selection/lifecycle/grants | operator request validated by Catalog | Catalog AppInstallation authority |
| Runtime workload/capability grants | Catalog AppInstallation authority requests the desired grants through a fenced command | Runtime Identity/Security epoch and projection writer; authoritative serving read remains inside Runtime |
| Handler declaration | L1 publisher | AppManifest ProvidedCapabilities |
| ServiceRelease bytes and digest | owning service release CI | artifact registry |
| Active handler/service selection | operator binding validated by Catalog | Catalog HandlerBinding |
| Endpoint resolution | deployment authority | workload/service binding; never App metadata |
| FrontendRelease bytes and digest | owning frontend release CI | artifact registry |
| Active frontend selection | operator binding validated by Catalog | Catalog frontend binding |
| Online configuration and its effective version | org admin / domain author | configuration domain authority; independent of Catalog publication; see [lifecycle boundary](configuration-and-execution-lifecycle.md) |
| Mutable navigation | org admin/L1 defaults | `_System_App`; never product App identity |
| Generic non-App pack lifecycle | pack author/controlplane | PackManifest and installed-pack lifecycle |

The release/deployment planes are independent of each other and of online configuration:

```text
AppRelease       = immutable L1 App contract + L1 metadata digest
ServiceRelease   = immutable executable image digest
FrontendRelease  = immutable frontend asset digest
Catalog revision = release serving selection and derived publication artifacts
```

An AppManifest may require compatible protocol, engine, handler contract, or
frontend host versions. Exact ServiceRelease and FrontendRelease digests are
selected by bindings, not embedded in AppRelease. Therefore service-only or
frontend-only upgrades do not create a new AppRelease. Online configuration
changes use domain-owned versions and require neither CatalogRevision nor
AppRelease publication. Existing Catalog projections of editable metadata are
migration findings under the [configuration boundary](configuration-and-execution-lifecycle.md).

## 6. Catalog-owned AppInstallation

### 6.1 Identity and authoritative binding

Catalog mints a lowercase canonical UUIDv7 `AppInstallationID`; Runtime's
255-byte opaque carrier ceiling remains only a wire safety bound. The ID is not
`PackInstallId` and is stable across upgrade, rollback, and service/frontend
changes.

Catalog owns exactly:

```text
OrgID + AppInstallationID
-> AppID + active AppReleaseDigest
-> exact resolved dependency AppReleaseDigests
-> authorized canonical workload identities
-> granted Runtime capabilities
-> effective owned namespaces (derived from the active release)
-> active HandlerBindings and frontend binding
-> lifecycle state + activation/ownership epochs
```

`AppID` cannot change for an existing AppInstallationID. Effective namespaces
are a validated projection of AppManifest, not a second authoring surface.
Granted capabilities are operator/Catalog authorization and must be subsets of
the release declarations and the workload/session/request intersections used by
P6.

For each `(OrgID, AppID)`, Catalog permits at most one live AppInstallation.
Live includes `PREPARING`, `ACTIVE`, `UPGRADING`, `ROLLING_BACK`, `REMOVING`,
and `FAILED`; every one of those states retains the unique live reservation.
Only completion of `REMOVING -> REMOVED` releases that reservation, after which
reinstallation may mint a new `AppInstallationID`. Historical `REMOVED` rows
remain immutable and may coexist. Migration, reconciliation, or projection
compilation encountering more than one live installation fails closed.

### 6.2 Lifecycle

The authoritative state machine is:

```text
ABSENT -> PREPARING -> ACTIVE
ACTIVE -> UPGRADING -> ACTIVE(new release) | ACTIVE(old release)
ACTIVE -> ROLLING_BACK -> ACTIVE(prior release) | ACTIVE(current release)
ACTIVE -> REMOVING -> REMOVED
PREPARING | UPGRADING | ROLLING_BACK | REMOVING -> FAILED
FAILED -> PREPARING | UPGRADING | ROLLING_BACK | REMOVING
```

The row keeps `active_release`, optional `candidate_release`, and the last
fully active release. A failure never exposes a partly prepared candidate and
never destroys the last active release. Recovery is idempotent from the
Catalog journal. `REMOVED` is a tombstone retained through dependency,
rollback, work-retention, and audit gates; reinstall mints a new
AppInstallationID.

Preparation validates publisher trust, package digest, dependency resolution,
namespace ownership, metadata ownership, compatibility, schema readiness,
workload identity, capability grants, and handler/frontend bindings. Activation
publishes one CatalogRevision and records the exact projection checksums and
epochs required for serving.

Publication is ordered and resumable:

1. Catalog commits the candidate release, installation journal, expected
   projection digests, the expected prior `SecurityEpoch`, and a stable security
   projection command identity while the candidate is non-active. Catalog does
   not preallocate the next epoch.
2. Owner compilers build and verify immutable Catalog/Runtime projections from
   that committed candidate; no serving pointer changes.
3. The Catalog activation controller sends the committed desired App grants and
   expected projection digest through that idempotent fenced command. Runtime
   Identity/Security validates the expected prior epoch, allocates a strictly
   newer `SecurityEpoch`, atomically writes the complete security projection and
   its epoch/digest attestation, and returns the immutable binding. Catalog
   records that acknowledgement in the journal; it never writes the projection
   or allocates the epoch.
4. Only after required schema/view preparation and projection acknowledgements
   match does one Catalog compare-and-swap publish the new CatalogRevision and
   ACTIVE AppInstallation tuple.
5. P7c/S1 separately decides whether a serving replica can admit that tuple;
   publication never bypasses its readiness and receipt checks.

A crash at any prefix resumes from the journal. A projection published for a
candidate that never activates grants no serving authority because the
AppInstallation/Catalog tuple is still non-active; repair either completes the
same digest/epochs or publishes a newer complete projection that removes it.

Conflicts fail closed. Catalog does not choose between colliding namespaces,
owners, releases, bindings, or migrations by last-write-wins.

### 6.3 Runtime security projection

Catalog is the sole upstream author of AppInstallation authorization. It may
request a projection advance only for committed non-active or active Catalog
authority state, using the stable command identity, committed desired App
grants, expected complete projection digest, and expected prior
`SecurityEpoch` recorded by Section 6.2.

Runtime Identity/Security is the sole `SecurityEpoch` allocator and the sole
writer of the complete per-org Runtime security projection and immutable
epoch/digest attestation. Under its org-scoped writer fence it validates the
Catalog command and all non-App security inputs, allocates a strictly newer
epoch, replaces the complete binding set atomically, and returns the exact
binding. An exact command replay returns the same binding; a changed payload or
stale expected epoch fails closed. Catalog records the returned binding in its
activation journal but cannot write, infer, repair, or preallocate Runtime
security state.

Runtime also owns persistence, leases, readiness, and the serving read of that
projection. It cannot create, edit, infer, or repair AppInstallation authority.
Signed delegation is only a carrier. Deployment lockfiles are desired-state
inputs only.

P7c/S1 admits a request only when the independently fenced TrustedCall,
SecurityEpoch, AppInstallation, CatalogRevision, SchemaGeneration, and
EngineGeneration agree. Missing, inactive, stale, checksum-invalid, or
incompatible facts fail before repository access. SecurityEpoch and
CatalogRevision remain independent axes.

## 7. Capability and HandlerBinding for record-access

### 7.1 Two capability layers

The CRM AppRelease provides:

```text
io.nexuscrm.sfcrm.ui.record-access.v1
```

This is an L1 product capability owned by the sfcrm App. Its public HTTP route,
Salesforce-compatible request/response grammar, sanitization, and frontend
behavior remain L1 facts.

Its handler requires:

```text
runtime.records.evaluate-access.v1
```

This is a neutral L0 authorization capability for one bounded access-evaluation
use case. L0 must not contain Salesforce route names, envelopes, errors, object
names, or frontend grammar. A later semantic-contract PR must preserve C0's
complete batch, omission, authorization, cancellation, and one-RPC behavior.

Capability IDs are immutable within a major version. Additive behavior that is
safe for every v1 consumer may evolve under protocol-minor compatibility.
Changed meaning, removed fields/outcomes, weaker authorization, or changed
boundedness requires a new terminal major ID. Alias or case normalization is
forbidden.

### 7.2 Handler declaration versus active binding

AppManifest declares:

```text
CapabilityID: io.nexuscrm.sfcrm.ui.record-access.v1
HandlerRef:   io.nexuscrm.sfcrm.handler.ui.record-access.v1
Requires:     runtime.records.evaluate-access.v1
```

It does not declare an endpoint, workload identity, ServiceRelease digest, or
route ownership epoch.

Catalog creates immutable HandlerBinding records containing:

```text
HandlerBindingID (Catalog-minted lowercase UUIDv7)
OrgID / AppInstallationID / AppReleaseDigest
CapabilityID / versioned HandlerRef
canonical WorkloadID
exact ServiceReleaseDigest
OwnershipEpoch
state
compatibility receipts
```

The natural uniqueness key is
`OrgID + AppInstallationID + CapabilityID + OwnershipEpoch`. There is at most
one ACTIVE binding for `OrgID + AppInstallationID + CapabilityID`.

```text
STAGED -> ACTIVE -> DRAINING -> RETIRED
STAGED -> FAILED
```

Activation is a compare-and-swap that increments OwnershipEpoch, validates the
active AppRelease declaration, authorized workload, service attestation, and
required Runtime capability, and atomically makes the prior binding DRAINING.
Unknown or incompatible declarations fail closed. A binding can retire only
after its route/work references and in-flight operations have drained or
reached their explicit retention gate.

HandlerRef's terminal `vN` is the handler contract major. ServiceRelease
attestation declares the exact HandlerRefs and protocol minors it implements;
HandlerBinding does not carry another handler-version field that could disagree.

The deployment authority resolves `(WorkloadID, ServiceReleaseDigest)` to
trusted endpoints. Endpoint health and placement are deployment facts.
AppManifest, MetadataBundle, and HandlerBinding never contain arbitrary URLs.
Gateway RouteEpoch selects the one active public route owner and references the
active binding; it does not become App or handler authority.

For the C0 slice, every accepted non-empty HTTP request must invoke exactly one
L1-to-Runtime data RPC for the entire batch, including object-type resolution.
The semantic capability accepts an empty batch, but the CRM route preserves its
current local empty success and invokes zero Runtime RPCs for empty or rejected
input. This avoids creating a new availability dependency for a request that
requires no Runtime fact. HandlerBinding activation cannot weaken this
at-most-one-RPC gate or introduce per-record/per-type L1 fan-out.

## 8. CRM AppPack one-time conversion

### 8.1 Scope

Only the current CRM `app-pack` path converts in this sequence. Platform,
tenant, connector, integration, and other non-App PackManifest categories are
unchanged. The issue-tracker fixture is not converted or promoted.

The conversion uses an explicit signed mapping:

```text
legacy CRM source identity -> io.nexuscrm.sfcrm
```

The mapping is reviewed input, not an ID derivation rule.

### 8.2 Inputs and idempotency

The one-time A1c migration receives:

- OrgID and the exact legacy installed-pack row, including PackInstallId only
  as legacy incarnation evidence;
- source kind, URI, revision, manifest bytes, component bytes, desired/applied
  payload hash, host-requirement digest, and persisted runtime declarations;
- the canonical R1b AppPackage, AppReleaseDigest, publisher attestation, and
  provenance;
- the explicit legacy-to-AppID mapping; and
- the desired authorized workloads, capability grants, and installation
  binding inputs.

The migration key is
`OrgID + legacy PackInstallId + applied payload hash + target AppReleaseDigest`.
The first successful attempt mints and journals one AppInstallationID. Retries
return that same result. A different target or legacy incarnation under the
same logical migration conflicts and fails; it is never overwritten.

### 8.3 Equivalence and conflict proof

Before committing target authority, the migration proves:

1. legacy desired and applied hashes converge and source deep-verify succeeds;
2. canonical AppPackage components are a complete one-to-one conversion of the
   legacy CRM behavior components, with no unknown or dropped kind;
3. component content digests and ownership/business keys match the materialized
   metadata rows and persisted runtime declarations;
4. AppManifest dependency, namespace, compatibility, and capability facts are
   the sole target representation of corresponding legacy facts;
5. publisher attestation and AppID reservation are valid;
6. no AppID/semver/digest, component owner, namespace, dependency, installation,
   or binding conflict exists; and
7. the resulting Catalog and Runtime projections match their independently
   generated expected checksums.

Any mismatch leaves legacy authority untouched and writes only a failed
migration journal entry. There is no partial AppInstallation and no
last-write-wins repair.

### 8.4 Coexistence and deletion

After conversion, the legacy CRM source is sealed at the converted hash. It may
remain a read-only compatibility projection solely for explicitly inventoried
unmigrated capabilities. It cannot accept install/reapply/upgrade writes, author
a newer App fact, or be selectable by a migrated capability. AppRelease
metadata upgrades remain blocked while any legacy consumer requires this
sealed projection; service-only and frontend-only upgrades remain independent.

This is finite coexistence, not a permanent adapter or dual read path: each
capability has exactly one serving owner/read path, no new legacy reader may be
added, and the deletion ledger names every remaining reader.

The CRM legacy path is deleted only after all of these executable gates pass:

1. R1b publishes and verifies the exact immutable AppPackage/AppRelease;
2. A1c migration, interruption recovery, conflict, rollback, and projection
   publication tests pass;
3. P7b proves complete snapshot parity and P7c/S1 switches each migrated
   Runtime closure without fallback;
4. every CRM capability has either moved to AppPackage/Catalog authority or
   has an explicit retained owner outside the legacy pack path;
5. production route/work owners use fenced bindings and no source-checkout,
   known-pack, active-app-pack, installed runtime declaration, or legacy
   MetadataService reader is reachable for the migrated facts;
6. rollback/handler/work retention has expired or migrated forward; and
7. a repository guard proves zero remaining CRM legacy readers while generic
   non-App pack tests still pass.

Deletion removes the CRM AppPack install/reapply authority, accepted source
revision locks, CRM installed-pack identity/projections, startup composition
fallbacks, and source-checkout serving dependency. It does not delete generic
PackManifest grammar or non-App installed-pack rows.

## 9. Phase handoffs

### 9.1 A1c — Catalog App authority and migration

**Inputs:** this ADR and manifest; canonical fixture AppPackages; existing P6
projection API; A1a legacy facts; explicit CRM mapping; C0 capability IDs.

**Outputs:** one non-serving Catalog AppID/publisher/AppRelease/AppInstallation/
HandlerBinding authority; one writer/state machine; fenced Runtime projection
publisher; idempotent CRM conversion journal; equivalence/conflict/deletion
guards.

**Must not:** switch a production consumer, create a second installation
registry, let Runtime/deployment author App state, or retain a permanent
adapter.

### 9.2 P7b — immutable RuntimeMetadataView

**Inputs:** canonical AppRelease/MetadataBundle fixtures; AppInstallation-scoped
effective dependency/namespace facts; P7a closure; CatalogRevision and schema/
engine compatibility axes.

**Outputs:** a complete immutable content-addressed RuntimeMetadataView and
consumer projections, checksum, full legacy parity oracle, and a non-serving
selection guard.

**Must not:** invent App semantics, read PackManifest as a second authority,
publish a serving current pointer, or add a repository fallback.

### 9.3 #3624 R1b — publication and BOM

**Inputs:** the canonical formats/digests/identifiers in this ADR, R1a delivery
inventory, and L1 CRM source bytes.

**Outputs:** a published immutable AppPackage object graph rooted at
AppReleaseDigest, detached attestation/provenance, reproducible build evidence,
and an L2 BOM reference to that root digest.

**Must not:** reinterpret App fields, create Catalog state, put service/frontend
digests into AppRelease, or make the BOM serving authority.

### 9.4 P7c/S1 — atomic serving cutover

**Inputs:** A1c authoritative installation/binding/projection state; P7b exact
view/checksum; R1b immutable artifact evidence; P6 TrustedCall and security
lease; compatible ServiceRelease/FrontendRelease bindings.

**Outputs:** one ServingAdmission-bound AppInstallation and RuntimeMetadataView,
one compatible receipt tuple, switched consumers, and deleted global/repository
fallbacks for the cut-over closure.

**Must not:** select before every input agrees, mix revisions, accept missing
scope, or leave the old and new read paths selectable.

A1c, P7b, and R1b may implement in parallel from this contract. P7c/S1 remains
blocked until all three complete. R1b additionally waits for #3624 R1a.

## 10. Rejected alternatives

- **Reuse `_System_App`:** rejected because it is mutable UI/navigation
  metadata with no release, publisher, workload, capability, or ownership
  semantics.
- **Reuse pack name or PackInstallId:** rejected because they identify a generic
  pack name and one install incarnation, not a stable product or Catalog
  installation.
- **Wrap the complete PackManifest forever:** rejected because it preserves two
  version/dependency/compatibility authorities and a source-checkout serving
  dependency.
- **Globally replace PackManifest:** rejected because non-App categories have
  valid independent pack lifecycles.
- **Put AppReleaseDigest inside AppManifest:** rejected as circular.
- **Use semver or Git SHA as release identity:** rejected because neither
  content-addresses all behavior bytes.
- **Embed service/frontend digests in AppRelease:** rejected because it couples
  independent release planes.
- **Put endpoint URLs in App metadata:** rejected because deployment owns
  endpoint resolution and trust.
- **Remote `RecordAccessReader` methods independently:** rejected because the
  object-unspecified branch would create `1 + object-types` L1-to-Runtime RPCs.
- **Treat Runtime security projection or signed claims as authority:** rejected
  because both are downstream serving materializations.

## 11. Failure conditions and definition of done

A1b fails if any implementation can:

- derive AppID from a legacy/UI/repository identifier;
- publish the same AppID/semver with different behavior bytes;
- author App dependency, namespace, compatibility, or version truth in both
  AppManifest and MetadataBundle/PackManifest;
- activate an AppInstallation without publisher, dependency, namespace,
  capability, projection, and snapshot checks;
- let Runtime, deployment, BOM, or delegation author AppInstallation state;
- route a capability to an arbitrary metadata URL or two active handlers;
- change AppRelease for a service-only/frontend-only/L2-only update;
- serve a migrated capability through both legacy and target reads; or
- make the CRM compatibility projection permanent or globalize the migration.

The decision is complete when the companion manifest and guard agree with this
ADR and target architecture, exact anchors resolve, required tests pass, and no
production behavior changes in the PR.
