ADR 0003: Session Store Openraft Replication

Status

Accepted

Date

2026-06-08

Amended 2026-07-12 by #127.

Context

Authoritative telecom session state cannot rely on single-node storage, wall-clock last-writer-wins, or best-effort replica repair. Session records need monotonic fencing, compare-and-set semantics, TTL handling, watch resume support, and stale replica recovery.

Decision

Authoritative session HA uses Openraft as its only election, vote, log-matching, commit, membership, and linearizable-read authority. ConsensusSessionStore is the production adapter; QuorumSessionStore is a compatibility type alias to that same implementation and is not a second consensus algorithm. The previous majority-visible-prefix coordinator is removed.

The target session-store contract includes:

  • A validated immutable topology: stable logical replica IDs, canonical network endpoints, expected TLS identities, unique failure/backing identities, one exact local logical ID, and a cluster/configuration/epoch identity whose descriptor digest exactly matches the admitted set. Logical IDs are never inferred from endpoint strings. Stable Openraft node IDs are cluster-scoped, nonzero, SQLite-safe signed-64-bit values derived from logical replica IDs; adding, removing, or reordering another member does not renumber them.
  • Monotonic fences and CAS for authoritative writes.
  • Durable Openraft vote, log, committed/applied/purged, membership, request outcome, and snapshot metadata, plus a committed 1-based application journal for lease acquire, renew, release, CAS, delete, TTL refresh, and batch operations.
  • One public 365-day maximum for Duration-based session refresh and lease TTLs, with zero accepted as immediate expiry and exact checked deadline arithmetic at every direct, nested, persistence, quorum, and transport boundary.
  • Structural owner and session-key identities: owner IDs and custom key-type names contain 1 through 128 UTF-8 encoded bytes; reserved key-type strings have one canonical well-known representation; ordering follows the persisted string; and model, persistence, and transport decode all fail closed.
  • Bounded iterative replication trees: depth 16 from a depth-1 root and 256 total operation nodes per entry, counting every node including Batch.
  • Encryption/sealing before client_write and decryption/unsealing only above the consensus adapter. Openraft logs, RPCs, follower apply, replay, outcomes, and snapshots contain opaque envelopes, never plaintext or HKMS/key-provider handles.
  • Durable request IDs and semantic request digests. A response-loss retry, including after leader change, returns the original committed outcome; reusing an ID for different intent fails closed.
  • One shared fixed eight-slot proposal-admission pool for normal mutations and finite-expiry floor commands. Admission stays inside the existing operation deadline; after client_write_ff acceptance, a detached supervisor retains the permit until the exact proposal resolves, so cancellation cannot create an unbounded detached queue.
  • One shared linearizability supervisor per node admits at most 64 total callers across active and waiting cohorts and owns exactly one Openraft ensure_linearizable call at a time. Pre-dispatch callers may share that exact result; later callers require a later check. Caller cancellation or deadline expiry cannot cancel a dispatched check or create an overlapping one. Openraft remains the sole leadership, quorum, read-index, and applied-state authority.
  • Openraft log reconciliation from committed authority. The SQLite adapter rejects truncation at or below its persisted committed/applied floor, rejects stale or cross-identity snapshots, atomically installs one validated state-machine image, and cleans bounded interrupted staging on restart. Persisted data created by the removed legacy coordinator uses #129's explicit offline campaign because that format cannot prove which divergent suffix was committed.
  • Watch/change-stream resume cursors.
  • Fail-closed no-quorum handling. Openraft may have committed before response delivery fails, so clients retry the same durable request ID or perform a linearizable read; they never infer rollback from a missing response.
  • Truthful capability reporting so standalone SQLite does not claim replicated behavior.
  • Fresh, bounded engine readiness through the same Openraft linearizable-read barrier and local apply wait used by real operations, independent of a bound listener or cached capability declarations. Production traffic composes that barrier with authenticated platform topology through the separate production profile/readiness APIs.

Configured topology admission now rejects empty/even/undersized or over-31 HA sets, missing or ambiguous self, and duplicate declared identities before I/O. Descriptor-only admission is explicitly lab/compatibility scoped. Production admission authenticates bounded platform-fact tokens for the exact epoch before the immutable descriptors reach the engine. Each node supplies its one local SQLite backend and exact remote consensus-peer map separately, so remote votes do not require dummy storage adapters or the legacy remote-backend protocol. ValidatedQuorumTopology::try_new_consensus_lab_singleton is a separate one-replica Openraft profile that reports single-replica, never HA, while exercising the same durable engine and state machine.

Production replication uses SessionConsensusServer and RemoteSessionConsensusPeer on the exact opc-session-consensus/2 ALPN. One immutable consensus identity binds the cluster ID, descriptor-derived configuration ID, and monotonic epoch into topology, storage, snapshots, and every RPC. Before Openraft dispatch, both sides extract the canonical SPIFFE URI from the live certificate and require it to match the logical ReplicaId, stable node ID, expected opposite member, cluster, configuration, epoch, RPC sender, server profile, and fresh challenge. DNS/FQDN/IP aliases remain routing inputs only. The legacy writable backend protocol is not a production HA authority and is isolated behind an explicit compatibility surface.

The exact consensus profile is transport/wire-schema revision 4 and error-set revision 6. Revision 4 makes the forwarded consumer scope explicit, so a peer cannot silently downgrade a consumer-scoped operation to an internal call; error revision 6 binds that semantic boundary into the exact profile. Older profiles fail before engine dispatch and require a drained full-membership upgrade.

Each directed peer retains a fixed primary/overflow pool of at most two authenticated connections after correlated validated successes or typed semantic Unavailable responses, with one in-flight RPC per lane. Sequential calls prefer primary, a concurrent call may use overflow, and further calls wait for lane acquisition under the shared absolute family deadline. Those deadlines are 2 seconds for AppendEntries/Openraft read-index, 5 seconds for Vote, and 10 seconds for InstallSnapshot, forwarded mutation, and consumer ReadBarrier. A fresh connection has a 1.5-second DNS/TCP/mTLS/bootstrap sub-bound contained inside that deadline. A complete, correlated, authenticated, validated success or typed semantic Unavailable response may return the selected lane to its pool; Unavailable preserves a known stream position but grants no success or authority. Cancellation, timeout, EOF, protocol, authentication, scope mismatch, rejection, lifecycle evidence mismatch, or any uncertain stream position evicts only the selected lane before Openraft retries.

TLS session caches, tickets, resumption, early data, and 0-RTT are disabled; every reconnect performs a full mutual-TLS certificate exchange so rotated SVIDs cannot inherit cached replica authority.

#163 now applies a finite maximum authentication age and exact local/peer certificate deadlines to every connection. Material admission and lifecycle evidence use the earliest expiry across each configured/presented chain while preserving distinct leaf and earlier-chain telemetry. Retained connections retire on coherent material-epoch or explicit reauthentication changes, transport waits and connection slots end by the hard deadline, and replacements repeat the full handshake. Already-admitted supervised mutations may finish later; they remain typed ambiguous and are never automatically replayed. The qualified CNF/operator profile must still prove fleet trust overlap/removal, short-lived-SVID expiry and root cutover, rollback, reconnect-storm behavior, and multi-process continuity under #164/#143. Immediate generic CRL/OCSP/certificate-or-identity denylist revocation is unsupported. Session/lease TTL is an application-state lifetime and does not set certificate expiry, trust-bundle validity, or authentication age.

Transport authentication does not replace topology admission or prove physical store provenance. The operator must still map each logical member to exactly one persistent backing store and reject duplicate stable node-ID derivations.

probe_durable_readiness supplies fresh, bounded point-in-time evidence without consulting cached capabilities. It calls Openraft's linearizable barrier and waits for local state-machine application through the returned log ID. Authoritative reads perform that same barrier; writes use client_write_ff under the shared eight-slot supervised admission bound. Listener readiness therefore cannot disagree with the store merely because a server socket is bound. This base method remains engine/lab evidence and MUST NOT authorize production traffic.

Production traffic uses topology created through ValidatedQuorumTopology::try_from_attested, the time-aware production profile, and a ProductionTopologyAttested report whose is_production_traffic_ready() result is true from probe_production_durable_readiness (or its refreshed-attestation form). Verified AuthenticatedPlatform evidence carries an absolute monotonic expiry; the open store retains a nondecreasing wall-clock high-water and repeats both checks after the Openraft await. A backward clock, exact expiry, foreign or non-production token, and an older delayed evaluation all fail closed. The process-local time authority is rebuilt by authenticating evidence again against current time after restart; the adapter decides whether a still-unexpired proof may be re-presented or must be replaced. The shared report's bounded DurableReadinessScope marks engine-only versus production-topology-attested evidence; consumers require the latter in production traffic gates.

The SDK state machine, rather than a competing quorum algorithm, deterministically applies session commands, advances leader-selected logical time, maintains fences and the committed application journal, and publishes watch events only after commit. Direct log append, whole-state rebuild, and caller-selected lease sequence APIs fail closed on ConsensusSessionStore.

The encryption boundary is deliberately above consensus: application -> EncryptingSessionBackend/RemoteSealingSessionBackend -> ConsensusSessionStore -> Openraft/storage. Encryption completes before client_write; follower apply, replay, snapshots, and quorum recovery operate only on opaque envelopes and never call HKMS. Reads use the outer wrapper to resolve the envelope's historical key. Tests inject plaintext and raw-key canaries through the actual wrapper and prove they are absent from consensus RPC payloads, SQLite/Raft log and outcome tables, WAL/SHM files, and snapshots; they also prove restart, snapshot install, and active-key rotation preserve decryptability without provider calls inside consensus.

This contract encrypts record payloads, not the entire database. Membership, log indexes, tenant/key routing fields, owners, fences, timestamps, envelope key IDs, and other SQLite/Raft metadata remain visible to the host storage boundary. Full-file or metadata confidentiality requires a separate approved storage layer and must not move nondeterministic key-provider calls into the replicated state machine.

The current networked profile remains experimental, not yet a production HA qualification claim. #127 establishes durable commit/sequencing authority with Openraft and removes the custom session quorum algorithm. #128 hardens and qualifies current-format Openraft follower recovery without adding another repair authority. #129 adds a default-deny, audited offline legacy-fork campaign: it binds a full-fleet plan, quarantines every explicitly selected PVC, installs one immutable operator-selected checkpoint on the whole legacy voter set, and commits fencing only through Openraft. See the legacy recovery runbook. #133 adds bounded local applied-state restore with an AEAD-sealed composite-key seek cursor, bounded candidate work, and prompt SQLite cancellation. It adds no remote quorum, digest comparison, or Merkle authority; neither recovery path becomes a second runtime consensus authority. Fixed-width private wire DTOs and checked domain conversion are implemented under #134. Invariant-safe owner/key model decoding, bounded count-only SQLite admission, and typed-invalid handover rejection are implemented under #135; checked TTL rejection is implemented under #137, and malformed sequence zero, checked increment, rebuild-prefix, SQLite signed-boundary, cache, and authenticated wire rejection are implemented under #138. Finite session-net connection reauthentication is implemented under #163; fleet credential qualification remains #164/#158 and distributed production qualification remains #143. Watch handoff correctness is implemented. Absolute-record-expiry admission is implemented under #148. Bounded nested-CAS protection is implemented under #147; outbound response allocation/frame bounds and slow-reader deadlines are implemented under #159. Distributed failure/resource qualification remains #143. #161 atomic reload, #162 coherent material epochs, and #163 connection reauthentication are implemented; #164 fleet qualification remains under umbrella #158. These remaining evidence gates keep the networked profile experimental.

The v5 wire uses u32 for restore/log request limits and the client restore response budget; a confidential authenticated strictly bounded restore cursor; u64 excluded counts, max_value_bytes, and size-bearing store errors; and checked conversion before backend dispatch or caller exposure. It omits restore loaded_count and complete and recomputes them after decode. Independent limits admit 256 batch operations, 1,024 restore records, 65,536 replication-log entries, and 65,536 rebuild entries, in addition to the configured frame-size bound. The exact profile pins wire-schema revision 7, error-set revision 9, a 2,096,128-byte restore wire-payload bound, 8 MiB retained-page and examined key/filter-metadata bounds, max_restore_scan_examined_rows = 4096, 128-byte owner/custom-key/state-type bounds, depth-16/256-node replication trees, and the 31,536,000-second TTL maximum. Revision 2 additionally pins min_frame_size = 8192, max_frame_size = 16777216, stable_id_max_bytes = 64, replication_tx_id_max_bytes = 128, and cas_request_id_bytes = 36. Transported stable IDs contain 1 through 64 bytes, transaction IDs contain 1 through 128 UTF-8 bytes, and CAS request IDs, when present, are canonical lowercase hyphenated UUIDs with the exact 36-byte encoding. Error-set revision 4 additionally carries checked replication-log range overflow, page-limit, and compacted-cursor outcomes; revision 5 adds non-CAS backend and lease ambiguity outcomes; revision 6 adds bounded-watch catch-up; and revision 7 adds absolute-record-expiry rejection. Revision 8 adds the bounded expiry-preflight limit outcome. The exact direct v5 profile is wire-schema revision 7/error-set revision 9; every non-current direct profile combination is incompatible. Deployments require a coordinated drained stop/upgrade/start. Public Request/Response remain, but Hello/HelloAck gain an optional contract_profile; exhaustive construction and matching must account for the new field. The public ContractProfile::max_frame_size field is also a Rust source break for external literals/destructuring and shares the coordinated revision-2 deployment boundary.

The cursor is variable-length up to the consensus RPC/key ceiling. Separate HMAC-derived AEAD and synthetic-nonce keys make identical semantic positions canonical. Only its cumulative examined-row position is clear and bound into cursor authentication. That permits a structural check of claimed progress, not proof of peer completeness; seek and snapshot fields remain confidential. Cursors survive a same-PVC restart but are node/incarnation-bound, so another node or installed snapshot returns typed stale state and requires a first-page restart.

Wire-schema revision 2 adds directional response-budget admission to the exact v5 handshake. Hello carries the client's requested response frame size; HelloAck returns the accepted response size (the client/server minimum) and the server's independent request-frame size. Each is a checked u32 between MIN_NEGOTIATED_FRAME_SIZE (8 KiB, or 8,192 bytes) and MAX_NEGOTIATED_FRAME_SIZE (16 MiB, or 16,777,216 bytes), and MIN_RESTORE_SCAN_RESPONSE_FRAME_SIZE aliases that same minimum. This makes unequal client/server limits explicit. The directional fields were introduced by wire-schema revision 2 and are retained by the current wire-schema revision 7/error-set revision 9 profile. Every non-current direct profile combination, including error revision 8 or older, is incompatible; the current ALPN is opc-session-net/5. Deployments require a coordinated drained stop/upgrade/start.

Every response and watch item is fully bounded-encoded before any frame prefix is emitted. Common non-pageable and complete-page successes use one bounded encode without a sizing preflight. For a replication-log page, an oversized pageable direct attempt emits no prefix; bounded logarithmic sizing probes and the final encode reuse one absolute deadline established before the first encode/probe and continuing through prefix, payload, and flush. Restore pages are validated as whole backend results and are never transport-shaped. Lazy exact-length boxed chunks are not coalesced; their total retained encoded-JSON byte storage never exceeds the negotiated cap. Chunk metadata and allocator slab/RSS overhead remain separate. The synchronous storage/sizing sinks check deadline and server-abort cancellation cooperatively between serializer writes/chunks; one bounded serializer callback is not asynchronously preemptible. A slow reader is disconnected and its slot is recovered.

Legacy direct-backend dispatch also has three bounded phases: one inbound idle-timeout to decode a complete frame, independent read/mutation/lease/watch admission plus one backend queue/work deadline, and one reserved response interval. The latter two form the checked post-decode lifetime. Peer EOF and shutdown cancel pending reads and idle watches. CAS keeps its operation-bound replay outcome; other mutations and leases are sent once and return typed non-retryable ambiguity after transmission when an exact result cannot be confirmed. Pre-transmission failure remains known not applied. Backend adapters own cancellation: blocking/spawned work must be bounded and supervised rather than detached on async-wrapper drop. Records and positional batch results are never truncated. Restore backends may independently return shorter cursor-correct pages under their count, payload, or work budgets; transport validates each complete page against the fixed wire cap and negotiated frame and never trims or rewrites it. Log reads may return only complete contiguous-sequence prefixes. An oversize restore page returns typed RestoreScanResponseTooLarge when representable or closes. Watch never skips an oversized entry; a fixed SDK-owned redaction-safe error is emitted when it fits and the stream ends, otherwise the connection closes. Nested rejected entries retain iterative consuming disposal.

Transport capability clamping takes the backend maximum and (frame - 8192) / 8 for both the accepted response and server request frames, rather than the raw frame size. The reserve and factor cover the record/key/error envelope, worst-case JSON byte-array expansion, and equal escaping/metadata headroom. The advertised max_value_bytes is executable for both directions with unequal limits. It is zero at the exact 8 KiB minimum; that minimum fits bounded metadata/envelopes, not a non-zero application payload. It remains static/descriptive evidence, not quorum readiness. The 1 MiB default advertises 130,048 bytes and the 16 MiB ceiling advertises 2,096,128. The wire ceiling is intentionally below standalone SQLite's local 4 MiB + 64 KiB stored-envelope restore capacity, which is not a session-net wire capability. This remains a per-frame limit: at the default 128 connection slots, simultaneous ceiling-sized encodes can retain about 2 GiB before metadata/TLS/runtime overhead. The aggregate scales with with_max_connections; aggregate byte permits and distributed resource/soak qualification remain #143.

Consequences

Standalone SqliteSessionBackend remains useful as a durable local backend, but it is not HA. Production CNFs need a separately qualified replicated profile; #127 provides the correct consensus authority but does not by itself complete #143's networked production qualification.

The SDK favors fail-closed reads over returning divergent session state when a majority cannot agree.

MAX_SESSION_TTL is exactly 365 days. Zero remains valid as immediate expiry; larger values return StoreError::InvalidSessionTtl or LeaseError::InvalidSessionTtl before application/backend effects. The implementation converts seconds/nanoseconds and adds deadlines with checked integer operations rather than floating point or panicking timestamp arithmetic. This prevents an oversized direct or authenticated input from unwinding a process; Openraft supplies commit proof independently.

The new public error variants require exhaustive callers. Protocol v4 introduced their private fixed-width DTOs in error revision 1; current v5 error revision 9 retains those encodings and adds the bounded expiry-preflight and topology-authority outcomes. Every non-current direct profile combination is rejected during negotiation. Operators must first audit persisted legacy replication logs: a TTL-bearing entry above 365 days now fails closed during replay/rebuild and is neither clamped nor rewritten automatically. Replicated deadline validation admits at most one microsecond above exact entry.timestamp + ttl solely for legacy seconds_f64 rounding; new deadlines remain exact, the TTL maximum is unchanged, and larger mismatches fail closed.

Under #135, OwnerId and custom session-key names accept 1 through 128 UTF-8 encoded bytes. SessionKeyType::Other now contains a validated CustomSessionKeyType; reserved names decode only to the canonical well-known variants, and ordering uses canonical string order. Serde, SQLite hydration, and session-net decode reuse that admission. Valid identity JSON strings retain their shape, but Rust construction is source-breaking and semantic admission is stricter. An older peer may emit values v5 rejects, so all clients, servers, and wrappers require coordinated stop/upgrade/start. Protocol v5's exact profile now binds this admission rule.

Existing SQLite replicas must be drained and checked with opc-session-store-audit identity-invariants using explicit non-zero --max-rows, --max-entry-json-bytes, and --max-total-json-bytes budgets plus one recorded RFC 3339 --expiry-reference. The per-entry budget cannot exceed the total or SQLite's signed i64 length range. The read-only/query-only audit scans one snapshot in fixed 256-row pages and emits version-4 count-only JSON. Only compliant with exit 0 passes; violations_found/1, incomplete/2, and redacted error/2 block upgrade. It never emits database paths or persisted raw values and never truncates, renames, repairs, or rewrites state. A violation requires a reviewed semantic-preserving migration or audited store replacement and a new audit.

Forwarding wrappers and authenticated CAS/batch dispatch obtain the bounded, payload-free authority verdict before idempotency admission, cache invalidation, provider/HKMS work, sealing, or backend dispatch. Invalid input and timeout/unavailability cause no provider call or requested mutation; only a consensus logical-time floor may have committed, so caller retry is safe. Payload envelopes, AAD, key selection, and HKMS placement are unchanged.

New handover envelopes use the OPCH magic and an exact version byte. The exact bounded non-OPCH classifier in RFC 004 §10.3 accepts current-valid original syntax and some bare payloads; ambiguous, truncated, oversized JSON-looking, malformed, unknown, or typed-invalid claims return a fieldless error before mutation. Successful detection is not provenance. The identity audit does not classify live or nested-log payload bytes, so products require the complete provenance-aware replay preflight. Once any live/replayable OPCH copy is written, old SDKs silently see opaque Stable data; downgrade requires a coherent drained checkpoint restore or reviewed reverse migration of every record/log/snapshot/restore copy across every handover reader/writer.

This closes the scoped #135 boundary, not production HA. #127 now owns durable session authority through Openraft; #134 closes the fixed-width legacy wire boundary only, and #143 still requires distributed and payload-protection-key qualification. Seamless SVID/trust-bundle lifecycle remains #158.

MAX_REPLICATION_OPERATION_DEPTH is 16 and MAX_REPLICATION_OPERATIONS_PER_ENTRY is 256. The root operation is depth 1, and every node—including Batch—counts once. Complete entries, rebuild prefixes, and returned pages are preflighted iteratively. A violation returns the fieldless StoreError::ReplicationOperationLimitExceeded without revealing the tree shape.

Protection wrappers transform every nested CAS, not only the root or first batch level. Replicate/rebuild transformations are fully staged before backend delegation; log/watch transformations complete before an entry/page is exposed. Provider calls are sequential. A late provider failure may follow earlier provider calls, but it causes no backend delegation on writes and no partial entry/page exposure on reads.

This added a public error variant before the v4 boundary. An older peer cannot decode it and, more critically, an older wrapper can forward deep plaintext/unsealed CAS payloads. Protocol v4 rejects the older wire participant and pins the depth-16/256-node limits and error revision, but it cannot attest that a protection wrapper is actually installed. All clients, servers, and wrapper participants require a coordinated upgrade plus composition verification, not a rolling compatibility claim.

Historical nested plaintext is not automatically scrubbed. Before upgrade, operators must audit persisted tree shape and payload encoding offline. An affected entry within the new limits may be explicitly rewritten/rebuilt through the configured protection wrapper. Over-limit history fails before transformation and requires a separately reviewed atomicity-preserving offline migration or audited store replacement before the new SDK starts; it must not be clamped or split ad hoc. A raw inner-backend rebuild is insufficient.

These guarantees close #147's traversal/confidentiality boundary only. They do not by themselves establish production HA. #143 remains the distributed and payload-protection-key qualification owner; seamless SVID/trust-bundle lifecycle remains #158.

Capability/profile validation and fresh readiness have different scopes. The former is static admission evidence. A v5 version/profile/authentication or malformed-handshake failure clears the remote cache and reports every capability boolean false with max_value_bytes = 0; a cache retained after transient transport loss remains descriptive only. Fresh readiness is a bounded observation that can become stale immediately, so a CNF must gate traffic continuously and each authoritative operation must reassess quorum.

Bounded response delivery does not roll back backend work. A mutation may have committed before encoding, write, or flush fails; the client must treat a missing response as ambiguous and recover through existing idempotency/request IDs, fencing, and an authoritative re-read. Diagnostics are limited to bounded operation-family/reason categories and must not include keys, payloads, owners, transaction IDs, peer identities, or backend/peer-controlled error text.

The revision-1 to revision-2 transition requires the same drained coordinated stop/upgrade/start as other exact-profile changes. #167 promotes the stable-ID rule from wire containment into the StableId domain type, SQLite/cache/ Openraft/restore/replication/watch boundaries, and a current version-4 count-only legacy audit without rewriting compliant record/log bytes. Before strict startup, quiesce writers and audit every retained record, log, snapshot, restore source, and replay source. Any out-of-profile value requires a decoder-first, product-aware migration or coherent store replacement under the #167 runbook and #168: the migration reader must decode the legacy representation before rewriting it, must not silently truncate/hash/rename durable identities, and the strict decoder must verify the result before writers restart. Rollback likewise installs a decoder for the retained target representation before old writers, or uses a coherent checkpoint/reviewed reverse migration. All participants must move together; independent OPCH/#135 rollback barriers still apply. #167 now supplies the production stable-ID model/persistence/privacy/audit contract. #168 supplies the bounded durable transaction-ID type, canonical coordinator mint, exact legacy preservation, and current version-4 audit/migration coordinated with #127/#128/#143. Session-net's bounded call remains the shared production transport contract. #177 removes opc-persist's private config TCP path and composes config consensus through the same transport-neutral peer/handler ports instead of a second timeout or credential lifecycle. A real-mTLS integration forms a three-node config Openraft cluster and commits/linearizably reads through those existing peer/server types. #163 tests qualify bounded retained-connection retirement, full reauthentication, and request/watch continuity on this shared transport. Multi-process rotation/soak and complete trust-bundle removal, short-lived-SVID expiry/root cutover, rollback, reconnect-storm, and seamless-continuity evidence retain their #164/#143 production gates. Immediate generic CRL/OCSP/certificate-or-identity-denylist revocation is unsupported. Remote-seal historical selection now uses the exact validated envelope key ID with KMS/HKMS-owned retention. The SDK has no local historical cache, retirement API, or enforcement gate. Distributed payload-protection and failure/soak/resource qualification remains #143.

A product composes one descriptor per physical vote. For example, logical self epdg-app-0 may select the member whose dial endpoint is the full epdg-app-0.epdg-app-quorum.epdg-gateway.svc.cluster.local:7443; the SDK does not shorten the FQDN or compare it with the logical ID. Any resolver override changes only where the client connects; the expected replica and SPIFFE identity remain fixed by the manifest.

Evidence

  • crates/opc-consensus/
  • crates/opc-session-store/src/consensus/
  • crates/opc-session-store/src/sqlite/consensus.rs
  • crates/opc-session-store/src/topology.rs
  • crates/opc-session-store/tests/consensus_openraft.rs
  • crates/opc-session-store/tests/quorum_topology.rs
  • crates/opc-session-store/tests/encryption.rs
  • crates/opc-session-store/tests/replication_structure_bounds.rs
  • crates/opc-session-store/tests/persisted_identity_bounds.rs
  • crates/opc-session-store/tests/sqlite_identity_audit.rs
  • crates/opc-session-store/tests/sqlite_identity_audit_cli.rs
  • crates/opc-session-store/tests/handover.rs
  • crates/opc-session-net/tests/three_node_quorum.rs
  • crates/opc-session-net/tests/authenticated_replica_identity.rs
  • crates/opc-amf-lite/tests/amf_lite_tests.rs
  • crates/opc-session-store/src/sqlite/mod.rs
  • crates/opc-session-testkit/
  • docs/ha-design.md
  • docs/operator-readiness.md