Introduction

The OpenPacketCore SDK is a toolkit for building 5G Core Network Functions (CNFs) that run on Kubernetes. It combines Rust-based policy engines with Go-based Kubernetes orchestration to give operators both safety and flexibility.

What the SDK Provides

  • Rust crates for protocol codecs (GTP-U, PFCP, NAS-5GS, NGAP v0, experimental Diameter base/application dictionaries, the experimental opc-proto-gtpv2c S2b subset, and the experimental opc-proto-ikev2 header/payload-chain scaffold), session management, configuration consensus, alarms, and runtime chassis.
  • Go packages (operator-sdk-go) for Kubernetes operators: conditions, bridge to Rust policy, drain orchestration, workload synthesis, runtime-gate helpers, Multus/SR-IOV attachment helpers, and metrics. Newly added packet-core helper surfaces are experimental mechanism helpers, not product CRDs or production controller claims.
  • Reference operator (sdk-reference-operator) demonstrating end-to-end reconciliation of a network function custom resource.

Getting Started

See Quickstart for environment setup and your first SdkManagedNetworkFunction deployment.

Architecture

The SDK is documented through RFCs (high-level design) and ADRs (decision records). Start with:

Architecture

Layered view of the SDK. Arrows point in the dependency direction (inward).

flowchart TB
  subgraph L1["Layer 1 — pure codecs & types (no async, no I/O)"]
    types[opc-types]
    codecs["opc-proto-* (pfcp, gtpu, gtpv2c, ngap, nas, diameter, ikev2)"]
    protocol[opc-protocol]
  end
  subgraph L2["Layer 2 — models & ports"]
    cfgmodel[opc-config-model]
    ports["opc-mgmt-* ports (schema, path, errors, principal, limits, audit, authz, opstate, transport)"]
    nacm[opc-nacm]
  end
  subgraph L3["Layer 3 — app orchestrator"]
    bus["opc-config-bus (validate → authorize → persist → publish; commit-confirmed expiry rollback; recovery fence)"]
  end
  subgraph L4["Layer 4 — adapters (async)"]
    netconf["opc-netconf-server (SSH/russh)"]
    gnmi["opc-gnmi-server (tonic, mTLS)"]
    cfgconsensus["opc-config-bus-consensus (sealed config adapter)"]
    persist[opc-persist]
    tls["opc-tls / opc-identity (SPIFFE)"]
  end
  subgraph L5["Layer 5 — runtime & operators"]
    runtime[opc-runtime]
    oplc["operator-lifecycle / operator-controller (Rust)"]
    gosdk["operators/operator-sdk-go + sdk-reference-operator (Go)"]
  end
  facade["opc-sdk (facade / prelude)"]

  netconf --> bus
  gnmi --> bus
  bus --> ports
  bus --> cfgmodel
  bus --> cfgconsensus
  cfgconsensus --> persist
  persist --> ports
  ports --> types
  cfgmodel --> types
  codecs --> protocol
  nacm --> ports
  tls --> ports
  runtime --> ports
  oplc --> runtime
  gosdk -. bridge CLI contract .-> oplc
  facade --> netconf
  facade --> gnmi
  facade --> bus
  facade --> codecs
  facade --> runtime

Legend: solid arrows are Cargo dependencies (direction = "depends on"); the dashed edge is the Go↔Rust policy-CLI process boundary (JSON contract, versioned by scripts/check-downstream-import.sh on the Go side).

OpenPacketCore SDK RFC Index

This directory contains the foundational RFCs for the OpenPacketCore SDK and CNF architecture. These documents are intended to be implementation inputs for engineers.

Foundation Set

RFCTitlePrimary Scope
001Transactional Management SubstrateConfig commits, persistence, recovery, NACM boundary
002YANG-to-Rust ProjectionCodegen, RFC 7951, validation, memory layout
003Security SubstrateSPIFFE, gNSI, tenant identity, keys, audit
004High-Performance Session StoreSession state, leases, fencing, handover, geo-redundancy
005Zero-Copy Protocol FrameworkParsers, codecs, lifetimes, fuzzing, spec tags
006Conformance and Evidence PipelineSBOM, VEX, provenance, signing, known gaps
007SBI Service FrameworkTS 29.500/29.510, NRF, OAuth2, overload, retries
008CNF Runtime ChassisStartup, supervision, shutdown, health, resource budgets
009Operator Lifecycle and UpgradeCRDs, rollout, migration, drain, rollback
010Data Governance and PrivacyData classes, redaction, retention, LI, regulated records
011Node and Data-Plane Resource ContractSR-IOV, Multus, AF_XDP, CPU, NUMA, pod security
012Testbed and Simulator FrameworkScenario DSL, simulators, fixtures, virtual time
013Fault Management and Alarm SubstrateAlarms, severity, probable cause, FM sinks
014Interactive Operational Console and Command FrameworkCNF command catalogs, human login, typed operations, first-class TUI
015Live SA Keymat MirrorKeys-never-persist failover, standby custody, mTLS keymat transport, re-pin composition
016Opaque Durable GTP-U Selector NamespaceExperimental durable whole-group selector authority, affine admissions, tombstones, eBPF control binding
017Mixed Selector Provenance and Loss-Qualified RestoreExperimental mixed per-atom provenance and marker-retaining namespace restore under RFC 016's ledger
  1. RFC 008: runtime chassis.
  2. RFC 003: security substrate.
  3. RFC 001: management substrate.
  4. RFC 002: YANG projection.
  5. RFC 007: SBI framework.
  6. RFC 004: session store.
  7. RFC 015: live SA keymat mirror.
  8. RFC 005: protocol framework.
  9. RFC 009: operator lifecycle.
  10. RFC 010: data governance.
  11. RFC 011: node/data-plane resources.
  12. RFC 013: fault management.
  13. RFC 014: interactive operational console.
  14. RFC 012: testbed framework.
  15. RFC 006: evidence pipeline.
  16. RFC 016: opaque durable GTP-U selector namespace (after RFC 004, RFC 006, RFC 011, ADR 0018, and ADR 0019).
  17. RFC 017: mixed selector provenance and loss-qualified restore (after RFC 016).

RFC 006 should be revisited after each implementation slice because it defines the evidence required to claim that the slice is complete.

OPC-SDK-RFC-001: Transactional Management Substrate

Status: Draft for Implementation
Version: 2.0.0
Date: 2026-05-19
Audience: SDK implementers, NF owners, security reviewers, test authors

1. Abstract

This RFC defines the transactional management substrate for OpenPacketCore network functions. It specifies the configuration commit state machine, the isolation boundary between the management plane and data plane, the reference persistent store, recovery behavior, authorization hooks, observability, and implementation acceptance criteria.

The core invariant is:

An NF's running configuration is a deterministic, validated, authorized, and durable projection of its YANG-defined configuration.

This RFC corrects the initial draft in four important ways:

  1. The commit pipeline is a single-writer state machine, not a long-held async mutex.
  2. The management plane is explicitly resource-isolated from the data plane.
  3. SQLite WAL is allowed only as a reference management-plane store with container storage preflight checks.
  4. Persistence, encryption, audit, rollback, and recovery are made explicit enough for independent implementation by multiple contributors.

2. Scope

2.1 In Scope

  • gNMI, NETCONF, and local operator configuration commits.
  • Candidate, running, startup, rollback, and shadow-security configuration stores.
  • Authorization of configuration mutations.
  • Durable commit history and audit trail.
  • Deterministic change notification to NF subsystems.
  • Reference SQLite persistence backend.
  • Interfaces that allow other persistence backends later.

2.2 Out of Scope

  • User-plane packet forwarding.
  • High-rate session state. See RFC 004.
  • Protocol parsing. See RFC 005.
  • Full supply-chain evidence generation. See RFC 006.
  • Cluster-wide consensus. This RFC covers per-replica local persistence and commit sequencing. Cluster-level orchestration must be layered above it.

3. Design Goals

3.1 Security

  • Default-deny authorization for all write operations.
  • Fail-closed behavior for corrupt storage, invalid identity, failed decryption, failed validation, and incomplete recovery.
  • No unredacted secret material in audit logs, telemetry, traces, or error messages.
  • Cryptographic binding between config payload, schema version, transaction metadata, and principal identity.
  • Tamper-evident audit history.

3.2 Performance

  • Configuration commits must not starve data-plane workers.
  • Data-plane readers must see configuration through wait-free or bounded-time snapshot access.
  • Commit admission must provide bounded memory growth and clear backpressure.
  • Heavy validation, serialization, compression, encryption, and fsync must not run on the async I/O worker set.

3.3 Maintainability

  • The state machine must be explicit and testable.
  • Generated and hand-written validation must use the same error model.
  • Storage backends must implement a narrow trait with deterministic semantics.
  • Each phase must have owner modules, metrics, logs, and fault injection tests.

3.4 Functionality

  • Support create, update, replace, delete, validate-only, commit-confirmed, rollback, and startup restore.
  • Support path-level audit and change notifications.
  • Support rollback points and schema migrations.
  • Support shadow-security configuration that is not exposed through ordinary gNMI Get.

4. Core Concepts

4.1 Stores

The SDK defines the following logical stores:

StorePurposeDurableExposed By gNMI Get
candidateTransaction-local mutable configNoNo
runningActive immutable configYesYes, after NACM filtering
startupOptional boot config alias or snapshotYesOperator controlled
rollbackExplicit rollback pointsYesMetadata only
shadow-securitygNSI/certificate/authz materialYesNo

The data plane MUST consume only immutable snapshots of running plus any explicitly subscribed derived state. It MUST NOT read from candidate, startup, or the raw persistence backend.

4.2 Config Snapshot

Generated root configs MUST implement:

#![allow(unused)]
fn main() {
pub trait OpcConfig: Clone + Send + Sync + 'static {
    type Delta: Send + Sync + core::fmt::Debug + 'static;

    fn schema_digest(&self) -> SchemaDigest;
    fn diff(&self, previous: &Self) -> Result<Vec<Self::Delta>, ConfigError>;
    fn apply_delta(&mut self, delta: Self::Delta) -> Result<(), ConfigError>;
    fn validate_syntax(&self) -> Result<(), ValidationError>;
    fn validate_semantics(&self, ctx: &ValidationContext) -> Result<(), ValidationError>;
}
}

Clone is required for the reference implementation, but large generated configs SHOULD use structural sharing internally so candidate creation does not copy every leaf for small patches.

4.3 Runtime Snapshot Access

The running config MUST be published through an atomic snapshot mechanism such as arc-swap or an equivalent SDK type:

#![allow(unused)]
fn main() {
pub trait ConfigSnapshot<C>: Send + Sync {
    fn load(&self) -> std::sync::Arc<C>;
    fn version(&self) -> ConfigVersion;
}
}

Data-plane reads MUST NOT acquire the commit lock, await I/O, allocate large buffers, or call validation hooks.

5. Commit State Machine

5.1 States

Each commit moves through the following states:

StateDescriptionMay FailDurable Side Effect
AdmittedRequest accepted into bounded queueYesNo
AuthenticatedPeer identity verifiedYesNo
AuthorizedNACM/path policy passedYesAudit denial
StagedCandidate built from running snapshotYesNo
SyntaxValidatedYANG constraints passedYesNo
SemanticallyValidatedNF validation passedYesNo
PreparedSerialized, encrypted, and ready to writeYesNo
PersistedCommit record and audit record fsyncedYesYes
PublishedRunning pointer atomically swappedNo in normal operationYes
NotifiedSubscribers informedBest effort per subscriberMetrics/audit only

No state is allowed to panic as part of ordinary error handling. A panic in the commit worker is a process bug and MUST be treated as StateMachineFault.

5.2 Corrected Phase Ordering

The commit worker MUST serialize commits, but it MUST NOT hold a tokio::sync::Mutex across .await, blocking validation, encryption, serialization, or database I/O. The recommended structure is:

  1. Northbound handlers push CommitRequest into a bounded mpsc queue.
  2. A single commit worker owns sequencing and transaction IDs.
  3. CPU-heavy validation runs through a bounded blocking/CPU pool.
  4. Crypto and serialization run through a bounded crypto pool.
  5. Persistence runs through a single writer backend handle.
  6. Publication is an atomic pointer swap.

This keeps ordering deterministic without turning the async runtime lock into a global bottleneck.

5.3 Commit Request

#![allow(unused)]
fn main() {
pub struct CommitRequest<C: OpcConfig> {
    pub request_id: RequestId,
    pub principal: TrustedPrincipal,
    pub transport: TransportType,
    pub source: RequestSource,
    pub operation: ConfigOperation,
    pub mode: CommitMode,
    pub deadline: std::time::Instant,
    pub idempotency_key: Option<IdempotencyKey>,
    pub base_version: ConfigVersion,
    pub candidate: Option<C>,
    pub changed_paths: Vec<YangPath>,
}

pub enum CommitMode {
    Commit,
    ValidateOnly,
    CommitConfirmed { timeout: std::time::Duration },
    Rollback { target: RollbackTarget },
}
}

idempotency_key SHOULD be supported for northbound clients that retry after UNAVAILABLE.

Candidate-bearing requests MUST carry the running config base_version used to build the candidate. The ConfigBus worker MUST reject the request before validation or publication when that value no longer matches the current running version, so stale full-candidate writers cannot overwrite an intervening commit.

5.4 Commit Result

#![allow(unused)]
fn main() {
pub struct CommitResult {
    pub tx_id: TxId,
    pub base_version: ConfigVersion,
    pub new_version: Option<ConfigVersion>,
    pub status: CommitStatus,
    pub changed_paths: Vec<YangPath>,
    pub apply_plan: Option<ApplyPlan>,
}
}

Failed commits MUST include stable machine-readable error codes. Error strings MUST NOT contain secrets or raw config fragments.

Candidate-bearing commit, commit-confirmed, and validate-only requests SHOULD return an ApplyPlan that classifies the operational impact of the SDK-derived changed paths after validation and before durable append. The default classifier returns hot plans so existing products remain compatible; products MAY install a ConfigImpactClassifier for domain-specific warm, drain-required, restart-required, or forbidden-live behavior. forbidden-live and apply-plan hard errors MUST fail closed before durable append/publication and attach the rejected plan to CommitError.apply_plan.

6. Management Thread Boundary

6.1 Required Execution Domains

The initial "Three-Pool" model is directionally correct but underspecified. The SDK MUST implement the following boundaries:

DomainWorkRequirement
Async I/OgNMI, NETCONF, gNSI, health, metricsNever perform CPU-heavy work or fsync
Commit workerSequencing, state machine ownershipSingle logical writer, bounded queue
Validation poolGenerated and NF semantic validationBounded threads and timeout
Crypto/serialization poolRFC 7951 serialization, compression, AEADBounded threads and memory
Persistence writerSQLite or backend write transactionSingle writer per local store
Data-plane workersPacket/session fast pathNo dependency on management pools

Implementations MAY combine validation and crypto pools for small deployments, but the default carrier profile MUST expose independent limits for both.

6.2 Starvation Protection

The SDK MUST provide:

  • Separate semaphores for validation, crypto, and persistence work.
  • Configurable max queued commits, default 32.
  • Configurable max pending bytes across staged candidates, default 64 MiB.
  • Per-request deadline propagation.
  • Admission rejection with gRPC UNAVAILABLE and retry metadata when queues are full.
  • A hard rule that data-plane threads never run management-plane blocking work.

Carrier CNF deployments SHOULD pin data-plane workers and management workers to different CPU sets using Kubernetes CPU Manager or an equivalent runtime mechanism. The SDK MUST work without CPU pinning, but the documented production profile MUST include it.

6.3 Time Budgets

Default phase budgets:

PhaseDefault Budget
Admission wait2 seconds
Syntax validation5 seconds
Semantic validation30 seconds
Serialization/encryption10 seconds
Persistence10 seconds
Notification fanout2 seconds per subscriber batch

Budgets MUST be configurable per NF. Expired commits MUST fail before publication. Persistence timeouts after partial backend work MUST be resolved by backend recovery logic before the next commit is accepted.

7. Persistence Abstraction

7.1 Trait

#![allow(unused)]
fn main() {
#[async_trait::async_trait]
pub trait ConfigStore: Send + Sync {
    async fn load_latest(&self) -> Result<Option<StoredConfig>, PersistError>;
    async fn load_rollback(&self, target: RollbackTarget) -> Result<StoredConfig, PersistError>;
    async fn load_by_replay_lookup_digest(&self, digest: &str)
        -> Result<Option<StoredConfig>, PersistError>;
    async fn append_commit(&self, record: CommitRecord, audit: Vec<AuditRecord>)
        -> Result<(), PersistError>;
    async fn append_commit_resolving(
        &self,
        record: CommitRecord,
        audit: Vec<AuditRecord>,
        resolution: ConfirmedCommitResolution,
    ) -> Result<(), PersistError>;
    async fn clear_recovery_required(&self, tx_id: TxId) -> Result<(), PersistError>;
    async fn mark_confirmed(&self, tx_id: TxId) -> Result<(), PersistError>;
    async fn create_rollback_point(&self, tx_id: TxId, label: Option<String>)
        -> Result<(), PersistError>;
    async fn preflight(&self) -> Result<PersistCapabilities, PersistError>;
}
}

append_commit MUST be atomic: either the commit record and its audit records are durable together, or neither is visible during recovery. append_commit_resolving additionally MUST compare the current applied head, resolve the exact pending commit-confirmed parent, and append its successor in one state-machine operation. Splitting those actions permits two leaders to make conflicting decisions and is prohibited. load_by_replay_lookup_digest MUST be one authoritative lookup; production stores must not walk a bounded ancestor prefix because history length cannot become an availability limit for outcome reconciliation.

Once append admission may have reached durable authority, loss of the response MUST be reported as OutcomeUnknown, not as a definite persistence or deadline failure. The commit bus fences subsequent writes until an authoritative lookup by request ID establishes an unkeyed result, or an exact same-key replay establishes a keyed result. A request that changes mode, candidate, rollback selector, confirmation timeout, caller-asserted base-version precondition, or authenticated caller context is a collision, not a replay. The fenced bus may answer the exact replay without performing a mutation, but it remains fenced until its local snapshot is rebuilt from the authoritative store. If authorities race after both miss the replay index, the compare-and-append loser MUST reconcile the winner through that index; an unreadable winner is OutcomeUnknown, never a definite persistence failure. Blind or semantically changed retry is not a valid recovery strategy.

7.2 Commit Record

#![allow(unused)]
fn main() {
pub struct CommitRecord {
    pub tx_id: TxId,
    pub parent_tx_id: Option<TxId>,
    pub version: ConfigVersion,
    pub committed_at: Timestamp,
    pub principal: TrustedPrincipal,
    pub source: RequestSource,
    pub schema_digest: SchemaDigest,
    pub plaintext_digest: Sha256Digest,
    pub encrypted_blob: EncryptedBlob,
    pub rollback_point: bool,
    pub confirmed_deadline: Option<Timestamp>,
}
}

The plaintext digest is verified only after successful AEAD decryption. It is not a substitute for AEAD integrity.

8. SQLite Reference Backend

8.1 Positioning

SQLite WAL is a sound reference backend for a single NF replica's management configuration and audit history because commits are low-rate, read access is local, recovery is simple, and the operational footprint is small.

SQLite MUST NOT be treated as a distributed consensus system. It MUST NOT be used for high-rate session state or cross-replica active/active configuration coordination.

8.2 Mandatory Container Storage Preflight

Before accepting writes, the SQLite backend MUST verify and report:

  • Database path is on a persistent volume when persistence is required.
  • Filesystem supports POSIX byte-range locking compatible with SQLite.
  • WAL, SHM, and database files are on the same filesystem.
  • The volume is not a known-unsafe network filesystem unless explicitly overridden by an operator with an evidence waiver.
  • fsync is not disabled by mount options or runtime configuration.
  • The database directory is writable only by the NF service account UID/GID.
  • Free space is above configured threshold.
  • Startup can create, checkpoint, close, and reopen a test WAL transaction.

If preflight fails, the NF MUST fail closed unless configured for an explicit ephemeral development mode.

8.3 PRAGMA Profile

The reference backend MUST apply and verify:

PRAGMA journal_mode = WAL;
PRAGMA synchronous = EXTRA;
PRAGMA foreign_keys = ON;
PRAGMA locking_mode = NORMAL;
PRAGMA busy_timeout = 5000;
PRAGMA temp_store = MEMORY;

locking_mode = EXCLUSIVE SHOULD NOT be the default in containers because it can break sidecar backup, online inspection, and some recovery workflows. The backend MAY offer exclusive mode for sealed appliances, but the default is NORMAL with a single SDK writer and no external writers.

synchronous = EXTRA is acceptable as a conservative default, but the backend MUST document that durability still depends on the underlying filesystem and storage class. Production deployments MUST use tested PVC/storage classes, not overlay filesystem layers for durable config.

8.4 Schema

CREATE TABLE schema_version (
    id INTEGER PRIMARY KEY CHECK (id = 1),
    schema_digest BLOB NOT NULL,
    sdk_version TEXT NOT NULL,
    created_at TEXT NOT NULL
);

CREATE TABLE config_history (
    tx_id BLOB PRIMARY KEY,
    parent_tx_id BLOB NULL REFERENCES config_history(tx_id),
    version INTEGER NOT NULL UNIQUE,
    committed_at TEXT NOT NULL,
    principal TEXT NOT NULL,
    source TEXT NOT NULL,
    schema_digest BLOB NOT NULL,
    plaintext_digest BLOB NOT NULL,
    encrypted_blob BLOB NOT NULL,
    rollback_point INTEGER NOT NULL DEFAULT 0,
    confirmed_deadline TEXT NULL,
    confirmed_at TEXT NULL
);

CREATE TABLE audit_trail (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    tx_id BLOB NOT NULL REFERENCES config_history(tx_id) ON DELETE RESTRICT,
    sequence INTEGER NOT NULL,
    yang_path TEXT NOT NULL,
    op_type TEXT NOT NULL CHECK(op_type IN ('CREATE', 'UPDATE', 'REPLACE', 'DELETE')),
    previous_value TEXT NULL,
    new_value TEXT NULL,
    redaction_applied INTEGER NOT NULL DEFAULT 0,
    previous_hash BLOB NOT NULL,
    entry_hmac BLOB NOT NULL,
    UNIQUE(tx_id, sequence)
);

CREATE INDEX audit_trail_tx_id_idx ON audit_trail(tx_id);
CREATE INDEX config_history_rollback_idx ON config_history(version, rollback_point);

8.5 WAL Maintenance

The backend MUST:

  • Set a bounded WAL autocheckpoint threshold.
  • Run explicit checkpoints during graceful shutdown and after large commits.
  • Export metrics for WAL size and checkpoint failures.
  • Refuse startup when WAL recovery fails.
  • Avoid deleting WAL or SHM files manually.

9. Encryption at Rest

Configuration encryption is specified here at the envelope level and governed by RFC 003 for key management.

9.1 Algorithm

  • Default AEAD: AES-256-GCM-SIV.
  • Alternative for non-AES-accelerated targets: XChaCha20-Poly1305, if allowed by the deployment security profile.
  • Random nonce generation is still REQUIRED even when using nonce-misuse resistant AEAD.

9.2 Envelope

struct ConfigEnvelopeV1 {
    magic: [u8; 4] = "OPCE";
    version: u16 = 1;
    alg_id: u16;
    key_id_len: u16;
    nonce_len: u16;
    aad_len: u32;
    key_id: [u8; key_id_len];
    nonce: [u8; nonce_len];
    aad: [u8; aad_len];
    ciphertext_and_tag: [u8; remaining];
}

AAD MUST include:

  • tx_id
  • parent_tx_id
  • version
  • committed_at
  • principal
  • schema_digest
  • store_kind

9.3 Key Derivation

When using a master secret, per-commit keys MUST be derived with HKDF-SHA256:

salt = tx_id || schema_digest
info = "openpacketcore/config/v1" || store_kind || key_id
key = HKDF(master_secret, salt, info, 32)

The backend MUST support key rotation by retaining enough key metadata to read old commits until the operator performs re-encryption or retention expiry.

10. Authorization Boundary

10.1 Auth Context

#![allow(unused)]
fn main() {
pub struct AuthContext {
    pub principal: TrustedPrincipal,
    pub spiffe_id: Option<SpiffeId>,
    pub transport: TransportType,
    pub source_ip: std::net::IpAddr,
    pub tenant: TenantId,
    pub authenticated_at: Timestamp,
}
}

10.2 NACM Requirements

The NACM engine MUST:

  • Normalize YANG paths before policy evaluation.
  • Reject ambiguous module prefixes.
  • Treat missing policy as deny.
  • Authorize every changed path, not just the top-level request path.
  • Authorize read, create, update, replace, delete, exec, and subscribe actions separately.
  • Enforce policy before candidate mutation and again before publication if the policy changed during a long-running commit.

Trie evaluation is acceptable for performance, but wildcard, subtree, module, and default-deny semantics MUST be tested against RFC 8341 behavior.

11. Notifications

After publication, the ConfigBus MUST notify subscribers with:

#![allow(unused)]
fn main() {
pub struct ConfigChange<C: OpcConfig> {
    pub tx_id: TxId,
    pub version: ConfigVersion,
    pub previous: std::sync::Arc<C>,
    pub current: std::sync::Arc<C>,
    pub deltas: std::sync::Arc<[C::Delta]>,
    pub changed_paths: std::sync::Arc<[YangPath]>,
}
}

Subscriber channels MUST be bounded. Slow subscribers MUST be isolated so they cannot block publication of future commits. Each subscriber must choose one of:

  • drop_oldest
  • drop_newest
  • disconnect_on_lag
  • force_resync

Byte-budgeted channels MUST charge an event before accepting it. The conservative charge includes both retained snapshots, all deltas, and changed paths. Config-model estimates include inline values and owned heap capacities in bytes (for example Vec<T>::capacity() * size_of::<T>(), using checked or saturating arithmetic) without cloning or serializing values. An unavailable estimate, arithmetic overflow, or a single event larger than the full budget engages the subscriber's lag policy; it MUST NOT fall back to shallow size_of accounting. disconnect_on_lag preserves the order of events already accepted and rejects the overflowing event before retention.

The byte limit is a conservative accounting bound rather than a strict allocator-resident-memory bound. Shared allocations are charged in full for each event occurrence. Allocator metadata, reference-count control blocks, and queue spare capacity are excluded and MUST be documented by management protocol adapters.

Critical NF subsystems that cannot tolerate missed notifications MUST expose a resync method and compare local applied version against ConfigBus::version().

12. Recovery

12.1 Startup

Startup MUST:

  1. Run storage preflight.
  2. Recover or checkpoint WAL if required.
  3. Load highest confirmed config version.
  4. Decrypt and authenticate envelope.
  5. Verify plaintext digest.
  6. Verify schema compatibility or run migration.
  7. Run syntax validation.
  8. Run semantic validation in startup mode.
  9. Publish running snapshot.
  10. Start northbound write admission only after running is published.

12.2 Rollback

If latest config fails startup semantic validation, the NF MAY try rollback points in descending version order. It MUST audit the rollback decision on the next successful write-capable startup. If no rollback point validates, the NF MUST fail closed and expose a read-only recovery endpoint only if explicitly enabled.

12.3 Commit-Confirmed

commit-confirmed MUST:

  • Persist the tentative config with a deadline.
  • Publish it as running.
  • Require explicit confirmation before deadline.
  • Automatically roll back to the parent config if not confirmed.
  • Emit warning telemetry before rollback.

The rollback timer MUST survive process restart by reading persisted confirmed_deadline.

13. Observability

Required metrics:

  • opc_config_commits_total{outcome,reason,transport}
  • opc_config_commit_duration_seconds{phase}
  • opc_config_commit_queue_depth
  • opc_config_commit_queue_rejections_total{reason}
  • opc_config_running_version
  • opc_config_subscriber_lag{subscriber}
  • opc_persist_wal_bytes
  • opc_persist_checkpoint_total{outcome}
  • opc_persist_fsync_duration_seconds
  • opc_nacm_decisions_total{action,outcome}

Required structured log fields:

  • request_id
  • tx_id
  • version
  • principal
  • tenant
  • transport
  • phase
  • outcome
  • error_code

Logs MUST NOT contain secret values or raw config blobs.

14. Testing Requirements

14.1 Unit Tests

  • State transition table.
  • NACM path normalization and default deny.
  • Candidate patch behavior.
  • Encryption envelope parse/decrypt failures.
  • Audit hash chain validation.
  • Subscriber lag policies.

14.2 Integration Tests

  • Concurrent commits serialize deterministically.
  • Validation timeout does not block health/read endpoints.
  • Persistence crash before commit is invisible after restart.
  • Persistence crash after commit is visible after restart.
  • WAL checkpoint and recovery on restart.
  • Commit-confirmed rollback after process restart.
  • Rollback point selection when latest config fails validation.

14.3 Fault Injection

  • Disk full.
  • fsync failure.
  • Corrupt WAL.
  • Corrupt encrypted blob.
  • Missing key.
  • Expired SPIFFE identity.
  • NACM policy change during long commit.
  • Slow or disconnected subscriber.

14.4 Performance Tests

Minimum carrier profile gates:

  • Data-plane config snapshot load p99 under 1 microsecond in-process.
  • Northbound read path remains available during 30 second semantic validation.
  • Commit queue rejects rather than exceeding configured memory limit.
  • 10,000 path-level audit records commit without unbounded memory growth.
  • SQLite backend sustains 10 commits/second for 60 seconds on reference PVC.

15. Module Ownership

Contributors should implement these modules independently with the listed ownership:

ModuleResponsibility
opc-config-busCommit worker, snapshot publication, subscriber fanout
opc-config-modelShared IDs, errors, request/result types
opc-nacmPath normalization and authorization decisions
opc-persistConfigStore trait and SQLite backend
opc-cryptoEnvelope encryption/decryption and key lookup adapter
opc-auditAudit records, redaction markers, hash chain
opc-config-testkitFault injection, mock store, mock NACM

Each module MUST expose a narrow public API, avoid cyclic dependencies, and include doc examples for the primary workflow.

16. Acceptance Criteria

This RFC is implemented when:

  1. A commit cannot publish unless authorization, validation, encryption, and durable append all succeed.
  2. Data-plane snapshot access is independent of commit queue and persistence health.
  3. SQLite preflight rejects unsafe durable deployments.
  4. Recovery handles clean restart, crash restart, rollback, and commit-confirmed expiry.
  5. Audit logs are tamper-evident and redacted.
  6. Metrics expose queue, phase latency, persistence, and authorization health.
  7. Fault injection tests cover all failures listed in Section 14.3.

OPC-SDK-RFC-002: YANG-to-Rust Projection and Codegen Engine

Status: Draft for Implementation
Version: 2.0.0
Date: 2026-05-19
Audience: SDK implementers, YANG model authors, NF teams, operator authors

1. Abstract

This RFC defines how OpenPacketCore projects YANG models into Rust data structures, validators, serializers, patch applicators, metadata tables, and operator-facing schemas. The generated code must preserve YANG semantics, support RFC 7951 JSON encoding, avoid stack blowups on large configurations, and provide deterministic APIs for the management substrate in RFC 001.

The key correction from the initial draft is that code generation MUST NOT rely on ad hoc recursive traversal or direct translation of arbitrary XPath strings into Rust closures. The SDK must compile YANG into a typed intermediate representation with bounded validation behavior, stable metadata, and differential tests against a reference YANG engine.

2. Scope

2.1 In Scope

  • YANG 1.1 module loading and schema resolution.
  • RFC 7951 JSON serialization and deserialization.
  • Rust type generation for config and state trees.
  • Validation for type constraints, must, when, leafref, unique, min-elements, max-elements, mandatory, and defaults.
  • gNMI/NETCONF patch application metadata.
  • Secret/redaction metadata for RFC 001 and RFC 003.
  • Runtime schema metadata consumed by gNMI, NETCONF, NACM, audit, and operator policy helpers.
  • Conformance tags for RFC 006.

2.2 Out of Scope

  • Runtime session state schema. See RFC 004.
  • Protocol wire codecs. See RFC 005.
  • UI form generation.
  • Go/Kubernetes CRD generation. Product operators own their API shape and may consume the generated Rust schema/policy metadata through RFC 009 helpers.
  • Support for proprietary YANG extensions unless explicitly registered in the extension registry defined here.

3. Design Goals

3.1 Security

  • Generated deserializers must reject unknown, ambiguous, duplicate, or malformed fields unless the relevant protocol explicitly allows them.
  • Secret leaves must use secret-aware generated types and redaction metadata.
  • Generated validators must not panic on hostile input.
  • Generated code must avoid unsafe unless an RFC-specific exception is approved and fuzzed.

3.2 Performance

  • Validation must be linear or near-linear in the size of the config for common cases.
  • Large lists must validate through generated indices, not repeated global depth-first searches.
  • Generated root structs must keep stack footprint bounded.
  • Patch application must avoid full-tree clone when structural sharing is enabled.

3.3 Maintainability

  • Code generation must be deterministic for identical inputs.
  • Generated files must have stable names, stable item order, and stable formatting.
  • Constraint lowering must go through a typed IR that can be inspected, tested, and rendered.
  • Generated APIs must be boring and consistent across all NFs.

3.4 Functionality

  • Support canonical YANG schema features required by 3GPP and IETF models.
  • Preserve presence, default, namespace, ordering, and key semantics.
  • Emit enough metadata for NACM, audit, gNMI paths, and conformance mapping.
  • Support schema migrations between SDK releases.

4. Inputs and Outputs

4.1 Inputs

The code generator consumes:

  • YANG module files.
  • A module lockfile containing exact module names, revisions, and checksums.
  • A generation profile.
  • Optional extension registry.

4.2 Outputs

For each generation unit, the tool emits:

  • Rust structs, enums, newtypes, validators, serializers, and patch applicators.
  • Static schema metadata tables.
  • Path constants and path parser helpers.
  • Redaction and NACM metadata.
  • Property test fixtures.
  • schema-digest.json for runtime compatibility checks.
  • conformance-tags.json for RFC 006.

Generated output MUST be reproducible from the lockfile and profile.

5. Schema Resolution Pipeline

5.1 Frontend

The frontend MUST parse YANG 1.1 and preserve:

  • Module and submodule identity.
  • Revision.
  • Namespace and prefix.
  • Imports and includes.
  • Extension statements.
  • Source locations for diagnostics.

The implementation MAY use libyang2 through a safe wrapper or a native Rust parser. In either case, the SDK MUST include differential tests against at least one reference YANG implementation for supported constructs.

5.2 Middle-End

The middle-end MUST produce a flattened schema IR by resolving:

  • typedef
  • grouping and uses
  • augment
  • deviation
  • refine
  • feature and if-feature
  • identity inheritance
  • module prefixes and namespaces

The flattened model MUST retain enough source mapping to produce diagnostics that point back to the original YANG module and line.

5.3 Backend

The backend emits Rust and schema metadata. It MUST:

  • Sort emitted items deterministically.
  • Use stable generated filenames.
  • Run generated Rust through rustfmt.
  • Fail generation if generated code does not compile.
  • Emit compile-time size checks.

6. Rust Type Mapping

6.1 Scalar Leaves

YANG TypeRust RepresentationRFC 7951 JSON Notes
int8, int16, int32i8, i16, i32JSON number
uint8, uint16, uint32u8, u16, u32JSON number
int64, uint64i64, u64JSON string to avoid precision loss
decimal64generated fixed-scale newtype or rust_decimal::DecimalJSON string
stringString or generated constrained newtypeJSON string
booleanboolJSON boolean
emptygenerated unit markerRFC 7951 [null]
enumerationgenerated Rust enumrenamed variants preserve YANG names
bitsgenerated bitflags/newtypespace-separated string
binarybytes::Bytes or Vec<u8>base64 string
identityrefgenerated enum or IdentityRef newtypenamespace-qualified string when needed
instance-identifierYangInstanceIdentifiernamespace-aware path string
leafrefgenerated newtype over target typeencoded like target leaf
uniongenerated ordered enumparse order follows YANG union member order

Generated constrained newtypes MUST enforce range, length, and pattern constraints during deserialization and validation.

6.2 Containers

YANG containers map to Rust structs. The generator must distinguish:

  • Presence containers.
  • Non-presence containers.
  • Optional generated fields.
  • Mandatory generated fields.

Large or optional containers SHOULD be boxed. The generator MUST box a field if embedding it would make the parent exceed the configured stack budget.

Default stack budget:

max_size_of_root = 4096 bytes
max_size_of_any_struct = 1024 bytes

Budgets are profile-configurable. Generated code MUST include compile-time assertions for these limits.

6.3 Lists

YANG list projection depends on key and ordering:

YANG List KindRust Representation
keyed, ordered-by systemBTreeMap<Key, Value>
keyed, ordered-by userVec<Value> plus generated key index
unkeyed config listVec<Value> with min/max validation
config false operational listVec<Value> or backend-specific iterator

The key type MUST be a generated struct when there are multiple key leaves. Duplicate keys MUST be rejected during deserialization and patch application.

6.4 Leaf-Lists

Leaf-lists map to Vec<T> plus generated validation for:

  • min-elements
  • max-elements
  • uniqueness, when required by YANG semantics
  • user ordering
  • default values

Generated code SHOULD build a temporary set for uniqueness checks rather than performing O(n^2) comparisons.

6.5 Choices and Cases

choice maps to a generated enum. The generator MUST preserve:

  • default case
  • mandatory choice behavior
  • when conditions on cases
  • removal of sibling case data when a different case is selected

Patch application MUST enforce case exclusivity.

7. Presence and Defaults

YANG requires distinguishing absent, defaulted, and explicitly set values. The generator MUST NOT collapse these states into plain Option<T> when protocol semantics require the distinction.

Generated fields SHOULD use a profile-selected representation such as:

#![allow(unused)]
fn main() {
pub enum LeafPresence<T> {
    Absent,
    Defaulted(T),
    Explicit(T),
}
}

For ergonomic NF logic, generated structs MAY expose helper accessors:

#![allow(unused)]
fn main() {
impl UpfInterface {
    pub fn mtu(&self) -> u16;
    pub fn mtu_presence(&self) -> LeafPresence<&u16>;
}
}

RFC 7951 serialization MUST follow the selected output mode:

  • ExplicitOnly: omit defaults unless explicitly set.
  • WithDefaults: include effective defaults.
  • Operational: include state and effective values.

8. RFC 7951 Encoding Requirements

The serializer/deserializer MUST handle:

  • Namespace-qualified member names where required.
  • 64-bit integers as strings.
  • decimal64 as strings.
  • empty as [null].
  • Base64 for binary.
  • Identity names with module prefixes when the identity is not in the parent namespace.
  • Instance identifiers with namespace-aware path segments.
  • Duplicate JSON object member rejection.
  • Unknown field handling according to protocol profile.

Round-trip tests MUST cover all scalar mappings.

9. Constraint IR and Validation

9.1 Constraint IR

The generator MUST lower must, when, range, length, pattern, and other constraints into a typed IR:

#![allow(unused)]
fn main() {
pub enum ConstraintExpr {
    Path(PathExpr),
    Literal(Literal),
    Function(FunctionCall),
    Compare { op: CompareOp, left: Box<ConstraintExpr>, right: Box<ConstraintExpr> },
    Boolean { op: BooleanOp, terms: Vec<ConstraintExpr> },
}
}

Direct string-to-Rust closure generation is forbidden because it is difficult to audit, hard to fuzz, and prone to semantic drift.

9.2 Supported XPath Profile

The initial SDK profile MUST support the XPath subset required by OpenPacketCore YANG models and selected IETF/3GPP dependencies. Unsupported expressions MUST fail generation with a clear diagnostic, not become runtime warnings.

The supported function list must be versioned. Each function implementation MUST have:

  • Unit tests.
  • Source-location diagnostics.
  • Differential tests against the reference YANG engine.

9.3 Validation Engine

Generated validation MUST be split:

  • validate_types
  • validate_cardinality
  • validate_choices
  • validate_when
  • validate_must
  • validate_leafrefs
  • validate_unique
  • validate_semantics hook for NF-owned logic

Validators MUST return structured errors:

#![allow(unused)]
fn main() {
pub struct ValidationError {
    pub path: YangPath,
    pub code: ValidationCode,
    pub message: String,
    pub source: Option<YangSourceLocation>,
}
}

Messages MUST be safe for northbound clients and MUST NOT expose secrets.

10. Leafref and Indexing

The initial draft required a depth-first search for each leafref. That is not acceptable for large configs.

The generator MUST create validation indices for referenced lists and leaves:

#![allow(unused)]
fn main() {
pub struct ValidationIndices<'a> {
    pub interfaces_by_name: BTreeMap<&'a str, &'a Interface>,
    pub slices_by_s_nssai: BTreeMap<SNssaiKeyRef<'a>, &'a Slice>,
}
}

Validation flow:

  1. Build indices in deterministic order.
  2. Reject duplicate keys.
  3. Validate all leafref constraints using the indices.
  4. Drop indices before publication.

Index building MUST be iterative and bounded by the configured validation memory budget.

11. Memory Safety and Stack Discipline

Generated code MUST be safe Rust by default.

11.1 Stack Budget

The generator MUST calculate size_of::<T>() for generated root and nested types through compile-time tests. Any type exceeding budget must be boxed, interned, or represented through a collection.

11.2 Traversal

Generated validation and serialization MUST avoid unbounded recursive traversal. Implementations SHOULD use explicit stacks:

#![allow(unused)]
fn main() {
let mut work = Vec::with_capacity(initial_capacity);
work.push(NodeRef::Root(root));
while let Some(node) = work.pop() {
    // validate node and push children
}
}

The SDK MUST define a maximum schema depth and maximum instance depth. Exceeding either MUST fail parsing or validation with a structured error.

11.3 Drop Behavior

Generated models MUST NOT create recursive self-referential types. If future extensions introduce recursive structures, the generator must provide iterative drop or arena ownership to avoid stack overflow.

11.4 Large Configs

The generator MUST support configs with:

  • 100,000 list entries in a single keyed list.
  • 1,000,000 scalar leaves across the tree in stress tests.
  • Deep but valid schemas up to the configured maximum depth.

Stress tests must verify no stack overflow and bounded peak memory.

12. Patch Application

Generated patch applicators MUST support:

  • gNMI Update
  • gNMI Replace
  • gNMI Delete
  • NETCONF merge
  • NETCONF replace
  • NETCONF create
  • NETCONF delete
  • NETCONF remove

Patch behavior MUST be generated from schema metadata, not hand-written per NF.

Patch application MUST:

  • Validate path existence and key predicates.
  • Preserve YANG default semantics.
  • Enforce list key immutability.
  • Enforce choice/case exclusivity.
  • Track changed paths for NACM and audit.
  • Avoid mutating running; only candidate may be modified.

13. Secret and Redaction Metadata

The generator MUST mark fields as secret when indicated by:

  • opc:secret
  • tailf:display-hint "password"
  • configured extension registry entries
  • explicit projection profile overrides

Generated secret fields SHOULD use a secret-aware type:

#![allow(unused)]
fn main() {
pub struct SecretLeaf<T> {
    inner: secrecy::SecretBox<T>,
}
}

Generated Debug, audit, telemetry, and error rendering MUST redact these values. Serialization for persistence may include encrypted secret values only through the RFC 001/RFC 003 envelope.

14. Operator Schema Boundary

The generator MUST expose enough Rust schema metadata for operator policy code to validate compatibility, migrations, admission, and config-apply decisions without hand-maintained side schemas.

Generated schema metadata MUST include:

  • canonical YANG paths and module identity, with every schema-node path segment fully prefix-qualified (for example /example:system/example:hostname);
  • config/state classification;
  • list-key ordering;
  • NACM action mapping;
  • redaction data classes;
  • schema digest data for compatibility checks.

The SDK does not generate Go structs or Kubernetes CRD fragments from opc-yanggen. Product operators own their Kubernetes API shape and may use the Rust operator-lifecycle, operator-controller, and operator-lifecycle-cli contracts to bridge those APIs into the SDK policy surface. Large NF configs are therefore split, referenced, or summarized by the product operator rather than by the YANG generator.

15. Schema Migration

Generated code MUST include schema digest metadata. On startup, RFC 001 uses the digest to determine whether persisted config can be loaded directly or requires migration.

Migration support MUST provide:

#![allow(unused)]
fn main() {
pub trait ConfigMigration {
    fn from_schema(&self) -> SchemaDigest;
    fn to_schema(&self) -> SchemaDigest;
    fn migrate(&self, input: serde_json::Value) -> Result<serde_json::Value, MigrationError>;
}
}

Migrations MUST be deterministic and tested with golden inputs.

16. Implementation Contracts

To keep the generated system modular and reviewable, every generated module MUST follow this layout:

generated/<module_name>/
  mod.rs
  types.rs
  paths.rs
  serde.rs
  validate.rs
  patch.rs
  metadata.rs
  redaction.rs
  tests/
    roundtrip.rs
    validation.rs
    patch.rs

Rules:

  • Hand-written code MUST NOT edit generated files.
  • Generated files MUST contain a header with generator version and schema digest.
  • Public generated APIs MUST be documented with YANG path and source module.
  • Each generated validation function MUST be small enough for review and have a stable name derived from the YANG path.
  • Conformance tags for RFC 006 MUST be emitted near the generated item that implements the requirement.

17. Testing Requirements

17.1 Generator Tests

  • Deterministic output for identical inputs.
  • Stable schema digest.
  • Unsupported YANG feature fails generation.
  • Differential validation against reference YANG engine.
  • Source-location diagnostics.

17.2 Generated Code Tests

  • RFC 7951 round trips for every scalar type.
  • Presence/default serialization modes.
  • Leafref validation with large lists.
  • must and when validation.
  • Choice/case exclusivity.
  • Patch operation matrix.
  • Secret redaction.
  • Stack size compile-time checks.

17.3 Fuzzing

Fuzz targets MUST include:

  • RFC 7951 JSON deserialization.
  • Path parsing.
  • Patch application.
  • Constraint evaluator.

Fuzz failures MUST be minimized and committed as regression tests.

17.4 Performance Gates

Minimum gates for a generated carrier profile:

  • Deserialize 10 MiB RFC 7951 config without stack overflow.
  • Validate 100,000 keyed list entries with leafrefs in O(n log n) or better.
  • Patch a single leaf in a large config without full serialization.
  • Generated root size_of below configured budget.
  • No unbounded recursion in validation or serialization paths.

18. Extension Registry

The SDK MUST maintain a versioned extension registry:

[[extension]]
name = "opc:secret"
behavior = "secret"

[[extension]]
name = "tailf:display-hint"
value = "password"
behavior = "secret"

Unknown extensions default to ignore-with-warning only if the generation profile allows it. Carrier profiles SHOULD fail generation for unknown extensions that affect config, security, or validation behavior.

19. Acceptance Criteria

This RFC is implemented when:

  1. Generated Rust preserves YANG presence, defaults, ordering, keys, and namespace semantics.
  2. RFC 7951 round trips pass for all supported types.
  3. Large config validation is bounded and does not use unbounded recursive DFS.
  4. Unsupported XPath/YANG constructs fail generation with diagnostics.
  5. Generated patch applicators support gNMI and NETCONF operation semantics.
  6. Secret metadata integrates with audit redaction and persistence.
  7. Operator policy helpers can consume generated schema metadata without a hand-maintained side schema or generated Go/Kubernetes projection.
  8. Output is deterministic and suitable for parallel implementation.

OPC-SDK-RFC-003: Security Substrate

Status: Draft for Implementation
Version: 2.0.0
Date: 2026-05-19
Audience: SDK implementers, security engineers, operator authors, NF teams

1. Abstract

This RFC defines the OpenPacketCore security substrate: workload identity, transport security, authorization, key management, secret handling, audit integrity, and runtime security administration. It integrates SPIFFE/SPIRE, gNSI, NACM, AEAD envelope encryption, and tenant-aware policy into a coherent boundary suitable for carrier-grade cloud-native network functions.

The initial draft correctly selected SPIFFE and gNSI, but it did not define a strong enough multi-tenant carrier boundary, key lifecycle, replay controls, or break-glass governance. This version makes those contracts explicit.

2. Security Objectives

2.1 Security

  • Authenticate every workload and operator action with cryptographic identity.
  • Authorize every operation by tenant, role, transport, method, and YANG path.
  • Encrypt all sensitive persistent configuration and session state.
  • Keep secret material out of logs, telemetry, panic messages, and ordinary gNMI reads.
  • Provide tamper-evident audit and durable security event trails.
  • Fail closed on invalid identity, unknown issuer, expired certificate, failed authorization, key lookup failure, or audit integrity failure.

2.2 Performance

  • TLS rotation must not drop established data-plane sessions unless policy requires it.
  • Authorization decisions must be cacheable and bounded.
  • Crypto operations must use the RFC 001 crypto pool or equivalent offload so they do not starve async or data-plane workers.
  • Security checks on high-rate paths must avoid heap allocation in the common case.

2.3 Maintainability

  • Identity parsing, authorization, key lookup, and redaction must be separate modules with narrow APIs.
  • Policy documents must be versioned, validated, and testable offline.
  • Security defaults must live in one profile file, not scattered constants.
  • The same security metadata must drive NACM, audit, and evidence generation.

2.4 Functionality

  • Support SPIFFE X.509-SVID identity.
  • Support trust domain federation.
  • Support gNSI certificate and authorization services.
  • Support break-glass with strict governance.
  • Support tenant-aware policy.
  • Support key rotation and historical decryption.

3. Threat Model

The SDK assumes attackers may:

  • Control an unprivileged pod in the same Kubernetes cluster.
  • Control another tenant namespace.
  • Replay old management-plane requests.
  • Attempt confused-deputy attacks through the operator.
  • Read persistent volumes or backend snapshots offline.
  • Corrupt local database files.
  • Delay, drop, or reorder network packets.
  • Trigger malformed gNMI, NETCONF, gNSI, or protocol inputs.
  • Observe timing, status codes, and logs.
  • Compromise a single NF replica.

The SDK does not claim to survive:

  • Total compromise of the root trust domain signing keys.
  • Compromise of the active KMS/HSM root keys without detection.
  • Kernel-level compromise of the node running the NF.
  • Malicious code compiled into the NF binary.

These residual risks MUST be documented in RFC 006 known gaps.

4. Identity Model

4.1 SPIFFE Workload Identity

Every NF replica MUST obtain an X.509-SVID from the local SPIRE Workload API.

Default SPIFFE ID format:

spiffe://<trust-domain>/tenant/<tenant-id>/ns/<namespace>/sa/<service-account>/nf/<nf-kind>/instance/<instance-id>

The original namespace/service-account pattern is insufficient for multi-tenant carrier isolation because namespaces are often operational boundaries, not contractual tenant boundaries. tenant-id MUST be explicit unless the deployment uses one trust domain per tenant.

4.2 Identity Claims

The SDK MUST parse the SVID into:

#![allow(unused)]
fn main() {
pub struct WorkloadIdentity {
    pub trust_domain: TrustDomain,
    pub tenant: TenantId,
    pub namespace: Namespace,
    pub service_account: ServiceAccount,
    pub nf_kind: NetworkFunctionKind,
    pub instance: InstanceId,
    pub spiffe_id: SpiffeId,
    pub expires_at: Timestamp,
}
}

Identity parsing MUST reject:

  • Unknown path formats.
  • Missing tenant.
  • Invalid NF kind.
  • Expired SVID.
  • SVIDs with trust domains not present in the active bundle set.

4.3 Workload Attestation

SPIRE registration entries MUST bind identity to Kubernetes selectors such as:

  • namespace
  • service account
  • pod label set
  • node attestation policy
  • image digest, when available through the attestor

The SDK MUST document the required SPIRE registration pattern. Relying only on service account name is not sufficient for production carrier profiles.

4.4 Trust Domain Federation

Federation MUST be explicit. The SDK MUST load and validate trust bundles for:

  • local workload trust domain
  • management/operator trust domain
  • optional peer-region trust domains

Federation policy MUST define which remote trust domains may perform which actions. Accepting a federated bundle MUST NOT automatically grant management privileges.

Example:

[[federation]]
trust_domain = "operator.openpacketcore.example"
allowed_tenants = ["tenant-a"]
allowed_roles = ["config-admin", "security-admin"]
allowed_transports = ["gnmi", "gnsi"]

4.5 Rotation

The SDK MUST watch SVID and bundle updates and hot-reload TLS acceptors and clients without process restart.

Rotation requirements:

  • After the controller accepts and publishes a new epoch, new handshakes use that coherent snapshot.
  • Existing connections are cooperatively retired after a material change, explicit reauthentication request, or configurable maximum connection age; replacements complete a full handshake.
  • Expired identities are not accepted.
  • Trust-anchor removal cuts over future handshakes: every chain that depends on the removed anchor is rejected.
  • Rotation failures emit critical telemetry.

The bounded response to compromise of a certificate/key under an issuer that remains trusted MUST be short-lived SVID expiry, not rotation or reauthentication. Replacing material moves cooperative workloads to the new SVID, but the old certificate/key can establish another full handshake until the earliest expiry across every certificate in its presented SVID chain while its issuer remains trusted. The TLS substrate does not implement immediate generic CRL, OCSP, certificate/identity denylist, or other selective same-issuer revocation. Removing a root is instead a trust-anchor cutover for all chains that depend on it; it is not a certificate-expiry deadline.

For Kubernetes projected Secrets, production consumers MUST resolve one relative ..data target and read the leaf chain, key, intermediates, and trust bundles directly from that immutable generation directory. Independently following each user-facing file symlink is forbidden because an atomic ..data replacement can otherwise produce mixed material. A source MUST check the generation after every read, discard a candidate if it changes, and stop after a fixed retry and work budget.

ProjectedSvidSource implements this boundary with public exact limits: 1 MiB for the chain file, 64 KiB for the key, 1 MiB per trust-bundle file, 4 MiB total, 16 bundle files, 16 chain certificates, 128 trust anchors, and three retries after the initial attempt. Each attempt has a five-second deadline. Polling cannot be configured below 100 milliseconds. Paths must be normalized relative paths below the projected generation. The source rejects non-regular material files and never places paths, PEM, SPIFFE IDs, keys, or parser text in status or events.

A validated candidate is published with a process-local monotonic generation. Rollback is another publication and therefore advances that generation. An invalid candidate retains the exact last-known-good identity, but never beyond the leaf's expiry; its ongoing expiry monitor schedules clearing from that leaf expiry and is not the authority for an earlier intermediate expiry. Expiry clears the source identity and reports a typed unavailable state. This source-level publication contract precedes #162's coherent per-handshake TLS epoch and #163's bounded connection reauthentication. Source Ready alone is not TLS readiness; consumers MUST gate on the controller status described below.

Because a rejected projected candidate deliberately leaves the identity-state watch unchanged, ProjectedSvidSource MUST synchronously record its fixed rejection outcome under the publication lock before notifying observers. This producer accounting MUST use the recorder selected when constructing the source, and MUST remain independent of watch delivery and controller lifetime; burst, coalescing, scheduler lag, recovery before controller construction, and source closure therefore cannot lose an outcome. There is no public outcome cursor or separately droppable monitor.

TLS consumers MUST construct the sole projected controller through TlsMaterialController::new_from_projected_source or new_pinned_from_projected_source. That one-time claim carries the source's exact identity channel and recorder into the controller, and rejects a second authority before it can split or duplicate telemetry. Generic controller constructors remain valid for independent-file, socket, and custom sources, but subscribing a projected source through them does not establish this production observability pairing.

opc-tls::TlsMaterialController MUST revalidate each identity state under fixed certificate, trust-anchor, private-key, and aggregate byte bounds before it can become handshake authority. It MUST pin the explicit local SPIFFE identity or the first accepted identity. It MUST pre-scan every certificate configured in the presented SVID chain and retain an invalid candidate's predecessor only until the earliest expiry in that chain. A redundantly presented root therefore bounds the controller lifetime; a root appearing only in a trust bundle is not independently scanned for this deadline. Production SVID chains SHOULD omit the trust anchor. Every accepted update or rollback receives a new opaque process-local epoch. Status and errors MUST contain only closed reason codes, epoch, availability, leaf expiry, and effective presented-chain expiry; identity text, paths, PEM, keys, and parser/application error text are forbidden.

Every production handshake MUST freeze one controller snapshot before rustls construction so certificate resolution and peer verification use the same leaf/key/chain/trust material. After mutual TLS and application negotiation, the caller MUST verify that epoch is still current before admitting the connection. A changed epoch MUST discard the connection and retry within the fixed SDK retry/concurrency limits. Tickets, resumption, early data, half-RTT data, and 0-RTT MUST remain disabled. This admission record carries the exact epoch, local leaf expiry, and effective local configured/presented-chain expiry; #163 separately combines the local and peer presented-chain expiries when retiring connections after admission.

These reload, admission, and retirement mechanisms now have single-host three- and five-process trust overlap/removal, root cutover/rollback, and one bounded short-lived-SVID-expiry regression slice. They are not deployed fleet qualification. Real network/storage faults, active-mutator restart, deployed reconnect/resource/soak bounds, remote-HKMS and signed independent evidence, plus the explicit unsupported generic-revocation limitation, remain open under #164/#143.

5. Transport Security

5.1 gRPC Transports

gNMI, gNSI, and internal gRPC APIs MUST use mTLS with SPIFFE identity verification.

Requirements:

  • TLS 1.3 required by default.
  • TLS 1.2 disabled by default and only allowed by explicit compatibility profile.
  • Peer certificate SAN MUST contain a valid SPIFFE URI.
  • Common Name MUST NOT be used for authorization.
  • ALPN and service/method authorization MUST be enforced.
  • Certificates MUST be validated against active SPIFFE bundles, not system web PKI.

5.2 Cipher Suites

Default modern profile:

  • TLS_AES_256_GCM_SHA384
  • TLS_CHACHA20_POLY1305_SHA256

FIPS profile:

  • MUST use a FIPS 140-3 validated module and only approved algorithms.
  • MUST document any difference from the modern profile.
  • MUST disable algorithms not available through the validated boundary.

The SDK MUST expose the selected security profile in metrics and evidence.

5.3 NETCONF over SSH

If NETCONF/SSH is enabled:

  • SSH host keys MUST be generated or provisioned through the security substrate.
  • Client identity MUST map to a TrustedPrincipal.
  • Password authentication MUST be disabled by default.
  • SSH certificate authorities SHOULD be used when SPIFFE-native SSH identity is unavailable.
  • SSH authorization MUST flow through the same NACM engine as gNMI.

6. Authorization

6.1 Principal Model

#![allow(unused)]
fn main() {
pub struct TrustedPrincipal {
    pub identity: WorkloadIdentity,
    pub tenant: TenantId,
    pub roles: Vec<Role>,
    pub groups: Vec<Group>,
    pub auth_strength: AuthStrength,
}
}

Roles and groups MUST come from signed policy or trusted identity attributes. They MUST NOT be accepted from unsigned client metadata.

6.2 Policy Layers

Authorization is evaluated in this order:

  1. Transport and peer authentication.
  2. Trust domain allowlist.
  3. Tenant boundary check.
  4. gRPC service/method authorization.
  5. NACM/YANG path authorization.
  6. Operation-specific guardrails, such as break-glass or key export denial.

Any deny at any layer is final unless a governed break-glass flow applies.

6.3 NACM Requirements

NACM MUST authorize:

  • read
  • create
  • update
  • replace
  • delete
  • exec
  • subscribe
  • security-admin

The engine MUST evaluate all changed paths after patch expansion. It is not enough to authorize the request's root path.

Authorization decisions SHOULD be cached by:

  • principal digest
  • tenant
  • policy version
  • normalized path
  • action

Cache entries MUST be invalidated on policy updates and SVID rotation.

6.4 Multi-Tenant Boundary

Cross-tenant access is denied by default. A principal from tenant A MUST NOT read or mutate tenant B config, session state, keys, or audit records unless a federated policy explicitly grants a scoped operation.

The tenant boundary MUST be enforced in:

  • identity parsing
  • authorization
  • persistence key namespace
  • session key namespace
  • audit query filters
  • telemetry labels, with cardinality controls
  • operator reconciliation

7. gNSI Services

The SDK MUST provide server-side support for:

ServicePurposeSDK Component
gnsi.certz.v1Certificate and trust material distributionopc-gnsi-server
gnsi.pathz.v1Path authorization policyopc-nacm
gnsi.authz.v1gRPC service/method authorizationopc-nacm

gNSI endpoints are security-critical. Access MUST require security-admin or a more specific role. gNSI mutations MUST be audited and persisted through the shadow-security store from RFC 001.

7.1 Shadow Security Store

Security material pushed through gNSI is stored in shadow-security.

Rules:

  • Not visible through ordinary gNMI Get.
  • Exportable only through explicitly authorized security APIs.
  • Encrypted at rest with a distinct key purpose from normal config.
  • Included in backup only when backup policy allows secret material.
  • Redacted in audit and telemetry.

7.2 Policy Staging

Authorization policy updates MUST support validate-only and staged apply. A policy that would lock out all security administrators MUST be rejected unless a break-glass recovery policy exists.

8. Break-Glass

Break-glass is dangerous and MUST be treated as an exceptional workflow, not a convenience override.

Requirements:

  • Disabled by default in production profiles unless explicitly enabled.
  • Requires a high-assurance principal.
  • Requires reason, ticket/reference, requested scope, and duration.
  • Maximum default duration: 15 minutes.
  • Requires dual authorization or an externally signed emergency token in carrier profiles.
  • Cannot bypass cryptographic verification, tenant boundary, or audit logging.
  • Cannot export raw key material unless a separate key recovery policy allows it.
  • Emits critical audit events at start, use, and expiry.
  • Emits high-priority telemetry.

Break-glass must grant the narrowest possible action set and path set.

9. Key Management

9.1 Key Hierarchy

The SDK uses purpose-separated keys:

PurposeExample Use
configRFC 001 encrypted config blobs
shadow-securitygNSI security material
sessionRFC 004 session store data
auditHMAC hash chains
backupencrypted export bundles

Keys MUST be separated by KMS key ID or HKDF info labels. Reusing one raw key for multiple purposes is forbidden.

9.2 Key Sources

Production profiles MUST obtain root or wrapping keys from one of:

  • KMS plugin.
  • HSM plugin.
  • Kubernetes Secret encrypted by a cluster KMS provider, only for lower assurance profiles.
  • SPIRE/SVID-authenticated key service.

Environment variables are forbidden for production key material.

9.3 Key Lookup API

#![allow(unused)]
fn main() {
#[async_trait::async_trait]
pub trait KeyProvider: Send + Sync {
    async fn get_active_key(&self, purpose: KeyPurpose, tenant: &TenantId)
        -> Result<KeyHandle, KeyError>;
    async fn get_key_by_id(&self, key_id: &KeyId)
        -> Result<KeyHandle, KeyError>;
    async fn rotate_key(&self, purpose: KeyPurpose, tenant: &TenantId)
        -> Result<KeyId, KeyError>;
}

#[async_trait::async_trait]
pub trait RemoteSealProvider: Send + Sync {
    async fn seal(&self, aad: &EnvelopeAad, plaintext: &[u8])
        -> Result<EncryptedPayload, KeyError>;
    async fn unseal(&self, key_id: &KeyId, aad: &EnvelopeAad,
        ciphertext_and_tag: &[u8]) -> Result<Zeroizing<Vec<u8>>, KeyError>;
}
}

KeyHandle MUST avoid exposing raw bytes unless required by the crypto module. If raw bytes are materialized, they MUST be zeroized after use where the crypto backend permits.

Deployments that require sealing through a provider that declares non-exportable custody can install one process-level KeyCustodyModule. The composite object MUST supply both CryptoModule evidence and RemoteSealProvider operations; evidence from one object MUST NOT authorize operations on another. Admission requires the module to declare, self-test, and service the explicit sealed_key_storage and zeroization capabilities and returns a bounded CapabilityReport. The SDK does not independently certify those declarations. The process slot is immutable after success, and the opaque AdmittedKeyCustody adapter has no public constructor or fallback.

The recorded self-test outcome is admission-time evidence; seal and unseal do not rerun an asynchronous power-on self-test. Before every operation, the SDK synchronously verifies that the module identity and validation declaration still match admission and that the complete frozen grant remains both advertised and serviceable. A module whose subsequent self-test or health state becomes invalid MUST withdraw the affected readiness capability. Provider NotFound and Unavailable classifications retain their public meaning; other provider context is collapsed to a fieldless redaction-safe error.

Successful provider-returned bound AAD MUST be rejected before parsing when it exceeds 64 KiB. Within that bound it MUST decode as the exact canonical SDK AAD shape and reserialize byte-for-byte from the caller's EnvelopeAad plus the returned KeyId. Oversized, malformed, non-canonical, or context-mismatched provider output fails closed.

The existing KeyProvider, KeyHandle, and direct RemoteSealProvider interfaces remain available for ordinary non-validated compatibility. Those values cannot construct AdmittedKeyCustody and MUST NOT inherit or advertise its admission evidence merely because another process component installed a module.

For remote sealing, key_id MUST come from a canonical, validated envelope. It selects the exact historical remote key and MUST NOT be replaced by the provider's current active key. KmsRemoteSealProvider snapshots one coherent RemoteSealMaterialController epoch before each encrypt request. Active-key publication affects only future seals; in-flight requests keep their snapshot. The controller retains only the current ID and opaque process-local epoch. It does not cache historical key material or authorization decisions, persist its epoch, watch a source, coordinate pods, or produce a fleet-comparable epoch. Each unseal calls the remote provider for the exact envelope key ID.

9.4 Rotation

Key rotation MUST support:

  • New writes using the active key.
  • Old reads using key ID from the envelope.
  • Optional background re-encryption.
  • Retention windows.
  • Emergency key revocation.

If a key is unavailable, the SDK MUST fail closed for writes and for reads that require the missing key.

For remote-seal rotation, KMS/HKMS is the authority for historical retention, revocation, and physical retirement. The SDK supplies exact historical-key selection and bounded live-state scan inputs, but it has no rewrap campaign, dependency-proof object, retirement API, or enforcement gate and cannot block an external KMS retirement. Operators MUST provision the new key before publishing it active, retain every old key while any artifact can reference it, and enforce retirement externally only after a composite proof:

  • a separately implemented rewrap has completed and a bounded, snapshot-bound, write-fenced scan verifies the resulting live state;
  • retained Raft logs and snapshots have been compacted, expired, or inspected and verified independently; and
  • backups, restore inputs, rollback checkpoints, and other offline sources have been inspected and then rewrapped, deleted, or retained with the old key.

A deployment-specific finite retention/TTL proof MAY replace rewrap only when it covers every live and replayable source and no record is unbounded. A restore scan alone does not prove logs, snapshots, backups, restore sources, or rollback artifacts. A partial or stale scan, concurrent writes, an unavailable source, or an ambiguous result blocks the operator's retirement decision. Emergency KMS revocation remains fail closed and may intentionally make dependent records unreadable.

RemoteSealProvider::unseal's historical KeyId argument is a breaking source API change. Provider implementations and callers MUST be upgraded together. It does not change the durable envelope or consensus/session wire format; the KMS request framing/schema is unchanged, but decrypt request contents now use the historical envelope ID. A code rollout MUST keep the old ID active until every reader, writer, and custom provider has stopped or upgraded, passed readiness, and can unseal by exact ID. Only then may the fleet publish a new active ID; upgraded pods may temporarily seal under different IDs because all upgraded reads select the envelope key.

Material rollback MUST first verify that KMS can encrypt/decrypt with the old ID and decrypt with the new ID, then republish the old ID on every upgraded process and verify new writes use it while both epochs remain readable. The new ID MUST remain retained while any artifact depends on it. Rolling back to a pre-change binary is safe only before a new ID is published, or after a complete rewrap/artifact proof has returned all dependencies to one key; otherwise use a coherent pre-publication checkpoint restore.

10. AEAD Envelope Encryption

10.1 Default Profile

Default persistent encryption uses AES-256-GCM-SIV for misuse resistance. Nonce reuse is still a bug and MUST be monitored.

10.2 FIPS Profile

Some FIPS validated modules may not expose AES-GCM-SIV. A FIPS profile MAY use AES-256-GCM only when:

  • Nonces are generated by a validated DRBG or deterministic counter scheme.
  • Nonce uniqueness is guaranteed per key.
  • The uniqueness state is crash-safe.
  • Tests prove duplicate nonce detection.

The active AEAD algorithm MUST be recorded in each envelope and in RFC 006 evidence.

10.3 Associated Data

AAD MUST bind ciphertext to:

  • tenant
  • purpose
  • tx/session identifier
  • schema digest or state type
  • key ID
  • version
  • principal, for config commits

AAD mismatch MUST produce a generic integrity error without exposing which field failed.

10.4 Replay and Rollback

Encryption alone does not prevent replay of an old valid blob. The management store MUST enforce monotonic config versions as specified in RFC 001. Session store backends MUST use generation numbers or lease fencing as specified in RFC 004.

11. Audit Security

11.1 Hash Chain

Audit records MUST include:

entry_hmac = HMAC(audit_key, tenant || sequence || canonical_entry || previous_hash)

The hash chain MUST be tenant-scoped and purpose-separated. Startup MUST verify the local audit chain unless the operator explicitly configures degraded recovery mode.

11.2 External Audit Sink

Carrier profiles SHOULD stream audit events to an external append-only system. Local SQLite audit is necessary for recovery and debugging but is not sufficient against host-level compromise.

11.3 Time

Audit timestamps MUST use UTC. The SDK SHOULD record both wall-clock timestamp and monotonic sequence number. Security decisions MUST NOT rely only on wall clock when monotonic ordering is required.

12. Redaction

The redaction subsystem consumes metadata generated by RFC 002.

Redaction MUST apply to:

  • Debug
  • structured logs
  • audit records
  • metrics labels
  • error messages
  • traces
  • panic hooks where possible
  • gNMI read responses after NACM filtering

Redaction MUST preserve enough information for debugging, such as value presence, length class, or stable digest when explicitly allowed by policy.

13. Observability

Required metrics:

  • opc_security_authn_total{outcome,reason,transport}
  • opc_security_authz_total{outcome,reason,action}
  • opc_security_svid_expires_seconds
  • opc_security_bundle_version
  • opc_security_rotation_total{kind,outcome}
  • opc_security_key_lookup_total{purpose,outcome}
  • opc_security_breakglass_active
  • opc_security_breakglass_total{outcome}
  • opc_security_audit_chain_verify_total{outcome}
  • opc_security_redactions_total{source}

For TLS readiness and lifecycle reporting, SVID expiry means the controller's effective earliest configured/presented-chain expiry, not an assumption that the leaf always expires first. Certificates present only in trust bundles are not independently included in that expiry value.

opc_security_svid_expires_seconds is that expiry as a Unix timestamp and is zero when no coherent unexpired controller snapshot is available. opc_security_bundle_version is the opaque process-local coherent material epoch; it is not a Kubernetes generation name, path, material hash, cluster identity, or value that may be compared across process restarts or replicas. The fixed opc_security_rotation_total label space is the Cartesian product of kind={tls_material,svid,trust_bundle} and outcome={success,retained_last_good,rejected,expired}. A source reason is classified as svid or trust_bundle only when its closed enum proves that component; ambiguous failures remain tls_material. Reload rejection with an unexpired predecessor (retained_last_good), rejection without one (rejected), and observed lifecycle expiry of a coherent source publication (expired) are distinct from peer authentication/trust failure. Expiry can be observed before pairing, while controller-active, or after controller rejection; only expiry of the active accepted ticket may clear the expiry gauge, and supersession alone does not synthesize an outcome. Controller private-key mismatch, local identity-pin, temporal-validity, and expiry reasons are provably SVID outcomes. Chain/workload-identity validation, source acquisition, material-limit, closure, and epoch failures do not prove one changed component and remain tls_material. Expiry does not suppress a later malformed-candidate rejection, and that later rejection MUST NOT increment expiry again.

Fleet rotation alerts and evidence MUST use the mechanically derived hard span in the consensus operator runbook, not a fixed sample duration. Evidence MUST bind exactly one invocation, non-secret live-lease binding, monotonic operation/nonce, member/checkpoint, phase/step, and fresh timestamp, and MUST be published with no-replace and crash-durable filesystem semantics. The lease token MUST travel only through a private descriptor and MUST NOT be logged or persisted. Emergency serving withdrawal MUST execute independently of evidence storage. A deliberate old-chain negative probe MUST remain visible to the authentication/trust alert and fail if its isolated delta is not exact.

Metrics MUST control label cardinality. Raw SPIFFE IDs SHOULD be exposed through logs, not high-cardinality metrics, unless explicitly enabled.

14. Module Ownership

ModuleResponsibility
opc-identitySPIFFE ID parsing, SVID watch, trust bundle watch
opc-tlsTLS acceptor/client reload and peer extraction
opc-authzPrincipal, roles, method policy, decision cache
opc-nacmYANG path authorization and RFC 8341 semantics
opc-gnsi-servergNSI service handlers and staged policy apply
opc-keyKeyProvider trait and KMS/HSM adapters
opc-cryptoAEAD envelopes and key derivation
opc-redactionSecret metadata and safe rendering
opc-auditHMAC chain, external sink adapter
opc-security-testkitfake SPIRE, fake KMS, policy fixtures

Agents must not mix transport identity parsing with NACM path logic. Each module should have deterministic test fixtures and no hidden global state.

15. Testing Requirements

15.1 Unit Tests

  • SPIFFE ID parser accepts valid pattern and rejects malformed identities.
  • Federation allowlist denies unknown trust domains.
  • Authorization cache invalidates on policy version change.
  • NACM denies missing rules.
  • Redaction covers generated secret fields.
  • AEAD envelope rejects wrong AAD, wrong key, corrupted tag, and wrong tenant.
  • Break-glass scope and TTL enforcement.

15.2 Integration Tests

  • SVID rotation without process restart.
  • Kubernetes ..data replacement during every projected-material read phase, proving that no mixed generation is published.
  • Projected-material exact-limit/one-over, last-good retention, expiry, rollback-generation, and redaction tests.
  • TLS material rotation during every handshake/application phase, exact epoch/effective-chain-expiry admission, identity continuity, rollback, repeated-rotation retry exhaustion, concurrent-operation bounds, cancellation, and redaction.
  • Trust-anchor cutover rejects every future handshake whose chain depends on the removed anchor.
  • gNSI policy staging and rollback.
  • Management commit rejected after NACM policy update removes permission.
  • Shadow-security store not visible through ordinary gNMI Get.
  • Key rotation reads old commits and writes new commits.
  • External audit sink outage does not drop local audit.

15.3 Fault Injection

  • SPIRE socket unavailable.
  • Expired SVID.
  • Corrupt trust bundle.
  • KMS timeout.
  • Missing historical key.
  • Duplicate AEAD nonce detector trigger, when applicable.
  • Audit HMAC mismatch.
  • Break-glass token replay.

15.4 Performance Gates

  • Authorization decision cache p99 under 50 microseconds for hot entries.
  • TLS reload completes without blocking new accepts longer than 100 milliseconds on reference hardware.
  • Key lookup cache hit p99 under 25 microseconds.
  • Redaction of a 10 MiB config audit diff completes within configured commit budget.

16. Acceptance Criteria

This RFC is implemented when:

  1. Every management connection is authenticated with SPIFFE-aware mTLS or an explicitly configured SSH identity profile.
  2. Tenant identity is explicit and enforced across authz, persistence, audit, and telemetry.
  3. gNSI services can stage, validate, apply, audit, and roll back security policy.
  4. Config, shadow-security, session, and audit keys are purpose-separated and rotatable.
  5. AEAD envelopes bind ciphertext to tenant, purpose, version, and schema/state metadata.
  6. Break-glass is scoped, time-limited, audited, and disabled by default in production unless carrier policy enables it.
  7. Security failure modes fail closed and are covered by fault injection tests.

OPC-SDK-RFC-004: High-Performance Session Store

Status: Draft; commit authority implemented, production qualification pending
Version: 2.1.0
Date: 2026-07-14
Audience: SDK implementers, NF owners, data-plane engineers, reliability engineers

1. Abstract

This RFC defines opc-session-store, the SDK substrate for high-rate network function state such as PDU sessions, PFCP associations, TEID mappings, QoS flow state, handover coordination metadata, and data-plane derived counters that need controlled persistence.

The initial draft correctly identified the need for partitioning, local-first operation, and distributed leases. It was not strict enough for 5G continuity: last-writer-wins based on synchronized clocks is not safe for authoritative session state. This version requires monotonic fencing tokens, compare-and-set updates, owner epochs, explicit handover state transitions, and a documented consistency model per data class.

The #127 implementation uses one shared Openraft engine for intra-cluster election, voting, log matching, commitment, membership, snapshots, and linearizable-read authority. ConsensusSessionStore is the operational store; QuorumSessionStore is a compatibility alias, not a second quorum algorithm. This RFC does not claim production qualification: #128 supplies current-format recovery and #129 supplies the audited offline legacy-fork campaign, while #133 supplies bounded restore from the Openraft-applied state without becoming readiness evidence by itself. Connection reauthentication and retained-connection continuity are implemented under #163. Distributed fleet qualification (#164/#143) remains a gate. Single-host three- and five-process tests cover trust overlap/removal and one bounded synthetic admission-loss/malformed-last-good plus short-lived-SVID-expiry recovery slice under mixed lease/CAS mutation, linearizable-read, watch, complete-restore, readiness, and connection-recycling traffic. Only typed backend-unavailable or operation-outcome-unavailable terminal results may enter qualification recovery. Mutation or lease outcomes that can make authority ambiguous discard the prior guard, reacquire same-owner authority at a strictly higher fence, and validate the exact scheduled record. Read-only get, restore-scan, and readiness outcomes retain the already-proven guard and validate that same exact record without minting unnecessary fencing authority. Evidence binds this routing as stage-aware-known-authority/v1. The fixed schedule drops one successful release response per mutator, allows eight outcomes per node, uses the fixed 26-second two-election-plus-operation transition envelope per recovery episode, and applies a 50 ms retry delay; phase completion requires every interruption to be reconciled. Lease loss, unexpected state, and invariant failures fail closed. The admission-loss exact-address restart is watcher-only before exit and joins the mutator set only after bounded journal reconciliation. Recovering a committed generation does not rearm the once-per-logical-mutator injection. One additional schedule-v4 phase kills a stable follower uncleanly while its mutation and watch tasks are active. Survivors advance committed canary and mixed traffic; the same-disk, exact-address restart must reconcile a bounded gap-free journal, prove the exact generation/owner/fence/payload, and resume at a strictly higher same-owner fence under the versioned same-disk-exact-address-active-mutator/v3 profile. That profile independently bounds termination/reaping at 5 seconds, outage/survivor progress at 26 seconds, replacement-child startup at 45 seconds, Openraft recovery/readiness observation at 37 seconds (a 26-second recovery envelope plus one reserved 11-second final all-voter readiness round comprising a 10-second backend operation and 1 second of bounded local result delivery), journal reconciliation at 25 seconds, and higher-fence mutation resume at 26 seconds. The sequential stages compose to a 164-second crash-to-resume ceiling, but each stage fails at its own deadline. This retains the v1 deadline-composition fix and corrects v2's stranded readiness-observation tail; it does not qualify a broader restart matrix or deployed production readiness. The tests do not cover deployed partitions, a broader restart/fault matrix, resource/soak, remote HKMS, deployed CNFs, or signed release evidence. Generic CRL/OCSP/denylist revocation is not implemented.

2. Scope

2.1 In Scope

  • Per-session control-plane state needed by AMF, SMF, UPF, and related NFs.
  • Data-plane lookup state that can be safely snapshotted or reconstructed.
  • Lease and fencing mechanisms for single-owner session mutation.
  • Local cache and distributed backend abstraction.
  • Geo-redundant replication for disaster recovery and warm standby.
  • Serialization, encryption, integrity, TTL, metrics, and fault injection.

2.2 Out of Scope

  • Configuration management. See RFC 001.
  • Packet parsing and protocol codecs. See RFC 005.
  • Full 3GPP procedure implementation. This RFC provides storage primitives and state-machine support used by NF-specific procedure logic.
  • Hard real-time packet forwarding in the remote store. Packet fast paths must use local data-plane structures.

3. Design Goals

3.1 Security

  • Encrypt session state before it leaves process memory unless the backend is explicitly trusted by profile.
  • Bind encrypted records to tenant, NF kind, session key, generation, and state type through AEAD AAD.
  • Prevent stale owners from overwriting newer session state.
  • Prevent cross-tenant key collision or data exposure.
  • Redact SUPI/GPSI and other subscriber identifiers in logs by default.

3.2 Performance

  • Keep packet forwarding off the remote store path.
  • Support 100,000+ session updates/second per NF replica for local in-memory or batched backend profiles.
  • Keep hot read p99 below 1 ms for local-cluster operations where the selected backend can meet it.
  • Provide bounded allocation and zero-copy or low-copy decode for common session reads.
  • Support batching, pipelining, and async replication without sacrificing fencing correctness.

3.3 Maintainability

  • Separate storage API, lease API, serialization, encryption, and replication.
  • Require backend capability declarations so NF code does not assume semantics a backend cannot provide.
  • Use typed session records instead of arbitrary blobs at module boundaries.
  • Provide a deterministic testkit for split-brain, failover, and handover races.

3.4 Functionality

  • Support create, get, update, delete, compare-and-set, TTL refresh, lease, renew, release, snapshot, and replication.
  • Support session handover prepare/activate/abort flows.
  • Support backend implementations for in-memory, Redis, Aerospike, and optional strongly consistent stores.
  • Support region-aware replication and recovery.

4. State Classes

The SDK distinguishes state by consistency need:

ClassExamplesConsistency Requirement
authoritative-sessionPDU session owner, AMF/SMF ownership, handover phaseSingle writer with fencing
dataplane-lookupTEID to session mapping, FAR/QER/PDR snapshotsLocal atomic snapshot, rebuildable
replicated-drWarm standby copy of session recordsAsync, ordered by generation
telemetry-derivedCounters, rates, last seen timestampsMergeable or lossy
ephemeral-procedureTemporary handover transaction stateTTL, fenced owner

Only telemetry-derived state may use last-writer-wins based on timestamps. Authoritative session state MUST NOT use wall-clock LWW.

5. Session Identity

Session keys MUST be tenant-scoped and type-scoped:

#![allow(unused)]
fn main() {
pub struct SessionKey {
    pub tenant: TenantId,
    pub nf_kind: NetworkFunctionKind,
    pub key_type: SessionKeyType,
    pub stable_id: StableId,
}
}

Examples:

  • SUPI-derived subscriber context key.
  • PDU session ID plus SUPI hash.
  • TEID mapping key.
  • PFCP session SEID key.
  • Handover transaction key.

StableId has a structural 1..=64-byte invariant shared by every local, SQLite, cache, quorum, restore, replication, watch, and session-net boundary. Valid pre-existing bytes retain their exact wire and SQLite representation. Empty or wider legacy values are not silently truncated or hashed; they fail the mandatory pre-upgrade audit and hydration.

Raw SUPI/GPSI MUST NOT be used directly as a backend key in production. Subscriber-derived keys MUST use StableId::derive_hmac_sha256 with a 16-through-64-byte tenant-specific KMS/HSM privacy key and one product-defined 1-through-256-byte canonical subject representation. The canonical profile is full-width 32-byte HMAC-SHA256 over the SDK domain followed by unsigned 64-bit big-endian length-prefixed tenant and subject bytes. Truncated keyed digests are not supported.

See docs/session-store-stable-id-migration.md for the required count-only audit, coordinated remediation, snapshot handling, and rollback procedure.

5.1 Owner and Session-Key Type Invariants

An OwnerId and the name of a deployment-specific SessionKeyType MUST each contain 1 through 128 UTF-8 encoded bytes. The limit applies to encoded bytes, not characters. Empty and oversized values MUST be rejected at construction and decode boundaries without including the raw value in an error.

SessionKeyType::Other MUST contain a structurally validated CustomSessionKeyType; callers MUST use the fallible SessionKeyType::other for runtime custom names. These canonical persisted strings are reserved for the corresponding well-known variants and MUST NOT be constructible as custom values:

  • subscriber-context
  • pdu-session
  • teid-mapping
  • pfcp-seid
  • handover-transaction

Parsing a reserved string MUST produce the well-known variant. Display, serialization, SQLite identity, key-digest input, and ordering MUST use the same canonical string; ordering MUST therefore be string ordering across known and custom values, not enum declaration order.

The invariant MUST be applied by Serde, SQLite record and restore hydration, active-lease reads before acquire/renew/release/fenced mutation, replication-log hydration including nested operations, and session-net request and response decode. Invalid persisted or remote data MUST fail closed before mutation or caller exposure. Diagnostics MUST be fieldless or fixed and MUST NOT expose the owner, key type, stable ID, row, transaction, or raw entry.

Valid protocol-v4 values retain their JSON byte-array shape. This does not make the change rolling-compatible: replacing SessionKey::stable_id: Bytes with StableId and replacing Other(String) with Other(CustomSessionKeyType) and making SessionKeyType::other fallible is a Rust source break. Both HandoverEnvelope::unpack_raw and HandoverSessionRecord::unpack_raw now return a typed Result; both public unpack_json methods change their error type, and HandoverError adds an InvalidEnvelope variant. Packers now write the versioned OPCH form while readers retain a bounded original/bare migration path. Rejecting values an older v3 peer could emit is also a semantic-admission break. Protocol v4 now binds that rule in its exact fixed-width DTO and handshake profile. Operators MUST stop, upgrade, and restart every session-net client, server, and protection wrapper plus every NF/product handover reader or writer as one coordinated unit. The v4 handshake does not make persisted OPCH bytes readable by old code.

5.2 Bounded Legacy SQLite Audit

Before a new binary opens persisted SQLite state written by an older SDK, the operator MUST drain all writers and run:

opc-session-store-audit identity-invariants \
  --database PATH \
  --max-rows N \
  --max-entry-json-bytes N \
  --max-total-json-bytes N \
  --expiry-reference 2026-07-13T18:00:00Z

All limits MUST be explicit and non-zero, and the per-entry JSON-byte limit MUST NOT exceed the total JSON-byte limit or SQLite's signed i64 length range. The command opens an existing database read-only/query-only, reads one consistent snapshot in fixed 256-row pages, applies the row budget across session_records, leases, key_fences, and session_replication_log, and bounds individual and cumulative replication JSON before strict typed decode and domain validation.

Report schema version 4 is count-only. It contains the supplied limits, the expiry reference, per-table scanned counts, counts for invalid owner fields, invalid session-key type fields, invalid stable-ID fields, invalid replication transaction-ID fields, invalid replication entries, and invalid relational record-expiry fields, and at most one bounded incomplete reason. Relational expiry MUST be classified against the reported reference; each nested legacy CAS expiry MUST be classified against its immutable replication-entry timestamp. Relational stable-ID checks read only SQLite type and length. It MUST NOT contain the database path, row identity, tenant, owner, key type, stable ID, payload, transaction, rejected row timestamp, or raw JSON. Omitting --expiry-reference selects current UTC, but a migration campaign MUST record and pass one explicit RFC 3339 reference so repeated audits are reproducible. The stable command outcomes are:

  • compliant on stdout with exit 0;
  • violations_found on stdout with exit 1;
  • incomplete on stdout with exit 2; or
  • redacted error on stderr with exit 2.

Only compliant after a complete snapshot inspection permits the identity portion of the upgrade to continue. violations_found, incomplete, and error MUST block startup. An incomplete audit reports one of row_budget_exceeded, entry_json_budget_exceeded, total_json_budget_exceeded, unsupported_schema, database_read_failed, or counter_overflow. The operator MAY increase budgets and re-audit, but the SDK and audit MUST NOT truncate, rename, normalize, delete, repair, or rewrite invalid identity or replication state automatically. A violation requires a separately reviewed product migration that preserves authoritative identity and history, or audited store replacement, followed by another complete audit. Every retained SQLite snapshot or restore/rebuild image that can become authoritative MUST pass the same audit. The identity procedure is docs/session-store-stable-id-migration.md; absolute-expiry re-authoring, OpenRaft recovery, and rollback are defined by docs/session-store-record-expiry-migration.md.

The identity audit MUST NOT be treated as a handover-payload preflight. It does not classify live payloads or payload bytes inside nested CAS log operations, so compliant says nothing about legacy envelope/bare compatibility. Every product using HandoverEnvelope MUST separately preflight the complete drained and decrypted replay population: live records, recursively nested replication log and snapshot records, restore/rebuild sources, and every retained copy that can become authoritative. It MUST use unpack_raw_with_format or typed unpack_json_with_format and verify the syntactic result against snapshot provenance and product payload semantics; decoder success alone is insufficient. A rejected or unprovable value MUST be resolved by a reviewed product migration or store replacement before startup; automatic guessing/truncation is forbidden.

This bounded identity admission closes #135's scoped model/persistence boundary. Protocol-v4 fixed-width wire admission is implemented under #134, and #127 now supplies Openraft durable commit authority. #128 supplies current-format divergence recovery and #129 supplies explicit offline legacy-fork recovery. #133 supplies bounded snapshot-bound applied-state restore; production qualification (#143) remains open. #161 atomic reload, #162 coherent material epochs, and #163 connection reauthentication are implemented; #164 fleet qualification remains under umbrella #158; payload-protection-key rotation and distributed production evidence remain #143.

6. Backend Capability Model

The initial get/set/delete trait is too weak. Backends MUST declare capabilities:

#![allow(unused)]
fn main() {
pub struct BackendCapabilities {
    pub atomic_compare_and_set: bool,
    pub monotonic_fencing_token: bool,
    pub per_key_ttl: bool,
    pub server_side_lease_expiry: bool,
    pub ordered_replication_log: bool,
    pub batch_write: bool,
    pub watch: bool,
    pub max_value_bytes: usize,
}
}

Carrier profiles MUST reject a backend for authoritative-session state unless it supports atomic compare-and-set and monotonic fencing tokens or an adapter can provide equivalent semantics.

7. Storage API

#![allow(unused)]
fn main() {
#[async_trait::async_trait]
pub trait SessionBackend: Send + Sync {
    async fn capabilities(&self) -> BackendCapabilities;

    async fn get(&self, key: &SessionKey)
        -> Result<Option<StoredSessionRecord>, StoreError>;

    async fn compare_and_set(&self, op: CompareAndSet)
        -> Result<CompareAndSetResult, StoreError>;

    async fn delete_fenced(&self, key: &SessionKey, fence: FenceToken)
        -> Result<(), StoreError>;

    async fn refresh_ttl(&self, key: &SessionKey, fence: FenceToken, ttl: Duration)
        -> Result<(), StoreError>;

    async fn batch(&self, ops: Vec<SessionOp>)
        -> Result<Vec<SessionOpResult>, StoreError>;
}
}

set without fencing is allowed only for state classes that explicitly do not require authoritative ownership.

7.1 TTL Admission and Deadline Arithmetic

The SDK-wide maximum for a session or lease TTL is the public MAX_SESSION_TTL, exactly 365 days. Duration::ZERO MUST be accepted and means immediate expiry. The exact maximum MUST be accepted. Any larger value MUST be rejected with StoreError::InvalidSessionTtl for store operations or LeaseError::InvalidSessionTtl for lease operations.

A zero-duration acquire MAY consume a fence, credential, and replication-log position before the lease is observed expired. Callers MUST use explicit release for revocation and MUST NOT treat a zero TTL as transaction rollback.

The ceiling accommodates long-lived packet-core sessions and planned maintenance or disaster-recovery windows while preventing a malformed value from creating an effectively permanent lease. A product profile MAY enforce a smaller operational limit.

This section bounds Duration inputs. Caller-authored absolute record expiry has the related but distinct authority contract in §7.2.

validate_session_ttl defines the duration check and checked_session_deadline defines conversion and deadline calculation. Implementations MUST convert seconds and subsecond nanoseconds using checked integer arithmetic and MUST use checked timestamp addition. Floating-point duration conversion, saturating/clamping an invalid input, and panicking timestamp arithmetic are forbidden.

Validation MUST occur before any application/backend effect for direct acquire, renew, or TTL-refresh calls; each TTL-bearing operation nested in a batch or replication entry; forwarding, encryption, cache, or quorum adapters; local Fake/SQLite backends; and session-net client/server admission. A client MUST reject before resolver or network work. A server necessarily receives and decodes the request, but MUST reject before backend dispatch and MAY return the typed error on that authenticated connection. Repeating the check at each public or trust boundary is intentional: direct callers and older peers must fail closed even if an outer layer omitted validation.

The new errors are public enum variants, so external exhaustive matches MUST be updated. Protocol v4 introduced their private fixed-width DTOs in error revision 1; current v5 error revision 9 retains those encodings and adds bounded expiry-preflight and topology-authority outcomes. The exact direct v5 profile is wire-schema revision 7/error-set revision 9; every non-current direct profile combination (including error revision 8 or older) MUST be rejected during the exact handshake. Deployments MUST use the coordinated v5 rollout in §12.3.

7.2 Absolute Record Expiry Authority

For a mutation with authority reference T, a finite StoredSessionRecord::expires_at MUST be accepted when it is in the past, equal to T, or no later than T + MAX_SESSION_TTL. The exact upper bound MUST be accepted and one nanosecond later MUST be rejected. Addition near the timestamp range maximum MUST saturate for comparison and MUST NOT unwind. MAX_RECORD_EXPIRY_CLOCK_SKEW is zero; an implementation MUST NOT silently extend retention to accommodate caller/coordinator clock skew. Deployments MUST synchronize coordinator clocks and a product MAY impose a smaller horizon.

expires_at = None is intentional non-expiring state. It MUST be accepted for AuthoritativeSession, DataplaneLookup, ReplicatedDr, and TelemetryDerived. It MUST be rejected for EphemeralProcedure, because that profile requires per-key expiry to collect abandoned procedure state. A violation MUST return the fieldless StoreError::InvalidRecordExpiry without the record, timestamp, key, or peer-controlled detail.

A direct Fake or SQLite backend MUST capture its injected clock once before a CAS or whole-batch preflight. All CAS slots MUST be checked before any slot can mutate. A forwarding, cache, or crypto wrapper MAY delegate an inner backend's explicit reference; it MUST NOT invent one from its own process clock. A remote or consensus client without coordinator authority MUST perform the time-independent None/state-profile check and leave the finite verdict to the authenticated mutation coordinator.

A legacy ReplicationEntry MUST validate every nested CAS against the entry's immutable timestamp, including replay and rebuild before mutation. This is a reproducible compatibility reference, not production consensus authority. The production OpenRaft leader MUST capture the command logical time, validate the CAS before proposal, and commit that same time with the command. Admission, state-machine apply, replay, follower apply, and journal publication MUST repeat the deterministic check against committed command metadata. A follower's wall clock MUST NOT alter the verdict. Authenticated cluster/configuration identity, leader term/membership, commitment, and applied index supply the coordinator authority described in §12.4.

Profile rejection MUST precede cache invalidation, provider work, or backend dispatch. A wrapper above remote/consensus authority MUST obtain a bounded, payload-free authenticated authority preflight before cache invalidation, provider/HKMS work, sealing, or backend dispatch. The authenticated CAS/batch dispatcher MUST repeat the preflight before idempotency admission. Invalid input and preflight timeout/unavailability perform no provider work or requested mutation; retry is safe because only a consensus logical-time floor may have committed. This rule does not change payload encoding, AAD, key selection, HKMS/KMS placement, or encryption at rest.

Existing valid row and JSON representations are unchanged. Legacy admission, product-aware re-authoring, OpenRaft recovery, and rollback MUST follow docs/session-store-record-expiry-migration.md. The audit MUST be run over a drained snapshot with one recorded --expiry-reference; runtime and audit MUST NOT guess intent, clamp a far-future value, or edit OpenRaft history in place.

8. Record Format

#![allow(unused)]
fn main() {
pub struct StoredSessionRecord {
    pub key: SessionKey,
    pub generation: Generation,
    pub owner: OwnerId,
    pub fence: FenceToken,
    pub state_class: StateClass,
    pub state_type: StateType,
    pub expires_at: Option<Timestamp>,
    pub payload: EncryptedSessionPayload,
}
}

generation is a monotonic per-session version. Every authoritative update MUST increment it atomically.

9. Lease and Fencing

9.1 Lease API

#![allow(unused)]
fn main() {
#[async_trait::async_trait]
pub trait SessionLeaseManager: Send + Sync {
    async fn acquire(&self, key: &SessionKey, owner: OwnerId, ttl: Duration)
        -> Result<LeaseGuard, LeaseError>;

    async fn renew(&self, lease: &LeaseGuard, ttl: Duration)
        -> Result<LeaseGuard, LeaseError>;

    async fn release(&self, lease: LeaseGuard)
        -> Result<(), LeaseError>;
}

pub struct LeaseGuard {
    pub key: SessionKey,
    pub owner: OwnerId,
    pub fence: FenceToken,
    pub acquired_at: Timestamp,
    pub expires_at: Timestamp,
}
}

9.2 Fencing Rules

Every successful lease acquisition MUST produce a monotonic fencing token for that session key. Backends MUST reject any write with a token lower than the current recorded token.

This prevents an old owner whose lease expired during a pause or partition from overwriting a newer owner after it resumes.

9.3 Lease Expiry

Lease expiry alone is not correctness. It is only a liveness mechanism. Safety comes from fencing.

Rules:

  • Lease TTLs MUST satisfy the 365-day bound in §7.1; zero is API-valid and creates a guard whose deadline is immediate, but does not satisfy the operational sizing rule below for an active owner.
  • Lease TTL MUST be longer than worst-case expected procedure pause plus backend failover detection time.
  • Renewals MUST happen before 50 percent of TTL elapsed by default.
  • A failed renewal MUST stop authoritative writes immediately.
  • Owners MUST treat unknown lease state as lost.
  • Stale writes MUST fail with a distinct StaleFence error.

9.4 Backend Notes

  • Redis implementations MUST use atomic Lua scripts or equivalent server-side transactions for acquire, renew, and fenced CAS. Redis deployments that can lose acknowledged writes during failover MUST NOT be used for strict authoritative state without an external consensus/fencing source.
  • Aerospike implementations SHOULD use generation checks and record UDF or transaction mechanisms where available.
  • In-memory backend is for single-process tests or single-replica development unless paired with a consensus lease manager.
  • Strongly consistent stores may be used for leases even when bulk state is in a faster backend.

10. 3GPP Session Continuity and Handover

10.1 Storage Guarantees Needed by Handover

5G handover procedures require avoiding duplicate authoritative writers while preserving continuity of PDU session and bearer/QoS state. The store must support:

  • Idempotent procedure steps.
  • Prepared-but-not-active state.
  • Activation with a fencing token.
  • Abort/rollback of prepared handover.
  • Recovery after source or target NF restart.
  • Detection of stale source updates after target activation.

A lease mechanism without fencing is not sufficient.

10.2 Handover State Machine

The SDK provides generic storage states:

#![allow(unused)]
fn main() {
pub enum HandoverPhase {
    Stable,
    Preparing { tx: HandoverTxId, target: OwnerId },
    Prepared { tx: HandoverTxId, target: OwnerId },
    Activating { tx: HandoverTxId, target: OwnerId },
    Active { owner: OwnerId },
    Aborting { tx: HandoverTxId },
}
}

NF-specific AMF/SMF/UPF logic maps 3GPP procedure messages to these states.

10.3 Procedure Rules

The session store MUST support these generic steps:

  1. Source owner holds a valid lease.
  2. Source creates Preparing record with current generation.
  3. Target acquires or is assigned a higher fence for activation.
  4. Target writes Prepared with expected generation.
  5. Activation performs a fenced CAS to Active { owner: target }.
  6. Source updates with old fence are rejected.
  7. Abort performs a fenced CAS back to Stable if activation did not complete.

All steps MUST be idempotent by HandoverTxId.

New handover envelopes MUST start with the OPCH magic, an exact format version, a bounded phase length, and the typed JSON phase. Every versioned header and phase is decoded strictly. For non-OPCH input, readers MUST apply this exact migration classifier:

  1. Fewer than four bytes are an unframed Stable payload.
  2. The first four bytes are a big-endian potential phase length. Zero, or a value from 1 through HANDOVER_PHASE_HEADER_MAX_BYTES (1,024) whose phase slice is truncated, is InvalidHeader.
  3. A complete phase slice within that bound is an original envelope only when it decodes as the current HandoverPhase. A JSON-looking invalid slice is InvalidPhase; a non-JSON-looking slice falls back to unframed Stable.
  4. A length above 1,024 is InvalidHeader when the bytes after the first word begin, after ASCII whitespace, like JSON. Otherwise it falls back to unframed Stable.

This bounded rule intentionally rejects ambiguous historical bare bytes and original envelopes whose phase is oversized or invalid under the current model. Syntax can also produce false positives: in a checkpoint known to predate OPCH, VersionedV1 is a bare-prefix collision, and an OriginalLengthPrefixed result MUST be confirmed from product provenance and payload meaning. Products MUST run the complete live/replay payload preflight in §5.2 and explicitly wrap the complete bytes of an authoritatively identified bare Stable value, or perform a reviewed semantic migration/store replacement. A successful transition writes the versioned form.

Writing the first OPCH record is a one-way migration barrier. A pre-OPCH reader silently interprets that record as opaque bare Stable data. Operators MUST NOT roll back binaries after the barrier unless the fleet remains drained and either one coherent fleet-wide pre-upgrade checkpoint is restored (with post-checkpoint mutations explicitly lost or reconciled) or every affected live and replayable payload—including nested logs, snapshots, and restore/rebuild sources—is reverse-migrated under a reviewed procedure. Every NF/product handover reader and writer MUST cross the barrier together; protocol negotiation alone cannot make the persisted payload backward-readable.

10.4 Packet Continuity

The session store does not itself guarantee zero packet loss. It provides the state consistency needed by NFs to implement make-before-break, buffering, or tunnel switching. NF-specific procedures MUST state their packet continuity behavior and evidence in RFC 006 reports.

11. Geo-Redundancy

11.1 Corrected Consistency Model

Asynchronous geo-replication is suitable for disaster recovery and warm standby. It is not sufficient for strict active/active mutation of the same authoritative session unless a higher-level single-owner protocol is used.

Authoritative state MUST use one of:

  • Home-region ownership per session.
  • Explicit ownership transfer with fencing.
  • A strongly consistent multi-region backend, if the deployment accepts the latency cost.

Wall-clock last-writer-wins is forbidden for authoritative session state.

11.2 Replication Log

Backends SHOULD expose an ordered replication log:

#![allow(unused)]
fn main() {
pub struct ReplicationEvent {
    pub key: SessionKey,
    pub generation: Generation,
    pub fence: FenceToken,
    pub state_class: StateClass,
    pub payload_digest: Sha256Digest,
    pub encrypted_payload: EncryptedSessionPayload,
}
}

Replication positions are 1-based and gap-free. Sequence zero is reserved for the empty-log head and MUST be rejected as an entry before mutation, external provider work, persistence, or transport dispatch. Rebuild input MUST be validated as one complete contiguous prefix before existing state is replaced. Sequence arithmetic and persistence-width conversions MUST be checked and fail closed without exposing entry contents in diagnostics.

Application of one replication entry is all-or-nothing across its complete operation tree. A failure in any later child MUST leave records, leases, fence/credential high-water marks, the log head and retained log, compaction state, and watcher-visible state exactly as they were before the entry. A successful compound entry MUST preserve child order, append the submitted outer entry once, and publish that outer entry to each eligible watcher only after the local backend transaction or atomic swap succeeds.

Whole-state rebuild MUST replay into an isolated stage or database transaction and replace prior state only after every supplied entry succeeds. Replay failure MUST preserve the complete prior state and established watch subscriptions. A successful rebuild MUST preserve those subscriptions but MUST NOT publish replayed history as new live append events; later locally successful appends remain observable normally. These are backend-local atomicity requirements. In the production HA profile, a caller MUST NOT invoke rebuild or append as an alternative authority path; only an Openraft-committed command or snapshot installation may replace authoritative state. #127 supplies that commit gate, #128 owns current-format reconciliation, and #129 provides the offline operator-directed legacy campaign documented in the recovery runbook.

An operator upgrading persisted state from an older SDK MUST audit every TTL-bearing replication entry before rollout. A legacy entry above 365 days fails closed during replay or rebuild under this contract; implementations MUST NOT silently clamp, discard, or rewrite it. Recovery or migration must follow a product-owned, audited procedure that preserves the authoritative-history contract.

For migration compatibility only, replicated absolute-deadline cross-field validation MAY admit at most one microsecond above the exact entry.timestamp + ttl result produced by an older seconds_f64 conversion. New deadline construction MUST remain exact. This tolerance does not increase MAX_SESSION_TTL; a larger mismatch MUST fail closed.

11.2.1 Bounded Protected Operation Trees

Each ReplicationEntry MUST contain at most MAX_REPLICATION_OPERATIONS_PER_ENTRY (256) operation nodes and MUST NOT exceed MAX_REPLICATION_OPERATION_DEPTH (16). The root operation is depth 1, and each child increases depth by one. Every node counts once toward the total, including each Batch container and every leaf operation. These rules apply to all variants, not only Batch and CompareAndSet.

Validation of an outbound entry or complete rebuild prefix MUST be iterative and MUST finish before payload transformation or backend dispatch. Validation of a complete returned page or item MUST finish before read-side transformation or caller exposure; the backend has necessarily already produced that read. A limit violation MUST return the fieldless StoreError::ReplicationOperationLimitExceeded; diagnostics MUST NOT reveal the observed count, depth, record, key, payload, provider detail, or tree shape. By-value public/wire boundaries MUST also dismantle rejected trees iteratively so the error path cannot recurse while dropping hostile nesting.

An encryption or remote-sealing wrapper MUST transform every CompareAndSet.new_record.payload at every permitted depth. Replicate and rebuild paths MUST stage the complete transformed entry or prefix before delegating to the backend. Replication-log and watch paths MUST decrypt or unseal each complete entry before exposing it. Traversal and reconstruction MUST be iterative and MUST preserve operation order and every non-payload field exactly.

Provider calls MUST run sequentially. If a late write-side provider call fails, earlier provider calls MAY already have occurred, but the wrapper MUST NOT delegate any part of the entry/prefix to its backend. If a read-side provider call fails, the wrapper MUST return an error without exposing a partially transformed entry or page; earlier provider calls, and earlier independent watch items already yielded, MAY have occurred.

This contract changed confidentiality semantics before the v4 boundary. A v3 peer built before this rule cannot decode the new error and its wrapper may forward a deeply nested CAS in plaintext/unsealed form. Protocol v4 rejects the older wire participant and pins both tree limits and error revision, but the handshake cannot attest that the product actually installed a protection wrapper. Operators MUST drain and upgrade every client, server, and protection-wrapper participant as one coordinated fleet and MUST verify wrapper composition before restoring traffic.

Persisted historical nested plaintext/unsealed payloads are not detected or scrubbed automatically. Before upgrade, an operator MUST audit operation-tree shape and payload encoding offline without emitting payloads into diagnostics. An affected entry already within the 16/256 limits MAY be explicitly rewritten or rebuilt through the configured encryption/sealing wrapper. An over-limit historical entry MUST fail before wrapper transformation and MUST NOT be fed to the new SDK unchanged, silently clamped, discarded, or split. It requires a separately reviewed offline migration that preserves the original atomic semantics, or store replacement under an audited product recovery procedure, before the new SDK reads the log. Rebuilding through the raw inner backend does not satisfy this requirement.

11.2.2 Intra-Cluster Consensus Authority

ConsensusSessionStore MUST be the only session-store implementation allowed to claim the quorum platform profile. QuorumSessionStore MAY remain as a source-compatibility type alias to that exact implementation, but MUST NOT own a parallel coordinator. Openraft, imported through the shared opc-consensus crate, owns election, voting, log matching, commit, membership, snapshot coordination, and linearizable-read authority. The SDK state machine owns only deterministic session semantics.

Public dynamic consensus construction, including membership-candidate construction, MUST be supported only on Linux. On every other platform it MUST return DynamicConsensusUnsupportedPlatform before topology or durable-state inspection and before creating a snapshot directory, consensus schema, or other consensus-owned filesystem state. The core initializer MUST independently return its typed UnsupportedPlatform storage error before those effects. Internal path-based snapshot helpers MUST NOT be treated as a portable consensus fallback. Standalone SqliteSessionBackend use remains cross-platform.

HA topology admission MUST start from the complete descriptor set and one explicit logical self ReplicaId. It MUST bind a cluster ID, the exact order-independent configuration digest over the cluster, epoch, and complete descriptor-fingerprint set, and a positive monotonic configuration epoch. Stable non-zero node IDs MUST be derived from cluster identity and the logical ReplicaId, and derived collisions MUST fail admission. Endpoints are routing data: a short logical ID such as epdg-app-0 can select a member whose endpoint is the FQDN epdg-app-0.epdg-app-quorum.epdg-gateway.svc.cluster.local:7443. No code may identify self, derive a vote, or rewrite a logical ID by comparing or shortening those endpoint strings.

The durable storage adapter MUST persist the Openraft vote and log, committed/applied/purged positions, membership, deterministic state-machine chain and logical time, and idempotent request outcomes. Application journal and watch events MUST become visible only after committed apply. A request ID MUST bind the semantic mutation intent; retry after ambiguous response delivery MUST return the original durable outcome, while reuse with different intent MUST fail closed. Caller-selected raw replication entries, whole-state rebuild, and lease sequencing MUST be rejected by this production adapter.

Snapshots MUST be bounded, checksummed, tied to the exact consensus identity, and installed atomically as one coherent state-machine image. They MUST contain only payloads already admitted by the protection wrapper described in §14.1. Automatic current-format reconciliation is supplied by #128. Pre-#127 persisted forks use #129's full-fleet, backup-before-mutation procedure and remain readiness-fenced until Openraft commits the recovery epoch.

The Linux snapshot namespace is a cooperative-service-UID trust boundary. The adapter MUST retain an O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC|O_NONBLOCK directory descriptor at admission, require fstat ownership by the effective UID and mode & 022 == 0, and reject an insecure pre-existing directory. A missing namespace MUST be created 0700; SDK snapshot files MUST be created 0600. Post-admission namespace operations MUST be directory-FD-relative so a parent-path replacement cannot redirect accepted work. Durable snapshot rows name logical basenames, not a mutable parent path.

Supported writers are cooperative SDK processes under one dedicated service UID, serialized by the snapshot/database leases. Operators MUST use a private parent directory, must not share that UID with untrusted workloads, and MUST fail closed on owner or mode mismatch. POSIX ACL group-class masks MUST not restore group-class write authority. This contract excludes root, CAP_DAC_OVERRIDE, CAP_FOWNER, non-cooperating same-eUID actors, and writable aliases of the retained directory. It does not claim universal unlink-by-FD semantics or privileged-attacker resistance.

probe_durable_readiness MUST use the same bounded authority path as an authoritative read: discover or follow the current leader, execute Openraft's linearizable-read barrier against the admitted voting configuration, and wait until the local state machine has applied through the returned log index. A bound listener, completed TLS handshake, cached capability set, local SQLite availability, or successful single-node restore scan MUST NOT produce Ready. That method is engine/lab evidence only: it does not authenticate observed physical node, failure-domain, or durable-backing facts, and its Ready result MUST NOT authorize production traffic.

Attested-HA production session traffic MUST use topology admitted through ValidatedQuorumTopology::try_from_attested, require Quorum from the store's time-aware production profile, then require DurableReadinessScope::ProductionTopologyAttested and is_production_traffic_ready() from probe_production_durable_readiness or its refreshed-attestation form. The evidence MUST have AuthenticatedPlatform provenance and bind every exact member, service identity, observed physical node, failure domain, durable backing, descriptor digest, collector, cluster/configuration/epoch, observation time, and expiry. Verification MUST anchor a monotonic expiry. Each open store MUST retain a bounded nondecreasing wall-clock high-water and MUST recheck both authorities after asynchronous quorum work, so clock rollback, exact expiry, and an older delayed probe racing a newer evaluation fail closed. Identity and production provenance MUST be rejected before a supplied time can advance that high-water. Explicit-time calls on one store MUST use one trusted nondecreasing clock source. The high-water and monotonic anchor are process-local. Restart MUST NOT deserialize or reuse a prior VerifiedQuorumTopologyAttestation; it MUST authenticate evidence again against current time and establish a new monotonic anchor. The adapter-owned proof/replay policy decides whether a still-unexpired underlying proof may be re-presented or replacement evidence is required. Token non-serializability alone is not proof anti-replay.

The attested-HA readiness report MUST carry the bounded DurableReadinessScope. Attested-HA callers MUST require ProductionTopologyAttested and is_production_traffic_ready() and MUST NOT route an EngineOnly report into the traffic gate. Fixed durable quorums use the separate typed authority and placement results in §11.2.2.1; an attested report cannot be substituted for their immutable-voter authority. Every readiness result is point-in-time evidence, never an ownership lease. Products MUST continuously gate ownership publication, VIP/service advertisement, and traffic on fresh attested-HA readiness or, for a fixed durable quorum, a fresh fixed-quorum authority observation. Restore scans MUST execute only after the Openraft barrier and local apply. One absolute deadline MUST begin at the public restore entry and cover the barrier/apply path, blocking-worker and asynchronous connection admission, SQLite progress, and blocking-task join. Each page MUST examine no more than 4,096 live candidates plus one non-decoded lookahead, return no more than 1,024 records, an aggregate local 4 MiB + 64 KiB of stored-envelope payload, or 8 MiB of retained record/key/metadata/payload/cursor bytes, examine no more than 8 MiB of key/filter metadata, and obey the SQLite VM-step, wall-time, and cancellation budgets. Candidate/lookahead SQL MUST NOT select payload blobs; admitted records are fetched by exact primary key inside the same transaction. Scope filtering occurs inside the backend over that bounded candidate window, so an empty page is valid only with a different durable cursor and nonzero excluded/examined count. Pagination MUST seek the existing composite primary key; it MUST NOT use OFFSET or add a digest-order authority. The cursor MUST confidentially and authentically bind that seek key, backend epoch, record revision, logical-time snapshot, scope, and examined progress. Any edit or mismatch MUST return RestoreScanCursorStale before the record query rather than skip, merge, or guess. Restore method availability alone is not readiness evidence.

11.2.2.1 Fixed Durable Quorum Authority and Placement Resilience

ValidatedQuorumTopology::try_from_fixed_durable_quorum admits only an exact three- or five-voter immutable Openraft configuration. It requires distinct logical replica IDs, network endpoints, authenticated TLS identities, and declared backing identities. It does not promote a caller-declared failure domain into physical-placement evidence. The default rejects correlated failure-domain descriptors; only an explicit reduced-resilience policy admits them so the deployment can report their resilience disposition truthfully. The fixed authority-profile marker and explicit placement policy are part of a domain-separated fixed-quorum authority identity: otherwise-identical strict and reduced-resilience fixed profiles MUST derive different authenticated peer, durable-store, and snapshot scopes. A fixed profile and a dynamic profile with the same descriptor set MUST also derive different scopes. Mixed-policy or mixed-profile peers MUST fail authenticated admission before Openraft or durable Raft initialization. Dynamic-profile identities remain descriptor- and epoch-derived and do not include the fixed-profile or placement-policy binding. ConsensusSessionStore::open_fixed_durable_quorum is supported only on Linux, where descriptor-pinned SQLite snapshots are available; other platforms MUST return FixedQuorumUnsupportedPlatform before durable initialization. Linux alone is insufficient for the fixed profile: the snapshot filesystem MUST support the exact fs-verity v1 profile (SHA-256, 4 KiB block size, no salt, and no signature). Build, installation, startup, and recovery MUST reject an unsupported filesystem or an unsealed fixed-profile artifact before it can be accepted as durable state. The dynamic profile remains available without this fixed artifact requirement, but MUST retain its bounded corruption detection and fail closed on invalid snapshot evidence. This is not an online migration: an existing pre-fixed or unsealed metadata-referenced artifact is not auto-sealed, auto-repaired, or accepted on open. Operators MUST preserve it and use the reviewed offline reseed/recovery/migration procedure before reopening; a startup retry, metadata edit, or byte-identical replacement does not cross this boundary. Fixed membership does not authorize dynamic membership transitions, a second consensus engine, a controller feed, or a new packet-core protocol path. try_from_fixed_durable_quorum_with_authenticated_placement may additionally verify a fresh exact-member AuthenticatedPlatform placement evidence set. Replacement evidence is verified through verify_fixed_durable_quorum_placement_evidence; neither constructor nor replacement proof changes the immutable voter configuration. A verified replacement proof is consumed only by the fixed probe's explicit placement-attestation form; it cannot refresh traffic authority.

probe_fixed_durable_quorum_readiness MUST return separate typed results for traffic authority and placement resilience. Traffic authority requires the exact persisted consensus identity and admitted 3/5 voter set, distinct authenticated voter identities and declared backing bindings, a clear recovery latch, and a fresh linearizable Openraft majority barrier. Lost membership, unavailable majority, or recovery state revokes traffic authority immediately. Placement uses the strict RequireIndependentFailureDomains policy by default: only fresh AuthenticatedPlatform evidence may report independent placement. The explicit AllowReducedResilience policy may report correlated or unknown placement as reduced resilience, never as independent. Expiring placement evidence may only downgrade that placement result; it MUST NOT alter fixed-quorum authority, Openraft sequencing, fencing, leases, or mutation admission. Concrete backing-instance and voter-incarnation hardening remain a separate concern; this contract does not manufacture those facts from paths or caller descriptors.

11.2.3 Replication-Log Range Cursors

get_replication_log(start, limit) MUST define one checked inclusive range. Sequence zero is a read-side empty-log sentinel and MUST normalize to inclusive sequence one; it remains invalid as an entry. A zero limit MUST return an empty page before backend I/O, provider work, an Openraft barrier, resolution, or network dispatch. A non-zero range begins at max(start, 1) and ends at that value plus limit - 1. The SDK-wide page limit MUST be 65,536 entries. A larger limit MUST return ReplicationLogPageTooLarge; interval overflow MUST return InvalidReplicationLogRange. start = u64::MAX, limit = 1 is valid, while any larger non-zero interval from that start MUST fail overflow. An empty log, the terminal cursor immediately after the head, or a future cursor MUST return an empty page.

A non-empty page MUST begin at the normalized first sequence, remain internally contiguous, and end no later than the checked last sequence. It MAY be shorter only at the current head or an outer response-frame boundary. Frame shaping MUST emit only the largest complete exact prefix and MUST leave the first unsent sequence as the next cursor. A backend or peer page wholly before or after the requested interval MUST fail with InvalidReplicationSequence before caller exposure. An authenticated compatibility client observing such a wire violation MUST discard both the connection and its cached capabilities before a later request re-handshakes.

If compaction has removed the requested first sequence, the backend MUST return ReplicationLogCursorCompacted { resume_from }, where resume_from is the first sequence after the compacted floor. It MUST NOT silently substitute the first retained entry. The caller MUST install a coherent snapshot or rebuild through its existing authority before using that resume point; the error is not permission to discard missing history. A zero-limit request MUST NOT consult the compaction floor.

After its linearizable barrier, ConsensusSessionStore MUST read one local applied state. It MUST NOT collect or union replication-log pages or compaction floors across replicas. Differing replica floors therefore yield typed local outcomes and cannot synthesize a page that skips committed history. This range contract does not create sequencing, commit, snapshot, restore, or watch authority and does not change payload envelopes, AAD, HKMS/provider placement, or encryption-at-rest boundaries.

11.2.4 Replication-Watch Cursors and Atomic Handoff

watch(start_sequence) MUST use one inclusive 1-based cursor contract. Sequence zero MUST normalize to one. Existing, future, and terminal u64::MAX positions are valid and MUST NOT receive a lower sequence. A watch that delivers u64::MAX MUST deliver it once and close because a reconnect successor cannot be represented. Otherwise a reconnect MUST use the checked successor of the last processed entry.

Backlog capture and live registration MUST be atomic with append/apply notification, or use an equivalent checked handoff that cannot lose or duplicate an entry. Every registration MUST retain its next eligible sequence. A notification below that sequence MAY be ignored when it is either below a requested future cursor or the atomic handoff proves it is already present in that watch's backlog. A position above the next eligible sequence is an integrity gap and MUST close the watch. Backlog and live state MUST each have fixed finite bounds. This SDK admits at most 64 captured backlog entries and 64 queued live entries per watch. More retained backlog MUST return ReplicationWatchCatchUpRequired without a skip cursor. The caller MUST invalidate dependent state, perform a coherent snapshot or full-cache catch-up, and reconnect from the position that procedure proves. Blind retry is forbidden. A compacted cursor remains the distinct ReplicationLogCursorCompacted { resume_from } result and MUST NOT use its resume point until a coherent snapshot covers the missing interval. Live channel overflow MUST evict the slow consumer; cancellation and stream close MUST NOT permit registrations to accumulate without bound.

The production Openraft adapter MUST complete its linearizable barrier before the atomic local handoff and MUST publish only application-journal entries emitted by state-machine apply. An uncommitted or merely log-appended local entry MUST NOT be observable. Raw append/rebuild beside Openraft remains forbidden. The legacy session-net client MUST complete watch setup within its absolute deadline before returning: an initial typed store rejection is returned exactly, not converted into disconnect/retry ambiguity. After acceptance it MUST require every authenticated-peer item to equal the next inclusive sequence and MUST terminate the dedicated connection on duplicate, gap, invalid, or otherwise corrupt metadata before an outer encryption wrapper performs provider work. Errors and diagnostics MUST be redaction-safe, and a subsequent independent request MUST use a usable freshly authenticated connection.

ReplicationWatchCatchUpRequired advances the quarantined protocol-v4 error set from revision 5 to revision 6. The wire schema remains revision 4. All legacy compatibility peers MUST be drained and upgraded together; this is not a rolling mixed-profile transition. The Openraft consensus profile, persisted SQLite/journal/snapshot format, payload envelopes, AAD, and HKMS/provider placement are unchanged.

Replicas MUST apply events only if generation and fence are newer according to the state class rules.

11.3 RPO and RTO

Every deployment profile MUST publish:

  • Recovery point objective for session state.
  • Recovery time objective for session service.
  • Maximum tolerated replication lag.
  • Which state classes are replicated.
  • Which state classes are rebuildable.

12. Serialization

Rust has no garbage collector, so the goal is allocation, CPU, and cache efficiency rather than "GC pressure" reduction.

12.1 Formats

Allowed formats:

  • FlatBuffers for read-mostly zero-copy records.
  • Prost/Protobuf for compatibility, with careful allocation profiling.
  • Postcard or bincode-like formats only for internal state with stable version policy.

Each state type MUST define:

  • schema version
  • compatibility policy
  • max encoded size
  • fuzz target
  • migration path

12.2 Decode Rules

Decoders MUST:

  • Validate length prefixes and offsets.
  • Reject trailing garbage unless explicitly allowed.
  • Avoid borrowing data beyond the lifetime of the source buffer.
  • Avoid panics on corrupt data.
  • Support partial decode for lookup keys where useful.

12.3 Legacy Direct-Backend Session-Net Protocol v5

The direct SessionBackend protocol is retained only behind the non-default legacy-session-net-compat feature for controlled migration and compatibility testing. It MUST NOT be enabled on a production consensus node or served on the consensus endpoint. When used for migration, it MUST use the exact opc-session-net/5 ALPN, contract version, and contract profile. It MUST NOT negotiate down or select a highest-common version. A mismatch MUST fail before backend dispatch, close the connection, and be non-retryable for that request.

The public semantic Request and Response types remain available, but their Serde boundary MUST delegate to private fixed-width v5 DTOs. Hello and HelloAck add an optional contract_profile; HelloAck also carries the server's optional cas_idempotency_epoch, and direct CAS carries an optional idempotency_epoch. Exhaustive Rust construction and matching MUST account for the new fields. The profile pins wire-schema revision 7 and error-set revision 9; max_restore_scan_page_payload_bytes = 2096128; owner, custom-key, and state-type bounds of 128 UTF-8 bytes; min_frame_size = 8192; max_frame_size = 16777216; stable_id_max_bytes = 64; replication_tx_id_max_bytes = 128; cas_request_id_bytes = 36; the 31,536,000-second session TTL maximum; restore-page maximum 1,024; and the depth-16/256-node replication-tree rules. Every transported stable ID MUST contain 1 through 64 bytes. Every transported replication transaction ID MUST contain 1 through 128 UTF-8 bytes and MUST be represented by the bounded ReplicationTxId domain type before mutation or durable sequence allocation. New committed coordinator writes MUST encode the 16-byte consensus request ID as exactly 32 lowercase hexadecimal bytes. A reader MUST preserve any valid legacy representation byte-for-byte and MUST NOT trim, case-fold, parse, or normalize it; exact equality remains idempotent redelivery and any distinct representation remains divergent. Every CAS request ID that is present MUST use the canonical lowercase hyphenated UUID representation and therefore contain exactly 36 bytes. The public profile's max_frame_size addition is a Rust source break for external struct literals and exhaustive destructuring and MUST be deployed in the same coordinated revision-2 fleet transition.

The fixed-width mapping is:

  • Hello requested_response_frame_size, HelloAck accepted_response_frame_size, and HelloAck server_request_frame_size: u32;
  • restore/log request limits and the client restore-response budget: u32;
  • restore request/response cursors and restore excluded count: u64;
  • backend max_value_bytes: u64; and
  • PayloadTooLarge.actual/max, RestoreScanPageTooLarge.requested/max, ReplicationLogPageTooLarge.requested/max, and RestoreScanResponseTooLarge.max_bytes: u64, including errors nested in batch results.

The restore wire page MUST omit loaded_count and complete; the receiver MUST derive them from the record vector and next_cursor. Conversion to or from a domain usize MUST be checked, and a non-representable value MUST fail before backend dispatch or caller exposure. Collection work MUST be bounded independently from encoded frame size: at most 256 batch operations, 1,024 restore records, 65,536 replication-log entries, and 65,536 rebuild entries. The configured frame limit remains a separate encoded-byte bound. Log requests and returned pages MUST also satisfy the exact range contract in §11.2.3 before dispatch or caller exposure.

Wire-schema revision 2 MUST negotiate directional frame budgets during the frozen bootstrap. The client's requested response size, the server's accepted response size, and the server's request size MUST each be at least MIN_NEGOTIATED_FRAME_SIZE (8 KiB, or 8,192 bytes), at most MAX_NEGOTIATED_FRAME_SIZE (16 MiB, or 16,777,216 bytes), and representable as u32. Their public bootstrap fields are Option<u32> so a revision-2 decoder can classify an otherwise decodable legacy minimal bootstrap. This MUST NOT be treated as bidirectional mismatch negotiation: a revision-1 decoder MAY reject unknown revision-2 fields by closing without a typed response. Revision-2 admission MUST require all three as Some. The accepted response size MUST be no greater than either the client's receive limit or the server's configured frame limit. The server request size independently states the maximum operation frame the server will accept. Peers MUST use these values for the lifetime of that connection and MUST NOT infer equal limits in both directions. MIN_RESTORE_SCAN_RESPONSE_FRAME_SIZE MUST alias MIN_NEGOTIATED_FRAME_SIZE; it is not a second negotiable minimum. The restore request's existing max_response_frame_size MUST remain an additional per-call cap and MUST NOT enlarge the negotiated response budget. Before binding or spawning, a server MUST reject a configured frame size below 8 KiB or above 16 MiB, a zero/runtime-unrepresentable connection-slot count, or an unrepresentable idle/restore timeout with InvalidInput. A zero timeout MAY remain an intentional immediate-fail policy. Before DNS, socket allocation, or watch-task spawning, a client MUST reject a configured frame size outside the same range. Bootstrap output MUST use the separate 8 KiB MAX_HANDSHAKE_FRAME_SIZE cap.

Every post-bootstrap response and watch item MUST be fully bounded-encoded into retained byte storage capped at the accepted response size before a length prefix is written. The common non-pageable and complete-page success path MUST perform one bounded encode without a separate sizing serialization. For a replication-log page, if the complete pageable response is oversized, that direct encode MUST emit no prefix; prefix selection MAY then perform bounded logarithmic sizing probes followed by one final bounded encode. Restore pages MUST be validated as whole backend results and MUST NOT be transport-shaped. No retained encoded-JSON byte storage may exceed the negotiated cap. The retained/requested encoded-JSON byte storage MUST remain no greater than the cap, including for non-power-of-two budgets. An implementation MUST NOT coalesce or create a temporary payload buffer when doing so would exceed that bound. This SDK satisfies the contract with lazy exact-length boxed chunks and no coalescing copy. Chunk-pointer metadata and allocator slab/RSS overhead are not encoded JSON bytes and MUST be accounted for separately by runtime resource qualification. One absolute deadline MUST be established before the first direct encode or sizing probe and reused through every probe, the final encode, length prefix, complete payload, and flush. Implementations MUST NOT restart the deadline per probe, phase, write, or chunk. Deadline expiry MUST terminate the connection and release its task and connection permits. Synchronous storage and sizing sinks MUST check the deadline and the server's abort cancellation signal cooperatively between serializer writes and retained chunks. Task abortion cannot preempt one synchronous serializer callback; therefore every wire field and collection processed between checks MUST remain bounded, and shutdown claims MUST include that finite callback interval.

Outbound behavior is family-specific:

  • The fixed-width Capabilities envelope MUST fit within the 8 KiB protocol minimum; an encoding failure MUST close without emitting an oversized frame. Scalar mutation results, replication/rebuild acknowledgements, and lease results MUST use an SDK-owned fixed, redaction-safe fallback when a backend-provided result cannot fit. If the fallback cannot fit, the connection MUST close without emitting an oversized frame.
  • Get results and CAS conflicts MUST NOT truncate a record. They MUST replace the complete record-bearing result with the fixed fallback or close.
  • Batch results MUST preserve exact request cardinality and order. They MUST NOT be truncated; an oversized complete result becomes one fixed batch error or a connection close.
  • Restore backends MAY independently return a shorter cursor-correct page under their count, payload, or work budgets. The transport MUST validate the entire returned page against the fixed 2,096,128-byte wire-payload cap and the negotiated frame; it MUST NOT trim or rewrite records or cursors. If the whole page is oversized, the server MUST return typed RestoreScanResponseTooLarge if representable or close.
  • Replication-log results MAY return only the largest complete contiguous entry prefix that fits. An entry MUST NOT be split, reordered, or skipped. If no requested entry fits, the server MUST use its fixed fallback or close.
  • Watch acknowledgement and each watch item MUST be bounded independently. The server MUST NOT skip an oversized entry because that would conceal a sequence gap. It MUST send a fixed error item when representable and then terminate the stream/connection, or close immediately when the fallback cannot fit.

Fallback text MUST be static SDK-owned text. It MUST NOT contain a key, owner, payload, transaction/request ID, peer identity, backend error string, or other peer-controlled text. Consuming rejection of nested replication operations MUST retain iterative disposal and the existing depth/node work bounds.

BackendCapabilities::max_value_bytes transported over session-net MUST be no greater than the backend limit, conservative_payload_budget(accepted_response_frame_size), or conservative_payload_budget(server_request_frame_size). That function MUST compute frame_size.saturating_sub(8192) / 8: the 8 KiB block reserves the record/key/error envelope, while the factor of eight covers four-byte worst-case JSON byte-array expansion plus equal escaping/metadata headroom. The advertised maximum MUST complete a real write/read round trip under unequal limits. At exactly 8 KiB, the conservative payload budget is zero: that minimum MUST fit maximum-profile metadata/envelopes but does not promise a non-zero application payload. Capability evidence remains descriptive and MUST NOT authorize quorum or traffic readiness. The 1 MiB default yields 130,048 payload bytes and the 16 MiB ceiling yields 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 is a per-frame limit, not aggregate admission: at the server's default 128 connection slots, simultaneous ceiling-sized encoded stores can retain about 2 GiB before chunk metadata, TLS, and runtime overhead. The aggregate scales with the configured connection limit. #143 owns aggregate byte permits and distributed resource/soak qualification.

Backend mutation and response delivery are not one transaction. A mutation MAY commit before response encoding, write, or flush fails. Direct CAS idempotency MUST be keyed by the authenticated logical peer plus canonical request UUID and MUST bind a redaction-safe digest of the complete operation, cluster and configuration identity, monotonic configuration epoch, and server-issued process epoch. Same-scope exact duplicates MUST share one in-flight execution and replay the exact success or conflict. Reuse by another peer or operation MUST return CasIdempotencyConflict before backend dispatch. Cancellation MUST leave a tracked ambiguous tombstone, never an untracked in-flight entry.

The compatibility cache MUST bound total and per-peer entries and bytes, retention age, and cleanup work. One peer MUST NOT evict another peer's active retry window. Restart or retention cleanup MUST rotate the process epoch, and an old epoch MUST return CasIdempotencyOutcomeUnavailable before mutation. Pressure that cannot retain a result MUST return the same typed unavailable outcome rather than evicting an active result and treating its UUID as new. The public client MUST NOT automatically resubmit a CAS after any ambiguous transport boundary. A caller that receives no valid response or the typed unavailable outcome MUST perform an authoritative re-read and derive a new mutation; it MUST NOT infer rollback or replay the historical operation under either the old or a fresh UUID.

Every authenticated request MUST have three bounded phases: one inbound idle-timeout to receive and decode a complete frame, one backend admission/work deadline started after decode, and one reserved bounded response interval. The checked sum of the latter two is the post-decode dispatch/response lifetime; full connection-slot occupancy includes the inbound phase as well. Reads, mutations, lease mutations, and watch setup MUST have independent fixed-size admission pools; restore MAY retain a stricter dedicated pool. Queue expiry before backend polling is known not applied. Read execution MUST be cancellable and release resources. Once a non-CAS or lease mutation has been polled, deadline, disconnect, cancellation, or response loss MUST either recover a durable operation-bound outcome or return the non-retryable BackendOperationOutcomeUnavailable / LeaseError::OperationOutcomeUnavailable class. The public legacy client MUST NOT reconnect and resubmit such a mutation. A transport failure proven to have occurred before the first request write remains known not applied and MAY be retried. CAS continues to use its stronger operation-bound idempotency outcome. Code that itself drops a polled mutation future receives no result and MUST treat that cancellation as the same unknown-outcome class.

The production Openraft adapter MUST create one durable request identity before leader selection and retain it across internal forwarding retries. A local failure before proposal submission MAY remain retryable. Once Openraft accepts the proposal into its client-write channel, loss of the result receiver, deadline expiry, or an unvalidated forwarded result MUST return CasIdempotencyOutcomeUnavailable for direct CAS and BackendOperationOutcomeUnavailable for every other mutation (mapped to LeaseError::OperationOutcomeUnavailable at the lease API). It MUST NOT return a generic retryable availability error. Durable state-machine request outcomes MUST make retry of the same internal identity idempotent.

The production adapter MUST use the shared fixed eight-slot proposal-admission pool for both normal mutations and finite-expiry logical-time-floor proposals. It MUST acquire a slot within the operation's original absolute deadline. Once client_write_ff returns the accepted proposal's result receiver, a detached supervisor MUST retain that slot until the receiver resolves; caller drop, peer EOF, and response timeout MUST NOT release it early. Saturation MUST fail closed before another proposal is submitted. A finite-expiry preflight MUST validate its descriptors against the logical time returned by its committed floor command before reporting success.

Every fresh read-index and mutation preflight MUST use one shared linearizability supervisor per Openraft node. Exactly one supervisor-owned ensure_linearizable call may be in flight, with at most 64 total admitted callers across the active and waiting cohorts. Only callers collected before dispatch may share that exact Openraft result; later callers require a later check. Caller cancellation or deadline expiry MUST NOT cancel a dispatched check, release its admission early, or start an overlapping check. The supervisor is a resource bound only: Openraft remains the sole source of leadership, quorum, read-index, and applied-state authority.

After a legacy request is transmitted, a malformed or wrong-family response, or a same-family response that violates request-bound key, owner, fence, credential, ordering, or cardinality semantics, MUST use the same typed ambiguous-outcome classification. Direct-CAS retry caches MUST NOT retain a backend availability result as a completed retryable outcome; they MUST retain an ambiguous tombstone instead.

Server cancellation and peer EOF MUST race pending backend work and idle watch streams. Backend adapters MUST treat future drop as a cancellation signal and retain bounded admission/supervision until underlying work exits. Read resources are released after bounded cancellation completes. A mutation may finish after its caller drops, but that caller MUST treat the outcome as unknown and re-read authoritative state. Durable operation-bound replay is Openraft/direct-CAS-specific, not a generic adapter promise. No timeout or shutdown path may create unbounded detached work. Static capabilities MUST fail closed when backend admission cannot be obtained and MUST NOT substitute for fresh readiness.

Outbound diagnostics SHOULD expose only bounded response_family categories and fixed reasons such as frame_too_large, page_shortened, write_timeout, transport, and encoding. They MUST NOT label or log session keys, payloads, transaction IDs, owners, SPIFFE IDs, backend/peer-controlled error text, or other high-cardinality identifiers. The fixed metric family opc_session_net_backend_lifetime_events_total MAY expose only queue_timeout, execution_timeout, cancellation, peer_disconnect, and ambiguous_outcome; it MUST NOT contain dynamic labels.

A fresh version/profile/authentication or malformed-handshake failure MUST clear the cached capabilities and report all capability booleans false with max_value_bytes = 0. A cache retained after transient transport loss is descriptive only and MUST NOT authorize a store operation, durable readiness, or traffic admission. A cache MUST be keyed by the exact profile and negotiated directional limits and cleared when a successful reconnect changes either limit. Callers MUST use fresh bounded quorum evidence.

The transition to v5 wire-schema revision 7 and error-set revision 9 is a coordinated stop/upgrade/start boundary, not a rolling deployment. Operators MUST drain traffic and writers; run the #135 identity audit; inventory every retained record, replication log, snapshot, restore source, and replay source for the stable-ID and transaction-ID bounds; and complete product-aware handover/nested-payload preflights. A retained-value migration MUST be decoder-first: while writers remain quiesced, every migration reader MUST be able to decode the legacy representation before any rewrite or replacement occurs. Stable IDs MUST follow the product-aware #167 model/persistence/privacy/audit policy. Durable transaction IDs MUST follow the #168 bounded-type and migration policy, including current report version 4 and coordinated cutover with #127/#128/#143. The migration MUST NOT silently truncate, hash, or rename a key or idempotency identity. Operators MUST verify that the strict revision-3 decoder accepts the result; then they MUST stop every session-net client, server, and protection wrapper plus every handover reader/writer; upgrade them together; verify exact-v5 authenticated restore/log traffic, rejection of modified/legacy restore cursors, sparse empty-page progress, and fresh quorum evidence; and only then restore traffic. Once an OPCH value has been written, v3 rollback additionally requires a coherent drained checkpoint restore or reviewed reverse migration of every live and replayable record, log, snapshot, and restore source. Revision 3 adds only an O(1) per-store cursor key to local restore metadata; it does not rewrite session records or create another authority. A pre-revision-3 consensus snapshot lacks that metadata and MUST NOT be installed after upgrade; operators MUST take and validate a coherent post-upgrade snapshot before claiming repair or rollback coverage. In-profile data needs no format conversion, but out-of-profile retained values MUST be migrated or replaced before strict transport starts. Binary rollback MUST restore one exact drained fleet profile and install a rollback-side decoder that can read the retained target representation before old writers restart; otherwise it MUST restore a coherent checkpoint or run a reviewed reverse migration. Mixed revision-3 and older participants fail closed. Rollback across the independent OPCH/#135 boundary retains its checkpoint/reverse- migration requirement.

That compatibility cutover is distinct from credential rotation. Only after every participant is admitted on the same revision-7 profile MAY operators use material-epoch or explicit reauthentication to recycle connections without draining application traffic. The lifecycle MUST NOT be used to mix protocol profiles, negotiate a downgrade, or turn a binary rollback into a rolling operation.

The cursor encoding is variable-length but strictly bounded by the consensus RPC/key ceiling. HMAC-derived AEAD and synthetic-nonce keys are separated; identical semantic positions encode identically. The seek key and snapshot metadata remain confidential, while a clear cumulative examined-row position is bound into cursor authentication. A receiver can reject a structurally inconsistent claimed step and the issuer authenticates the position when the cursor returns, but neither fact proves peer-page completeness or server honesty. Production completeness comes from the local Openraft-applied state after its linearizable barrier. Cursors are backend-incarnation/node-bound: same-PVC restart can resume, but another node or installed snapshot MUST return typed stale-cursor state and the caller MUST discard partial pagination and restart at the first page.

#159 establishes only session-net response/write and wire-containment bounds. It does not close #167 or #168 and does not provide #143's payload-key/distributed production qualification. #163 real-mTLS transport tests cover local/peer leaf-expiry retirement, overlapping trust, complete replacement negotiation, old-trust rejection, and request/watch continuity. TLS material tests separately prove effective configured/presented-chain expiry through a real mutual-TLS handshake, while lifecycle unit tests prove the corresponding local/peer retirement deadlines and fixed metric reasons. Additional non-ignored single-host three- and five-process tests now exercise one bounded multi-process slice: a test-only consensus-RPC admission loss on one stable follower while a different member retains last-good material after malformed trust; exact-address restart, catch-up, and repair; and a same-issuer leaf with a 75-second remaining-validity/expiry budget through the expiry - 30 seconds soft boundary, hard drain, source/controller LastGoodExpired, survivor durable readiness and encrypted-canary progress, and same-process valid replacement while bounded mixed lease/CAS mutation, linearizable-read, watch, complete-restore, readiness, and connection-recycling traffic remains active. After repair, one stable follower is also killed uncleanly with active mutation/watch tasks; survivors commit during the outage and its same-disk, exact-address restart must reconcile the exact record/watch state and resume at a higher fence under the v3 stage bounds described above. The six sequential stage bounds compose to a 164-second crash-to-resume ceiling; the total does not replace any individual stage deadline. Other active-mutator restart patterns, a real/deployed partition, a broader restart/fault matrix, resource/soak, remote-HKMS, deployed-CNF, signed release, and evidence-schema/production-profile claims remain unqualified. Generic CRL/OCSP/certificate-or-identity-denylist revocation is not implemented. #177 removes opc-persist's separate config TCP path and reuses the shared consensus peer/handler boundary instead of defining another timeout or credential lifecycle. An in-process real-mTLS integration forms a three-node config Openraft cluster and commits/linearizably reads through the existing peer/server types. Any compatibility transport work must preserve the single Openraft authority rather than reopen direct mutation as a quorum path. #161 atomic reload, #162 coherent material epochs, and #163 finite connection reauthentication are implemented. Fleet SVID/trust-bundle qualification remains #164 under umbrella #158.

12.4 Consensus-Only Session Transport

The production session HA transport MUST use SessionConsensusServer and RemoteSessionConsensusPeer on the exact opc-session-consensus/2 ALPN. The server MUST own only a SessionConsensusRpcHandler; it MUST NOT accept a SessionBackend, lease manager, direct mutation request, caller-authored replication append, or rebuild request. The consensus ALPN and legacy opc-session-net/5 ALPN MUST NOT be multiplexed as equivalent authority on one production listener.

The exact consensus contract profile MUST use transport/wire-schema revision 5, application revision 4, and error-set revision 6. The revision-5 transport profile retains the explicit forwarded consumer scope, so a peer cannot silently downgrade a consumer-scoped operation to an internal call; application revision 4 fences the former 728bc5 application-revision-3 Postcard tag-27 FinalizeOperatorRecoveryV2 encoding, which conflicts with the merged roster profile's tags 27 through 30. Error revision 6 binds that semantic boundary into the exact profile. Any other transport, application, or error-set revision MUST fail before engine dispatch. Operators MUST drain traffic and writers, stop every consensus member, upgrade the full membership together, verify exact-profile handshakes, and only then restore traffic. Mixed-profile rolling operation is unsupported.

Each connection MUST perform mutual TLS and bind all of the following before engine dispatch:

  • the live certificate's one canonical SPIFFE URI;
  • the logical ReplicaId and derived stable node ID of each side;
  • the expected opposite peer and authenticated request sender;
  • the cluster ID, exact configuration digest, and positive configuration epoch;
  • the engine RPC family, peer role, exact transport revision/profile, and a fresh challenge.

The sender authenticated by the outer transport MUST equal the sender carried inside the bounded engine request. DNS names, FQDNs, IP addresses, resolver aliases, and Kubernetes pod hostnames MUST affect only connection routing and MUST NOT be accepted as substitutes for any logical, stable, or certificate identity.

One absolute family deadline MUST begin before lane acquisition and cover bounded encode, write, and response read. The outer hard/direct complete ceiling for AppendEntries/Openraft read-index MUST be 2,000 ms, Vote 5,000 ms, and InstallSnapshot, forwarded mutation, and consumer ReadBarrier 10,000 ms. An Openraft network call uses the smaller soft TTL described below. If no valid cached connection exists, resolution, TCP connect, mutual TLS, identity admission, and bootstrap MUST use the lesser of two thirds of the remaining family budget and a 1,500 ms cold sub-bound. The reserved final third MUST remain available for the first negotiated RPC; cold time MUST NOT be added to the family deadline. Each directed peer MAY cache a fixed primary/overflow pool of at most two authenticated connections, with at most one in-flight RPC per lane. Sequential calls MUST prefer primary; a concurrent call MAY use overflow; when both lanes are busy, further calls MUST wait for either lane under the same absolute family deadline. It MUST recache a selected lane only after a complete, correctly correlated, authenticated, validated successful response or typed semantic Unavailable response. The Unavailable exception preserves a known stream position but grants no success or authority. A cached lane MUST NOT clear shared reconnect cooldown until such a reusable response has proved that lane usable. Cancellation, timeout, EOF, malformed/cross-correlated response, protocol, authentication, scope mismatch, rejection, lifecycle evidence mismatch, or any uncertain stream position MUST evict that lane. A late connection or response after cancellation MUST NOT be reused. The transport MUST carry only the shared bounded consensus envelope; the session-store adapter compact-encodes Openraft RPCs, and the network layer MUST NOT interpret commands or decide leadership, voting, log matching, commit, or repair. An identity, authentication, schema, payload-bound, or sender mismatch MUST fail before Openraft dispatch with redaction-safe diagnostics.

For an Openraft engine RPC, the adapter MUST pass RPCOption::soft_ttl() to a deadline-aware network peer and MUST NOT install another hard timeout around that peer future. The peer MUST apply the lesser of the supplied soft TTL and its configured family ceiling, classify deadline expiry before returning, and evict any socket with an uncertain stream position. Openraft's outer hard_ttl() remains the sole hard cancellation authority. A compatibility or in-process peer that does not own transport deadlines MAY retain its existing call behavior and rely on that outer hard deadline; the deadline-aware default MUST NOT introduce a new soft cancellation boundary for such a peer.

Every authenticated client, peer, and listener MUST apply one finite ConnectionLifecyclePolicy. Its hard deadline MUST be the earliest of the configured maximum authentication age, the expiry of every certificate in the local configured/presented SVID chain, and the expiry of every certificate actually presented by the peer. A redundantly presented root contributes to that bound. A certificate appearing only in a configured trust bundle is not independently scanned for the deadline, and the time an anchor is removed is not an expiry deadline. Production SVID chains SHOULD omit the trust anchor. Soft retirement MUST begin early enough to leave at most one configured drain window before the hard deadline. A coherent TLS material-epoch change or an explicit process-local reauthentication generation MUST also schedule retirement, using deterministic directed-peer jitter no greater than the configured bound.

After soft retirement the client MUST NOT assign a new operation to the connection and the server MUST NOT read or dispatch another request. An operation admitted before retirement MAY return once within its existing operation deadline, but transport ownership MUST stop waiting by the lifecycle hard deadline and MUST release every connection/task slot. Dropping the backend future requests cancellation but MUST NOT be interpreted as rollback: bounded supervised mutation work MAY finish later. Such an outcome MUST remain typed ambiguous, MUST NOT be automatically replayed, and requires authoritative readback or the operation's existing idempotency/fencing contract. A replacement MUST repeat DNS/route resolution, mutual TLS, live certificate identity, nonce/challenge, ALPN, version, and exact contract-profile checks. TLS resumption, cached peer authority, plaintext fallback, and task-abort reauthentication MUST NOT replace that handshake.

After authentication and bootstrap acknowledgement, if no byte of the next request arrives before the listener idle deadline, the server MUST record the fixed idle_timeout lifecycle-retirement reason, enter and complete the normal drain/slot-release path, dispatch no request, and finish the connection handler as a successful bounded lifecycle outcome rather than a timeout failure. This rule applies to both the consensus listener and the legacy direct listener. Silence during TLS/application bootstrap MUST remain a timeout failure. Once any byte of an authenticated frame arrives, the remaining length prefix and payload MUST complete within the original absolute idle deadline; an incomplete active frame MUST remain a timeout failure and MUST NOT be relabeled as idle retirement.

If a lifecycle retirement boundary (maximum authentication age, local or peer certificate expiry, material epoch, or explicit reauthentication) is observed after mutual TLS but before any bootstrap acknowledgement bytes are written, the generic transport MUST return one complete authenticated BootstrapResponse::ConnectionRetiring result. The consensus bootstrap context MUST use SessionConsensusBootstrapResponse::Rejected(SessionConsensusPeerError::Rejected) as the corresponding no-dispatch control. That nested value is reserved in this context only: ordinary authentication, identity or scope, contract, and protocol failures MUST retain their existing classifications and MUST NOT be emitted or interpreted as this control; a post-bootstrap engine Rejected result MUST remain an ordinary call response. The sequential client MUST NOT send application or Openraft request bytes before bootstrap succeeds. After it decodes the complete retirement control, it MUST discard that connection and retry the pending operation only through the existing bounded deadline and backoff path.

EOF, an incomplete control, or an acknowledgement whose write has partially completed MUST fail closed. Once an acknowledgement write may have emitted a byte, the server MUST close rather than append a bootstrap retirement frame. The client MUST NOT infer no-dispatch from that incomplete stream. The server MUST count a connection-attempt success only after completely writing the bootstrap retirement control; the client MUST count its own success only after decoding the complete control. In both cases success means authenticated transport/control completion, not application admission. That decode MUST initiate the client's existing bounded deadline/backoff path and count a reconnect attempt; the client MUST NOT count a reconnect failure or a connection-failure outcome for that complete control.

The legacy direct profile MAY automatically retry a mutation after retirement only when it has decoded the complete fixed ConnectionRetiring response, which proves server dispatch did not occur. EOF, a partial retirement frame, write failure without a complete buffered proof, or a generic transport error MUST remain an ambiguous mutation outcome. A legacy watch MUST keep the caller stream alive across planned retirement, pin any partially read item to its old connection, advance the resume cursor only after caller delivery, and resume from checked last_delivered_sequence + 1. Cursor overflow, compaction or another permanent backend error, cancellation, and bounded slow-consumer failure MUST terminate explicitly rather than reconnect forever.

This bootstrap behavior was introduced for the then-frozen direct revision-6 variant and is retained by the current revision-7 profile, alongside the existing consensus error value in their restricted bootstrap contexts. It changes no public API, direct or consensus profile revision, persisted SQLite/journal/snapshot format, Openraft commit authority, payload envelope, encryption-at-rest boundary, or HKMS/provider placement. Older same-profile decoders fail closed on the control rather than negotiating a downgrade, so mixed-patch rolling rotation is not seamless. This closes only the narrow authenticated post-TLS/pre-acknowledgement race; it does not satisfy the remaining #164 fleet qualification.

This authenticated transport plus #127 commit authority is still not a production qualification. #128 supplies current-format divergence recovery, #129 supplies the audited offline legacy-fork campaign without reopening a runtime consensus path, and #133 provides bounded applied-state restore without reopening a direct backend/rebuild port. #143 remains the distributed partition/restart/resource/soak and payload-key gate. #161 atomic reload, #162 material epochs, and #163 bounded reauthentication are implemented, including scoped retained-connection, request, and watch continuity evidence. Production rotation already has single-host multi-process trust transitions and the exact synthetic fault/expiry slice described above. It MUST additionally qualify deployed trust/root cutover, real network/storage faults and a broader restart matrix, deployed mixed traffic/watch/restore under those real faults, reconnect-storm bounds, resources, soak, remote HKMS, deployed CNFs, and signed release evidence under #164/#143. The lack of immediate generic CRL, OCSP, or certificate/identity-denylist revocation MUST remain explicit. #158 remains the umbrella until that fleet evidence passes.

12.5 Typed Session-Quorum Consumer Transport

StatelessSessionConsumerClient, PersistentSessionConsumerClient, and SessionQuorumConsumerServer provide the typed least-authority application-consumer boundary. They MUST use mutual TLS and three independent exact lanes: the general opc-session-consumer/1 ALPN at transport revision 6, the epoch-fenced V2 opc-session-consumer/2 ALPN at transport revision 5, and the protected-roster opc-session-consumer/3 ALPN at transport revision 5. These are separate exact protocols from both opc-session-consensus/2 and the quarantined opc-session-net/5 compatibility protocol. /2 MUST NOT fall back to /1, and a lane authenticated for one ALPN MUST NOT carry or be reused for another lane's Hello or request envelope. When protected-roster ingress is enabled, the listener advertises /3 before /2 and /1; each client offers exactly one ALPN. /3 admits only its protected roster operation set under its tenant/scope/fence authority, and /2 MUST NOT count or reclaim /3 lanes. The server MUST reject every /2 Hello revision other than 5 before dispatch.

General revision 6 does not interoperate with older general revisions. Because this SDK is unreleased, a general-lane revision cutover MUST drain consumer clients and listeners; fallback, general-lane dual mode, and mixed-revision operation are unsupported. Deployment MUST provision revision-5 /2 listener support, V2-capable store authority, and the required protected-wrapper V2 journal before enabling an explicit V2 client call. /1, /2, and /3 coexist only as independently authenticated and decoded lanes, never as common or dual-revision authority. Removing either additive lane requires draining its callers first. General revision-6 and V2 revision-5 private JSON DTO bytes are canonical; reordered or otherwise noncanonical encodings, aliases, omissions, and unknown fields MUST fail closed.

StatelessSessionConsumerClient remains a public, source-compatible production/compatibility fresh-authentication typed least-authority surface required by #649, #688, and #691; it is neither hidden, deprecated, nor test-only. PersistentSessionConsumerClient remains the required warm fixed-pool primitive for #695/ePDG latency, so production deployments requiring warm reuse should use it. This distinction is not an API-removal or feature-gating claim.

The consumer listener authenticates the peer from the live mTLS connection and authorizes it only through the store-issued current-member manifest and the configured consumer allow-list. Consensus-member identities are excluded from the consumer role. Every bootstrap and request carries the exact cluster/configuration/epoch scope, which the listener and quorum-side service MUST verify before backend work. Consumer identity and scope values are security-sensitive: diagnostics, profile inventories, and observability MUST record only redaction-safe status/count information, never their concrete values.

The V1 API exposes typed session reads, bounded mutation/lease operations, bounded restore scans, capability discovery, a coarse committed-change watch, and #696's generic one-record atomic fenced transition. The latter includes an exact-key observation, one lease-acquire or lease-renew action plus one bounded record mutation, and exact retained-status readback. V2 exposes only typed V2 capability, V2 history state, one epoch-fenced transition, SessionConsumerV2Operation::FencedTransitionV2Batch, and exact V2 transition status. The batch is an ordered, same-epoch 1..=256 transition batch and is not all-or-nothing: each item has its own outcome and earlier items may have effects when a later item does not. V2 does not add raw consensus or replication operations, membership/voting authority, or product-specific roster/policy semantics. Before V1 activation for the exact current voter scope, capability, observation, status, and first transition admission require fresh authenticated replies from every exact voter; an unavailable or incompatible voter fails closed. A quorum is not a mixed-version proof. It does not expose membership, voting, peer discovery, replication-log read/append, raw replication operation trees, snapshots, rebuild/recovery, product composition, or any topology/consensus authority. It also excludes every legacy RemoteSessionBackend API and all consensus/replication/snapshot/rebuild/membership/admin APIs. The server constructor accepts only the existing least-authority SessionQuorumConsumer port, and all accepted mutations route through the durable quorum leader path.

The raw physical store and authenticated-consumer transport advertise exactly AtomicFencedTransitionCapability::V1. A protected EncryptingSessionBackend or RemoteSealingSessionBackend advertises the separate FencedTransitionV2Capability::V2 only when it owns the separately scoped SDK caller-side FencedTransitionV2PreparedJournal and its exact inner physical boundary supports V2 history. That journal binds the stable backend authority, protection mode, and payload namespace; it is distinct from the permanent capped V1 prepared-transition journal. The session-store documentation defines its provision/open/recovery rules. V2 transport neither creates nor substitutes for this durable recovery boundary, and MUST NOT fall back to V1. A legacy protection wrapper without that journal, an older binary, and raw V1 transport MUST fail closed for the V2 history path.

The first authorized transition after that unanimous proof carries the scope identity and canonical voter-set commitment inside its same single user command and application position. Apply atomically installs its receipt/effects, the one-way persistent schema-version downgrade fence, and an optional single-row exact-current-scope activation certificate; it creates no separate user mutation or log position. Those internal proof fields are not consumer-wire semantics. After this command commits, normal linearizable Raft quorum availability suffices for capability, observation, execution, and status; a leader change or minority loss does not require another all-voter probe. A topology cutover deletes only the old scope certificate, retaining the schema fence and receipt bindings. Its successor scope must repeat the every-voter proof and first activating or recovery transition, while stable request-ID/body receipt recovery survives the rollover.

The exact #684 layout has no ledger. A current writable open may add the exact empty Prepared ledger and zero marker without changing the predecessor schema version, so no V1 authority exists and predecessor readers remain safe until activation. Activated main databases, snapshots, and recovery preserve the schema fence and receipt bindings plus any exact-current certificate; snapshot install never regresses Activated to Prepared or erases/substitutes a same-scope certificate. A legitimately unactivated successor scope may have no certificate pending its new proof. Exact predecessor binaries reject the higher schema fence. An offline pre-V1 minority is not safe to catch up merely because it did not acknowledge activation: an old reader could silently omit new snapshot state, which the persistent fence prevents by rejecting the activated image.

Each V2 request connection carries a connection-local, nonzero u32 sequence that increases monotonically and never wraps, paired with a fresh full-width 128-bit OS-CSPRNG nonce and the fixed 32-byte request commitment. The commitment is SHA-256 of opc-session-consumer-v2-call-phase, the big-endian /2 revision 5, and the exact serialized V2 request bytes. The server admits only the exact next sequence and the client accepts only the exact composite response tuple. A V2 lane retires after at most 4,096 sequential calls. There is exactly one in-flight call per connection, with no multiplexing. This is structural: it isolates cancellation and pre-staged/late responses and removes write-position ambiguity.

The /2 HelloAck request-frame ceiling is independent from the client's response-frame capacity: any nonzero request ceiling through the fixed 16 MiB maximum is valid, and the exact bounded encoder MUST reject an oversized Call before writing its length prefix. The response-frame capacity retains the larger fixed minimum required by bounded batch responses.

The client uses a fixed, fair pool of four request connections by default (at most 16 when configured), with 64 pending calls by default and a hard maximum of 256. A pending call may wait or age for at most 250 ms. The retained Watch transport uses two reserved slots by default (at most 16 when configured), but typed tenant/NF consumer Watch does not acquire them while its cursor is global.

PersistentSessionConsumerClient has an ALPN-specific V2 idle pool while V1 and V2 share one configured request width, pending queue, and prewarm gate. prewarm_v2 establishes revision-5 lanes within that aggregate width without dispatching an operation, execute_v2 dispatches only on a V2 lane, and v2_diagnostics reports only V2 redaction-safe pool state. V1 and V2 idle sockets and authenticated Hello exchanges are never cross-reused; their logical lane and pending-call admission is aggregate. Their aggregate persistent width is the configured request width (at most 16), and may rebalance only by retiring an opposite-protocol idle lane (never active work or V2 poison debt) before the existing setup deadline; it does not allocate a connection, task, channel, or pool per subscriber. DNS resolution, TCP, TLS, and Hello are performed only on establishment or re-establishment of the relevant lane, not on reuse.

Stateless-client clones share bounded physical admission per clone lineage. The V1 family, including protected-roster /3, has exactly 16 request connections, and ordinary V2 /2 has an independent 16, for at most 32 request connections in total. Watch admission remains separately capped at 16. The respective permit MUST be acquired before resolve/TCP and held for the complete physical connection lifetime, including by a persistent client derived from that stateless lineage. Independently constructed stateless clients define independent logical clients, as independently constructed persistent clients do. The typed persistent watch surface MUST preserve exhaustion of either bound as Overloaded and record that bounded outcome; it MUST NOT relabel intentional load shedding as endpoint unavailability.

The hard listener limit is 256 live connections and its retained connection-task set is bounded by that limit; each watch owns one delivery task. Consumer frames are at most 16 MiB and a configured listener frame limit cannot be lower than the 8 MiB batch-response limit plus 4 KiB framing allowance. The bootstrap and active-frame idle bound remains 5 seconds and one complete request/response operation remains bounded to 10 seconds. A watch has a 64-item, 512 KiB transport queue, rechecks cancellation at least every 50 ms, and is also bounded by the 256 KiB store-side projection buffer. The fixed V1 request identity is 16 bytes; a V2 singleton or V2 batch item identity is 56 bytes; consumer identity input is capped at 253 UTF-8 bytes. One V1 generic batch has at most 256 operations and retains at most 8 MiB of serialized response data. A V2 batch has at most 256 operations and separately limits its fully Postcard-encoded request vector and outcome vector to 1 MiB each; the outer authenticated-consumer JSON frame remains subject to its negotiated frame bound.

The complete operation timeout MUST validate strictly greater than zero and no greater than 10 seconds. The configured idle timeout is at most 5 seconds and caps every active partial frame on all client bootstrap and unary reads; partial bytes do not reset that bound. Each discarded checked-out request lane MUST have exactly one reconnect/replacement accounting outcome. Under concurrent shutdown callers, phase progression is monotonic from running to draining to forced and MUST NOT regress.

Every client and listener retains the existing finite TLS lifecycle bounds: by default authentication age is at most 15 minutes, connection-retirement drain is at most 30 seconds, and material-rotation jitter is at most 30 seconds. A consumer shutdown drains for at most 5 seconds. An establishment attempt has a 1,500 ms setup limit and a call makes at most two pre-write establishment attempts. Resolution occurs only for establishment or re-establishment, never for a reused connection. Every cold request and rolling-prewarm setup MUST enter one pool-wide recovery lane after bounded physical admission. One failed setup or proven cached-lane loss publishes the shared exponential lifecycle backoff floor (50 ms by default) plus at most 25 ms jitter, clipped to each logical deadline; concurrent waiters MUST NOT start independent resolver/TCP/TLS/Hello attempts. Reauthentication, material changes, certificate expiry, idle retirement, cancellation, malformed frames, EOF, or an uncertain stream position terminate the connection and release its transport task slot; they do not create another request on that connection. "Material changes" here means an accepted material-epoch change. It MUST schedule each already-admitted lane at a stable directed authenticated-edge deadline no later than the configured rotation-jitter maximum; admitted work and reuse remain permitted before that deadline and retire at it. An explicit generation change MUST invalidate cached admission immediately. A fresh client or server handshake MUST exactly match the current generation and material epoch at its final pre-publication sample and MUST NOT use the cooperative jitter exception. TLS MUST expose only the single fixed-domain, fixed-range consumer jitter duration needed by session-net. It MUST NOT expose a comparable edge-key object, caller-selected digest range, identity, or digest bytes to callers or session-net diagnostics. A rejected publication that retains the admitted epoch MUST NOT interrupt an active frame. Each logical request pool MUST use exactly one maintenance task to remove cached lanes autonomously at the earliest idle/lifecycle deadline. Maintenance task/table cardinality MUST NOT scale with lanes, subscribers, or records.

The caller owns the request ID for every mutation or lease operation. Only a failure classified as NotTransmitted may be automatically retried, and then only with the identical request ID and body. Positive ciphertext acceptance MUST be observed below TLS as well as at the framed plaintext writer, so a later outer TLS error cannot relabel an accepted prefix as NotTransmitted. Anything possibly written is OutcomeUnknown: the client evicts that lane and MUST NOT replay the request. The SDK MUST NOT mint a new request ID. Recovery may retry only the identical request body under the retained ID through the durable request binding; reuse of that ID for a different request is a closed conflict. Applications otherwise must perform authoritative readback and apply the existing fencing/idempotency contract.

For persistent V2 calls, NotTransmitted is a pre-write result. A ReadUnavailable result is a post-write read loss for a non-effectful V2 capability, history, or status operation and may be retried as that read. OutcomeUnknown { request_id } is returned when an epoch-fenced V2 transition may have been delivered; the exact caller-owned FencedTransitionV2RequestId and complete body remain the recovery identity. The client MUST discard that lane and MUST NOT mint a successor ID or automatically replay the transition; recovery uses exact V2 status under the same ID/body and the durable V2 journal where protection is present. OutcomeUnknownBatch { request_ids } is returned when a V2 batch may have been delivered; request_ids preserves input order, and the client MUST use each matching ID for exact V2 status recovery rather than blindly replaying any mutation.

FencedTransitionStorageExhausted is a retained, complete-body-bound, deterministic no-effect receipt, returned only after ordinary stale-fence, CAS, and lease admission for an otherwise successful transition. Its SQLite representability check covers requested generation/fence, exact acquire fence and global successors, credential allocation, application/watch sequences, and restore-scan revision. While retained, exact ID/body replay and status return Recorded(Err(StorageExhausted)); another body conflicts. No lease, record, watch, restore, or ordinary mutation effect occurs. Existing fenced receipts, generic-ID conflicts, HistoryFull, and RetentionExhausted precede this decision, and revoked authority masks storage state. At maximum application sequence it binds with the current nonzero sequence/digest and advances only logical time, the applied pointer, and the receipt; later blank or membership entries may still apply, without promising normal mutations remain available.

For a fenced transition the public consumer request ID MUST be byte-identical to its nested FencedTransitionRequestId. The quorum adapter namespaces the internal receipt ID by authenticated consumer identity and stable cluster identity, enforces the exact current cluster/configuration/epoch scope under the activation lifecycle above, and submits no separate BindConsumerRequest or binding log entry. The transition receipt therefore binds the complete canonical body in the same single consensus entry as the lease and record effect. A changing authorized configuration scope does not change that internal receipt ID, so an authorized successor can recover the same retained result; a revoked predecessor cannot observe it. Status is read-only, and NotFound does not prove that an earlier delayed proposal cannot still commit, does not permit deletion, and does not permit reuse of the stable transition ID.

Each explicit prewarm MUST perform a rolling resolver/TCP/TLS/Hello refresh of every configured request lane and preserve refreshed plus unprocessed healthy capacity after a partial failure. Prewarm and readiness may prove authenticated consumer transport capacity only; they never prove quorum or product readiness. Readiness deliberately becomes false while a request lane is leased; reserved Watch transport slots are non-gating. Diagnostics are fixed and nonidentifying: setup phase, pool wait, active/maximum/idle counts, reuse/reconnect, queue/in-flight/oldest age, and bounded outcome classes. They MUST NOT include endpoints, identities, scope values, credentials, keys, payloads, request or correlation IDs, owners, or fences. Any performance evidence for this transport is synthetic only and makes no ePDG production-SLO claim.

Reauthentication, accepted material rotation, idle/lifecycle retirement, and the bounded public shutdown apply to both ALPN-isolated persistent pools. Shutdown begins both drain paths together so V2 cannot extend the bounded V1 shutdown window. Per-pool maintenance remains constant; no lane-, subscriber-, or record-scaled maintenance state is introduced.

The v7 qualification profile remains the revision-2 persistent-transport inventory and records its connection, frame, request/response, watch, task, and lifecycle limits beside the consensus profile. The published v6 profile remains the unchanged revision-1 contract. General /1 revision 6 retains the bounded transport properties, the revision-3 generic #696 family, and revision-4 exact retained status recovery for ordinary leases. V2 /2 revision 5 retains its fixed full-width attempt tuple, exact request commitment, exact below-TLS write observation, rolling fresh prewarm, and pool-wide cold-setup serialization. Protected-roster /3 remains an isolated revision-5 roster-only lane. The live v8 exact-head schema remains experimental and fixes qualification_complete=false. No profile or evidence records consumer identity or scope material. Synthetic warm accept/reuse checks gate only their transport method; elapsed samples are non-gating and are not an SLO.

V2 is an additive protocol family; it neither changes the V1 revision-6 wire enum nor upgrades a V1 request. Its exact capability, history, execution, and status outcomes remain on the separate ALPN and V2 identity described above.

Revision 5 retains revision 4's StorageExhausted only inside the closed fenced-transition Recorded status result. Frozen session-net v5 maps this outcome fail-closed as an unknown capability; its wire enum and revision remain unchanged. Product and ePDG composition remain outside this generic API.

13. Local Cache

The SDK SHOULD provide a two-level model:

  1. Local in-process cache for hot reads.
  2. Distributed backend for ownership, recovery, and replication.

Cache entries MUST include generation and fence. Stale cache entries MUST NOT be used for authoritative writes. Data-plane lookup snapshots SHOULD be updated through atomic swap or RCU-like mechanisms.

Cache invalidation options:

  • backend watch stream
  • polling by generation
  • explicit publish from owner
  • TTL expiry

NF owners must choose a cache mode per state class.

14. Security

14.1 Encryption

Session payloads MUST be encrypted before storage unless the profile explicitly marks the backend as inside the same cryptographic boundary.

The production HA composition MUST place encryption or remote sealing above consensus:

application -> EncryptingSessionBackend / RemoteSealingSessionBackend
            -> ConsensusSessionStore -> Openraft -> SQLite/snapshots

Protection MUST finish before client_write. Openraft replication, follower apply, replay, durable request-outcome storage, and snapshot build/install MUST therefore receive only opaque RFC 003 envelopes. The consensus engine, network adapter, and deterministic state machine MUST NOT receive plaintext payloads, an HKMS/KMS provider, key material, or a provider key handle. Read-side decryption/unsealing MUST happen only after the consensus read returns through the wrapper, using the envelope key ID for historical-key selection. Provider unavailability MAY block new protection or plaintext reads, but MUST NOT cause provider I/O during deterministic apply or make already sealed Raft replay and quorum formation depend on provider availability.

The following protected-transition rules apply only to #701's protected V2 composition. Its raw physical store and authenticated-consumer transport execute only the V1 fenced_transition protocol. This is distinct from #702's epoch-fenced V2 protocol, which has its separate FencedTransitionV2Capability::V2, FencedTransitionV2PreparedJournal, 56-byte identity, and /2 consumer lane.

For #701 protected transitions, the outer EncryptingSessionBackend or RemoteSealingSessionBackend advertises AtomicFencedTransitionCapability::V2 only when it owns an SDK caller-side durable PreparedFencedTransitionJournal and composes over an exact V1 physical boundary that explicitly witnesses unchanged protected bytes. This #701 journal, rather than a legacy prepared token or application-persisted request state, is the durable recovery authority. The application retains only the caller-stable FencedTransitionRequestId, which it MUST reuse for that same logical operation after restart.

Protected preparation validates the request, capability-gates the exact inner V1 boundary, and checks that the journal has no binding for the ID before effects. It obtains record-expiry preflight before provider work, seals each create/update body exactly once, leaves delete and refresh provider-free, obtains the inner physical token, and durably inserts the complete outer token with create-only semantics before returning success. Execution, status, and recovery reload and authenticate the exact journal token; execution and status may dispatch only its retained physical bytes. They MUST NOT reseal, unseal, read back, reconstruct, or consult the current active key/provider. Missing, incompatible, corrupt, or byte-mismatched journal state fails closed with no provider or transport I/O; execute reports the known-local case as NotTransmitted, while status and recovery return their typed local fail-closed result. Any may-have-sent result remains OutcomeUnknown under the expected request ID. NotFound is non-exclusion: it never permits deletion or reuse of the ID and does not prove a delayed proposal cannot commit.

The consumer physical bridge is only for use underneath such a protected journaled wrapper. It is atomic-subset-only, fails every unrelated SessionBackend operation locally without I/O, and does not implement SessionLeaseManager. Its opaque local marker commits only the authenticated consumer SPIFFE identity and stable cluster ID; it excludes endpoint/address, TLS server identity, leader, configuration ID/epoch, certificate/key, and material epoch. Authorized endpoint, leader, topology, TLS-leaf, server, and provider/key rotation can therefore use the same durable journal path, key, and volume. This does not claim host failover, host/volume-loss recovery, journal replication, or a second consensus transition.

SessionConsumerPreparedFencedTransitionBackend is the public protected V1 prepared fenced-transition facade for that wrapper. Its persistent_exact_voter_prewarm_roster constructor consumes the complete set of persistent clients for one scope, activates and prewarms every V1 physical voter internally, and returns an opaque roster value. Only the facade's local-AEAD and remote-sealing constructors can consume that value; neither a raw physical backend nor a dispatchable voter value leaves the net crate. A partial roster or any non-V1 voter fails activation. The facade rejects an incomplete, duplicate, or differently authenticated/bound roster and canonicalizes admitted voters by node ordinal; caller input order is not authority. For every caller-stable FencedTransitionRequestId, the router derives the same origin from the authenticated scope, canonical roster, and ID. It uses the existing V1 fenced_transition operation only. It is not #702's V2 receipt-history protocol or FencedTransitionV2PreparedJournal, and grants neither activation or readiness-probe authority nor a new quorum, replication, membership, or consensus authority.

ProtectedFencedTransitionBackend is a sealed, methodless marker; it has no SessionBackend supertrait. EncryptingSessionBackend and RemoteSealingSessionBackend implement it around inner types that separately implement SessionBackend. Therefore the router composes directly over the real SessionConsumerFencedTransitionBackend and MUST NOT grant that physical adapter synthetic lease authority. The existing SessionConsumerPreparedCheckpointBackend remains the distinct complete protected-session path for prepared CAS and lease APIs, which require ProtectedSessionBackend and SessionLeaseManager.

Preparation retains the exact outer protected journal token privately in a move-only affine handle. The sealed boundary MUST NOT expose a dispatchable physical prepared token. The router revalidates that exact outer token before every physical mutation and receipt-status attempt and derives only its authenticated-consumer request view; it MUST NOT reconstruct the request or replace the retained token with current provider/key state. Mutation starts at the deterministic origin and visits the canonical voter roster. It MAY advance to another voter only after a proven pre-dispatch NotTransmitted result. Cancellation during the pre-dispatch setup is safe and retryable because no application bytes can cross the transport boundary. After dispatch begins, a possible send, including OutcomeUnknown, or cancellation is ambiguous and MUST permanently make the handle receipt-only; it MUST NOT regain mutation authority. BeforeCallWrite(SessionConsumerClientError::Scope) is a terminal topology authority revocation and MUST be returned as rejected StoreError::TopologyAuthorityRevoked; it MUST NOT be downgraded to a generic NotTransmitted result or trigger successor mutation dispatch.

Receipt status is read-only. A single status call uses the next deterministic canonical successor voter. A status-until-terminal call repeats bounded canonical-roster passes under the single immutable caller absolute deadline, with every physical attempt additionally capped by the prepared physical-attempt budget. It can end with a deadline; NotFound, unavailable, or a per-attempt deadline remains nonterminal while outer budget remains. None is proof that a delayed mutation cannot commit. A terminal receipt is cached locally. Restart recovery returns only the status-only SessionConsumerRecoveredFencedTransitionStatus handle; it deliberately has no execute authority, even if the recovered token is otherwise valid.

Journal provisioning and reopening are distinct. A deployment MUST call PreparedFencedTransitionJournal::create_new exactly once for a missing path and open_existing on every restart. Reopening never creates or initializes a missing, pristine, truncated, reset, or partial database; the deprecated open alias is reopen-only. The independent stable integrity key is unique to that exact journal path/storage boundary, MUST NOT be reused for another journal, and MUST NOT be logged. On Unix the SDK descriptor-walks and retains the full path chain, requires a private effective-user-owned 0700 parent and regular single-link 0600 file, and revalidates the path, file, and SQLite main-file movement state around each operation. The deployment MUST give that effective user exclusive writer authority over the durable path; an actor with equivalent same-user replacement authority is part of the trusted storage boundary. The containing directory MUST reserve the database leaf and its SQLite sidecar names exclusively for this journal, on a local filesystem with truthful POSIX locking, fsync, directory-sync, and storage-barrier semantics; NFS-like mounts are unsupported. Within one process, callers MUST clone the admitted SDK journal handle instead of reopening the same inode or opening it directly through SQLite. The SDK enforces one live SQLite connection per admitted inode so its pre-open header check cannot release another connection's process-scoped POSIX locks. Platforms without those checks fail closed for the #701 protected V2 composition.

The #701 journal uses zeroize-on-drop HMAC-SHA-256 state, SQLite WAL, a fixed pre-open page/cache-header profile, synchronous=EXTRA, bounded SQLite limits and catalog/membership scans, and bounded opaque rows containing no plaintext. A full authenticated journal rejects a new ID before expiry, provider, or inner-prepare work. Payloads, identities, request IDs, paths, keys, provider material, token bytes, and journal contents MUST NOT appear in examples, fixtures, logs, diagnostics, or evidence. The #701 prepared-token and PreparedFencedTransitionJournal schemas are downgrade fences: unknown versions, raw V1, and older binaries MUST NOT operate this protected journaled path. #702's separate FencedTransitionV2Capability::V2 and FencedTransitionV2PreparedJournal do not upgrade, replace, or share the #701 protocol or journal. The #701 journal layer makes no journal GC, retention, ledger-lifetime, or capacity-lifecycle claim.

The schema-3 journal also commits a fresh per-journal incarnation, bounded membership count, and root over the exact retained request-ID/tag set with a separate HMAC. Health, lookup/recovery, and insertion verify that complete small bounded set; lookup authenticates the selected token, and insertion verifies its new row and updates the membership commitment in its single transaction before commit. A fixed covering index keeps the proof independent of token size. That authenticated index is the presence authority; the bounded proof cross-validates every index rowid, request ID, and fixed tag against the table, and compares independently bounded table and primary-index scans. The schema stores the fixed tag before the potentially overflowing body, so the global proof never reads retained bodies. A selected row then validates its body against the authenticated tag. Divergent table, primary, or secondary-index state therefore fails closed instead of becoming absence. A finite VDBE-work budget applies before schema initialization and to every journal operation, in addition to bounded SQL, catalog, and membership limits. The SQLite catalog is an exact whitelist of the SDK tables, generated primary-key autoindex, and membership index, rejecting every other object, including reserved-prefix catalog entries, before setup. This detects offline row deletion, addition, primary-key replacement, index divergence, and tag corruption inside the same durable file. A corrupt selected body fails its exact row authentication and cannot be treated as absent or rebound. It cannot detect restoration of an older complete valid database snapshot: that rollback is outside the same-durable-file guarantee unless deployment provides an external monotonic anti-rollback anchor.

The token wire form begins with a fixed magic, schema version, and body length; version dispatch precedes decoding the frozen V1 body. A golden compatibility corpus pins both lease forms, every mutation, no-expiry and finite-expiry record shapes, and every supported local/remote/consensus protection-stack shape and order. SDK-owned canonical and complete-body scratch allocations wipe on drop. External serialization and persistence buffers remain the caller's responsibility and inherit the same prohibition on diagnostic or metric emission.

Remote unseal MUST pass the canonical envelope key ID through the provider boundary after validating envelope shape and record AAD. The active remote key is an atomic process-local material epoch used only by future seals; a seal already in flight keeps its selected key ID, but may still fail because of timeout, provider outage, or revocation. Mixed-epoch fleet writes remain readable while KMS retains each envelope's exact key. Missing, revoked, malformed, cross-tenant, and wrong-AAD inputs fail with coarse, redacted crypto errors. Provider, endpoint, tenant, key ID, and payload text MUST NOT be included in those errors.

KMS/HKMS owns remote historical-key retention and retirement. The SDK keeps no local historical material or authorization cache and exposes no retirement API or enforcement gate. It supplies exact key selection and bounded live-state scan inputs, not a rewrap campaign or complete dependency proof. Operators MUST combine a snapshot-bound, write-fenced live-state scan after rewrap with separate compaction, expiry, or inspection of logs and snapshots, plus inspection and rewrap/deletion/retention decisions for backups, restore sources, and rollback checkpoints. A restore scan alone does not prove those retained artifacts. Incomplete/stale evidence or an unbounded record blocks retirement.

EnvelopeV1 MUST be validated rather than trusted as a marker. Construction, wire decode, durable-row decode, log append, replay, and snapshot validation MUST reject a malformed or non-canonical RFC 003 envelope, mismatched embedded key ID, invalid algorithm nonce/tag shape, non-session AAD, or mismatch between the AAD's visible tenant/NF/state/generation/fence fields and the record. Consensus admission of a SQLite file MUST atomically fence all standalone backend operations through retained or newly opened handles; only internal state-machine apply and barrier-gated committed reads may bypass that fence.

AAD MUST include:

  • tenant
  • NF kind
  • session key digest
  • state type
  • generation
  • fence
  • backend namespace

The bounded iterative transformation in §11.2.1 is mandatory for replication wrappers. Protecting only the root or one Batch level is not conformant. The envelope protects payload bytes, not the complete SQLite database. Raft and SQLite metadata—including membership, terms/indexes, tenant and key routing, owners, generations, fences, timestamps, request identities, and envelope key IDs—remains visible to the host storage boundary. A deployment requiring metadata or full-file encryption MUST add and qualify an approved database or volume layer without moving provider access below the wrapper. Scoped three-node in-process Openraft evidence uses actual file-backed nodes and controllable RPC, explicitly forces snapshot installation, shuts down/restarts, and restores both key epochs while counters prove replication, replay, quorum formation, and snapshots perform no provider calls. Production KMS framing is tested separately. This does not qualify multi-process or deployed-network behavior; distributed protection/failure/soak evidence (#143) remains a separate production-profile gate.

14.2 Integrity

AEAD integrity is required. Additional MAC fields MAY be used for backends that need independent integrity checks, but they do not replace AEAD.

14.3 Privacy

Logs and metrics MUST NOT expose raw subscriber identifiers. The SDK SHOULD use stable keyed digests for correlation when needed.

14.4 Transport Credential Rotation

Session TTL is application-state lifetime and MUST NOT be used as a certificate lifetime, trust-bundle lifetime, or maximum-authentication-age policy. A production networked session-store profile MUST rotate workload certificates and trust bundles without interrupting service, use short-lived SVID expiry as the bounded same-issuer credential-compromise/revocation response, and document a maximum authentication age on long-lived connections. #161 atomic reload, #162 coherent material epochs, and #163 finite connection retirement/reauthentication are implemented. On epoch change or an explicit orchestration request, both sides MUST stop new admission, end the transport wait and release connection slots within the finite hard deadline, and repeat the full mutual-TLS and application handshake on replacements. Already-admitted supervised mutations retain the ambiguity/readback contract above if their bounded backend work finishes later.

Rotation and reauthentication move cooperative participants but do not revoke the old certificate/key. Its holder can establish a fresh connection until the earliest expiry across every certificate in that presented chain while its issuer remains trusted. Immediate generic CRL, OCSP, certificate/identity-denylist, and other selective same-issuer revocation are not implemented. Root removal is a trust-anchor cutover for all chains that depend on it, not an expiry deadline or selective revocation.

The projected source's ongoing expiry monitor clears retained source material at leaf expiry. It is not the authority for an earlier intermediate expiry. TlsMaterialController MUST pre-scan every configured SVID-chain certificate, mark material unavailable at the earliest effective chain expiry, and provide the TLS readiness status. A production projected source MUST be paired through TlsMaterialController::new_from_projected_source or new_pinned_from_projected_source; direct subscription through a generic controller constructor does not bind source failures and controller gauges to one recorder. Source Ready alone MUST NOT satisfy this section.

An operator MUST publish overlapping old/new trust before new leaves, preserve the exact stable SPIFFE and consensus scope, trigger reauthentication, and verify that every directed peer path has authenticated on current material before removing old trust. Removing that anchor cuts over later handshakes; trigger reauthentication and prove that every chain depending on it is rejected. The negative proof MUST remain visible to authentication/trust alerting and MUST be accepted only when an immediate checkpoint proves the exact qualified per-member delta with no concurrent increase, process reset, or alert silence. Rollback before old-trust removal restores the prior leaf/material publication and triggers another monotonic reauthentication generation; rollback after removal MUST first restore overlapping trust and prove the controller status is Ready, then restore the old leaf and trigger reauthentication. A rollback MUST NOT reuse an old authenticated connection as evidence. Its deadline MUST be derived from the exact fleet size and all bounded two-pass operations, and the selected complete rollback material MUST be revalidated against the deadline remaining immediately before every publication. Evidence MUST bind one invocation, live-lease binding, monotonic operation/nonce, exact member/checkpoint, phase/step, and fresh timestamp; it MUST NOT contain the lease token. Serving withdrawal MUST remain executable when evidence storage is unavailable. Reconnect-storm, deployed root cutover, real partition/restart including active-mutator crash/restart, broader fault behavior, deployed mixed traffic/watch/restore, resource/soak, remote-HKMS, deployed-CNF, signed release, and wider distributed production evidence remain #164/#143 under umbrella #158. The single-host tests described in §12.3 cover bounded mixed traffic only through their exact synthetic fault/expiry slice and make no evidence-schema/profile claim. They do not change Openraft's sole commit authority, payload encryption, AAD, key-provider/HKMS placement, durable formats, or encryption-at-rest responsibilities.

15. Observability

Required metrics:

  • opc_session_store_ops_total{op,state_class,outcome}
  • opc_session_store_latency_seconds{op,state_class}
  • opc_session_store_cas_conflicts_total{state_class}
  • opc_session_store_stale_fence_total{state_class}
  • opc_session_lease_acquire_total{outcome}
  • opc_session_lease_renew_total{outcome}
  • opc_session_lease_lost_total{reason}
  • opc_session_replication_lag_seconds{region}
  • opc_session_cache_hit_ratio{state_class}
  • opc_session_record_bytes{state_type}
  • opc_session_restore_pages_total{outcome,cursor_profile,complete}
  • opc_session_restore_page_records{cursor_profile}
  • opc_session_restore_page_examined{cursor_profile}
  • opc_session_restore_page_payload_bytes{cursor_profile}
  • opc_session_net_connection_retirements_total{reason}
  • opc_session_net_connection_lifecycle{state}
  • opc_session_net_connection_drain_events_total{event}
  • opc_session_net_connection_attempts_total{outcome}
  • opc_session_net_reconnect_events_total{outcome}
  • opc_session_net_watch_slow_consumers_total

The lifecycle metric labels MUST come only from their closed SDK-owned reason, state, event, and outcome enums. They MUST NOT contain endpoints, DNS names, SPIFFE IDs, certificates, key material, transaction IDs, record keys, or payload/backend text. The closed connection-retirement reason set includes idle_timeout; it MUST NOT be counted as the timeout connection-attempt failure. Resolver, TCP, TLS, bootstrap, and partial active-frame deadlines remain timeout failures. When the transport observes a newer material or explicit-reauthentication epoch, it MUST terminate the old attempt as superseded. When an attempt guard is dropped before any explicit terminal classification, it MUST use abandoned rather than infer timeout or supersession. Both outcomes participate in the quiescent started = terminal + outstanding accounting invariant but MUST NOT be treated as peer timeouts. Exporters MAY expose transient skew between the separate relaxed counters while connection handlers are changing state.

  • opc_session_restore_page_latency_seconds{cursor_profile}
  • opc_session_restore_restarts_total{reason} where reason is one of stale_cursor, work_budget, response_too_large, or cancelled

Restore metric labels MUST NOT include cursor bytes, key fields, tenant, owner, payload, peer-controlled text, paths, or certificate identity. A product MAY expose these metrics through its existing metrics facade; #133 does not add a second registry or metrics authority.

Required logs for state transitions:

  • session_key_digest
  • tenant
  • state_class
  • generation
  • fence
  • owner
  • handover_tx_id, when applicable
  • outcome

Raw subscriber identifiers MUST be redacted.

16. Module Ownership

ModuleResponsibility
opc-session-modelKeys, record headers, generations, state classes
opc-session-backendBackend trait and capability model
opc-session-leaseLease manager and fencing rules
opc-session-cacheLocal cache and snapshot publication
opc-session-codecSession serialization and migrations
opc-session-cryptoPayload envelope integration with RFC 003
opc-session-replicationRegion log and apply rules
opc-handoverGeneric handover storage state machine
opc-session-testkitFake backend, split-brain tests, stale fence tests
opc-consensusThe workspace's single Openraft import, identity, bounded codec, and consensus transport contracts
opc-session-store::consensusOpenraft adapter, deterministic session state machine, SQLite log/state/snapshot storage, and linearizable readiness
opc-session-net consensus profileMutual-TLS consensus-only peer transport; no direct backend mutation or rebuild authority

Agents implementing backends must not modify NF-specific handover logic. Agents implementing handover logic must use the public lease/CAS APIs and not bypass fencing.

17. Testing Requirements

17.1 Unit Tests

  • Session key tenant separation.
  • CAS success and conflict.
  • Stale fence rejection.
  • Lease acquire/renew/release.
  • TTL refresh with valid and stale fences.
  • TTL zero, the exact 365-day maximum, maximum plus one, and Duration::MAX across direct, batch, replicated, persisted, and authenticated-wire paths; rejected values must have no partial effect.
  • Serialization corrupt input rejection.
  • Protocol-v5 golden frames with no target-width integer fields; checked fixed-width maximum/overflow conversion; exact collection limits; omitted restore fields recomputed; and size errors nested in batch results.
  • Revision-2 negotiation with equal and unequal client/server limits, rejection below MIN_NEGOTIATED_FRAME_SIZE (8,192 bytes), the restore-minimum alias, executable conservative maximum-payload round trips, and fail-closed revision-1/revision-2 profile mismatch.
  • Exact-limit and one-byte-over outbound encoding for every response/watch family; no oversized allocation or emitted prefix on rejection; non-truncated record/batch behavior; contiguous log and cursor-correct restore pages; fixed fallback redaction; and iterative consuming rejection of nested trees.
  • One absolute write deadline for prefix/payload/flush; authenticated slow-reader reaping; handler/connection-slot return to baseline; repeated reconnect bounds on memory/tasks/file descriptors/CPU; and deterministic shutdown/abort while response serialization or socket writes are blocked.
  • Exact-v5 handshake success plus older ALPN/version, profile, authentication, malformed acknowledgement, and replay rejection before backend dispatch; incompatible peers clear cached capabilities to all false/zero.
  • Exact 1-byte and 128-byte owner/custom-key acceptance, empty and 129-byte rejection, canonical reserved-name handling, string ordering, and hostile Serde/session-net decode rejection without raw-value disclosure.
  • Exact stable-ID 1/64-byte and replication-transaction-ID 1/128-UTF-8-byte acceptance/rejection, plus canonical lowercase hyphenated 36-byte CAS UUID admission across requests, responses, batches, nested replication carriers, log pages, and watch items.
  • Valid legacy SQLite hydration; hostile owner/key types in records, active leases, key fences, and nested replication logs; no-effect rejection; and the bounded count-only audit's budgets, status/exit codes, and redaction.
  • Versioned and bounded/current-valid original handover-envelope round trips; exact non-OPCH classifier cases (including ambiguous bare rejection); and malformed, zero-length, truncated, oversized, and typed-invalid rejection before mutation.
  • AEAD AAD mismatch rejection.
  • Nested replicated CAS protection at depths 1 through 16, rejection at depth 17, exact 256-node acceptance and 257-node rejection, and fieldless errors.
  • Replicate/rebuild/log/watch round trips through encryption and remote-sealing wrappers, including late-provider failure with no backend delegation or partial entry/page exposure.
  • Cache generation checks.

17.2 Integration Tests

  • Two owners racing for the same session.
  • Owner pause beyond TTL, new owner writes, old owner resumes and is rejected.
  • Handover prepare/activate/abort idempotency.
  • Backend restart with leases recovered or invalidated according to profile.
  • Geo-replication applies newer generation and rejects older generation.
  • Cache invalidation after remote update.
  • Coordinated v5 multi-replica admission and fresh-readiness behavior, including fail-closed mixed-profile peers and non-authoritative cached capabilities.
  • Ambiguous mutation outcomes under response rejection/write timeout, proving callers recover through idempotency, fencing, and authoritative re-read rather than assuming rollback or blindly replaying the operation.
  • A real Openraft proposal that commits before its forwarded result is delayed beyond the caller deadline returns typed ambiguity and produces exactly one durable application-journal event.
  • Real SQLite external write-lock contention and async-future cancellation are bounded, retain at most one supervised worker, release it after interruption, and never classify a started mutation as safely retryable.
  • Concurrent pristine three-node formation and mutation submission with one gap-free committed application journal on every replica.
  • A one-node partition produces bounded readiness/write failure, then heals and rejoins without admitting a second authority path.
  • Cross-node lease/CAS visibility and follower linearizable reads use the same Openraft barrier as probe_durable_readiness.
  • Plaintext canaries written through the encryption wrapper are absent from SQLite database/WAL/SHM files, Raft logs and outcomes, captured consensus frames, and snapshots; restart and active-key rotation retain decryptability.
  • Non-ignored three- and five-process single-host projected-mTLS cases combine one stable follower's test-only consensus-RPC admission loss with a different member's malformed-trust retained-last-good state, then prove survivor readiness/encrypted-canary progress, exact-address restart/catch-up, and repair. They separately drive a same-issuer leaf with a 75-second remaining-validity/expiry budget through its fixed 30-second soft-retirement window, hard drain, LastGoodExpired, survivor progress, and same-process valid replacement. Replacement advances only the recovered member's explicit reauthentication generation, proves fresh bidirectional mTLS/bootstrap paths on every incident edge, leaves unrelated survivor explicit/material-epoch retirement counters unchanged, and settles all lifecycle drains plus survivor availability episodes before the next traffic baseline. The schedule-bound member-scoped-reauth-settled-baseline/v4 checkpoint starts its 86-second absolute bound and 60-second two-stage server tail at the atomic projected-data rename, then requires a final 2.5-second outbound-ledger quiet tail. A prepublication common-key pulse and conservative 13-second observations require one active key to advance on every survivor observer and bound that pulse's worst-case actual event gap to 26 seconds. An independent 26-second checkpoint requires every active key on every observer and cannot be reset by a faster key. Each survivor may record at most one availability episode while the expired member rejoins. Consecutive typed retry outcomes inside that episode remain separately bounded by the unchanged eight-outcome ceiling; all must settle inside the 26-second SLO, and a second or late episode fails closed. Fault-era new-attempt and reconnect deltas retain a fixed 85/161 per-node bound: ordinary 24/40, fifteen five-second refresh rounds over four/eight incident paths, and one scheduled post-hard-expiry survivor-to-expired network-negative attempt per involved node. The reverse probe fails local material preflight without dialing. Terminal outcomes may additionally include only the exact attempts already outstanding at the baseline and must satisfy interval conservation; Schedule v6 binds new-attempts-plus-baseline-outstanding/v1 and common-key-pulse-all-active-key-coverage/v1. Cancellation-classified abandoned outcomes, protocol/backend outcomes, and drain overruns remain forbidden; the clean scoped-reauthentication interval retains a zero-failure budget. Continuity polling is a non-intrusive workload snapshot; authoritative final watch-head settlement still performs the fail-closed replication-head read.

Run those two exact cases serially:

cargo test --locked -p opc-session-testkit --test qualification_mtls_multiprocess --no-default-features three_process_projected_mtls_unavailable_malformed_and_expiry_recovery -- --exact --test-threads=1
cargo test --locked -p opc-session-testkit --test qualification_mtls_multiprocess --no-default-features five_process_projected_mtls_unavailable_malformed_and_expiry_recovery -- --exact --test-threads=1

They are synthetic regression evidence, not a deployed network partition or a production qualification. The v2 stage correction does not relax Openraft's sole commit authority or change HKMS/provider placement, payload encryption, AAD, SQLite/Openraft durable formats, or encryption-at-rest responsibilities. The bounded lease/CAS/read, watch, restore-scan, readiness, and connection-recycling workload remains active throughout both cases, and a restarted watcher reconciles the exact committed journal prefix before resubscription.

17.3 Fault Injection

  • Backend timeout.
  • Partial batch failure.
  • Redis/Aerospike failover.
  • Clock skew.
  • Network partition between owners and backend.
  • Replication lag spike.
  • Corrupt encrypted payload.
  • Missing session key decryption key.

17.4 Performance Gates

Profiles must state which backend they apply to. Minimum SDK reference gates:

  • Local cache read p99 under 50 microseconds.
  • In-memory fenced CAS p99 under 100 microseconds.
  • Backend adapter exposes measured p50/p99 for get, CAS, lease acquire, and renew.
  • 100,000 updates/second per replica for in-memory or batched local profile.
  • No packet fast-path benchmark depends on remote backend availability.

18. Acceptance Criteria

This RFC is implemented when:

  1. Authoritative session writes require monotonic fencing and CAS.
  2. Stale owners cannot overwrite newer session state after lease expiry.
  3. Handover state transitions are idempotent and recoverable.
  4. Geo-replication does not use wall-clock LWW for authoritative state.
  5. Backend capabilities are declared and enforced by profile.
  6. Session payloads are encrypted and tenant-bound.
  7. Local cache supports fast reads without compromising write correctness.
  8. Fault injection covers split-brain, failover, replication lag, and stale fences.
  9. Every Duration-based TTL boundary accepts zero and the exact 365-day maximum, rejects larger values with the appropriate typed error before application/backend effects, and performs exact checked deadline arithmetic without unwinding.
  10. Every replication operation tree is iteratively bounded to depth 16 and 256 total nodes; every nested CAS is protected on write and unprotected on read; and transformation failure cannot delegate or expose a partial entry/prefix/page.
  11. Owner IDs and custom session-key types have structural 1-through-128-byte invariants at every model, persistence, and transport decode boundary; legacy SQLite admission is bounded, count-only, read-only, and fail-closed; and invalid state is never silently rewritten.
  12. ConsensusSessionStore is the only quorum-profile authority, all election, voting, log matching, commitment, membership, snapshots, and linearizable reads use the shared Openraft engine, and raw append/rebuild/lease sequencing cannot bypass it.
  13. Durable readiness executes an Openraft linearizable barrier and waits for local committed apply; listener bind, TLS success, capabilities, local SQLite availability, and restore method availability cannot report ready.
  14. The encryption/remote-sealing wrapper runs above consensus, plaintext and provider/key handles never enter Raft apply/log/snapshot transport, and the documented payload-envelope versus full-database boundary is qualified.
  15. Divergence recovery (#128), operator-safe legacy-fork recovery (#129), bounded applied-state restore (#133), and finite connection reauthentication (#163) are implemented; distributed production qualification (#143) and fleet credential-rotation evidence (#164) under umbrella #158 have passed their own acceptance gates before a production claim (#161/#162 are implemented prerequisites).

OPC-SDK-RFC-005: Zero-Copy Protocol Framework

Status: Draft for Implementation
Version: 2.0.0
Date: 2026-05-19
Audience: SDK implementers, protocol crate authors, fuzzing engineers, NF teams

1. Abstract

This RFC defines the protocol codec framework for OpenPacketCore. It covers zero-copy parsing, encoding, lifetime discipline, allocation budgets, parser security, fuzzing, conformance tags, and implementation layout for 3GPP and IETF protocol crates.

The initial draft correctly required nom, bytes, fuzzing, and exact spec citations. It was incomplete in two areas: the codec trait did not express borrowed lifetimes safely, and the round-trip property was too simplistic for protocols with canonical encodings, unknown fields, padding, or lossy normalization. This version corrects those issues.

2. Scope

2.1 In Scope

  • Binary protocol parsing and encoding.
  • Borrowed zero-copy PDU views.
  • Owned conversion for async and cross-thread use.
  • Length, bounds, recursion, and integer safety.
  • Fuzzing, property tests, and corpus management.
  • Spec traceability for RFC 006.
  • Protocol crate layout and module boundaries.

2.2 Out of Scope

  • Management config projection. See RFC 002.
  • Session persistence. See RFC 004.
  • Full NF procedure state machines.
  • Kernel bypass packet I/O frameworks, except for buffer ownership contracts.

3. Design Goals

3.1 Security

  • No out-of-bounds reads or writes.
  • No panics on untrusted input.
  • No unbounded recursion, loops, allocation, or CPU use from hostile packets.
  • Constant-time comparison for secrets, MACs, authentication tags, and keys.
  • Strict validation of length fields, IE cardinality, duplicate handling, and unknown critical elements.

3.2 Performance

  • Parse common fast-path headers without heap allocation.
  • Avoid copying payloads where a borrowed view is sufficient.
  • Encode into caller-provided buffers with exact or bounded capacity planning.
  • Support partial decode when only routing keys are needed.
  • Provide per-protocol allocation and latency budgets.

3.3 Maintainability

  • Each protocol crate uses the same module layout.
  • Every message and field cites the exact spec section/table.
  • Parser errors are structured and stable.
  • Unsafe code is forbidden by default.
  • Generated tables are separated from hand-written parser logic.

3.4 Functionality

  • Support borrowed and owned message representations.
  • Support streaming/incomplete input where protocols require reassembly.
  • Support extension headers and unknown IE preservation when required.
  • Support canonical encoding and raw-preserving encoding modes.

4. Parsing Model

4.1 Borrowed Views

Protocol decoders SHOULD return borrowed views over the input buffer:

#![allow(unused)]
fn main() {
pub struct GtpHeader<'a> {
    pub flags: u8,
    pub msg_type: u8,
    pub length: u16,
    pub teid: u32,
    pub payload: &'a [u8],
}
}

Borrowed views MUST NOT outlive the input buffer. They MUST NOT store pointers into mutable buffers that can be changed while the view exists.

4.2 Owned Messages

Every borrowed PDU that may cross an async boundary, thread boundary, queue, or long-lived store MUST provide an owned conversion:

#![allow(unused)]
fn main() {
pub trait ToOwnedPdu {
    type Owned;
    fn to_owned_pdu(&self) -> Self::Owned;
}
}

Owned PDUs MAY use bytes::Bytes to retain cheap shared ownership of the original packet.

4.3 No Self-Referential Types

Generated or hand-written protocol structs MUST NOT be self-referential. If a message needs both raw bytes and parsed fields, use either:

  • borrowed view tied to external input lifetime, or
  • owned Bytes plus offsets validated at construction.

5. Codec Traits

The SDK defines separate traits for borrowed decode, owned decode, and encode.

#![allow(unused)]
fn main() {
pub type DecodeResult<'a, T> = Result<(&'a [u8], T), DecodeError>;

pub trait BorrowDecode<'a>: Sized {
    fn decode(input: &'a [u8], ctx: DecodeContext) -> DecodeResult<'a, Self>;
}

pub trait OwnedDecode: Sized {
    fn decode_owned(input: bytes::Bytes, ctx: DecodeContext) -> Result<Self, DecodeError>;
}

pub trait Encode {
    fn encode(&self, dst: &mut bytes::BytesMut, ctx: EncodeContext) -> Result<(), EncodeError>;
    fn wire_len(&self, ctx: EncodeContext) -> Result<usize, EncodeError>;
}
}

This avoids pretending that a borrowed PDU can be represented by a lifetime-free Self.

5.1 Decode Context

#![allow(unused)]
fn main() {
pub struct DecodeContext {
    pub protocol_version: ProtocolVersion,
    pub max_depth: usize,
    pub max_ies: usize,
    pub max_message_len: usize,
    pub unknown_ie_policy: UnknownIePolicy,
    pub duplicate_ie_policy: DuplicateIePolicy,
    pub validation_level: ValidationLevel,
}
}

Protocol crates MUST define safe defaults.

5.2 Error Model

#![allow(unused)]
fn main() {
pub struct DecodeError {
    pub code: DecodeErrorCode,
    pub offset: usize,
    pub spec_ref: Option<SpecRef>,
}
}

Errors MUST be safe to expose in logs. They MUST NOT include raw packet payload unless debug packet capture is explicitly enabled.

6. nom Usage

nom is the default parser combinator framework for binary TLV, bitfield, and header-oriented protocols.

Rules:

  • Use nom::number::complete or nom::number::streaming deliberately.
  • Map nom::Err::Incomplete to a structured incomplete-input error.
  • Do not discard remaining input unless the message definition allows trailing padding.
  • Wrap nom errors at module boundaries; do not expose combinator internals in public API.
  • Prefer small named parser functions over deeply nested combinator expressions.

Protocols based on ASN.1 PER, JSON, HTTP/2, or other specialized encodings MAY use proven dedicated parsers instead of nom, but they must implement the same SDK codec, error, fuzzing, and evidence contracts.

7. Buffer Management

Encoders MUST use bytes::BytesMut or bytes::BufMut.

Encoding rules:

  • wire_len MUST use checked arithmetic.
  • encode MUST fail before writing if required capacity exceeds configured maximum.
  • Encoders SHOULD reserve exact capacity when cheap to compute.
  • Encoders MUST produce canonical output unless raw-preserving mode is selected.
  • Partial writes on error SHOULD be avoided. If unavoidable, document the behavior and do not reuse the buffer without caller awareness.

8. Allocation Budgets

Each protocol crate MUST define an allocation profile:

#![allow(unused)]
fn main() {
pub struct AllocationBudget {
    pub decode_heap_allocations_fast_path: usize,
    pub decode_max_temporary_bytes: usize,
    pub encode_max_temporary_bytes: usize,
}
}

Default fast-path target:

  • Fixed header decode: 0 heap allocations.
  • Routing-key partial decode: 0 heap allocations.
  • Full message decode: protocol-specific, bounded.

Variable IE lists SHOULD use:

  • iterators over borrowed IE views,
  • smallvec for small bounded lists,
  • caller-provided scratch buffers, or
  • validated owned vectors when required.

9. Security Invariants

9.1 Length and Offset Safety

All length calculations MUST use checked arithmetic. Parsers MUST verify:

  • field length is within remaining input,
  • nested IE length does not exceed parent length,
  • padding length is valid,
  • extension header chains terminate,
  • total parsed elements do not exceed max_ies,
  • recursion or nesting does not exceed max_depth.

9.2 Integer Safety

All offset, length, and capacity calculations MUST use:

  • checked_add
  • checked_sub
  • checked_mul
  • usize::try_from

Integer truncation with as is forbidden in parser and encoder length paths.

9.3 Constant-Time Operations

Constant-time comparison is REQUIRED for:

  • MACs
  • authentication tags
  • keys
  • nonces when secrecy or oracle behavior matters
  • authentication tokens

Checksums over public packet data do not require constant-time comparison, but checksum parsing must still be bounds-safe and panic-free.

9.4 Denial of Service Controls

Every decoder MUST enforce:

  • maximum message length,
  • maximum IE count,
  • maximum nesting depth,
  • maximum extension chain length,
  • maximum decompressed length if compression exists,
  • maximum parse time indirectly through bounded loops.

Protocol crates MUST expose these limits through profile configuration.

10. Validation Levels

The decoder supports levels:

#![allow(unused)]
fn main() {
pub enum ValidationLevel {
    HeaderOnly,
    Structural,
    Strict,
    ProcedureAware,
}
}
  • HeaderOnly: parse enough for routing.
  • Structural: verify lengths and container structure.
  • Strict: enforce field cardinality, enum ranges, and critical IE rules.
  • ProcedureAware: call NF-specific semantic validators.

Data-plane fast paths SHOULD use the minimum level needed for safe routing and leave expensive semantic validation to control-plane paths where appropriate.

11. Unknown and Duplicate Elements

Protocol crates MUST define:

  • Unknown IE behavior.
  • Duplicate IE behavior.
  • Critical/mandatory IE behavior.
  • Extension preservation behavior.

If a protocol requires preserving unknown elements for forwarding or round-trip, the borrowed view MUST expose raw slices and owned conversion MUST retain them.

12. Round-Trip Properties

The simplistic property encode(decode(input)) == input is not universally valid. The SDK requires three properties:

12.1 Canonical Round Trip

For generated valid model values:

decode(encode(model)) == model

12.2 Raw-Preserving Round Trip

For accepted inputs where unknown/padding preservation is enabled:

encode_raw_preserving(decode_raw_preserving(input)) == input

12.3 Reject Stability

For rejected inputs, the decoder returns a structured error and never panics, hangs, or allocates beyond budget.

13. Fuzzing

Every protocol crate MUST include fuzz targets for:

  • full decode,
  • header-only decode,
  • encode after generated model mutation,
  • round-trip properties,
  • length and extension chains,
  • security fields where applicable.

Fuzz gates SHOULD be time and coverage based, not only iteration-count based. Minimum admission gate:

  • 30 minutes sanitizer-enabled fuzzing per new parser target in CI or nightly.
  • 1,000,000 generated cases for property tests where practical.
  • All crashes minimized and committed as regression tests.

Required sanitizers where supported:

  • AddressSanitizer for native dependencies.
  • UndefinedBehaviorSanitizer for C/C++ parser dependencies.
  • Miri for unsafe Rust, if any unsafe exception is approved.

14. Spec Traceability

Every public PDU, IE, field enum, and procedure-relevant constant MUST cite:

  • standards body,
  • document number,
  • release or revision where applicable,
  • section,
  • table or figure where applicable,
  • conformance status.

Example:

#![allow(unused)]
fn main() {
/// @3gpp TS 29.281 Release 18, Section 5.1, Table 5.1-1
/// @conformance full
pub struct Gtpv1uHeader<'a> { ... }
}

These tags feed RFC 006 evidence extraction.

15. Protocol Crate Layout

Each protocol crate MUST use:

crates/opc-proto-<name>/
  src/
    lib.rs
    error.rs
    context.rs
    header.rs
    ie.rs
    message.rs
    parser.rs
    encode.rs
    validate.rs
    spec.rs
    generated/
      tables.rs
  tests/
    corpus.rs
    roundtrip.rs
    conformance.rs
  fuzz/
    fuzz_targets/
      decode.rs
      header.rs
      roundtrip.rs

For protocols without IEs, ie.rs may be omitted. Generated tables MUST live under generated/ and be reproducible.

16. Implementation Contracts

Contributors implementing protocol crates MUST follow these rules:

  • Start from spec.rs constants and conformance tags.
  • Implement error.rs and context.rs before parser logic.
  • Implement header parsing before full message parsing.
  • Add fuzz target with the first parser.
  • Do not add unsafe.
  • Do not use unwrap, expect, or indexing on untrusted input.
  • Keep parser functions small and named after spec structures.
  • Add one regression test per newly handled malformed input class.

Agents may work independently on:

  • header parser,
  • IE parser,
  • encoder,
  • validation,
  • fuzz/test corpus,
  • generated spec tables.

17. Testing Requirements

17.1 Unit Tests

  • Minimum and maximum length messages.
  • Truncated input at every byte position for fixed headers.
  • Invalid enum values.
  • Duplicate IE policies.
  • Unknown IE policies.
  • Extension header chain termination.
  • Checked arithmetic overflow cases.

17.2 Integration Tests

  • Decode real capture fixtures.
  • Encode/decode canonical known-good messages.
  • Partial decode for routing keys.
  • Owned conversion across async boundary.
  • Protocol-specific strict validation.

17.3 Performance Tests

Each protocol crate MUST benchmark:

  • header-only decode,
  • full structural decode,
  • strict validation,
  • encode,
  • owned conversion.

Benchmarks MUST report:

  • p50/p99 latency,
  • heap allocations,
  • bytes copied,
  • throughput in messages/second.

17.4 Negative Corpus

Every parser MUST maintain a negative corpus:

  • truncated,
  • overlong,
  • nested too deep,
  • duplicate mandatory fields,
  • unknown critical fields,
  • invalid length,
  • invalid padding,
  • integer overflow candidate.

18. Acceptance Criteria

This RFC is implemented when:

  1. Borrowed decoders express lifetimes safely and owned conversion is available.
  2. Fast-path header decode is allocation-free for supported protocols.
  3. All length and offset math is checked.
  4. Decoders reject hostile input without panic, hang, or unbounded allocation.
  5. Round-trip tests distinguish canonical and raw-preserving modes.
  6. Fuzz targets and regression corpora exist for every protocol crate.
  7. Spec traceability tags feed RFC 006 evidence.
  8. Protocol modules follow the standard layout for parallel implementation.

OPC-SDK-RFC-006: Conformance and Evidence Pipeline

Status: Draft for Implementation
Version: 2.0.0
Date: 2026-05-19
Audience: release engineers, security engineers, standards reviewers, SDK implementers, NF teams

1. Abstract

This RFC defines the OpenPacketCore evidence pipeline: standards conformance mapping, test evidence, SBOM generation, VEX, provenance, artifact signing, performance baselines, known-gap management, and release gates.

The purpose is not to create marketing compliance claims. The purpose is to produce machine-readable, signed evidence that states exactly what is implemented, tested, partially implemented, not implemented, or intentionally out of scope.

The initial draft correctly required conformance tags, SBOMs, signed bundles, and performance baselines. This version expands those into a full evidence system suitable for high-integrity carrier CNFs and parallel implementation.

2. Scope

2.1 In Scope

  • Standards requirement inventory.
  • Code-to-spec and test-to-spec mapping.
  • Conformance status extraction.
  • Known-gap registry.
  • SBOM and VEX generation.
  • Build provenance and artifact signing.
  • Performance baseline capture.
  • Evidence bundle format.
  • Release and PR gates.

2.2 Out of Scope

  • Legal certification by standards bodies.
  • Operator-specific acceptance testing.
  • Live-network certification.
  • Runtime audit storage. See RFC 003.

3. Design Goals

3.1 Security

  • Evidence must be tamper-evident and tied to artifact digests.
  • Supply-chain metadata must include source, dependencies, build environment, container base images, and vulnerability status.
  • Claims must be traceable to tests, source, and reviewed gaps.
  • Signing keys or identities must be auditable.

3.2 Performance

  • Evidence generation must be incremental for PR workflows.
  • Full release evidence may be more expensive but must be reproducible.
  • Performance baselines must record environment details so regressions are meaningful.

3.3 Maintainability

  • Conformance tags must use a strict schema.
  • Known gaps must be first-class records, not prose-only notes.
  • Evidence tools must fail closed when claims are ambiguous.
  • Output formats must be stable for downstream automation.

3.4 Functionality

  • Produce human-readable and machine-readable reports.
  • Support partial, full, not-implemented, not-applicable, and gap statuses.
  • Attach tests and benchmark results to claims.
  • Sign artifacts and attestations.
  • Support release promotion gates.

4. Evidence Model

4.1 Claim Types

The evidence pipeline recognizes:

ClaimMeaning
implementedCode exists for the requirement
testedAutomated tests exercise the requirement
partialSome required behavior is missing
not-implementedNo implementation exists
not-applicableRequirement does not apply to this SDK/NF/profile
gapKnown missing behavior with owner and mitigation
waivedTemporary exception approved by policy

No release may claim full conformance for a requirement unless it has both implemented and tested evidence, plus no open blocking gap.

4.2 Requirement IDs

Every tracked requirement receives a stable ID:

REQ-<source>-<document>-<release>-<section>-<ordinal>

Example:

REQ-3GPP-TS29281-R18-5.1-001

Requirement IDs are stored in a versioned inventory file. Comments in code may reference IDs, but comments do not define the inventory.

4.3 Evidence Records

{
  "requirement_id": "REQ-3GPP-TS29281-R18-5.1-001",
  "status": "partial",
  "source_refs": ["crates/opc-proto-gtp/src/header.rs:Gtpv1uHeader"],
  "test_refs": ["crates/opc-proto-gtp/tests/roundtrip.rs:test_gtpu_header"],
  "gap_refs": ["GAP-000123"],
  "artifact_digests": ["sha256:..."],
  "reviewed_by": ["standards-reviewer"],
  "last_updated": "2026-05-19T00:00:00Z"
}

The pipeline MUST validate evidence records against a JSON schema.

5. Conformance Tracking

5.1 Inventory

The repository MUST maintain:

evidence/
  requirements/
    3gpp-ts-29.281-r18.yaml
    ietf-rfc-7951.yaml
  mappings/
    code-map.yaml
    test-map.yaml
  gaps/
    known-gaps.yaml

Requirement inventories SHOULD be generated from structured sources when available. When manual extraction is required, each requirement must include source document, release/revision, section, and reviewer.

5.2 Code Tags

Code tags use strict syntax:

#![allow(unused)]
fn main() {
/// @spec 3GPP TS 29.281 R18 5.1 Table 5.1-1
/// @req REQ-3GPP-TS29281-R18-5.1-001
/// @conformance partial
/// @gap GAP-000123
pub struct Gtpv1uHeader<'a> { ... }
}

Allowed tag keys:

  • @spec
  • @req
  • @conformance
  • @gap
  • @security
  • @performance
  • @test

Unknown tags MUST fail evidence extraction in release mode.

5.3 Test Tags

Tests SHOULD reference requirement IDs:

#![allow(unused)]
fn main() {
#[test]
#[req("REQ-3GPP-TS29281-R18-5.1-001")]
fn gtpu_header_roundtrip() { ... }
}

The extraction tool MUST support Rust test attributes or a sidecar test mapping file. A requirement with code but no test remains implemented, not full.

5.4 Status Rules

Status calculation:

InputsResult
code + passing tests + no blocking gapsfull
code + some tests + open nonblocking gapspartial
code + no testsimplemented-untested
gap with no codenot-implemented
reviewed N/A recordnot-applicable
approved waiverwaived

The machine-readable report MUST include both raw evidence and calculated status.

6. Known Gaps

6.1 Gap Record

Known gaps MUST be structured:

id: GAP-000123
title: GTP-U extension headers not fully decoded
status: open
severity: medium
applies_to:
  - REQ-3GPP-TS29281-R18-5.2-004
owner: opc-proto-gtp
created: 2026-05-19
target_release: 0.3.0
mitigation: Reject unsupported extension headers in strict mode.
security_impact: Low if strict mode is enabled.
performance_impact: None.

6.2 Gap Gates

Release mode MUST fail when:

  • A partial or not-implemented status has no gap.
  • A gap has no owner.
  • A gap has no mitigation or explicit "no mitigation" rationale.
  • A gap target release is overdue.
  • A security-critical gap lacks security approval.

The root known-gaps.md MAY be generated from known-gaps.yaml, but the YAML is the source of truth.

7. SBOM and VEX

7.1 SBOM Requirements

Every release MUST include CycloneDX JSON SBOMs for:

  • Rust workspace dependencies.
  • Container images.
  • Helm charts and embedded images.
  • Generated artifacts where dependencies differ.
  • Native libraries linked into binaries.

SBOMs MUST include:

  • direct and transitive dependencies,
  • package URLs where available,
  • license data,
  • hashes,
  • supplier/source repository where available,
  • build target,
  • feature flags,
  • container base image digests.

7.2 VEX Requirements

VEX records MUST state vulnerability applicability:

  • affected,
  • not affected,
  • fixed,
  • under investigation.

Each VEX decision MUST include:

  • CVE or advisory ID,
  • package and version,
  • scanner database timestamp,
  • justification,
  • reviewer or automated policy source,
  • expiry for temporary decisions.

Release mode MUST fail on unresolved critical vulnerabilities unless an approved VEX record exists.

8. Provenance and Signing

8.1 Artifact Digests

Every artifact must be addressed by digest:

  • binaries,
  • container images,
  • Helm charts,
  • SBOMs,
  • evidence bundles,
  • performance reports,
  • conformance reports.

Tags are not sufficient.

8.2 Provenance

Release builds MUST produce SLSA-style provenance, preferably in in-toto/DSSE format, including:

  • source repository URL,
  • commit SHA,
  • dirty tree status,
  • builder identity,
  • build workflow reference,
  • build inputs,
  • dependency lockfiles,
  • environment image digest,
  • output artifact digests.

8.3 Signing

Release artifacts and attestations MUST be signed with Sigstore/Cosign or an approved offline carrier signing profile.

Keyless profile:

  • OIDC issuer and subject must be policy-allowed.
  • Transparency log entry must be verifiable.
  • Certificate identity must match release workflow.

Offline profile:

  • Public key must be published through an approved channel.
  • Signing key custody and rotation must be documented.
  • Transparency log use SHOULD be retained where possible.

8.4 Bundle Signing

Signing only evidence-bundle.tar.gz is not enough. The bundle MUST include a manifest of file digests, and the manifest or DSSE envelope MUST be signed. Individual high-value artifacts SHOULD also carry their own attestations.

9. Performance Evidence

9.1 Benchmark Classes

Performance evidence MUST cover:

  • RFC 001 config commit phases.
  • RFC 002 generated validation and patch application.
  • RFC 004 session store operations.
  • RFC 005 protocol decode/encode.
  • Security operations from RFC 003 where relevant.

9.2 Environment Capture

performance-baseline.json MUST include:

  • CPU model and count,
  • memory size and speed where available,
  • kernel version,
  • container runtime,
  • Kubernetes version when applicable,
  • storage class for persistence tests,
  • network plugin for distributed tests,
  • compiler version,
  • cargo profile,
  • feature flags,
  • git commit,
  • date/time,
  • benchmark tool version.

9.3 Regression Policy

Each benchmark defines:

  • metric,
  • baseline,
  • allowed regression threshold,
  • required sample count,
  • noise handling,
  • owner.

Data-plane PRs MUST fail when they exceed regression thresholds unless a performance waiver is approved.

10. Evidence Bundle

10.1 Files

The release evidence bundle MUST contain:

evidence-bundle/
  manifest.json
  conformance-report.json
  conformance-report.md
  known-gaps.json
  sbom/
    workspace.cdx.json
    containers.cdx.json
  vex/
    vex.json
  provenance/
    build.intoto.jsonl
  signatures/
    cosign.bundle
  performance/
    performance-baseline.json
    raw/
  tests/
    test-summary.json
    junit/
  security/
    vulnerability-report.json
    policy-results.json

10.2 Manifest

manifest.json MUST include:

  • evidence schema version,
  • SDK version,
  • git commit,
  • artifact digests,
  • file digests,
  • signing identity,
  • generation tool version,
  • generation timestamp,
  • known incomplete sections.

Manifest file and artifact paths MUST be normalized relative bundle paths. Absolute paths, parent/current-directory components, platform-specific path prefixes, duplicate entries, conflicting digests, and malformed SHA-256 values MUST fail closed before signing or verification.

Standalone manifest signatures and complete bundle signatures use distinct versioned domain separators. Complete bundle signing bytes deterministically bind the canonical manifest plus the digest of every embedded report. Canonical manifest object fields, digest entries, and metadata keys use explicit lexical ordering that MUST NOT vary with JSON-library map features. The authenticated verifier identity MUST exactly match signing_identity; a release verifier that cannot report its authenticated identity is insufficient. If a release gate receives any artifact separately from the bundle, a verified signed bundle is mandatory and the gate MUST evaluate the exact signed bytes rather than a substitutable second copy. The signed manifest MUST also carry the domain-separated canonical digest of every structured record, gap, and waiver input used by the release gate. Raw records in a separately supplied signed conformance report MUST match the v1 report projection of those bound inputs, including gap references. Waiver references and full waiver records remain in the signed gate-input digest because the frozen v1 report schema does not carry them. A configured expected commit requires provenance, and the expected, provenance, conformance-report, and manifest commit identities MUST agree. Mismatch errors MUST NOT echo those values.

The domain-separated format intentionally does not verify signatures from the pre-domain-separated implementation. Evidence producers upgrading to this format MUST regenerate and re-sign their bundles; verifiers MUST NOT fall back to the ambiguous legacy payload.

10.3 Packet-core evidence packs

A release evidence bundle MAY include one or more packet-core evidence packs for protocol fixtures, attach procedure results, and kernel dataplane/XFRM proof. These packs are intended to make smoke artifacts and test evidence from different network functions comparable, not to create product-specific certification claims.

Each pack is a JSON object conforming to packet-core-evidence-pack.schema.json and contains:

  • protocol_evidence: protocol fixture evidence records.
  • attach_evidence: attach and session-establishment procedure results.
  • kernel_dataplane_evidence: kernel dataplane, XFRM, routing, and firewall state summaries.

Packet-core evidence schemas are versioned independently within RFC 006 and are currently experimental. A pack MUST declare experimental: true until the schema graduates. Every pack MUST pass redaction validation before it is included in a bundle; validation fails closed if any string field contains a raw IMSI, MSISDN, IMEI, NAI, Session-Id, LI identifier, or key material.

Downstream products (for example, ePDG smoke artifacts) MAY map their own evidence into this SDK format. Doing so documents how the product evidence corresponds to SDK schema fields; it does not imply the SDK has certified the product.

11. PR and Release Gates

11.1 PR Gates

Required for every PR:

  • Build.
  • Unit tests.
  • Formatting and lint checks.
  • Incremental evidence extraction.
  • New public protocol/config items include spec or explicit non-spec tags.
  • New gaps are structured and owned.
  • Security-sensitive changes run targeted tests.

11.2 Release Gates

Required for every release:

  • Full test suite.
  • Fuzzing gate for changed protocol crates.
  • SBOM generation.
  • VEX evaluation.
  • Vulnerability scan.
  • Provenance generation.
  • Artifact signing.
  • Conformance report.
  • Known-gap validation.
  • Performance baseline.
  • Evidence bundle signing.

Release MUST fail closed if evidence generation fails.

12. Implementation Evidence Requirements

Generated code is allowed only when evidence remains strict.

Rules:

  • Every new protocol struct must include spec tags.
  • Every new generated config item must include YANG path metadata.
  • Every new security behavior must include a threat/test note.
  • Every generated test must map to a requirement or state it is purely internal.
  • Contributors must not mark conformance full; only the evidence calculator may calculate final status.
  • Ambiguous or unsupported spec behavior must create a gap record.

The evidence pipeline is the guardrail that prevents plausible-looking code from silently becoming unsupported compliance claims.

13. Tooling Architecture

crates/opc-evidence/
  src/
    inventory.rs
    extract.rs
    conformance.rs
    sbom.rs
    vex.rs
    provenance.rs
    performance.rs
    bundle.rs
    policy.rs
    report.rs

Tool responsibilities:

  • inventory: load and validate requirement inventories.
  • extract: scan source and test tags.
  • conformance: calculate status.
  • sbom: invoke or parse SBOM generators.
  • vex: correlate vulnerabilities and VEX decisions.
  • provenance: collect build attestation metadata.
  • performance: normalize benchmark output.
  • bundle: create manifest and bundle.
  • policy: enforce PR/release gates.
  • report: emit Markdown and JSON.

14. Schemas

The repository MUST version JSON schemas for:

  • requirement inventory,
  • evidence record,
  • conformance report,
  • gap record,
  • performance baseline,
  • bundle manifest,
  • VEX policy result,
  • packet-core protocol evidence,
  • packet-core attach evidence,
  • packet-core kernel dataplane evidence,
  • packet-core evidence pack.

Schema changes MUST be backward compatible within a major SDK release or include a migration tool.

15. Testing Requirements

15.1 Unit Tests

  • Tag parser accepts valid tags and rejects invalid tags.
  • Requirement inventory schema validation.
  • Gap gate logic.
  • Status calculation matrix.
  • Manifest digest calculation.
  • VEX decision expiry.

15.2 Integration Tests

  • End-to-end evidence generation on fixture crate.
  • Release gate fails on undocumented partial conformance.
  • Release gate fails on unsigned artifact.
  • Release gate fails on unresolved critical CVE.
  • Performance regression gate fails on threshold breach.
  • Known-gaps Markdown generation from YAML.

15.3 Tamper Tests

  • Modify artifact after manifest generation.
  • Remove test evidence for full claim.
  • Change SBOM after signing.
  • Use disallowed signing identity.
  • Replay old VEX with expired decision.

16. Acceptance Criteria

This RFC is implemented when:

  1. Conformance claims are calculated from requirement inventory, code tags, tests, and gaps.
  2. A requirement cannot silently remain partial without a structured known gap.
  3. SBOM and VEX are generated and release-gated.
  4. Provenance ties artifacts to source commit, builder, inputs, and digests.
  5. Evidence bundles include signed manifests and verifiable artifact digests.
  6. Performance baselines include environment details and regression thresholds.
  7. PR and release gates fail closed on missing or inconsistent evidence.
  8. Generated code must supply traceable tags and tests before it can support conformance claims.

OPC-SDK-RFC-007: SBI Service Framework

Status: Draft for Implementation
Version: 1.0.0
Date: 2026-05-19
Audience: SBI NF implementers, security engineers, operator authors, test authors

1. Abstract

This RFC defines the OpenPacketCore Service Based Interface (SBI) framework for 5G control-plane CNFs. It standardizes HTTP/2 transport behavior, 3GPP ProblemDetails, OAuth2/JWT-SVID authentication, NRF discovery, service registration, retry/backoff, overload control, circuit breaking, idempotency, callback delivery, OpenAPI/model generation, observability, and conformance tests.

Without this RFC, every SBI-producing NF would independently implement common TS 29.500/29.501 behavior. That would create incompatible error semantics, token validation, discovery caching, and overload behavior across AMF, SMF, PCF, NRF, UDM, AUSF, NSSF, NEF, NWDAF, BSF, CHF, SCP, and SEPP.

2. Scope

2.1 In Scope

  • SBI HTTP/2 server and client substrate.
  • TS 29.500 common headers and ProblemDetails behavior.
  • TS 29.510 NRF registration, heartbeat, discovery, and access token client helpers.
  • OAuth2 bearer token validation and client-credentials acquisition.
  • SPIFFE JWT-SVID client authentication to NRF where configured.
  • Retry, timeout, backoff, idempotency, and callback delivery.
  • Per-peer, per-slice, and per-service overload controls.
  • Circuit breakers and outlier detection.
  • OpenAPI-driven model generation and compatibility.
  • Metrics, tracing, audit, and evidence hooks.

2.2 Out of Scope

  • NF-specific SBI resource semantics. Those live in per-NF crates.
  • Management-plane gNMI/NETCONF. See RFC 001 and RFC 003.
  • Protocol codecs below HTTP/2. See RFC 005.
  • Session persistence. See RFC 004.

3. Design Goals

3.1 Security

  • Authenticate every SBI peer with mTLS and, where applicable, OAuth2 access tokens.
  • Bind peer identity, NF type, NF instance ID, PLMN, tenant, slice, and token scopes into authorization decisions.
  • Prevent topology scraping, token replay, confused-deputy calls, callback spoofing, and cross-slice data exposure.
  • Avoid logging raw SUPI/GPSI, bearer tokens, assertion JWTs, or subscriber payloads.

3.2 Performance

  • Use HTTP/2 connection pooling and bounded concurrency per peer.
  • Avoid per-request DNS/NRF discovery.
  • Make token verification hot-path cacheable.
  • Provide low-latency fast paths for common ProblemDetails and header parsing.
  • Enforce backpressure before request queues grow unbounded.

3.3 Maintainability

  • Keep TS 29.500 common behavior in opc-sbi, not in every NF.
  • Generate typed models from version-pinned OpenAPI definitions where possible.
  • Keep retry and overload policy declarative through YANG.
  • Provide one shared testkit for SBI peers and NRF behavior.

3.4 Functionality

  • Support SBI producer and consumer roles.
  • Support NRF registration, heartbeat, discovery, subscriptions, and token acquisition.
  • Support service-version negotiation.
  • Support callbacks with retry and dead-letter behavior.
  • Support direct NF-to-NF routing and SCP-mediated routing.

4. Standards Baseline

The initial target is 3GPP Release 17 with explicit support for selected Release 18 behavior when per-NF specs require it.

Required references:

  • TS 29.500: Common API framework, HTTP behavior, headers, ProblemDetails.
  • TS 29.501: Principles and guidelines for services definition.
  • TS 29.510: NRF NFManagement, NFDiscovery, AccessToken.
  • TS 33.501: SBI security and OAuth2 usage.
  • RFC 6749: OAuth2.
  • RFC 6750: Bearer token usage.
  • RFC 7515/7517/7519: JWS, JWK, JWT.
  • RFC 7662: Token introspection, if enabled by profile.
  • RFC 9110/RFC 9113: HTTP semantics and HTTP/2.

The exact release and supported service APIs are captured in RFC 006 evidence.

5. Crate Model

The shared crate is opc-sbi.

crates/opc-sbi/
  src/
    lib.rs
    error.rs
    problem.rs
    headers.rs
    identity.rs
    oauth.rs
    nrf/
      mod.rs
      registration.rs
      discovery.rs
      heartbeat.rs
      access_token.rs
      cache.rs
    client/
      mod.rs
      pool.rs
      retry.rs
      circuit_breaker.rs
      overload.rs
    server/
      mod.rs
      auth.rs
      extractors.rs
      middleware.rs
    callback/
      mod.rs
      dispatcher.rs
      dead_letter.rs
    models/
      generated/
    observability.rs
    testkit/

NF crates MUST use opc-sbi for common SBI behavior. They MUST NOT duplicate ProblemDetails encoding, bearer-token parsing, NRF discovery caching, or retry policy.

6. Transport Contract

6.1 HTTP/2

SBI uses HTTP/2 by default. The framework MUST:

  • Use TLS 1.3 by default.
  • Verify peer certificate identity through RFC 003.
  • Support direct NF endpoints and SCP endpoints.
  • Enforce max header list size, max frame size, max body size, stream concurrency, and idle timeouts.
  • Reject HTTP/1.1 in production profiles unless a per-NF compatibility profile explicitly permits it.

6.2 Connection Pooling

The client pool key MUST include:

  • target NF instance or service set,
  • transport mode: direct or SCP,
  • trust domain,
  • tenant,
  • service name,
  • API version,
  • TLS profile,
  • OAuth2 audience/scope set.

Pools MUST enforce:

  • maximum connections per peer,
  • maximum concurrent streams per connection,
  • idle connection eviction,
  • connection max age,
  • backpressure when all streams are saturated.

6.3 Deadlines

Every outbound SBI request MUST carry a deadline from the caller. The framework MUST enforce request timeout locally and SHOULD propagate timeout hints through headers where 3GPP permits.

7. ProblemDetails

7.1 Error Type

opc-sbi owns the canonical ProblemDetails type:

#![allow(unused)]
fn main() {
pub struct ProblemDetails {
    pub status: http::StatusCode,
    pub cause: Option<CauseCode>,
    pub title: Option<String>,
    pub detail: Option<String>,
    pub instance: Option<String>,
    pub invalid_params: Vec<InvalidParam>,
    pub supported_features: Option<String>,
}
}

NF code returns domain errors; the framework maps them to ProblemDetails.

7.2 Mapping Rules

ProblemDetails mapping MUST be:

  • deterministic,
  • spec-cited,
  • test-covered,
  • safe for logs and clients,
  • evidence-linked through RFC 006.

No domain handler may return ad hoc JSON error bodies on SBI routes.

8. Common Headers

The framework MUST parse and render configured TS 29.500 headers, including:

  • 3gpp-Sbi-Message-Priority
  • 3gpp-Sbi-Correlation-Info
  • 3gpp-Sbi-Binding
  • 3gpp-Sbi-Routing-Binding
  • 3gpp-Sbi-Target-apiRoot
  • Retry-After
  • Location
  • Authorization

Header parsing MUST reject malformed values with structured errors. Sensitive headers MUST be redacted.

9. Identity and Authorization

9.1 Peer Identity

The server middleware extracts:

#![allow(unused)]
fn main() {
pub struct SbiPeer {
    pub spiffe: Option<SpiffeId>,
    pub nf_instance_id: Option<NfInstanceId>,
    pub nf_type: Option<NfType>,
    pub tenant: TenantId,
    pub plmn: Option<PlmnId>,
    pub snssai: Option<Snssai>,
}
}

Identity MAY come from mTLS SPIFFE, NRF-issued token claims, or a legacy certificate mapping profile. Unsigned metadata headers MUST NOT establish identity.

9.2 OAuth2 Validation

SBI producers that require OAuth2 MUST validate:

  • issuer,
  • audience,
  • expiry and not-before,
  • signature and key ID,
  • scope,
  • NF type and instance binding,
  • tenant and slice binding where configured,
  • replay-sensitive claims when configured.

Token validation results MAY be cached until the earlier of token expiry or policy version change.

9.3 OAuth2 Client Credentials

SBI consumers MUST acquire tokens through NRF or configured authorization server. Client authentication methods:

  • SPIFFE JWT-SVID, preferred.
  • mTLS-bound client authentication.
  • Private key JWT.
  • Kubernetes Secret client secret only in explicit compatibility profile.

Long-lived shared client secrets are forbidden in production carrier profiles unless an RFC 006 waiver exists.

10. NRF Integration

10.1 Registration

opc-sbi MUST provide helpers for NF registration, update, deregistration, and heartbeat.

NF profiles MUST be generated from typed NF metadata and canonical YANG. Raw free-form JSON construction is forbidden outside test fixtures.

10.2 Heartbeats

The heartbeat driver MUST:

  • derive interval from NRF response where present,
  • jitter heartbeat timing,
  • mark the NF degraded on repeated heartbeat failure,
  • keep serving existing local traffic according to per-NF policy,
  • deregister gracefully on shutdown when possible.

10.3 Discovery

The discovery client MUST provide:

  • query construction with typed filters,
  • response validation,
  • cache with TTL and stale-if-error policy,
  • negative caching,
  • per-service-set load balancing,
  • SCP preference where configured,
  • tenant and slice filter enforcement.

Discovery cache entries MUST be invalidated on canonical config changes that affect peers, PLMN, slice, trust anchors, or routing mode.

10.4 Subscriptions

NRF subscription handling MUST support retry, backoff, and dead-letter behavior for failed notifications. Subscription callbacks MUST be authenticated and authorized like any other SBI request.

11. Routing Modes

Supported modes:

ModeBehavior
directConsumer dials producer discovered from NRF or static peer config
scpConsumer sends through SCP with routing headers
seppInter-PLMN traffic goes through SEPP policy
staticExplicit peer list from YANG, for lab or interop

The mode is selected per service, tenant, PLMN, and slice. Inter-PLMN traffic MUST NOT bypass SEPP when policy requires SEPP.

12. Retry, Idempotency, and Callback Delivery

12.1 Retry Policy

Retry policy MUST be declarative:

#![allow(unused)]
fn main() {
pub struct RetryPolicy {
    pub max_attempts: u8,
    pub base_delay: Duration,
    pub max_delay: Duration,
    pub jitter: Jitter,
    pub retry_on_status: Vec<StatusCode>,
    pub retry_on_transport_error: bool,
}
}

The framework MUST NOT retry non-idempotent requests unless the request carries an idempotency key or the operation is explicitly marked idempotent by the service definition.

12.2 Idempotency

For operations that can be retried, the framework SHOULD provide:

  • idempotency key generation,
  • inbound idempotency cache,
  • replay-safe response caching,
  • expiry and memory bounds.

12.3 Callback Delivery

Callback dispatchers MUST support:

  • bounded queues,
  • retry budget,
  • backoff,
  • callback authentication,
  • dead-letter sink,
  • observability,
  • cancellation on subscription deletion.

Callback storms MUST be rate-limited per callback target.

13. Overload Control

13.1 Admission

The framework MUST provide admission control before request bodies are fully read when possible.

Admission keys:

  • peer identity,
  • NF type,
  • tenant,
  • slice,
  • service,
  • operation,
  • priority.

13.2 Response Semantics

Overload responses MUST use:

  • HTTP 429 for rate limiting,
  • HTTP 503 for temporary service overload,
  • Retry-After where retry is appropriate,
  • ProblemDetails with a stable cause code.

13.3 Priority

Requests with emergency, lawful, registration, paging, or charging criticality MAY receive higher priority only when the per-NF spec and 3GPP behavior justify it. Priority policy MUST be explicit, audited, and tested.

13.4 Circuit Breakers

Outbound circuit breakers MUST track:

  • consecutive failures,
  • error-rate window,
  • latency outliers,
  • half-open probes,
  • per-peer and per-service state.

Circuit breaker state MUST be visible in metrics and debug endpoints without exposing secrets or topology beyond authorized users.

14. Generated Models and OpenAPI

opc-sbi SHOULD generate models from version-pinned OpenAPI sources where available. Generated code MUST:

  • be reproducible,
  • preserve unknown extension fields only when configured,
  • avoid ad hoc stringly typed JSON in NF handlers,
  • include spec tags for RFC 006,
  • pass serialization round trips.

OpenAPI mismatches with normative 3GPP text MUST create RFC 006 known gaps or generator overrides with citations.

15. Configuration Model

Each SBI NF YANG SHOULD expose:

  • sbi/listeners
  • sbi/clients
  • sbi/nrf
  • sbi/oauth2
  • sbi/retry-policy
  • sbi/overload
  • sbi/circuit-breakers
  • sbi/callbacks

These may be embedded under the shared listeners, peers, rate-limits, and policy containers defined by the cloud-native pattern.

16. Observability

Required metrics:

  • opc_sbi_requests_total{nf,service,operation,outcome}
  • opc_sbi_request_duration_seconds{service,operation}
  • opc_sbi_problem_details_total{service,cause,status}
  • opc_sbi_oauth_validation_total{outcome,reason}
  • opc_sbi_nrf_discovery_total{outcome}
  • opc_sbi_nrf_cache_entries{service}
  • opc_sbi_nrf_heartbeat_total{outcome}
  • opc_sbi_circuit_state{peer,service,state}
  • opc_sbi_overload_rejections_total{service,reason}
  • opc_sbi_callback_delivery_total{target,outcome}

Tracing MUST propagate W3C traceparent and 3GPP correlation headers when present.

17. Module Ownership

ModuleResponsibility
opc-sbi-problemProblemDetails model and mappings
opc-sbi-headers3GPP header parse/render/redaction
opc-sbi-authOAuth2/JWT-SVID validation and token acquisition
opc-sbi-nrfNRF registration, heartbeat, discovery, cache
opc-sbi-clientHTTP/2 pool, deadlines, retries, circuit breakers
opc-sbi-serverAxum/tower middleware, extractors, admission
opc-sbi-callbackCallback queues, retry, dead-letter
opc-sbi-codegenOpenAPI/model generation
opc-sbi-testkitMock NRF, mock producer, token fixtures

Agents must not implement NF-specific business logic in opc-sbi.

18. Testing Requirements

18.1 Unit Tests

  • ProblemDetails mappings.
  • Header parsing and redaction.
  • Token validation matrix.
  • Retry idempotency policy.
  • Circuit breaker transitions.
  • NRF cache expiry and invalidation.

18.2 Integration Tests

  • Mock NRF registration, heartbeat, discovery, and token issuance.
  • Producer validates mTLS and OAuth2 together.
  • Consumer refreshes token before expiry.
  • SCP routing header generation.
  • Callback retry and dead-letter.
  • Overload rejection with Retry-After.

18.3 Fault Injection

  • NRF unavailable.
  • Expired token.
  • Bad JWK key ID.
  • Peer certificate rotation.
  • DNS failure.
  • HTTP/2 stream reset.
  • Slow callback target.
  • Discovery cache stale while NRF down.

18.4 Performance Gates

  • Hot token validation cache p99 under 25 microseconds.
  • ProblemDetails mapping allocation-free for common static errors.
  • Discovery cache lookup p99 under 10 microseconds.
  • Client pool does not allocate per request beyond body/model needs.
  • Overload admission rejects before full body read for oversized bodies.

19. Acceptance Criteria

This RFC is implemented when:

  1. All SBI NFs use shared ProblemDetails, header, auth, retry, and NRF code.
  2. OAuth2 validation and client-credential acquisition are test-covered.
  3. NRF registration, heartbeat, discovery, and cache behavior are shared.
  4. Retry behavior is idempotency-aware.
  5. Overload control returns consistent 429/503/Retry-After semantics.
  6. Circuit breaker state is observable and bounded.
  7. Generated models are reproducible and evidence-tagged.
  8. A shared SBI testkit can exercise producer and consumer behavior for every SBI NF.

OPC-SDK-RFC-008: CNF Runtime Chassis and Resource Governance

Status: Draft for Implementation
Version: 1.0.0
Date: 2026-05-19
Audience: NF implementers, platform engineers, SREs, security reviewers

1. Abstract

This RFC defines the common Rust runtime chassis used by every OpenPacketCore CNF. It standardizes process startup, task supervision, shutdown, health probes, admin endpoints, runtime pools, resource budgets, panic policy, configuration bootstrap, signal handling, telemetry initialization, memory behavior, and operational debug surfaces.

The goal is that AMF, SMF, UPF, NRF, PCF, SEPP, SMSC, and all other CNFs share one predictable runtime skeleton instead of each inventing its own Tokio setup, shutdown behavior, health semantics, and task lifecycle.

2. Scope

2.1 In Scope

  • Runtime initialization.
  • Tokio worker and blocking pool configuration.
  • Task supervision and cancellation.
  • Startup and readiness phases.
  • Graceful shutdown and drain.
  • Health and admin HTTP endpoints.
  • Runtime resource budgets and backpressure hooks.
  • Panic and fatal-error policy.
  • Metrics, logging, and tracing bootstrap.
  • Memory, allocator, and OOM behavior.
  • Common CLI/env/bootstrap contract.

2.2 Out of Scope

  • NF-specific protocol logic.
  • Kubernetes controller behavior. See RFC 009.
  • Node/NIC scheduling and SR-IOV contracts. See RFC 011.
  • Config commit semantics. See RFC 001.

3. Design Goals

3.1 Security

  • Fail closed when required bootstrap security material is unavailable.
  • Keep debug endpoints disabled or authorization-gated in production.
  • Ensure panic output and fatal-error reports are redacted.
  • Make shutdown safe: no partial config writes, key leaks, or unaudited emergency exits.

3.2 Performance

  • Avoid runtime-pool contention between management, control, crypto, and data-plane work.
  • Bound queues, tasks, memory, and blocking work.
  • Make health checks cheap and non-blocking.
  • Provide predictable drain behavior under load.

3.3 Maintainability

  • Provide one reusable opc-runtime crate.
  • Make lifecycle phases explicit and testable.
  • Provide standard task naming and metrics.
  • Keep per-NF custom code in callbacks, not in process scaffolding.

3.4 Functionality

  • Support control-plane, data-plane, and library-like CNF profiles.
  • Support local developer mode and production mode.
  • Support graceful restart, termination, and Kubernetes probe integration.
  • Support runtime introspection without exposing secrets.

4. Runtime Crate

The shared crate is opc-runtime.

crates/opc-runtime/
  src/
    lib.rs
    bootstrap.rs
    profile.rs
    supervisor.rs
    task.rs
    shutdown.rs
    health.rs
    admin.rs
    resources.rs
    panic.rs
    telemetry.rs
    memory.rs
    signals.rs
    testkit.rs

Every NF binary SHOULD be a thin wrapper around opc_runtime::run.

5. Runtime Profile

#![allow(unused)]
fn main() {
pub struct RuntimeProfile {
    pub mode: RuntimeMode,
    pub nf_kind: NetworkFunctionKind,
    pub instance_id: InstanceId,
    pub async_workers: WorkerCount,
    pub blocking_threads: ThreadLimit,
    pub crypto_threads: ThreadLimit,
    pub management_threads: ThreadLimit,
    pub max_tasks: usize,
    pub max_queued_bytes: usize,
    pub shutdown_grace: Duration,
    pub drain_timeout: Duration,
}
}

Profiles:

  • dev: permissive, local files, debug endpoints enabled on loopback.
  • lab: production-like, explicit waivers allowed.
  • production: fail closed, debug gated, strict resource limits.
  • conformance: deterministic test profile.
  • perf: optimized benchmark profile with fixed CPU/resource assumptions.

6. Startup State Machine

Every CNF starts through:

PhasePurpose
ProcessInitparse CLI/env, install panic hook, initialize logging
TelemetryInitmetrics/tracing/logging exporters
SecurityInitidentity, trust bundles, key providers
ConfigBootstrapload initial config through RFC 001
ResourcePreflightverify CPU, memory, filesystem, devices
ServiceBindbind listeners but do not report ready
PeerWarmupoptional NRF registration, discovery, backend connection
Readyreadiness probe returns success
Drainingtermination accepted, new work limited
Stoppedall supervised tasks exited

Startup MUST fail closed in production if any required phase fails.

7. Task Supervision

7.1 Task Model

All long-lived tasks MUST be registered with the supervisor:

#![allow(unused)]
fn main() {
pub struct TaskSpec {
    pub name: TaskName,
    pub kind: TaskKind,
    pub criticality: Criticality,
    pub restart: RestartPolicy,
    pub shutdown: ShutdownPolicy,
}
}

Task kinds:

  • listener
  • protocol-worker
  • session-worker
  • management-worker
  • background-sync
  • metrics-exporter
  • watcher
  • timer

7.2 Criticality

CriticalityBehavior on Failure
fatalTransition CNF to fatal shutdown
degradeMark degraded and optionally restart
best-effortLog/metric and continue

Critical task failures MUST be visible through readiness and alarm state.

7.3 Restart Policy

Restart policy MUST include:

  • max restarts per window,
  • backoff,
  • jitter,
  • failure classification,
  • whether restart is allowed after config changes.

Unbounded task restart loops are forbidden.

8. Runtime Pool Isolation

The runtime MUST expose separate execution domains:

  • async I/O workers,
  • blocking/CPU pool,
  • crypto pool,
  • management pool,
  • data-plane workers where applicable.

Data-plane CNFs SHOULD integrate with RFC 011 CPU pinning and IRQ affinity. Management-plane work MUST NOT execute on data-plane pinned workers.

9. Resource Governance

9.1 Budgets

Each CNF declares:

#![allow(unused)]
fn main() {
pub struct ResourceBudget {
    pub max_heap_bytes: Option<usize>,
    pub max_tasks: usize,
    pub max_channels: usize,
    pub max_queue_bytes: usize,
    pub max_request_body_bytes: usize,
    pub max_open_files: usize,
    pub max_backend_connections: usize,
}
}

Budgets MUST be profile-configurable and observable.

9.2 Backpressure

The runtime provides shared primitives:

  • bounded mpsc channels,
  • byte-accounted queues,
  • weighted semaphores,
  • admission guards,
  • deadline propagation,
  • cancellation tokens.

Unbounded channels are forbidden in production runtime code unless an RFC 006 waiver exists.

9.3 Memory Behavior

The runtime SHOULD:

  • expose allocator metrics where available,
  • support an optional hardened allocator profile,
  • fail fast on configured memory-budget breach,
  • avoid memory-heavy debug dumps in production,
  • support heap profile endpoints only under explicit authorization.

10. Shutdown and Drain

10.1 Signals

The runtime MUST handle:

  • SIGTERM: graceful drain.
  • SIGINT: graceful drain in dev, configurable in production.
  • fatal internal errors: controlled shutdown path when possible.

10.2 Drain Sequence

Drain order:

  1. Stop accepting new external work.
  2. Mark readiness false.
  3. Notify NRF/deregister where applicable.
  4. Stop management writes except emergency recovery.
  5. Drain protocol workers up to timeout.
  6. Flush audit and evidence breadcrumbs.
  7. Checkpoint local state where applicable.
  8. Shut down listeners and background tasks.

Each NF can add steps but MUST preserve safety ordering.

10.3 Kubernetes Integration

terminationGracePeriodSeconds MUST be at least shutdown_grace plus probe latency margin. PreStop hooks MAY call admin drain but MUST NOT be the only drain mechanism.

11. Health and Admin Surface

11.1 Endpoints

Default admin listener:

  • /livez
  • /readyz
  • /startupz
  • /metrics
  • /debug/runtime gated
  • /debug/tasks gated
  • /debug/config-version gated

Production debug endpoints MUST require authorization or be disabled.

11.2 Health Semantics

/livez means the process event loop is alive. It MUST NOT depend on external peers.

/readyz means the CNF can serve its intended role. It SHOULD include:

  • config applied,
  • critical tasks healthy,
  • required listeners bound,
  • required security material valid,
  • required backends reachable according to NF policy.

12. Panic and Fatal Error Policy

12.1 Panics

Production builds MUST install a panic hook that:

  • redacts secrets,
  • records task name,
  • increments fatal metrics,
  • emits a structured fatal log,
  • triggers supervisor policy.

Panics in parser or protocol handlers are bugs and MUST be covered by RFC 005 fuzzing regression tests.

12.2 unwrap and expect

Runtime and NF code MUST avoid unwrap and expect outside tests, build scripts, and explicitly justified invariants. Justifications MUST be grep-able and evidence-linked.

13. Bootstrap Contract

CLI/env values are limited to bootstrap concerns:

  • config bootstrap source,
  • management bind address,
  • admin bind address,
  • production/dev mode,
  • identity socket path,
  • tracing exporter endpoint,
  • initial log level,
  • feature gates for explicit waivers.

Dense protocol behavior MUST come from canonical config, not env vars.

14. Telemetry Initialization

The runtime initializes:

  • structured JSON logging,
  • OpenTelemetry tracing,
  • Prometheus metrics,
  • build info,
  • runtime profile info,
  • panic/fatal counters.

Required metrics:

  • opc_runtime_build_info{nf,version,git_sha}
  • opc_runtime_tasks{nf,kind,state}
  • opc_runtime_task_restarts_total{nf,task}
  • opc_runtime_queue_depth{nf,queue}
  • opc_runtime_queue_bytes{nf,queue}
  • opc_runtime_shutdown_total{nf,reason}
  • opc_runtime_panic_total{nf,task}
  • opc_runtime_memory_bytes{nf,kind}
  • opc_runtime_ready{nf}

15. Time and Clocks

The runtime MUST provide a clock abstraction for tests:

#![allow(unused)]
fn main() {
pub trait Clock: Send + Sync {
    fn now(&self) -> Timestamp;
    fn monotonic(&self) -> Instant;
}
}

Security expiry and audit timestamps use wall clock plus monotonic sequencing where required. Timers use monotonic time.

16. Module Ownership

ModuleResponsibility
opc-runtime-bootstrapCLI/env/profile loading
opc-runtime-supervisortask registry, restart, failure policy
opc-runtime-shutdownsignal handling and drain orchestration
opc-runtime-healthhealth model and probe endpoints
opc-runtime-admingated debug/admin routes
opc-runtime-resourcesbudgets, queues, semaphores
opc-runtime-telemetrylogging, metrics, tracing init
opc-runtime-testkitfake clock, fake tasks, shutdown tests

Agents implementing NF business logic should consume opc-runtime; they should not fork startup/shutdown code.

17. Testing Requirements

17.1 Unit Tests

  • Startup state transitions.
  • Task restart/backoff.
  • Fatal vs degraded task failure.
  • Bounded queue byte accounting.
  • Panic hook redaction.
  • Health state aggregation.
  • Clock abstraction.

17.2 Integration Tests

  • SIGTERM drains in order.
  • Readiness flips false before listeners stop.
  • NRF deregistration hook is called during drain.
  • Background task failure degrades readiness.
  • Debug endpoints are disabled or authorized in production.

17.3 Fault Injection

  • Hung task on shutdown.
  • Task restart loop.
  • Telemetry exporter unavailable.
  • Missing identity socket.
  • Memory budget breach.
  • Queue saturation.
  • Panic in a worker task.

17.4 Performance Gates

  • /livez p99 under 1 millisecond in healthy process.
  • Supervisor task spawn overhead negligible relative to direct spawn in NF startup tests.
  • Runtime metrics collection does not allocate on every scrape for static metric sets.
  • Queue admission overhead p99 under 10 microseconds.

18. Acceptance Criteria

This RFC is implemented when:

  1. Every NF binary uses opc-runtime for startup, supervision, health, and shutdown.
  2. Long-lived tasks are supervised and named.
  3. Readiness semantics are consistent across CNFs.
  4. Shutdown drains safely and predictably.
  5. Production debug endpoints are gated or disabled.
  6. Runtime pools and queues are bounded.
  7. Panic and fatal-error handling is redacted and observable.
  8. Runtime behavior is covered by shared testkit and fault injection tests.

OPC-SDK-RFC-009: Operator Lifecycle, Upgrade, Migration, and Rollback

Status: Draft for Implementation
Version: 1.0.0
Date: 2026-05-19
Audience: operator authors, NF owners, release engineers, SREs

1. Abstract

This RFC defines the lifecycle contract between the OpenPacketCore Kubernetes operator, lifecycle CRDs, canonical YANG configuration, NF pods, persistent state, and release artifacts. It specifies reconciliation phases, version skew, CRD conversion, YANG schema migration, state migration, rollout strategies, rollback, drain, status conditions, and release gates.

This RFC turns the thin-CRD/fat-YANG pattern into an upgrade-safe product contract across all CNFs.

2. Scope

2.1 In Scope

  • Lifecycle CRD reconciliation.
  • Operator/NF version compatibility.
  • CRD versioning and conversion webhooks.
  • Canonical config revision and schema migration.
  • NF image rollout strategies.
  • Session-aware drain and handover coordination.
  • Rollback and downgrade policy.
  • Status, events, and GitOps health gates.
  • Multi-cluster rollout topology.

2.2 Out of Scope

  • Runtime process shutdown internals. See RFC 008.
  • Session storage primitives. See RFC 004.
  • Evidence bundle generation. See RFC 006.
  • Node resource scheduling. See RFC 011.

3. Design Goals

3.1 Security

  • Prevent unsigned, unverified, or policy-disallowed images from rolling out.
  • Prevent config downgrades that bypass validation or reintroduce forbidden settings.
  • Ensure rollback preserves audit and does not silently lose regulated data.
  • Keep break-glass upgrade paths narrow, explicit, and auditable.

3.2 Performance

  • Rollouts must avoid unnecessary full-cluster disruption.
  • Stateful NFs must drain or transfer ownership before termination.
  • Operator reconciliation must avoid hot loops and unbounded API traffic.
  • Large config migrations must be staged and observable.

3.3 Maintainability

  • Every lifecycle phase has stable names, conditions, and event reasons.
  • Compatibility matrices are machine-readable.
  • Migration functions are versioned, deterministic, and tested.
  • Per-NF deviations are explicit.

3.4 Functionality

  • Support install, update, scale, config change, restart, drain, rollback, restore, and delete.
  • Support CRD conversion webhooks.
  • Support canary and partitioned rollouts.
  • Support GitOps promotion gates.

4. Version Model

4.1 Versions

The operator tracks:

  • operator version,
  • CRD API version,
  • lifecycle contract version,
  • NF image version and digest,
  • NF binary SDK version,
  • YANG schema digest,
  • canonical config revision,
  • session state schema version,
  • evidence bundle digest.

4.2 Compatibility Matrix

Every release MUST publish:

operator: 0.4.0
supports:
  crd_versions: ["v1alpha1", "v1alpha2"]
  lifecycle_contracts: ["v1alpha1"]
  nf_images:
    opc-amf: ">=0.3.0 <0.5.0"
    opc-smf: ">=0.3.0 <0.5.0"
  yang_schema_digests:
    opc-amf:
      - "sha256:..."

The operator MUST reject unsupported combinations unless an explicit waiver is present and policy allows it.

5. Lifecycle State Machine

Every reconcile moves through:

PhasePurpose
AdmittedCR accepted by admission policy
Resolvedimage, config, secrets, devices, and dependencies resolved
Provisioningworkload resources created/updated
Bootstrappingpod reachable and management plane alive
Configuringcanonical config applied
Verifyingdrift, health, and readiness checked
Readyservice is available
Drainingrollout/delete drain in progress
Migratingschema/state migration in progress
Degradedservice impaired but not terminal
Failedreconciliation cannot proceed without operator action
Terminatingdeletion finalizers active

Phase names are public API and MUST be stable.

6. Conditions and Events

Required conditions:

  • Admitted
  • Resolved
  • Provisioned
  • Bootstrapped
  • ConfigResolved
  • AppConfigApplied
  • Drift
  • MigrationReady
  • MigrationApplied
  • DrainReady
  • RollbackAvailable
  • Ready

Each condition MUST include:

  • status,
  • reason,
  • message,
  • observed generation,
  • last transition time.

Event reasons MUST be stable and documented. Events MUST NOT contain secrets or raw config payloads.

7. Admission and Policy

Admission MUST verify:

  • image digest present,
  • image signature valid,
  • evidence bundle available where required,
  • CRD field validation,
  • canonical config reference exists,
  • manual/break-glass authority policy,
  • required secrets and service accounts,
  • pod security exceptions,
  • per-NF node resource references.

Admission should reject failures early rather than allowing a reconcile to fail late in the workload.

8. Canonical Config Lifecycle

8.1 Revision

canonicalConfigRevision is opaque but immutable for a given config artifact. Changing config content MUST change the revision or digest.

8.2 Apply

The operator applies config through RFC 001 management APIs. It MUST:

  • verify schema digest,
  • run validate-only before commit where supported,
  • use idempotency keys for retries,
  • record applied revision and tx ID,
  • read back running config for drift detection.

8.3 Drift

Drift states:

  • InSync
  • DriftDetected
  • BreakGlassActive
  • ResyncRequired
  • Unknown

Runtime state such as counters and sessions MUST be filtered out of drift comparison.

9. CRD Versioning and Conversion

Public lifecycle CRDs MUST use hub-and-spoke conversion once a second served version exists.

Rules:

  • one storage version at a time,
  • conversion webhooks are deterministic,
  • lossy conversion is forbidden unless the target version has an explicit status condition and known gap,
  • deprecated fields retain read compatibility for at least one minor release,
  • removed fields require migration notes and evidence.

Conversion tests MUST include round trips for every CRD version pair.

10. YANG Schema Migration

YANG migration follows RFC 002. Operator responsibilities:

  • detect persisted schema digest,
  • select migration chain,
  • run validate-only against target NF before commit,
  • back up previous config envelope before migration,
  • record migration tx ID,
  • fail closed if migration chain is missing.

Per-NF migrations MUST be deterministic and golden-tested.

11. State Migration

Session and durable state migrations are separate from config migrations.

State migration plans MUST define:

  • source version,
  • target version,
  • online/offline mode,
  • rollback support,
  • validation query,
  • maximum expected duration,
  • data-loss risk,
  • RPO/RTO impact.

Authoritative session migrations MUST preserve RFC 004 generation and fencing semantics.

12. Rollout Strategies

Supported strategies:

StrategyUse
rollingstateless or safely drainable NFs
partitionedstateful sets and ordered migrations
canaryhigh-risk release or config change
blue-greenmajor upgrades or incompatible config/state changes
manualoperator-approved special cases

Each NF declares allowed strategies.

13. Drain and Handover

Before terminating or replacing a pod, the operator MUST invoke or observe NF drain where the NF is stateful.

Drain contract:

#![allow(unused)]
fn main() {
pub enum DrainMode {
    RejectNewWork,
    TransferOwnership,
    FlushAndStop,
    ImmediateEmergency,
}
}

Drain MUST:

  • mark readiness false before removing work,
  • stop new session ownership,
  • transfer or release leases where possible,
  • flush audit and local state,
  • respect timeout,
  • expose progress in status.

UPF, AMF, SMF, ePDG, N3IWF, SMSC, and IMS NFs MUST define NF-specific drain behavior.

14. Rollback and Downgrade

14.1 Rollback

Rollback is allowed when:

  • previous image digest is still policy-allowed,
  • previous config schema is compatible or migration back exists,
  • state schema supports downgrade or state can be rebuilt,
  • evidence permits rollback.

14.2 Downgrade

Downgrade is forbidden by default for stateful NFs unless explicitly supported. If downgrade is unsupported, the operator MUST fail before changing workload resources.

14.3 Failed Rollout

On failed rollout:

  1. Stop further pod replacement.
  2. Preserve logs/events/evidence references.
  3. Mark Degraded or Failed.
  4. Attempt rollback only if policy says automatic rollback is safe.
  5. Require manual approval for destructive recovery.

15. Backup and Restore

Before high-risk migration, the operator MUST ensure backups exist for:

  • canonical config,
  • shadow-security material where policy allows,
  • session state if durable and required,
  • audit state,
  • CR status needed for recovery.

Restore MUST be tested per NF and recorded in RFC 006 evidence.

16. Multi-Cluster Lifecycle

In multi-cluster deployments:

  • management cluster owns desired lifecycle state,
  • workload clusters own local pod status,
  • status aggregation is explicit,
  • cluster identity is part of every condition source,
  • rollout waves are region-aware,
  • rollback can be per-cluster or global.

The operator MUST avoid applying incompatible migrations to only part of a fenced session ownership domain.

17. Observability

Required metrics:

  • opc_operator_reconcile_total{kind,outcome}
  • opc_operator_reconcile_duration_seconds{kind,phase}
  • opc_operator_rollout_total{kind,strategy,outcome}
  • opc_operator_migration_total{kind,type,outcome}
  • opc_operator_drain_total{kind,outcome}
  • opc_operator_drift_observations_total{kind,state}
  • opc_operator_rollback_total{kind,outcome}
  • opc_operator_version_skew{kind}

Required status fields:

  • current image digest,
  • desired image digest,
  • applied config revision,
  • applied config hash,
  • running schema digest,
  • last successful tx ID,
  • evidence bundle digest,
  • migration state.

18. Module Ownership

ModuleResponsibility
operator-lifecycleshared phase/condition composition
operator-compatcompatibility matrix parser/evaluator
operator-config-applyvalidate-only, commit, readback
operator-conversionCRD conversion webhook helpers
operator-migrationconfig/state migration orchestration
operator-rolloutrolling/canary/blue-green strategies
operator-drainNF drain API clients and progress
operator-backupbackup/restore orchestration
operator-testkitfake NF, fake config bus, fake session store

Agents must keep NF-specific reconcile logic behind interfaces and avoid duplicating phase/condition code.

19. Testing Requirements

19.1 Unit Tests

  • Compatibility matrix evaluation.
  • Phase transition reducer.
  • Condition reason stability.
  • CRD conversion round trips.
  • Migration chain selection.
  • Rollback eligibility.

19.2 Integration Tests

  • Install fresh NF.
  • Config-only update.
  • Image-only update.
  • Image plus config update.
  • Failed validate-only blocks rollout.
  • Drift detection and resync.
  • Canary success and failure.
  • Rollback with compatible config.

19.3 Fault Injection

  • Operator restart mid-rollout.
  • NF pod deleted during migration.
  • gNMI commit timeout.
  • Conversion webhook unavailable.
  • Backup failure.
  • Session drain timeout.
  • Partial multi-cluster rollout failure.

19.4 Performance Gates

  • Reconcile avoids hot loops under persistent failure.
  • 1,000 lifecycle CRs do not exceed configured API QPS.
  • Drift compare for large config stays within budget.
  • Status update rate is bounded.

20. Acceptance Criteria

This RFC is implemented when:

  1. Operator/NF/version compatibility is machine-readable and enforced.
  2. Lifecycle phases and conditions are stable across all CNFs.
  3. Config apply uses RFC 001 validate/commit/readback behavior.
  4. CRD conversions are deterministic and tested.
  5. YANG and state migrations are explicit and evidence-linked.
  6. Stateful rollouts drain or transfer ownership before termination.
  7. Rollback eligibility is evaluated before workload mutation.
  8. Multi-cluster rollout status is explicit and safe.

OPC-SDK-RFC-010: Data Governance, Privacy, and Regulated Records

Status: Draft for Implementation
Version: 1.0.0
Date: 2026-05-19
Audience: security engineers, privacy reviewers, NF owners, LI/charging implementers, SREs

1. Abstract

This RFC defines the data governance substrate for OpenPacketCore CNFs. It standardizes classification, handling, redaction, retention, encryption, backup, export, audit, and evidence rules for subscriber identifiers, session records, charging data, lawful-intercept material, analytics, security logs, and management configuration.

The purpose is to ensure that every CNF treats sensitive telecom data consistently and that privacy behavior is implemented as an auditable platform contract, not as scattered per-NF convention.

2. Scope

2.1 In Scope

  • Data classification taxonomy.
  • SUPI/GPSI/MSISDN/IP address handling.
  • Charging, audit, lawful-intercept data classification, analytics, and session state records.
  • Redaction and pseudonymization.
  • Retention and deletion.
  • Backup and restore handling.
  • Export and external sink policy.
  • Tenant/slice/PLMN data boundaries.
  • Evidence and test requirements.

2.2 Out of Scope

  • Cryptographic key management internals. See RFC 003.
  • Session store consistency. See RFC 004.
  • Evidence bundle mechanics. See RFC 006.
  • Product lawful-intercept mediation, collection workflows, and target-specific LI policy engines. The SDK classifies and protects LI material; it does not implement an LI product subsystem.
  • Jurisdiction-specific legal interpretation.

3. Design Goals

3.1 Security

  • Minimize sensitive data exposure by default.
  • Encrypt regulated data at rest and in transit.
  • Prevent cross-tenant, cross-slice, and cross-PLMN data leakage.
  • Make audit and regulated exports tamper-evident.
  • Ensure backup and debug workflows preserve classification.

3.2 Performance

  • Redaction and classification must be cheap enough for hot-path logging.
  • High-volume telemetry must avoid high-cardinality raw identifiers.
  • Bulk retention jobs must be bounded and schedulable.
  • Analytics minimization must be profile-driven and measurable.

3.3 Maintainability

  • One classification vocabulary across all CNFs.
  • Generated redaction metadata from RFC 002 drives code behavior.
  • Retention policies are declarative through YANG.
  • Exceptions are structured known gaps or waivers.

3.4 Functionality

  • Support operational debugging without leaking raw subscriber data.
  • Support charging and audit records with correct retention.
  • Classify lawful-intercept material and keep it separated from ordinary telemetry, analytics, support bundles, and exports.
  • Support analytics minimization and privacy-preserving export.

4. Data Classification

4.1 Classes

ClassExamplesDefault Handling
publicbuild version, static feature flagslog/export allowed
operationalreadiness, queue depth, non-sensitive counterslog/export allowed with cardinality controls
network-sensitivetopology, NF instance IDs, peer FQDNsrestricted logs, auth-gated debug
subscriber-idSUPI, IMSI, GPSI, MSISDN, PEIredacted or keyed digest
subscriber-sessionPDU session, TEID, SEID, IP address, QoS stateencrypted, access-controlled
security-secretkeys, tokens, credentials, OP/OPc/Knever logged, secret types
charging-recordCDR, usage, rating inputsretained/exported by charging policy
lawful-interceptwarrant, target selectors, X2/X3 productsLI plane only
analytics-sensitiveNWDAF source events, location, behavior tracesminimized before export
audit-regulatedadmin actions, break-glass, security eventstamper-evident retention

Each data field in generated models and hand-written domain types MUST be classified.

4.2 Classification Metadata

#![allow(unused)]
fn main() {
pub enum DataClass {
    Public,
    Operational,
    NetworkSensitive,
    SubscriberId,
    SubscriberSession,
    SecuritySecret,
    ChargingRecord,
    LawfulIntercept,
    AnalyticsSensitive,
    AuditRegulated,
}
}

Generated YANG metadata and Rust annotations MUST feed the same classification registry.

5. Identity and Pseudonymization

Raw SUPI/GPSI/MSISDN/PEI MUST NOT appear in:

  • metric labels,
  • info/warn/error logs,
  • ordinary traces,
  • backend keys,
  • Kubernetes Events,
  • unauthenticated debug output.

The default correlation form is a tenant-scoped keyed digest:

digest = HMAC(tenant_privacy_key, data_class || identifier_type || raw_value)

Digest keys MUST be purpose-separated from encryption keys. Rotating digest keys changes correlation IDs; this must be documented in operational runbooks.

6. Redaction

Redaction levels:

LevelBehavior
dropomit the field entirely
maskshow fixed placeholder
classshow class and presence only
length-classshow approximate length bucket
digestshow keyed digest
cleartextallowed only by explicit policy

cleartext is forbidden for security-secret and restricted for lawful-intercept.

Redaction MUST apply to:

  • logs,
  • traces,
  • metrics,
  • audit views,
  • admin/debug endpoints,
  • panic hooks,
  • error messages,
  • test snapshots committed to git.

7. Retention Policy

Each data class has a retention policy:

#![allow(unused)]
fn main() {
pub struct RetentionPolicy {
    pub class: DataClass,
    pub min_duration: Option<Duration>,
    pub max_duration: Option<Duration>,
    pub deletion_mode: DeletionMode,
    pub legal_hold_supported: bool,
    pub export_allowed: bool,
}
}

Retention MUST be configured through canonical YANG and surfaced in evidence.

Default posture:

  • operational telemetry: short retention,
  • audit-regulated: longer tamper-evident retention,
  • charging-record: charging policy retention,
  • lawful-intercept: legal/LI policy retention,
  • security-secret: no export, rotate/delete per key policy.

Legal hold prevents deletion of matching regulated records. It MUST:

  • be authenticated and authorized,
  • be audited,
  • include scope and expiry,
  • be visible to retention jobs,
  • not expose target selectors outside authorized LI/audit roles.

Deletion jobs MUST be idempotent and evidence-producing. They MUST avoid deleting records under legal hold.

9. Data Boundaries

The platform enforces boundaries by:

  • tenant,
  • slice/S-NSSAI,
  • PLMN,
  • region,
  • NF instance,
  • data class.

Every storage key, audit query, export job, and backup manifest MUST include boundary metadata. Cross-boundary export is denied by default.

10. Backups and Restore

Backups MUST preserve:

  • classification metadata,
  • encryption envelope metadata,
  • tenant and slice boundary,
  • retention policy,
  • legal hold flags,
  • manifest digests.

Restore MUST verify that destination tenant/slice/PLMN policy allows the data. Restoring LI or security-secret material into a different environment is denied unless an explicit recovery policy allows it.

11. Charging Records

Charging records are regulated operational records. CNFs that produce charging data MUST:

  • classify records as charging-record,
  • avoid raw identifiers in logs,
  • use durable, auditable write path,
  • support duplicate detection/idempotency,
  • expose export status,
  • test retention and replay behavior.

Charging exports MUST be signed or transmitted over authenticated channels.

12. Lawful Intercept Data

LI data is a special class with strict separation:

  • X1 management/control material,
  • X2 intercept-related information,
  • X3 content/user-plane products.

LI records MUST NOT share ordinary audit, telemetry, or debug paths unless the path is explicitly LI-authorized. LI selectors and products MUST be encrypted, audited, and retained according to LI policy.

CNFs that are not LI functions MUST NOT adopt LI vocabulary for ordinary analytics or operational telemetry.

13. Analytics and Privacy

Analytics-producing CNFs, especially NWDAF, MUST implement minimization before export.

Minimization methods:

  • field drop,
  • coarsening,
  • keyed hash,
  • aggregation threshold,
  • k-anonymity threshold,
  • differential privacy noise where policy requires it.

The active minimization policy version MUST be recorded with each analytics export.

14. Debug and Support Bundles

Support bundles MUST:

  • exclude secrets by default,
  • redact subscriber identifiers,
  • include manifest and classification summary,
  • require authorization,
  • be time-bounded,
  • be audited,
  • be signed or checksummed.

Debug packet captures are disabled by default and require explicit policy.

15. Configuration Model

Shared YANG groupings SHOULD include:

  • data-governance/classification-overrides
  • data-governance/retention
  • data-governance/export-policy
  • data-governance/legal-hold
  • data-governance/redaction
  • data-governance/support-bundle

NF-specific YANG can refine but not bypass the baseline.

16. Observability

Required metrics:

  • opc_data_records_total{class,operation,outcome}
  • opc_data_redactions_total{class,level}
  • opc_data_retention_deletions_total{class,outcome}
  • opc_data_legal_holds{class,state}
  • opc_data_exports_total{class,outcome}
  • opc_data_policy_version_info{class,version}
  • opc_data_privacy_minimization_total{method,outcome}

Metrics MUST NOT use raw subscriber identifiers as labels.

17. Evidence Requirements

RFC 006 evidence MUST include:

  • classification registry,
  • retention policy report,
  • redaction test report,
  • export policy report,
  • legal hold test report,
  • privacy minimization report for analytics NFs,
  • known gaps for any class not fully handled.

18. Module Ownership

ModuleResponsibility
opc-data-governanceclass registry, retention policy, legal-hold policy, and annotations
opc-redactionredaction renderers and generated metadata adapter
opc-privacydigesting, minimization, support bundle policy
opc-exportsigned/exported data handling
opc-evidencedata-governance evidence reports and release gates
opc-sdk-integrationintegration tests covering redaction, retention, export, and analytics policy

Agents implementing NF features must classify new fields before exposing logs, metrics, storage, or exports.

19. Testing Requirements

19.1 Unit Tests

  • Classification coverage.
  • Redaction levels.
  • Keyed digest stability.
  • Retention eligibility.
  • Legal hold blocks deletion.
  • Support bundle manifest redaction.

19.2 Integration Tests

  • NF logs contain no raw SUPI/GPSI/MSISDN.
  • Metrics reject high-cardinality raw labels.
  • Backup/restore preserves classification.
  • Export denied across tenant boundary.
  • Analytics minimization records policy version.

19.3 Fault Injection

  • Missing privacy digest key.
  • Retention job interrupted.
  • Export sink unavailable.
  • Backup manifest tampered.
  • Legal hold expiry during deletion.

19.4 Performance Gates

  • Hot-path redaction p99 under 5 microseconds for scalar identifiers.
  • Digest generation p99 under 25 microseconds.
  • Retention jobs respect configured I/O budget.
  • Metrics classification checks do not allocate on common paths.

20. Acceptance Criteria

This RFC is implemented when:

  1. Every generated and hand-written sensitive field has a data class.
  2. Raw subscriber identifiers do not appear in logs, metrics, traces, events, backend keys, or support bundles by default.
  3. Retention and legal hold policies are declarative and tested.
  4. Backups, restores, and exports preserve classification metadata.
  5. LI data is separated from ordinary telemetry and analytics.
  6. Analytics exports record minimization policy.
  7. RFC 006 evidence reports classification, redaction, retention, and privacy behavior.

OPC-SDK-RFC-011: Node and Data-Plane Resource Contract

Status: Draft for Implementation
Version: 1.0.0
Date: 2026-05-19
Audience: UPF/data-plane engineers, platform engineers, Kubernetes operators, security reviewers

1. Abstract

This RFC defines the node, kernel, NIC, CNI, CPU, memory, and pod-security contract required by OpenPacketCore data-plane and signaling-heavy CNFs. It standardizes how CNFs request and verify SR-IOV, Multus, AF_XDP, XDP/eBPF, hugepages, NUMA alignment, CPU pinning, IRQ affinity, device plugins, kernel features, and pod security exceptions.

The goal is to make data-plane performance and privilege requirements explicit, admissible, testable, and portable across carrier Kubernetes environments.

2. Scope

2.1 In Scope

  • Node capability discovery.
  • Kubernetes scheduling/resource requests.
  • Multus and SR-IOV attachment contracts.
  • AF_XDP/XDP/eBPF requirements.
  • CPU pinning, NUMA, hugepages, and IRQ affinity.
  • Pod security exceptions and capability minimization.
  • Data-plane preflight and readiness.
  • Metrics and conformance tests for platform resources.

2.2 Out of Scope

  • Packet parser behavior. See RFC 005.
  • Session state consistency. See RFC 004.
  • Runtime task supervision. See RFC 008.
  • Vendor-specific NIC tuning beyond declared capability adapters.

3. Design Goals

3.1 Security

  • Grant only the minimum Linux capabilities needed by each CNF.
  • Bind privileged data-plane pods to explicitly labeled nodes.
  • Prevent untrusted workloads from using OpenPacketCore data-plane device resources.
  • Make kernel/eBPF program loading auditable.

3.2 Performance

  • Preserve CPU, cache, NUMA, NIC queue, and IRQ locality.
  • Avoid noisy-neighbor interference on data-plane cores.
  • Provide deterministic preflight before declaring readiness.
  • Expose packet drop and queue pressure metrics.

3.3 Maintainability

  • One shared contract for platform assumptions.
  • Per-NF specs declare deviations through structured resource profiles.
  • Device and kernel feature detection is reusable.
  • CI can verify chart/resource generation without real NICs.

3.4 Functionality

  • Support UPF AF_XDP fast path.
  • Support ePDG/N3IWF IPsec and tunnel workloads.
  • Support L4 UDP fan-in proxy.
  • Support SCTP-heavy AMF/SMS/IMS workloads.
  • Support lab mode without hardware acceleration.

4. Resource Profiles

Each CNF declares a resource profile:

#![allow(unused)]
fn main() {
pub enum DataPlaneProfile {
    ControlPlaneOnly,
    SignalingHeavy,
    KernelNetworking,
    AfXdpFastPath,
    SriovFastPath,
    IpsecGateway,
}
}

Profiles determine required node labels, capabilities, CNI attachments, and preflight checks.

IpsecGateway is a resource and admission profile only in the current SDK. It does not imply that this repository ships IKEv2, ESP, xfrm orchestration, or N3IWF/NWu procedure implementations. Those protocol crates are required for a selected ePDG/N3IWF/untrusted-access product target, but are not a blocker for the current AMF-lite/N2/N1 first-NF profile.

5. Node Capability Discovery

The platform MUST provide a node capability report:

node:
  kernel: "6.8.0"
  bpf:
    cap_bpf: true
    xdp_supported: true
    btf_available: true
  cpu:
    manager_policy: static
    isolated_cores: "2-15"
    numa_nodes: 2
  memory:
    hugepages_2Mi: 4096
    hugepages_1Gi: 8
  nics:
    - name: ens5f0
      driver: ice
      sriov_vfs: 16
      xdp_modes: ["native", "skb"]
      queues: 32

The operator or node agent MUST publish this through labels, annotations, or a custom resource.

6. Scheduling Contract

Data-plane CNFs MUST use:

  • node selectors for required hardware,
  • tolerations for dedicated nodes,
  • pod anti-affinity where replicas need failure-domain separation,
  • topology spread constraints,
  • resource requests/limits matching CPU Manager static policy,
  • hugepage requests where required,
  • device plugin resource requests for SR-IOV or specialized devices.

The operator MUST reject a lifecycle CR if no eligible node can satisfy the declared profile, unless lab mode allows software fallback.

7. CPU and NUMA

7.1 CPU Pinning

Data-plane workers SHOULD run on exclusive CPUs. Management and async control tasks MUST NOT run on those same pinned data-plane CPUs.

The runtime receives an explicit CPU allocation:

#![allow(unused)]
fn main() {
pub struct CpuLayout {
    pub data_plane_cores: Vec<CpuId>,
    pub control_plane_cores: Vec<CpuId>,
    pub management_cores: Vec<CpuId>,
    pub numa_node: Option<NumaNodeId>,
}
}

7.2 NUMA Locality

NIC queues, AF_XDP UMEM, hugepages, and worker threads SHOULD be NUMA-local. Preflight MUST warn or fail according to profile when locality is broken.

7.3 IRQ Affinity

The platform SHOULD pin NIC IRQs to the correct NUMA-local cores. The CNF MUST report IRQ affinity mismatches when detectable.

8. Memory and Hugepages

CNFs using DPDK-like or AF_XDP memory pools MUST declare:

  • hugepage size,
  • hugepage count,
  • per-queue buffer count,
  • max packet size,
  • headroom,
  • NUMA node.

The pod MUST request hugepages explicitly. Overcommitting data-plane memory is forbidden in production profiles.

9. Network Attachments

9.1 Multus

Each data-plane interface is a named attachment:

multus:
  n3:
    networkAttachmentDefinition: upf-n3
    interfaceName: n3
  n4:
    networkAttachmentDefinition: upf-n4
    interfaceName: n4
  n6:
    networkAttachmentDefinition: upf-n6
    interfaceName: n6

Canonical YANG defines interface roles; lifecycle CR values reference attachment objects only.

9.2 SR-IOV

SR-IOV profiles MUST define:

  • resource name,
  • VF trust/spoof-check settings,
  • VLAN policy,
  • link state policy,
  • allowed device drivers,
  • whether IPAM is static or dynamic.

The operator MUST validate that referenced SR-IOV resources are allowlisted for the NF kind.

10. AF_XDP and XDP/eBPF

AfXdpFastPath is a resource and admission profile only in the current SDK. It does not imply that this repository ships AF_XDP sockets, UMEM management, RX/TX rings, or packet I/O runtime support. Those crates are required for a selected UPF or other accelerated data-plane product target, but are not a blocker for the current AMF-lite/N2/N1 first-NF profile.

10.1 Kernel Requirements

AF_XDP fast-path profiles MUST declare:

  • minimum kernel version,
  • required BPF features,
  • required XDP mode,
  • required capabilities,
  • required maps and pin paths,
  • whether generic XDP fallback is allowed.

10.2 Capabilities

Allowed capabilities for AF_XDP profile:

  • CAP_BPF
  • CAP_NET_ADMIN
  • CAP_NET_RAW

CAP_SYS_ADMIN is forbidden in production profiles. If a kernel requires CAP_SYS_ADMIN, the node is not eligible.

10.3 eBPF Program Governance

eBPF programs MUST be:

  • built from source in release pipeline,
  • included in SBOM/evidence,
  • signed or digest-pinned,
  • loaded only from approved paths,
  • audited on load/unload,
  • pinned under controlled bpffs path.

11. Pod Security Exceptions

Baseline pod security remains:

  • run as non-root,
  • read-only root filesystem,
  • no privilege escalation,
  • drop all capabilities except explicit allowlist,
  • seccomp profile enabled,
  • AppArmor/SELinux profile where supported.

Every exception MUST be declared in:

  • per-NF spec,
  • Helm values,
  • operator admission policy,
  • RFC 006 evidence.

12. Data-Plane Preflight

Before readiness, data-plane CNFs MUST verify:

  • required interfaces exist,
  • link state is up where required,
  • MTU matches config,
  • NIC driver and queues match profile,
  • XDP attach succeeded,
  • BPF maps created,
  • hugepages allocated,
  • CPU layout applied,
  • session table initialized,
  • drop counters accessible.

Failures mark readiness false and emit alarms.

13. Lab and Fallback Modes

Lab mode MAY use:

  • veth instead of SR-IOV,
  • generic XDP instead of native XDP,
  • software packet path,
  • relaxed CPU pinning,
  • no hugepages.

Lab fallback MUST be visible in status and MUST NOT be silently used in production.

14. Observability

Required metrics:

  • opc_node_capability_info{node,kernel,profile}
  • opc_dataplane_interface_up{nf,interface}
  • opc_dataplane_rx_packets_total{nf,interface}
  • opc_dataplane_tx_packets_total{nf,interface}
  • opc_dataplane_drops_total{nf,interface,reason}
  • opc_dataplane_queue_fill_ratio{nf,interface,queue}
  • opc_dataplane_xdp_attach_total{nf,outcome}
  • opc_dataplane_bpf_map_entries{nf,map}
  • opc_dataplane_numa_mismatch{nf}
  • opc_dataplane_irq_affinity_mismatch{nf}

15. Configuration Model

Shared YANG groupings SHOULD include:

  • resources/cpu
  • resources/numa
  • resources/hugepages
  • resources/interfaces
  • resources/xdp
  • resources/sriov
  • resources/preflight

Lifecycle CRDs reference Kubernetes resource names; dense tuning lives in YANG.

16. Module Ownership

ModuleResponsibility
opc-node-capabilitiesnode feature report parser/model
opc-resource-admissionoperator resource validation
opc-cpu-layoutCPU/NUMA layout helpers
opc-net-attachMultus/SR-IOV model helpers
opc-af-xdp-platformAF_XDP preflight and map metadata
opc-bpf-governanceBPF artifact digest/load audit
opc-resource-testkitfake node capabilities and chart tests

Agents implementing UPF or similar CNFs must consume these modules rather than hard-coding node assumptions.

17. Testing Requirements

17.1 Unit Tests

  • Node capability parsing.
  • Resource profile validation.
  • CPU layout validation.
  • SR-IOV allowlist policy.
  • Capability exception rendering.
  • Lab fallback status.

17.2 Integration Tests

  • Helm renders correct resource requests.
  • Operator rejects unsatisfied node profile.
  • AF_XDP preflight succeeds with fake capabilities.
  • Production profile rejects CAP_SYS_ADMIN.
  • Readiness false when required interface is missing.

17.3 Fault Injection

  • XDP attach failure.
  • Hugepage allocation failure.
  • NIC link down.
  • NUMA mismatch.
  • IRQ affinity mismatch.
  • Device plugin resource unavailable.

17.4 Performance Gates

  • Preflight completes within configured startup budget.
  • Data-plane metrics scrape does not stall packet workers.
  • Resource admission for 1,000 CNF CRs stays within operator API budget.

18. Acceptance Criteria

This RFC is implemented when:

  1. Data-plane CNFs declare structured resource profiles.
  2. Operator admission rejects unsatisfied production resource requirements.
  3. CPU, NUMA, hugepage, NIC, and CNI assumptions are explicit.
  4. AF_XDP/eBPF programs are governed by signed/digest-pinned artifacts.
  5. Pod security exceptions are minimal and evidence-linked.
  6. Readiness depends on data-plane preflight.
  7. Lab fallback cannot silently enter production.

OPC-SDK-RFC-012: Common Testbed, Simulator, and Scenario Framework

Status: Draft for Implementation
Version: 1.0.0
Date: 2026-05-19
Audience: test engineers, NF implementers, conformance owners, SREs

1. Abstract

This RFC defines the shared OpenPacketCore testbed and simulator framework. It standardizes reusable peer simulators, virtual time, traffic scenarios, protocol fixtures, conformance packs, chaos hooks, load profiles, and evidence output.

The purpose is to prevent every CNF from building isolated mocks that cannot compose into end-to-end 5G scenarios. The framework lets multiple contributors implement NFs independently while verifying them against the same scenario language and peer behavior.

2. Scope

2.1 In Scope

  • Peer simulators for UE, gNB, AMF, SMF, UPF, NRF, AUSF, UDM, PCF, NSSF, SCP, SEPP, SMSC, and other core peers.
  • Protocol fixture management and PCAP replay.
  • Virtual time and deterministic timers.
  • Scenario DSL.
  • Conformance scenario packs.
  • Load and soak profiles.
  • Chaos and fault injection hooks.
  • Evidence output for RFC 006.

2.2 Out of Scope

  • Production NF logic.
  • Standards certification by external bodies.
  • Full radio access network simulation beyond interfaces required for core testing.

3. Design Goals

3.1 Security

  • Test secrets must be synthetic and clearly marked.
  • Fixtures containing real subscriber data are forbidden.
  • Negative tests must cover malformed and hostile peer behavior.
  • Testbed artifacts must not weaken production code paths.

3.2 Performance

  • Simulators must support both deterministic unit-scale tests and high-rate load tests.
  • Virtual time should make timer-heavy procedures fast and deterministic.
  • Load profiles must be reproducible.

3.3 Maintainability

  • One scenario DSL across all CNFs.
  • Reusable protocol fixtures and peer simulators.
  • Test evidence links back to RFC 006 requirement IDs.
  • Each simulator has a documented fidelity level.

3.4 Functionality

  • Support component, integration, end-to-end, conformance, chaos, and performance testing.
  • Support both in-process and Kubernetes-deployed test modes.
  • Support golden traces and expected state assertions.

4. Crate and Tooling Layout

crates/opc-testbed/
  src/
    lib.rs
    scenario.rs
    virtual_time.rs
    assertions.rs
    fixtures.rs
    pcap.rs
    load.rs
    evidence.rs
    chaos.rs
    simulators/
      nrf.rs
      amf.rs
      smf.rs
      upf.rs
      epc.rs
      gnb.rs
      ue.rs
      ausf.rs
      udm.rs
      pcf.rs
      nssf.rs
      scp.rs
      sepp.rs

Each NF MAY also provide opc-<nf>-testkit, but NF testkits SHOULD build on opc-testbed.

5. Scenario DSL

Scenarios are declarative:

id: AMF-REG-001
title: UE registration success
requirements:
  - REQ-3GPP-TS23502-R17-4.2.2-001
topology:
  nfs:
    amf: { image: opc-amf:test }
    nrf: { simulator: nrf-basic }
    ausf: { simulator: ausf-5g-aka }
    udm: { simulator: udm-auth-sdm }
steps:
  - send_ngap:
      from: gnb-1
      to: amf
      message: InitialUEMessage.registration_request
  - expect_sbi:
      from: amf
      to: ausf
      operation: Nausf_UEAuthentication.Authenticate
  - expect_ngap:
      from: amf
      to: gnb-1
      message: InitialContextSetupRequest
assertions:
  - amf.ue_context.state == REGISTERED

The DSL MUST be versioned and schema-validated.

6. Simulator Fidelity Levels

LevelMeaning
stubfixed responses only
stateful-mockprotocol-aware state machine, simplified
procedure-faithfulfollows normative procedure enough for conformance
load-modeloptimized for traffic generation
adversarialemits malformed, delayed, duplicated, or hostile behavior

Every simulator MUST declare its fidelity level per interface.

7. Virtual Time

The testbed MUST provide a virtual clock compatible with RFC 008 runtime clocks.

Use cases:

  • NAS timers,
  • PFCP heartbeat,
  • NRF heartbeat,
  • retry/backoff,
  • session lease expiry,
  • SMS retry/expiry,
  • retention jobs.

Tests MUST NOT sleep real time for long protocol timers when virtual time can advance deterministically.

8. Protocol Fixtures and PCAP

Fixtures MUST include:

  • source standard reference,
  • release/version,
  • generation tool or capture provenance,
  • whether synthetic or captured,
  • sanitization status,
  • expected decode result,
  • linked requirement IDs.

Real customer/subscriber captures are forbidden in the public repository.

PCAP replay MUST support:

  • timestamp-preserving mode,
  • accelerated mode,
  • deterministic mode,
  • packet mutation for fuzz-style tests.

9. Peer Simulators

Minimum simulator set:

  • UE/NAS procedure driver.
  • gNB/NGAP over SCTP driver.
  • NRF SBI simulator.
  • AUSF/UDM auth and subscription simulators.
  • SMF/UPF/PFCP simulator pair.
  • EPC and untrusted-access peer skeletons such as PGW S2b and Diameter metadata peers. These must consume SDK protocol-crate decoded views and must not introduce local product parsers.
  • PCF policy simulator.
  • NSSF slice selection simulator.
  • SCP routing simulator.
  • SEPP partner simulator.
  • SMSC/SMSF/SMPP simulators.

Simulators MUST expose deterministic state assertions.

10. Test Modes

ModePurpose
in-processfast component integration
multi-processlocal network behavior
kindKubernetes operator/chart validation
hardware-labSR-IOV/AF_XDP/real NIC validation
chaosfailure injection
soaklong-running reliability

The same scenario SHOULD run in multiple modes where practical.

11. Fault Injection

Faults:

  • packet loss,
  • reordering,
  • duplication,
  • malformed protocol messages,
  • delayed responses,
  • peer restart,
  • NRF outage,
  • token expiry,
  • backend timeout,
  • clock skew,
  • node drain,
  • network partition.

Faults MUST be declarative in scenarios and evidence-linked.

12. Load Profiles

Load profiles define:

  • arrival distribution,
  • subscriber population,
  • slice distribution,
  • DNN distribution,
  • session duration,
  • mobility/handover rate,
  • message mix,
  • target throughput,
  • duration,
  • pass/fail SLOs.

Profiles MUST be reproducible from seeds.

13. Assertions

Assertions may target:

  • protocol messages,
  • SBI calls,
  • config state,
  • session store records,
  • metrics,
  • logs,
  • traces,
  • alarms,
  • Kubernetes status,
  • evidence output.

Assertions MUST avoid depending on nondeterministic ordering unless explicitly marked.

14. Evidence Output

Each scenario run emits:

{
  "scenario_id": "AMF-REG-001",
  "requirements": ["REQ-..."],
  "mode": "kind",
  "seed": 1234,
  "artifacts": ["trace.json", "metrics.prom", "events.json"],
  "outcome": "pass"
}

RFC 006 consumes these records for conformance reports.

15. Security and Privacy Rules

The testbed MUST:

  • generate synthetic subscriber identities,
  • mark all test keys as non-production,
  • reject fixture import without sanitization metadata,
  • prevent real bearer tokens from being stored in artifacts,
  • redact logs and traces through RFC 010 redaction.

16. Module Ownership

ModuleResponsibility
opc-testbed-scenarioDSL schema, parser, executor
opc-testbed-timevirtual clock and timer control
opc-testbed-fixturesfixture registry and provenance
opc-testbed-pcapPCAP replay and mutation
opc-testbed-sim-nrfNRF simulator
opc-testbed-sim-ranUE/gNB/NAS/NGAP drivers
opc-testbed-sim-sbigeneric SBI producer/consumer mock
opc-testbed-chaosfailure injection
opc-testbed-evidenceRFC 006 result emission

Agents implementing a new NF must add scenarios before declaring conformance.

17. Testing Requirements

17.1 Unit Tests

  • DSL schema validation.
  • Virtual time advancement.
  • Fixture provenance validation.
  • Deterministic seed behavior.
  • Assertion engine.

17.2 Integration Tests

  • Scenario runs against fake NF.
  • Mock NRF discovery and token flow.
  • PCAP replay into protocol parser.
  • Kind-mode lifecycle install and readiness.
  • Evidence JSON emitted and validated.

17.3 Fault Injection Tests

  • Peer timeout.
  • Malformed message.
  • Duplicate message.
  • Clock skew.
  • Node drain in kind.
  • Backend outage.

17.4 Performance Gates

  • In-process scenarios start under 100 milliseconds.
  • Virtual-time timer tests avoid long real sleeps.
  • Load generator reports achieved TPS and latency.
  • Scenario artifacts remain within configured size budgets.

18. Acceptance Criteria

This RFC is implemented when:

  1. A versioned scenario DSL exists.
  2. Shared peer simulators cover core 5G procedures.
  3. Virtual time is integrated with runtime/test clocks.
  4. Fixtures carry provenance and sanitization metadata.
  5. Scenarios emit RFC 006 evidence records.
  6. NF testkits build on the shared framework.
  7. Conformance and chaos scenarios are reusable across local and Kubernetes modes.

OPC-SDK-RFC-013: Fault Management and Alarm Substrate

Status: Draft for Implementation
Version: 1.0.0
Date: 2026-05-19
Audience: SREs, NF implementers, operator authors, observability engineers

1. Abstract

This RFC defines the OpenPacketCore fault management and alarm substrate. It standardizes alarm identity, severity, probable cause, affected object, raise/update/clear semantics, deduplication, suppression, correlation, Kubernetes condition mapping, gNMI/NETCONF notification projection, external fault-management sink integration, and evidence requirements.

Metrics, logs, and traces describe behavior. Alarms describe actionable service faults. Carrier CNFs need both.

2. Scope

2.1 In Scope

  • Alarm model and lifecycle.
  • Severity and probable-cause taxonomy.
  • Affected-object naming.
  • Raise, update, clear, acknowledge, suppress.
  • Alarm correlation and deduplication.
  • Mapping to Kubernetes conditions and events.
  • Mapping to gNMI/NETCONF notifications.
  • External FM sink integration.
  • Alarm metrics, audit, and tests.

2.2 Out of Scope

  • Full OSS/BSS ticketing implementation.
  • Vendor-specific FM protocols unless implemented as adapters.
  • Raw log aggregation.
  • Performance SLO alerting rules outside CNF-generated alarms.

3. Design Goals

3.1 Security

  • Alarms must not leak secrets or raw subscriber identifiers.
  • Alarm administration must be authorized.
  • Suppression and acknowledgement are audited.
  • LI/security alarms must preserve regulated handling boundaries.

3.2 Performance

  • Raising an alarm must be cheap and non-blocking.
  • Alarm storms must be deduplicated and rate-limited.
  • External sink outages must not block packet or request handling.

3.3 Maintainability

  • One alarm vocabulary across all CNFs.
  • Stable alarm IDs and probable causes.
  • Generated YANG notification projection.
  • Shared testkit for alarm lifecycle.

3.4 Functionality

  • Support active and historical alarms.
  • Support severity changes.
  • Support clear conditions.
  • Support suppression windows.
  • Support external sinks and local query.

4. Alarm Model

#![allow(unused)]
fn main() {
pub struct Alarm {
    pub alarm_id: AlarmId,
    pub alarm_type: AlarmType,
    pub severity: Severity,
    pub probable_cause: ProbableCause,
    pub affected_object: AffectedObject,
    pub tenant: Option<TenantId>,
    pub slice: Option<Snssai>,
    pub region: Option<RegionId>,
    pub text: RedactedText,
    pub details: AlarmDetails,
    pub raised_at: Timestamp,
    pub updated_at: Timestamp,
    pub cleared_at: Option<Timestamp>,
    pub correlation_id: Option<CorrelationId>,
}
}

AlarmId MUST be stable for the same active fault instance.

5. Severity

Severity levels:

SeverityMeaning
criticalservice outage, data loss, security boundary failure
majorserious degradation or redundancy loss
minorlimited impairment with workaround
warningapproaching fault or policy exception
indeterminatefault detected but impact unknown
clearedfault no longer active

Severity mapping MUST be consistent across CNFs.

6. Probable Cause Taxonomy

The SDK maintains a versioned taxonomy:

  • config-apply-failed
  • config-drift-detected
  • certificate-expiring
  • certificate-expired
  • identity-unavailable
  • authorization-policy-invalid
  • session-store-unavailable
  • lease-lost
  • backend-timeout
  • nrf-unreachable
  • sbi-overload
  • peer-unreachable
  • packet-drop-threshold
  • dataplane-preflight-failed
  • storage-corruption
  • audit-chain-invalid
  • key-unavailable
  • li-delivery-failed
  • charging-export-failed
  • privacy-policy-violation

Per-NF causes may be added but MUST be namespaced.

7. Affected Object

Affected objects use structured names:

#![allow(unused)]
fn main() {
pub enum AffectedObject {
    NfInstance { kind: NfKind, instance: InstanceId },
    Interface { nf: InstanceId, name: String },
    Peer { nf: InstanceId, peer_id: String },
    SessionStore { nf: InstanceId, shard: Option<String> },
    Slice { snssai: Snssai },
    Tenant { tenant: TenantId },
    Certificate { key_id: KeyId },
    DataPlaneQueue { nf: InstanceId, interface: String, queue: u16 },
}
}

Raw subscriber identifiers MUST NOT be affected-object names.

8. Alarm Lifecycle

States:

  • raised
  • updated
  • acknowledged
  • suppressed
  • cleared
  • expired

Lifecycle rules:

  • A repeated raise with same dedup key updates the active alarm.
  • Clear requires a matching active alarm or creates a no-op metric.
  • Acknowledgement does not clear.
  • Suppression does not delete history.
  • Severity downgrade is an update, not clear plus raise.

9. Deduplication and Correlation

Dedup key:

alarm_type || probable_cause || affected_object || tenant || slice

Correlation groups related alarms, such as:

  • NRF unavailable causing SBI discovery failures.
  • certificate expiry causing mTLS failures.
  • session store outage causing lease lost alarms.

Correlation MUST NOT hide critical alarms; it only helps presentation.

10. Suppression

Suppression may be:

  • maintenance window,
  • known outage,
  • test mode,
  • dependency alarm correlation.

Suppression requires authorization and audit. Security-critical alarms SHOULD not be suppressible unless carrier policy explicitly allows it.

11. Storage

The alarm store MUST support:

  • active alarm query,
  • historical alarm query,
  • append-only lifecycle events,
  • bounded retention,
  • tenant/slice filtering,
  • tamper-evident audit for admin actions.

Local storage may use RFC 001 persistence for management alarms. High-volume alarm history SHOULD be exported to an external FM system.

12. Projection to Kubernetes

Alarms map to Kubernetes Conditions and Events:

  • critical/major active alarms can drive Ready=False or Degraded=True according to NF policy,
  • warning alarms usually do not change readiness,
  • clear events update conditions when no other active alarm holds the state.

Condition reason strings MUST be stable.

13. Projection to gNMI/NETCONF

The alarm subsystem MUST expose:

  • active alarms operational tree,
  • alarm history operational tree,
  • notifications for raise/update/clear,
  • authorized acknowledge/suppress operations.

YANG notification generation SHOULD use RFC 002 metadata and RFC 006 evidence tags.

14. External FM Sinks

Sink adapters:

  • webhook,
  • Kafka/NATS,
  • OpenTelemetry events,
  • SNMP/NETCONF adapter where needed,
  • carrier OSS adapter.

External sink failure MUST:

  • raise a sink alarm,
  • buffer within limits if policy allows,
  • never block fast paths,
  • expose drop counters.

15. Alarm Sources

Common sources:

  • RFC 001 config commit failures,
  • RFC 003 identity/key/cert failures,
  • RFC 004 session store and lease failures,
  • RFC 007 SBI overload/discovery failures,
  • RFC 008 runtime task failures,
  • RFC 009 lifecycle migration failures,
  • RFC 011 data-plane preflight and drop thresholds,
  • RFC 010 privacy/legal-hold/export failures.

16. Observability

Required metrics:

  • opc_alarm_active{severity,cause}
  • opc_alarm_events_total{event,severity,cause}
  • opc_alarm_suppressed_total{cause}
  • opc_alarm_sink_delivery_total{sink,outcome}
  • opc_alarm_sink_queue_depth{sink}
  • opc_alarm_clear_without_active_total{cause}

Alarm text MUST be redacted through RFC 010.

17. Configuration Model

Shared YANG groupings SHOULD include:

  • alarms/severity-policy
  • alarms/suppression
  • alarms/sinks
  • alarms/retention
  • alarms/readiness-impact
  • alarms/correlation

Per-NF YANG may add alarm thresholds, such as packet drop ratio or peer outage duration.

18. Module Ownership

ModuleResponsibility
opc-alarm-modelalarm structs, severity, causes
opc-alarm-storeactive/history store
opc-alarm-managerraise/update/clear/dedup
opc-alarm-policysuppression and readiness impact
opc-alarm-k8scondition/event mapping
opc-alarm-yanggNMI/NETCONF operational projection
opc-alarm-sinkexternal sink adapters
opc-alarm-testkitalarm lifecycle fixtures

Agents adding new alarms must add taxonomy entries, tests, and evidence tags.

19. Testing Requirements

19.1 Unit Tests

  • Dedup key stability.
  • Severity transition.
  • Clear behavior.
  • Suppression authorization.
  • Redaction.
  • Readiness impact policy.

19.2 Integration Tests

  • Runtime task failure raises alarm.
  • Alarm maps to Kubernetes condition.
  • Alarm notification appears on gNMI subscription.
  • External sink receives raise/update/clear.
  • Sink outage buffers or drops according to policy.

19.3 Fault Injection

  • Alarm storm.
  • Sink outage.
  • Store unavailable.
  • Unauthorized suppression attempt.
  • Duplicate raise from many tasks.

19.4 Performance Gates

  • Alarm raise common path does not block longer than 100 microseconds.
  • Alarm storm of 10,000 duplicate events deduplicates without unbounded memory.
  • External sink outage does not impact protocol request p99.

20. Acceptance Criteria

This RFC is implemented when:

  1. Every CNF uses shared alarm model and manager.
  2. Alarm severity and probable cause taxonomy are stable and versioned.
  3. Raise/update/clear semantics are deterministic.
  4. Kubernetes conditions and events are derived consistently.
  5. gNMI/NETCONF alarm operational state and notifications are available.
  6. Suppression and acknowledgement are authorized and audited.
  7. External sink failures do not block service paths.
  8. Alarm behavior is covered by shared testkit and evidence.

OPC-SDK-RFC-014: Interactive Operational Console and Command Framework

Status: Draft for Implementation

Version: 0.1.0

Date: 2026-07-09

Audience: SDK implementers, CNF teams, SREs, platform IAM teams, security reviewers, TUI engineers

1. Abstract

This RFC defines a first-class interactive operational console for OpenPacketCore CNFs. The console restores the discoverable, persistent network-element shell experience familiar to mobile-core operators while preserving the SDK's declarative management invariant:

Infrastructure as Code owns desired configuration; the operational console observes state and invokes explicitly modeled operational actions.

The RFC standardizes:

  • a transport-neutral command catalog that CNFs use to describe their operational vocabulary;
  • an SDK registration API for mapping commands to YANG operational state, subscriptions, and typed actions;
  • catalog discovery over the existing authenticated gNMI and NETCONF management plane;
  • configurable human login through OIDC, OpenShift OAuth, SSH credentials, or workload identity;
  • persistent, identity-bound console sessions;
  • a responsive terminal user interface with contextual help, completion, streaming output, cancellation, paging, filtering, and safe history;
  • authorization, auditing, redaction, resource limits, versioning, and conformance requirements.

The TUI is not an optional wrapper around a client library. It is the primary human interface and an implementation acceptance boundary for this RFC.

2. Decision and Invariants

OpenPacketCore will provide an SDK-owned operational console framework and a reference Rust TUI application. A consuming CNF declares the commands that are meaningful for that network function and supplies operational state or typed action implementations. The SDK owns parsing, discovery, help, completion, transport selection, authentication integration, authorization hooks, audit, limits, presentation, and terminal behavior.

The following invariants are normative:

  1. The console MUST NOT expose configuration mutation through gNMI Set, NETCONF <edit-config>, candidate/running datastore mutation, or an equivalent escape hatch.
  2. A user MUST be able to discover ordinary operational commands without knowing a YANG path, XPath, protobuf service name, or transport protocol.
  3. CNFs MUST describe commands as bounded declarative data. A target MUST NOT send executable client code, scripts, terminal escape sequences, or native plugins to the console.
  4. The console MUST translate parsed commands into typed management operations. It MUST NOT send an arbitrary shell command string for remote execution.
  5. Every target operation MUST be authenticated, authorized, and audited at execution time. Help visibility is not an authorization decision.
  6. Trusted management-environment configuration supplies identity authorities, trust anchors, broker policy, and tenant-assignment policy; validated IdP claims supply human identity; broker policy and issued credentials bind the allowed tenant. A target or catalog MUST NOT supply or override any login, issuer, token, JWKS, callback, broker, or redirect endpoint, signed or unsigned.
  7. The interactive event loop MUST remain responsive while authentication, discovery, reads, subscriptions, actions, rendering, or reconnection are in progress.
  8. Remote text and values MUST be treated as untrusted input and rendered without allowing terminal control-sequence injection.
  9. One-shot and automation modes MAY reuse the same parser and execution engine, but they MUST NOT weaken or displace the interactive experience.

3. Scope

3.1 In Scope

  • Interactive login, connection, reconnection, logout, and identity display.
  • Persistent operator sessions against one selected CNF target.
  • Contextual ? help and tab completion at every grammar position.
  • Hierarchical operational command registration and discovery.
  • Read, monitor, bounded diagnostic, and authorized operational-action command classes.
  • gNMI Capabilities, Get, and Subscribe client adapters.
  • NETCONF capability discovery, <get>, <get-data>, and modeled RPC/action client adapters.
  • Future protocol adapters, including gNOI-style operational services.
  • Structured table, tree, detail, JSON, and streaming presentation.
  • Local paging, filtering, counting, and export of authorized results.
  • Human authentication provider integration and short-lived management credentials.
  • NACM read/subscribe/exec authorization and management audit integration.
  • CNF and TUI conformance testkits.

3.2 Out of Scope

  • A configuration shell or replacement for IaC workflows.
  • An arbitrary remote POSIX shell.
  • An SSH daemon that executes commands inside the CNF container.
  • Owning user passwords, MFA enrollment, or a general-purpose identity provider.
  • Treating every YANG node as a well-designed human command automatically.
  • Replacing OSS/BSS, fleet automation, or Kubernetes operators.
  • A browser-based management console. Such a console may consume the same catalog in a later RFC.
  • High-volume telemetry storage or analytics.

4. Terminology

TermMeaning
ConsoleThe complete human operational interface, including login, persistent target session, command engine, and terminal presentation.
TUIThe interactive terminal application. It includes the line-oriented network-element shell, help/pager overlays, and optional full-screen views.
Command catalogA bounded declarative description of the commands available from one authenticated target.
Command specificationOne stable command identity, grammar, help, operation plan, authorization metadata, and presentation specification.
Operation planA transport-neutral read, subscribe, or action plan produced after a command is parsed.
Management contextTrusted configuration describing targets, server trust, login provider, access broker, tenant policy, and client defaults for an environment.
Access brokerA management-domain service that exchanges an authenticated human session for short-lived protocol credentials.
CNF command moduleCNF-supplied registration code that adds validated command specifications and action implementations to the SDK framework.

5. Operator Experience

5.1 Primary Interaction

The expected primary flow is:

$ opc connect epdg-prod-1
Authentication required for context "production"
Opening the configured identity provider in your browser...

Logged in as alice@example.com
Tenant: mobile-prod
Connected to epdg-prod-1 (ePDG 2.4.1)
Management: gNMI + NETCONF

epdg-prod-1> ?
  show         Display operational state
  monitor      Stream changing operational state
  diagnose     Run bounded diagnostic operations
  clear        Clear explicitly modeled operational state
  describe     Explain a command, object, or capability
  whoami       Display the authenticated management identity
  exit         Close this console session

epdg-prod-1> show ?
  alarms                   Active alarms
  health                   Component health
  ike                      IKE operational state
  ipsec                    Child SA and tunnel state
  peers                    AAA and packet-core peers
  system                   Runtime and system information

epdg-prod-1> show ike security-associations peer 192.0.2.20
SPI              PEER          STATE        AGE       CHILD-SAS
0x7a94b23f       192.0.2.20    established  00:14:38  2

epdg-prod-1> monitor alarms severity major
Monitoring alarms. Press Ctrl-C to stop.
...

epdg-prod-1> diagnose ping 198.51.100.10 source-interface s2b
PING 198.51.100.10 from s2b
5 transmitted, 5 received, 0% loss, avg 8.3 ms

The user is not expected to know that these commands map to gNMI paths, NETCONF subtree filters, or YANG actions.

5.2 Interaction Requirements

The TUI MUST provide:

  • ? help after any complete or partial token;
  • tab completion after any complete or partial token;
  • unambiguous abbreviations in interactive mode only;
  • exact grammar in one-shot or script mode;
  • inline indication of required and optional arguments;
  • human-readable validation errors with the invalid token identified;
  • command examples and longer help through describe;
  • Ctrl-C to cancel the active operation without closing the console;
  • Ctrl-C at an idle prompt to clear the edit buffer;
  • Ctrl-D on an empty buffer or exit to close the console when no operation is active;
  • asynchronous progress indication for operations that do not return promptly;
  • terminal resize handling without losing the current command line;
  • pagination and horizontal handling for output larger than the viewport;
  • a no-color mode and usable output when color is unavailable;
  • stable machine-readable output when explicitly requested;
  • visible target, connection, and degraded/reconnecting state in the prompt.

Unknown commands MUST provide bounded suggestions. Ambiguous abbreviations MUST list the conflicting continuations instead of choosing one. Before confirming an abbreviated operate command, the TUI MUST expand and display its canonical syntax and target summary.

help [<command-prefix>] displays the command tree, help search <terms> searches bounded summaries/descriptions, and describe command <canonical command> displays grammar, arguments, effect, examples, availability, and output shape. Empty, locally filtered, permission-denied, unsupported, and temporarily unavailable results MUST use distinct messages and status values.

The console SHOULD support interactive-only local output pipelines such as:

epdg-prod-1> show alarms | include certificate
epdg-prod-1> show ike security-associations | count
epdg-prod-1> show peers | json

These are bounded local transformations over structured results. They are not shell pipelines and MUST NOT invoke local or remote programs.

5.3 Responsiveness Requirements

The TUI MUST use a non-blocking event loop separated from network and rendering workers by bounded channels. Under the console test profile:

  • keystroke echo and local cursor movement SHOULD complete within 32 ms at p95;
  • cached help and completion SHOULD appear within 100 ms at p95;
  • a progress indicator SHOULD appear within 150 ms when a remote operation has not produced output;
  • catalog validation and command-tree construction SHOULD complete within 100 ms for the maximum accepted catalog on the reference test host;
  • large results MUST stream or page within bounded memory rather than freezing input until the complete result is buffered;
  • cancellation MUST be observed by the local execution engine promptly and propagated to the active protocol adapter; the reducer SHOULD acknowledge local cancellation within 100 ms at p95 even when output is saturated.

Network handshake and remote processing latency are measured separately from local interaction latency. A slow target MUST NOT make typing, help, resize, or cancel handling unresponsive.

The conformance report defines the reference host, terminal emulator, catalog size, stream rate, resize load, sample window, and exact measurement points. Interaction latency MUST also be measured while the maximum supported output stream is active. The 150 ms progress threshold is a default subject to usability validation and is disabled in append-only accessibility mode.

5.4 Accessibility and Terminal Compatibility

The console MUST:

  • be fully keyboard operable;
  • not rely on color alone to communicate state or severity;
  • support plain output suitable for screen readers and log capture;
  • sanitize control characters and ANSI/OSC sequences in target-supplied text;
  • calculate display width safely for Unicode and malformed input;
  • degrade to a line-oriented interface when full-screen terminal capabilities are unavailable;
  • respect an explicit no-color configuration and the conventional NO_COLOR environment setting.

An append-only accessibility mode MUST avoid cursor addressing, animated spinners, overwritten progress lines, and asynchronous insertion into the current edit line. Severity and state are always expressed in text. TERM=dumb and non-TTY output use this mode or a documented machine-output mode rather than attempting full-screen behavior.

Full-screen dashboards and detail panes MAY be added, but the hierarchical shell MUST remain complete and usable by itself.

6. Architecture

                         Trusted management context
                      (targets, issuer, broker, trust)
                                      |
                                      v
+----------------------+      +----------------------+      +------------------+
| Rust operational TUI |----->| Management clients   |----->| CNF management   |
|                      |      |                      |      | endpoints         |
| command tree         |      | gNMI adapter         |      |                  |
| help/completion      |      | NETCONF adapter      |      | capabilities     |
| session state        |      | future adapters      |      | opstate          |
| safe rendering       |      +----------------------+      | subscriptions    |
+----------+-----------+                                    | modeled actions  |
           |                                                +---------+--------+
           |                                                          |
           | catalog                                                  |
           v                                                          v
+----------------------+                                  +---------------------+
| Command engine       |<---------------------------------| CNF command module  |
|                      |        declarative catalog       |                     |
| parse -> plan        |                                  | typed paths         |
| authorize -> execute |                                  | action providers    |
| render events        |                                  | presentation hints  |
+----------------------+                                  +---------------------+

Human login:

TUI -> configured IdP/OpenShift OAuth -> access broker -> short-lived
management credentials -> authenticated CNF connections

6.1 Separation of Responsibilities

ResponsibilitySDK/frameworkCNFPlatform/IAM
Command grammar model and validationYesSupplies entriesNo
Help, completion, parsing, and TUIYesSupplies descriptions/dataNo
Operational state implementationDefines contractYesNo
Typed action implementationDefines contractYesNo
gNMI/NETCONF client and server adaptersYesBinds serverNo
Login provider frameworkYesNoConfigures provider
Login page, password, and MFANoNoIdentity provider
Access-broker policy and trustDefines contract/referenceVerifies credentialOperates/configures
NACM policyDefines/enforcesIntegratesProvisions through IaC
Command-specific presentationValidates/rendersDeclares hintsNo
Console conformance testsYesMust passMay gate deployment

6.2 Dependency and Runtime Rules

The command model and execution semantics form the domain core. They MUST NOT depend on tonic, SSH/XML libraries, terminal libraries, OAuth clients, or other adapter wire types. Reader, subscriber, action, login, clock, and audit ports are owned by the domain/application layer; gNMI, NETCONF, identity-provider, broker, and terminal adapters implement those ports at the edges.

The Rust implementation uses Tokio for asynchronous composition. It MUST NOT perform blocking identity, network, filesystem, terminal, or rendering work on an async worker, and it MUST NOT hold a synchronous mutex across .await. Dynamic dispatch is appropriate at configurable provider/adapter boundaries; hot parsing and rendering paths SHOULD prefer static dispatch where practical.

Library errors use bounded, typed, payload-safe error enums. The binary may add operator context at its composition root, but adapter errors, XML/protobuf objects, tokens, and server payloads MUST NOT leak into domain errors. Library code MUST NOT panic on catalog, command, authentication, protocol, or terminal input.

Rust library crates use typed thiserror errors; the opc-console binary may use anyhow only at the outer composition/reporting boundary. Production code does not use unwrap, expect, or panic on externally influenced paths.

7. Command Model

7.1 Command Classes

Every command MUST declare one effect class:

ClassExamplesExpected behavior
observeshow health, show sessions, show peersRead-only, bounded result
monitormonitor alarms, watch peer stateLong-lived authorized subscription
probeping, traceroute, peer reachability testBounded active diagnostic with rate and destination limits
operateclear SA, reset peer, drain instanceOperational mutation with exec authorization and explicit confirmation policy
configureset address, change policy, edit datastoreProhibited by this RFC

The registry MUST reject the configure class and any operation plan containing a management configuration mutation. Calling an operation "operational" does not make it safe; probe and operate commands require explicit limits and authorization.

The classification test is based on the effect, not the command name or transport:

  • an operation is prohibited when it changes candidate, running, startup, rollback, or shadow-security configuration; invokes the config bus; changes a generated config-model value; establishes durable desired behavior that should survive reconciliation; or provides another path to accomplish those effects;
  • an operate action may change runtime/session state or an operational record only through a separately modeled action with an observable lifecycle, authorization path, bounded effect, and audit contract;
  • an operational action MUST NOT create hidden desired state or fight the Kubernetes operator/IaC reconciler;
  • when an effect could reasonably be represented as desired configuration, it is configuration unless the model and CNF owner document why it is a transient incident-response operation.

Accepted examples include clearing one IKE SA, acknowledging an alarm, running a bounded ping, or temporarily draining an instance with explicit expiry and observable status. Rejected examples include changing a peer address, setting a persistent drain flag, changing routing policy, installing a certificate, or writing a runtime override that survives reconciliation. Ambiguous actions fail registry review closed until their ownership is resolved.

Catalog metadata is descriptive and is not the enforcement boundary. Every CNF MUST maintain an independent server-side allowlist of operational action IDs and effect policies that applies to NETCONF, registered gRPC services, and any future adapter even when the caller does not use the console. The server denies unknown or configuration-capable actions before invoking CNF code. The allowlist is compiled or provisioned through trusted CNF composition/IaC and cannot be widened by the catalog or an action request.

SDK action handlers receive a restricted OperationalActionContext containing only the declared operational capabilities, deadline/cancellation, principal, audit, and bounded result sink. It does not expose ConfigBus, config-store writers, operator reconciliation inputs, or unrestricted service locators. The action-module dependency policy rejects direct dependencies on config-bus, config-store writer, and operator-reconciliation crates. Composition tests run every registered action against instrumented config/reconciliation fakes and fail on any attempted write. Catalog validation, dependency checks, restricted handler capabilities, server admission, and datastore/reconciliation evidence are all required; no one layer is sufficient by itself. These controls verify the supported composition boundary, not malicious code compiled deliberately outside it.

7.2 Command Specification

The transport-neutral model is conceptually:

#![allow(unused)]
fn main() {
pub struct CommandSpec {
    pub id: CommandId,
    pub version: CommandVersion,
    pub grammar: CommandGrammar,
    pub summary: HelpText,
    pub description: HelpText,
    pub examples: Vec<CommandExample>,
    pub effect: EffectClass,
    pub availability: CapabilityRequirement,
    pub authorization: AuthorizationSpec,
    pub operation: OperationPlan,
    pub presentation: PresentationSpec,
    pub limits: CommandLimits,
    pub deprecation: Option<Deprecation>,
}
}

All strings and collections MUST have explicit size limits. The wire catalog MUST use stable enums and versioned schemas rather than serializing Rust trait objects or closures.

CommandId is the stable API identity. Human syntax may gain aliases or be reorganized while the identity remains stable for audit, telemetry, and compatibility.

7.3 Grammar

A grammar is a bounded tree of:

#![allow(unused)]
fn main() {
pub enum GrammarNode {
    Literal {
        token: CommandToken,
        aliases: Vec<CommandToken>,
        help: HelpText,
    },
    Argument {
        name: ArgumentName,
        value: ValueSpec,
        sensitive: bool,
        completion: CompletionSpec,
    },
    Optional(Vec<GrammarNode>),
    Choice(Vec<Vec<GrammarNode>>),
}
}

Unbounded recursion, arbitrary regular expressions, executable validators, and target-supplied parser code are prohibited. Grammar depth, branch count, token length, and total nodes MUST be bounded by opc-mgmt-limits.

Arguments SHOULD use generated YANG-derived types or SDK value types:

  • IP address and prefix;
  • interface or peer identifier;
  • duration and bounded integer;
  • enum and boolean;
  • timestamp;
  • tenant-safe session key;
  • explicitly classified subscriber identifier;
  • generated action input object.

7.4 Operation Plans

#![allow(unused)]
fn main() {
pub enum OperationPlan {
    Get(ReadPlan),
    Subscribe(SubscribePlan),
    Invoke(ActionPlan),
    Composite(CompositeReadPlan),
}
}

A ReadPlan or SubscribePlan references schema-validated generated paths and binds parsed arguments only into declared list keys or query fields. It MUST NOT build an XPath or query by concatenating untrusted text.

CompositeReadPlan may combine a bounded number of independent read operations for presentation. Arbitrary client-side programs, loops, branches, or scripts are prohibited. Complex domain behavior belongs in a typed server-side action.

An ActionPlan references a modeled YANG RPC/action or a registered typed operational service. It includes input bindings, deadline, output limits, idempotency semantics, and cancellation behavior.

7.5 Presentation Specification

Presentation is declarative and operates over typed results:

#![allow(unused)]
fn main() {
pub enum PresentationSpec {
    Table(TableSpec),
    Detail(DetailSpec),
    Tree(TreeSpec),
    EventStream(EventStreamSpec),
    Scalar(ScalarSpec),
}
}

Table columns reference validated response fields and may declare headings, width policy, alignment, units, and redaction classification. Presentation specifications MUST NOT contain general template languages, code, terminal control sequences, filesystem paths, or network URLs.

JSON and other machine-readable output are generated from the authorized typed result, not by scraping the rendered table.

Data classification is anchored in trusted generated schema metadata and local governance policy, not in the target-supplied catalog. A catalog may increase sensitivity but cannot lower that minimum; unknown fields default to sensitive. One governance projection applies consistently to table, detail, tree, JSON/NDJSON, history, export, errors, completion, and audit so a different renderer cannot bypass redaction.

7.6 Completion

Completion sources are:

  • static literals and aliases;
  • schema enums and bounded numeric/value hints;
  • generated identifiers from already authorized, low-cardinality operational state;
  • an explicitly registered bounded completion provider.

Remote completion is an authenticated management read and MUST pass the same authorization and audit boundary as explicit commands. High-cardinality or sensitive values, including subscriber identifiers, MUST NOT be enumerable by default. Completion results MUST be capped, cancellable, cache-bounded, and safe to render.

Phase 1 completion is limited to literals, aliases, types, and schema enums. Remote completion is opt-in after the local editor and authorization boundary pass conformance; every command must remain discoverable and usable without remote completion.

8. CNF Registration API

8.1 Registration Contract

A CNF registers commands during management-plane composition:

#![allow(unused)]
fn main() {
pub trait OperationalCommandModule: Send + Sync {
    fn register(
        &self,
        registry: &mut CommandRegistry,
    ) -> Result<(), CommandRegistrationError>;
}
}

Illustrative ePDG registration:

#![allow(unused)]
fn main() {
registry
    .command(EpdgCommandId::ShowIkeSecurityAssociations)
    .syntax("show ike security-associations [peer <address>]")
    .summary("Display active IKE security associations")
    .effect(EffectClass::Observe)
    .get(EpdgPaths::ike_security_associations())
    .table([
        column("SPI", EpdgFields::initiator_spi()),
        column("Peer", EpdgFields::peer_address()),
        column("State", EpdgFields::state()),
        column("Age", EpdgFields::age()),
        column("Child SAs", EpdgFields::child_sa_count()),
    ])?;

registry
    .command(EpdgCommandId::DiagnosePing)
    .syntax("diagnose ping <destination> [source-interface <interface>]")
    .summary("Test reachability from an ePDG interface")
    .effect(EffectClass::Probe)
    .limits(CommandLimits::probe_defaults())
    .invoke(EpdgActions::ping())?;
}

The concrete API MAY use builders, macros, or generated modules. It MUST retain typed path/action references so schema drift fails at generation, compilation, or startup validation instead of during an operator session.

8.2 Registry Freeze and Publication

After registration, the CNF composition root freezes the registry against the active schema registry:

#![allow(unused)]
fn main() {
let catalog = command_registry.freeze(schema_registry)?;
let provider = ConsoleCatalogProvider::new(catalog, authorizer, capabilities);
management_binding.with_console_catalog(provider);
}

freeze returns an immutable ValidatedCommandCatalog; it does not serialize transport data. ConsoleCatalogProvider applies current capability and principal visibility, then the gNMI and NETCONF bindings project the same result through the well-known console YANG model. Transport bindings MUST NOT reinterpret command grammar or operation semantics.

The final API shape may differ, but registration, validation/freeze, principal-visible projection, and transport serialization MUST remain separate steps with separately testable errors.

8.3 Registry Validation

Startup validation MUST reject:

  • duplicate command IDs;
  • ambiguous grammar paths;
  • alias collisions;
  • unknown schema paths or action identities;
  • unauthorized use of reserved SDK top-level words;
  • presentation fields not present in the result schema;
  • missing effect, authorization, deadline, or limit metadata;
  • a configure operation or any config mutation primitive;
  • unbounded result, subscription, input, or completion declarations;
  • unsafe help or display text;
  • command/catalog versions incompatible with the SDK server binding.

Failure MUST be visible during CNF startup admission. Production profiles MUST fail closed rather than silently omit an invalid command module.

8.4 Standard and CNF-Specific Commands

The SDK supplies a standard base vocabulary for common models, including:

  • show system;
  • show health;
  • show alarms;
  • show config-application-status;
  • monitor alarms;
  • describe, whoami, capabilities, and session-local commands.

CNFs augment this vocabulary with domain commands such as ePDG IKE/IPsec state, SMF PDU sessions, AMF UE context summaries, and UPF forwarding state.

SDK command words and IDs occupy a reserved namespace. CNF extensions MUST use stable product/module namespaces internally even when the visible grammar is natural and concise.

The SDK MUST publish a command-language style guide covering top-level verbs, singular/plural nouns, filter ordering, identifiers, units, time display, empty-result language, destructive verbs, and common aliases. New CNF modules receive an operator-experience review against this vocabulary so show peers, for example, does not mean materially different things across CNFs without explicit qualification.

8.5 Test and Preview Support

The SDK MUST provide a command-module testkit that can:

  • validate a registry without starting a CNF;
  • render a complete command tree;
  • snapshot ?, completion, and describe output;
  • execute commands against fake operational providers;
  • exercise denied, empty, large, slow, and malformed responses;
  • preview tables at common terminal widths;
  • assert that no config mutation is reachable;
  • emit catalog compatibility evidence.

CNF owners are responsible for the human command experience, not only for making their operation plan compile.

9. Catalog Discovery

9.1 Well-Known Model

The SDK will define an openpacketcore-console YANG module with an operational catalog rooted at a well-known path:

/openpacketcore-console:console/catalog

The model exposes:

  • catalog schema version;
  • catalog content ID/digest;
  • target product and command-module identities;
  • minimum and maximum compatible console protocol versions;
  • authenticated principal visibility revision;
  • command specifications;
  • supported output and action capabilities.

The catalog is available through authenticated gNMI Get and NETCONF <get> or <get-data>. The same semantic content MUST be produced regardless of the transport used to retrieve it.

9.2 Discovery Sequence

The console performs:

  1. select the management context explicitly, from a context-qualified target, or from the user's active context;
  2. load and validate that context's target-discovery, issuer, broker, and trust configuration;
  3. acquire authentication according to the configured mode: OIDC/OpenShift establishes a human and broker session, SSH loads/proves an approved agent identity and optionally exchanges it with a broker, and SPIFFE automation obtains a workload identity without a human login;
  4. resolve the target through static context data or the context's authenticated discovery service;
  5. acquire target-scoped protocol credentials from the mode's credential source: broker-issued mTLS/SSH credentials, an approved SSH agent/certificate, or the SPIFFE Workload API;
  6. establish the catalog transport, mutually authenticating the client and verifying the target server identity;
  7. request protocol and model capabilities;
  8. retrieve the principal-visible catalog;
  9. validate catalog size, syntax, schema references, and compatibility;
  10. build the local command trie and initialize presentation state;
  11. display the ready prompt;
  12. connect additional transports eagerly or lazily according to the trusted transport policy.

For human modes the TUI prints "Logged in" only after step 3; automation modes print the redaction-safe workload identity state instead. It prints "Connected" only after step 6 and the ordinary target prompt only after step 11. When an existing session or credential is reused, the same states occur without an unnecessary browser round trip. A failure leaves the console in the corresponding visible local state with status, reauthenticate, disconnect, and exit available.

Catalog bootstrap has an explicit trusted order because full capability selection is not yet available. The management context supplies the allowed bootstrap transports and order; the authenticated target-discovery record supplies matching endpoints. For each candidate, the client completes the authenticated handshake, then gNMI Capabilities or the NETCONF hello/YANG library exchange, and accepts it only if the well-known console model/catalog is supported. It may try the next candidate only for pre-dispatch Unavailable or Unimplemented; authentication, target-identity, policy, or malformed-capability failures stop bootstrap. The resulting capability set then drives the general adapter-selection algorithm in Section 12.2.

The console MAY load a memory-cached catalog optimistically after target and principal identity are established, but it MUST validate the advertised content ID before executing target commands. The cache key includes verified target identity, stable principal key, tenant, authentication strength/credential profile, visibility revision, schema/model set, capabilities, and allowed adapter set. It is purged on logout, principal or tenant change, assurance downgrade, policy/visibility change, and target identity change. Lost refresh signals are covered by bounded TTL and revalidation. The content digest covers the canonical principal-visible catalog. A persistent role-filtered cache requires a separate encrypted-cache threat review and is not part of the MVP.

9.3 Principal-Visible Catalog

Command inventory is confidential management metadata by default. Reading the catalog root requires a deny-by-default discover authorization decision, and the server MUST filter command entries unavailable to the authenticated principal. Static visibility evaluates the command's declared paths/actions against the set of execution adapters both allowed by trusted policy and available on the target; it does not depend on which transport retrieved the catalog. Composite commands are visible only under their declared all-path or partial-result policy. Input-dependent and instance authorization remains an execution-time decision and is not disclosed by catalog filtering.

Catalog filtering improves usability but never grants authority. The server MUST reauthorize every operation because policy, tenant, target state, and instance keys can change after catalog retrieval.

The catalog includes a visibility revision. A policy or capability change MUST advance that revision and signal catalog refresh on transports that support it; clients still revalidate on bounded TTL because signals can be lost. The TUI MUST preserve the current line when refreshing the command tree where possible.

9.4 Untrusted Catalog Handling

An authenticated target may still be faulty or compromised. The client MUST:

  • cap encoded and decoded catalog size;
  • cap command count, grammar depth, branches, arguments, help length, examples, presentation fields, and completion declarations;
  • reject duplicate fields and unknown mandatory semantics;
  • reject control characters and terminal escape sequences;
  • parse without panics on malformed input;
  • avoid loading target-provided code, fonts, themes, URLs, or files;
  • fail closed for the target-specific catalog while retaining safe local commands such as disconnect, status, and exit.

10. Login and Management Identity

10.1 Ownership

OpenPacketCore owns the login integration framework, native-client behavior, access-broker contract, and terminal session lifecycle. It does not own the customer's password page, MFA policy, users, or identity database.

The page opened by opc login is selected by the trusted management context:

  • Keycloak or another OIDC provider opens its configured authorization page;
  • OpenShift integrated OAuth opens the cluster OAuth authorization page;
  • an enterprise provider may federate to Entra ID, Okta, Ping, or another IdP;
  • SSH-agent and workload profiles do not open a browser.

The only browser content served by the native CLI is a minimal loopback callback success/error page telling the user to return to the terminal.

10.2 Management Context

Illustrative configuration:

apiVersion: management.openpacketcore.io/v1alpha1
kind: ManagementContext
metadata:
  name: production
spec:
  targets:
    discovery: https://management.example.com/targets
  trust:
    serverBundle: /etc/openpacketcore/production-targets.pem
    brokerBundle: /etc/openpacketcore/production-broker.pem
  authentication:
    provider: oidc
    issuer: https://sso.example.com/realms/packet-core
    clientId: opc-cli
    audience: opc-management
    scopes: [openid, profile, email]
    flow: authorization-code-pkce
  accessBroker:
    endpoint: https://access.management.example.com

OpenShift example:

spec:
  authentication:
    provider: openshift-oauth
    issuer: https://oauth-openshift.apps.cluster.example.com
    clientId: opc-cli
    scopes: [user:info]
    flow: authorization-code-pkce

The IaC invariant in this RFC applies directly to CNF desired state. Management security configuration is a separate bootstrap trust boundary, but production contexts MUST also be versioned and installed through IaC or a signed environment bundle with configured signer trust, expiry, anti-rollback, atomic update, and owner-only local storage. Unsigned manual contexts are limited to an explicit development/compatibility profile.

CLI flags, environment variables, target discovery, and CNF command catalogs MUST NOT override issuer, authorization/token/JWKS endpoints, broker, callback policy, requested scopes, target identity, or trust roots in a production context.

Target discovery records MUST be authenticated and freshness-bound. Each record binds the logical target name to expected cryptographic server identity, tenant and NF-kind constraints, endpoints, and allowed transports. The client verifies that identity atomically during the actual mTLS or SSH handshake; a broadly trusted CA certificate without the expected target binding is not sufficient.

Context selection is deterministic. An explicit --context wins, followed by a context-qualified target name, followed by the locally selected active context. If none exists, or a short target name is ambiguous across contexts, the CLI MUST ask the user to select from locally trusted contexts and MUST NOT guess. opc login <context> establishes or refreshes that environment's login session; opc connect invokes the same flow implicitly when required.

10.3 Authentication and Credential Interfaces

Identity-provider login, broker exchange, and protocol credential possession are separate ports:

#![allow(unused)]
fn main() {
pub trait HumanAuthenticator: Send + Sync {
    async fn authenticate(
        &self,
        context: &ManagementContext,
        interaction: &dyn LoginInteraction,
    ) -> Result<HumanAuthSession, LoginError>;
}

pub trait CredentialBroker: Send + Sync {
    async fn exchange(
        &self,
        session: &HumanAuthSession,
        keys: &ClientPublicKeys,
    ) -> Result<ManagementCredentialSet, BrokerError>;
}

pub trait ProtocolCredentialSource: Send + Sync {
    async fn credentials(
        &self,
        target: &VerifiedTarget,
    ) -> Result<ManagementCredentialSet, CredentialError>;
}
}

The interfaces also define refresh and logout/revocation operations omitted from the illustrative async signatures. The concrete Rust form may use object-safe boxed futures or an equivalent adapter; implementations selected at composition boundaries MUST be object-safe when runtime provider selection requires it.

ModeHuman loginBrokerProtocol credentialsIntended use
oidcKeycloak or enterprise OIDCYesBroker-issued mTLS/SSHProduction human access
openshift-oauthOpenShift integrated OAuthYesBroker-issued mTLS/SSHProduction OpenShift human access
ssh-agentExisting SSH identityOptionalAgent key or broker-issued SSH certificateDisconnected/compatibility human access
spiffe-workloadNoneNoWorkload API X.509-SVIDNon-human automation only

Provider-specific differences remain behind these ports. OIDC discovery and OpenShift OAuth authorization-server discovery are not assumed to be interchangeable, and workload identity is not described as human login.

10.4 Native Interactive Flow

For OIDC-capable providers, the CLI uses an external browser, authorization code, PKCE S256, state, nonce where applicable, an exact loopback redirect, and a public client registration without an embedded client secret. Device authorization MAY be enabled for headless environments only when explicitly configured and advertised by the provider.

A production context that claims jump-host support MUST configure and test a headless login path. opc login --no-browser uses device authorization when the provider advertises it, or an approved SSH-agent/workload profile. The TUI MUST display the exact verified HTTPS origin and user code, distinguish pending authorization from denial or expiry, support cancellation and bounded retry, and never infer a device endpoint from an untrusted target.

Normative standards include:

The CLI MUST NOT collect the user's IdP password or use a resource-owner password grant.

Production provider profiles additionally require exact issuer comparison, HTTPS endpoint validation against context-approved trust, bounded metadata and JWKS caches, an algorithm allowlist, controlled key refresh, and no cross-origin metadata redirects outside explicit context policy. The loopback listener binds only loopback addresses on an ephemeral port, accepts one state-matching callback, has a short deadline, and closes after success or failure. OpenShift OAuth uses its explicit authorization-server metadata and identity-validation adapter rather than pretending its tokens are OIDC ID tokens.

10.5 Access Broker and Protocol Credentials

The production human-login profile uses a management access broker so every CNF does not integrate independently with every identity provider.

The flow is:

  1. the CLI authenticates the human with the configured provider;
  2. the CLI sends a broker-audience OAuth access token only to the configured broker; an OIDC ID token is never accepted as the broker bearer credential;
  3. the broker validates issuer, audience, signature, time claims, session state, and provider-specific identity resolution;
  4. trusted broker policy maps the identity to an allowed tenant and management credential profile;
  5. the CLI generates ephemeral client key material or uses an approved agent, obtains a broker nonce, and proves possession of the corresponding key in the exchange;
  6. the broker issues short-lived gNMI/mTLS and NETCONF/SSH credentials bound to one management login session and proof of possession;
  7. each CNF authenticates the broker-issued credential and creates the same stable management-user principal;
  8. CNF-side signed policy supplies roles and NACM groups.

The broker MUST NOT accept a tenant selected solely by the CLI. Roles and NACM groups MUST NOT be trusted from unsigned client metadata or ordinary command arguments.

The broker profile defines the required token audience/resource, accepted access-token type, issuer allowlist, maximum token age, replay cache, nonce/key proof, and sender-constrained-token policy. Opaque OpenShift access tokens are validated through the configured authoritative OpenShift integration. A token that cannot be audience-bound and replay-controlled for the broker is rejected in the production profile.

The SDK will separate stable authorization identity from rotating authentication state:

#![allow(unused)]
fn main() {
pub struct ManagementPrincipalKey {
    pub issuer: IssuerId,
    pub subject: SubjectId,
    pub tenant: TenantId,
}

pub struct AuthenticationContext {
    pub login_session: LoginSessionId,
    pub credential_id: CredentialId,
    pub expires_at: Timestamp,
    pub auth_strength: AuthStrength,
    pub credential_profile: CredentialProfileId,
}
}

The precise X.509 and SSH certificate wire profiles require security review before implementation. Both profiles MUST cryptographically bind the stable principal and authentication context, use client-generated or agent-held private keys, exclude roles/groups, and map to one stable authorization key. Roles and authorization caches key on ManagementPrincipalKey; audit and session enforcement additionally bind AuthenticationContext. A session, credential, assurance, or profile change invalidates authentication-dependent caches without changing the stable user grant key.

This is an explicit extension to RFC 003, whose current gRPC profile only defines SPIFFE workload principals. Phase 2 is blocked until an RFC 003 amendment is accepted and implemented defining PrincipalIdentity::{Workload, ManagementUser}, the canonical management-user certificate identity, issuing trust and bundle distribution to CNFs, tenant validation, X.509 and SSH mappings, service/method authorization, rotation, expiry, and cross-transport conformance tests.

10.6 Session and Credential Handling

AuthSession and protocol credentials MUST be opaque secret-bearing types:

  • no secret-bearing Debug or display output;
  • no access, refresh, device, or broker tokens in logs, traces, audit payloads, panic text, command history, process arguments, or environment dumps;
  • ephemeral private keys remain in memory or an approved agent and are zeroed when feasible;
  • persistent refresh credentials require an OS credential store or approved agent and explicit policy;
  • credentials have configurable short lifetimes with a hard production cap;
  • refresh occurs before expiry without freezing the TUI;
  • target connections are rotated or re-established after credential renewal;
  • logout closes target sessions, removes local credentials, and requests broker/provider revocation where supported;
  • the TUI warns before expiry and provides reauthenticate without discarding the current command line.

The prompt and whoami show redaction-safe identity, tenant, authentication strength, and expiry. They never show tokens, certificate material, or raw authorization policy.

CNFs enforce credential expiry and a maximum connection/stream age server-side; the TUI is not the enforcement point. The production credential profile sets the numeric maximum lifetime before Phase 2. Logout closes local connections and revokes broker/provider refresh state, but an offline certificate may remain usable until its short expiry unless the deployment provides a target-consumed revocation feed. The CLI reports that residual validity honestly. Renewal of a subscription or accepted action reconnects observation by stable operation/stream semantics and never replays the action.

11. Authorization and Audit

11.1 Authorization Mapping

Commands map to existing SDK authorization classes:

Command effectAuthorization
catalog discoverydeny-by-default management discover plus static command visibility
remote completionexplicit enumerate plus per-instance read filtering and completion policy
observeNACM read for every selected schema path
monitorNACM subscribe for every selected schema path
probeNACM exec for the static modeled action path, plus action policy
operateNACM exec for the static modeled action path, plus action policy and confirmation

A composite read is allowed only when every required path is allowed, unless the command explicitly defines a schema-safe partial-result policy. Partial results MUST identify omitted sections without revealing denied values.

An authorization failure MUST NOT cause the console to retry through another protocol in an attempt to obtain a different decision.

discover and enumerate are explicit extensions to the shared authorization facade. enumerate is never implied by ordinary read access. A remote completion plan requires data-classification approval, per-instance filtering, minimum-prefix policy where appropriate, per-principal rate/query budgets, no pagination, and a principal-scoped short-lived cache purged on logout or policy change.

11.2 Confirmation

Confirmation is a usability safeguard, not authorization. An operate command declares:

  • whether confirmation is required;
  • a redaction-safe target summary;
  • whether a reason/ticket is required;
  • whether the action supports dry-run;
  • idempotency and retry behavior;
  • cancellation semantics.

The TUI MUST require an explicit response for destructive actions. One-shot mode requires an explicit non-interactive confirmation flag and MUST NOT infer consent from standard input being non-terminal.

11.3 Audit Events

Every target-observed catalog retrieval, completion query, read, subscription start/stop, action, denial, and cancellation emits an authoritative target enforcement audit event. A client-side stop/cancel attempt that cannot reach the target emits only a local intent event and remains explicitly unconfirmed. Authentication and connection transitions emit the applicable broker/target event.

Before executing a mutating operational action, the target durably appends its intent/authorization audit event. If that append fails, execution fails closed. It appends outcome afterward. If the side effect may have occurred but outcome append fails, the server and TUI report a potentially completed action, enter a security-degraded state, and block further mutations until audit health is restored; they do not claim the side effect was rolled back. Non-mutating operations follow RFC 003 audit availability policy.

The console also emits a supplemental local intent event containing command ID and UX metadata. It is not authoritative evidence of target authorization. Target and console events share a request/correlation ID where the protocol permits, but the server never trusts client-supplied command metadata for an authorization decision.

The combined audit model includes:

  • stable command ID and version;
  • effect class;
  • authenticated principal and login session ID;
  • target identity and selected transport;
  • authorized schema/action identities;
  • request/correlation ID;
  • start time, duration, result class, and cancellation state;
  • redaction marker and bounded argument classification;
  • confirmation and reason metadata for operational mutations.

Raw tokens, secrets, unrestricted command text, subscriber identifiers, and unredacted payloads MUST NOT enter audit records.

12. Execution and Protocol Mapping

12.1 Transport-Neutral Client Traits

The console engine depends on narrow capabilities:

#![allow(unused)]
fn main() {
pub trait OperationalReader {
    async fn get(&self, request: ReadRequest) -> Result<ReadResult, MgmtError>;
}

pub trait OperationalSubscriber {
    async fn subscribe(
        &self,
        request: SubscribeRequest,
    ) -> Result<OperationalStream, MgmtError>;
}

pub trait OperationalActionInvoker {
    async fn invoke(
        &self,
        request: ActionRequest,
    ) -> Result<ActionExecution, MgmtError>;
}
}

A single oversized ManagementClient trait is discouraged because transports and CNFs support different capability sets.

12.2 Adapter Selection

The session negotiates capabilities and selects an adapter per operation:

Semantic operationPreferred adapterAlternatives
capabilities/cataloggNMI capabilities + GetNETCONF hello + get/get-data
bounded state readgNMI GetNETCONF get/get-data
state monitorgNMI SubscribeNETCONF notification when modeled
YANG RPC/actionNETCONF RPC/actionregistered typed service
standard operational serviceregistered gRPC/gNOI-style adaptermodeled NETCONF action

The TUI hides protocol selection, but status and diagnostic logs expose the selected adapter without leaking credentials or payloads.

The trusted management context contains a TransportPolicy with allowed adapters, deterministic preference order, optional per-operation overrides, and eager/lazy connection policy. Adapter selection is:

  1. start with adapters allowed by the trusted context;
  2. retain only adapters whose authenticated capability set implements the semantic operation;
  3. order candidates by the per-operation override or the default table above, then by the context preference order;
  4. reuse a healthy authenticated connection or lazily establish one;
  5. select the first successful candidate and record the choice in local status and audit.

Fallback has two explicit cases:

  • when the adapter proves the operation was not dispatched and the failure is Unavailable or Unimplemented, the engine may select the next adapter; this is an initial dispatch, not a replay;
  • when dispatch may have occurred, any retry of an action class, including probe, requires declared idempotency plus a protocol-independent idempotency key and target-enforced deduplication shared across eligible adapters; otherwise the outcome is ambiguous and no retry occurs.

Syntax, authentication, authorization, policy, validation, malformed response, resource exhaustion, and security failures are never fallback triggers. All attempts preserve one deadline, principal, authorization target, request/correlation ID, validation/limit policy, and result schema.

Protocol fallback is permitted only for transport unavailability or an unimplemented capability according to policy. It MUST NOT bypass an authentication, authorization, validation, or resource-limit failure.

12.3 Action Lifecycles

Actions may be:

  • immediate: one structured result;
  • streaming: bounded result events until completion or cancellation;
  • accepted: a stable operation ID followed through operational state or a subscription.

Long-running operations SHOULD use the accepted model so reconnecting the TUI does not lose server-side operation identity. The command catalog declares whether disconnect or cancellation terminates the server-side action.

The MVP permits one foreground remote operation per console. An accepted server-side operation may be detached only by stable operation ID and later queried or reattached through a modeled status path. General concurrent background jobs are deferred.

Retries require declared idempotency. The client MUST NOT automatically retry a non-idempotent operate command after an ambiguous transport failure.

12.4 Stream Integrity and Backpressure

Control events for cancellation, authentication expiry, connection state, catalog invalidation, and terminal shutdown use a bounded priority path that bulk output cannot starve. Data-event channels are bounded and declare one of these policies:

  • lossless: apply bounded backpressure; if the limit/deadline is exceeded, terminate the view with an explicit truncation/gap event;
  • coalescing: replace older updates only for the same schema key when the command explicitly declares state-coalescing semantics, and display the number and interval of coalesced updates.

Silent event drops are prohibited. Every stream exposes receive timestamps, target timestamps and sequence information when supplied by the protocol, initial-snapshot/synchronization markers, reconnect boundaries, and known gap or drop counts.

On transport loss, a monitor does not silently continue. It either:

  • resumes from a proven replay/resume point supported by the adapter;
  • restarts with a visible reconnect marker and new initial snapshot while marking the intervening interval unknown; or
  • terminates and returns to the prompt.

The catalog and adapter capability select the behavior. A local pause pauses viewing, not the remote source; if its bounded buffer fills, the console emits an explicit gap/truncation result rather than hiding loss.

12.5 Cancellation and Deadlines

Every local execution carries a deadline and cancellation handle. The adapter propagates protocol cancellation when the selected transport supports it; otherwise it closes/detaches the stream or session according to the declared lifecycle and reports an ambiguous outcome when remote completion is unknown. Ctrl-C:

  1. returns control to the TUI event loop immediately;
  2. marks the local execution cancelled;
  3. sends protocol cancellation when supported;
  4. continues bounded background cleanup;
  5. records the final known state in audit and local status.

The TUI must distinguish "cancel requested" from "remote action confirmed cancelled." It MUST NOT claim that a side effect did not occur after an ambiguous failure.

13. TUI Design

13.1 Session State Model

Authentication, transport, catalog, foreground operation, and active view are orthogonal state axes rather than one linear enum:

#![allow(unused)]
fn main() {
pub struct ConsoleState {
    pub lifecycle: LifecycleState,
    pub authentication: AuthenticationState,
    pub transports: TransportSetState,
    pub catalog: CatalogState,
    pub foreground: ForegroundState,
    pub view: ViewState,
}
}

The reducer derives Ready only when lifecycle, authentication, at least one required transport, and catalog validity permit execution. Transitions are visible but unobtrusive. The prompt MUST distinguish a ready target from a disconnected, degraded, expired, stale-catalog, or reconnecting target.

Typing and safe local help remain available during recoverable transitions, but target commands entered while not ready are rejected with the blocking state; they are never queued for later automatic execution. ? and tab may use only a currently validated catalog and must visibly mark stale offline help. logout closes target connections before discarding or revoking credentials.

13.2 Event Model

The UI consumes structured events:

#![allow(unused)]
fn main() {
pub enum ConsoleEvent {
    Connection(ConnectionEvent),
    Authentication(AuthenticationEvent),
    Catalog(CatalogEvent),
    Command(CommandEvent),
    Output(OutputEvent),
    Progress(ProgressEvent),
    Warning(ConsoleWarning),
    Error(ConsoleError),
}
}

Network tasks MUST NOT write directly to the terminal. They send bounded events to the UI, which owns terminal state and sanitization.

13.3 Input and Command Editing

The interactive editor MUST provide:

  • history navigation;
  • beginning/end and word movement;
  • token-aware completion;
  • multiline editing only when an argument format explicitly requires it;
  • safe paste handling;
  • search over history that the history policy allowed to persist;
  • clear indication of incomplete grammar;
  • preservation of the current line across asynchronous notifications.

The baseline key contract includes arrow and Home/End navigation, Ctrl-A/E, Ctrl-W and word movement/deletion, Backspace/Delete, history search, completion cycling, quoting/escaping rules, and Unicode grapheme-aware cursor movement. Literal ? in an argument uses quoting or escaping; unquoted ? requests contextual help. The implementation MUST publish the complete keymap through a local help keys view.

Bracketed paste MUST NOT cause pasted newlines to execute multiple commands without an explicit review/confirmation policy.

Asynchronous notifications are batched according to a bounded display policy, rendered above the edit line, and followed by deterministic restoration of the prompt, buffer, cursor, and completion state. Progress updates replace only their own progress region. Bulk notifications MUST NOT continuously steal the cursor; the console summarizes them and offers a dedicated view.

13.4 History

Argument specifications carry sensitivity and data-classification metadata. History policy may:

  • persist the complete command when all arguments are safe;
  • persist a redacted command form;
  • persist only the stable command ID;
  • omit the entry entirely.

Tokens, passwords, private material, authentication codes, sensitive subscriber identifiers, and action secrets MUST never be persisted. History files require owner-only permissions and bounded retention.

The default production policy persists safe commands, stores a visibly redacted non-replayable entry when only selected arguments are classified, and omits secret-bearing commands. Selecting a non-replayable history entry shows why it cannot execute until the missing value is re-entered.

13.5 Rendering and Pager

The renderer MUST:

  • consume typed rows/events incrementally;
  • cap buffered rows and bytes;
  • preserve column identity across terminal resize;
  • provide a detail view when columns cannot fit safely;
  • clearly label truncation, omission, redaction, stale data, and partial results;
  • never interpret remote ANSI, OSC, hyperlink, clipboard, or title sequences;
  • allow the user to stop rendering without falsely cancelling a server-side action;
  • separate local rendering failure from remote operation failure.

The built-in pager remains inside the console process. Invoking an external pager is disabled by default in production profiles because environment, history, temporary-file, and terminal-control behavior cross additional trust boundaries.

The pager owns navigation keys only while active: arrows and PageUp/PageDown move, / searches rendered safe text, n advances a match, w toggles wrap/horizontal handling, Enter opens a structured detail view, and q closes the view. Closing the pager means "stop viewing" only. For a live source, the TUI separately offers pause, follow, detach where supported, and stop; only stop requests remote cancellation. Ctrl-C follows the currently displayed ownership hint and never silently conflates those actions.

Machine output has a versioned envelope and stable schema identity. One-shot bounded results use JSON; streams use NDJSON by default. Structured results go to stdout, while progress and redaction-safe diagnostics go to stderr. An interactive | json view remains inside the pager unless the user explicitly requests a classified file export.

File export uses owner-only permissions, an atomic create/write/rename flow, and no overwrite without explicit confirmation. The exporter preserves data classification and redaction metadata, reports partial writes without printing payloads, and never routes classified output through a temporary world-readable file.

13.6 Errors

Errors use the shared management status taxonomy and a stable console category:

  • syntax or local validation;
  • authentication required/expired;
  • permission denied;
  • target unavailable/reconnecting;
  • capability unavailable;
  • remote deadline/cancellation;
  • malformed target response;
  • output truncated by policy;
  • ambiguous action outcome;
  • internal console defect.

The TUI gives a concise operator message and an optional redaction-safe detail view. It does not print raw server errors, XML, protobuf debug output, tokens, paths containing sensitive values, or backtraces by default.

13.7 Terminal Lifecycle

The TUI owns raw mode, alternate-screen use, cursor visibility, mouse mode, and signal handling through one terminal guard. It MUST restore the terminal on normal exit, startup failure, handled panic, SIGINT/SIGTERM, broken pipe, and other supported termination paths. Ctrl-Z suspends only after restoring the terminal and resume re-enters and redraws from reducer state.

The line-oriented shell SHOULD preserve normal scrollback. Views that use the alternate screen MUST document entry/exit and restore the previous screen. Pseudo-terminal tests deliberately crash and signal the process to prove that the terminal is not left in raw mode, with a hidden cursor, or with paste/mouse modes enabled.

14. Security and Threat Model

14.1 Threats

The design assumes an attacker may:

  • operate a malicious or compromised CNF endpoint;
  • return a hostile catalog or operational value;
  • inject terminal control sequences into remote data;
  • attempt catalog or response memory exhaustion;
  • cause high-cardinality completion queries;
  • replay action requests or login callbacks;
  • substitute an identity-provider or broker URL;
  • steal local history or configuration files;
  • observe process arguments, logs, audit, or terminal scrollback;
  • interrupt transport after an action is accepted but before its result;
  • exploit protocol fallback to seek a weaker authorization path.

Authorization and target-data confidentiality guarantees assume the CNF's RFC 003 enforcement boundary remains intact. A fully compromised target can ignore NACM or falsify state. Against that target, the console still guarantees local grammar/operation allowlisting, resource bounds, governance projection, credential non-disclosure, and terminal safety; it cannot prove correctness or confidentiality of target-owned behavior.

14.2 Required Controls

  • Trusted contexts pin or validate target, issuer, and broker trust.
  • Login redirects are derived only from validated provider metadata for the configured issuer.
  • OAuth state, PKCE, nonce where applicable, exact redirects, issuer, audience, signature, and time claims are validated fail closed.
  • Catalogs, responses, help, completion, and rendering are size bounded.
  • All remote strings are terminal sanitized.
  • Commands bind arguments through typed fields rather than string-built queries.
  • Roles/groups come from signed policy, not unsigned transport metadata.
  • Completion, help filtering, reads, subscriptions, and actions respect tenant and authorization boundaries.
  • Action retries obey declared idempotency.
  • Secrets and classified values are excluded or redacted from history, logs, metrics, error messages, and audit.
  • Protocol fallback cannot downgrade security decisions.
  • Parser and catalog decoders are fuzzed and contain no untrusted-input panic paths.

14.3 Terminal Output Is a Security Boundary

Operational fields may contain peer names, alarm text, interface labels, error text, or identifiers influenced by external systems. Before width calculation or display, the renderer MUST neutralize:

  • C0/C1 controls except the console's own intentional line handling;
  • ANSI CSI sequences;
  • OSC title, hyperlink, and clipboard sequences;
  • bidirectional text controls according to policy;
  • invalid or excessively combining Unicode;
  • embedded newlines in fields not declared multiline.

Raw export, when authorized, MUST write through an explicit file-output path with classification checks. --raw does not mean "write untrusted bytes to the terminal."

15. Failure and Recovery

15.1 Connection Loss

On connection loss the TUI:

  • marks the prompt disconnected;
  • retains the current edit buffer;
  • cancels or detaches operations according to their declared lifecycle;
  • reconnects with bounded exponential backoff and jitter when policy allows;
  • reauthenticates if credentials expired;
  • revalidates target identity and catalog content ID;
  • does not replay non-idempotent actions automatically.

15.2 Catalog Failure

If catalog validation fails, target commands are unavailable. The console retains safe local commands:

  • status;
  • show connection;
  • reauthenticate;
  • disconnect;
  • diagnostics console;
  • exit.

It MUST NOT fall back to executing unknown raw paths or arbitrary RPC names as a convenience.

15.3 Authentication Failure

Unknown issuer, invalid metadata, TLS failure, callback mismatch, token validation failure, broker denial, credential expiry, tenant mismatch, or grant-source failure all fail closed. The operator receives a stable error category and correlation ID without token contents or internal policy details.

16. Versioning and Compatibility

The system versions independently:

  • catalog wire schema;
  • command ID/version;
  • visible syntax and aliases;
  • operation/result schema;
  • presentation schema;
  • login-provider profile;
  • management credential profile;
  • console binary.

The catalog declares its compatible console protocol range. Unknown optional fields may be ignored only when the wire schema marks them optional. Unknown effect classes, operation primitives, required presentation semantics, or security requirements cause that command or catalog to fail closed according to compatibility policy.

Command deprecation includes replacement command ID, message, and earliest removal version. Interactive aliases may preserve familiar legacy syntax, but audit always records the stable command ID.

17. Proposed Crate and Component Boundaries

ComponentPurpose
opc-mgmt-commandCatalog types, grammar, operation plans, presentation specs, validation, and CNF registry
opc-mgmt-actionTyped action provider, action lifecycle, cancellation, idempotency, and result contracts
opc-mgmt-clientTransport-neutral reader, subscriber, invoker, capability, and session contracts
opc-gnmi-clientProduction gNMI capabilities/Get/Subscribe client adapter
opc-netconf-clientProduction NETCONF hello/get/get-data/RPC/action client adapter
opc-auth-clientManagement contexts, login providers, native OAuth behavior, credential handles, refresh/logout
opc-access-brokerReference broker service and management-user credential issuance contract
opc-consoleRust binary containing the interactive shell/TUI and optional one-shot mode
opc-console-testkitCatalog, pseudo-terminal, rendering, auth, protocol, and CNF experience conformance tools

Exact crate consolidation may change during implementation. The architectural boundaries must remain narrow even if several are initially delivered in one crate.

Existing crates are reused:

  • opc-mgmt-opstate for reads and subscriptions;
  • opc-mgmt-schema and opc-yanggen for generated paths and projections;
  • opc-mgmt-path for normalized schema identity;
  • opc-mgmt-authz and opc-nacm for read/subscribe/exec authorization;
  • opc-mgmt-principal for trusted principal construction and signed grants;
  • opc-mgmt-audit for operation audit;
  • opc-mgmt-errors and opc-mgmt-limits for shared status and bounds;
  • opc-mgmt-transport, opc-tls, and opc-identity for transport trust;
  • opc-redaction and opc-data-governance for output classification;
  • opc-gnmi-server and opc-netconf-server for server exposure.

The existing gNMI smoke client is test-oriented and MUST NOT silently become the production client without a boundary, API, security, and lifecycle review.

18. Observability

Metrics use bounded labels and MUST NOT include raw users, targets with high cardinality, command arguments, YANG instance paths, subscriber IDs, or login session IDs.

Suggested metrics:

  • opc_console_sessions_total{outcome,auth_provider};
  • opc_console_active_sessions;
  • opc_console_command_total{command_id,effect,outcome} with an allowlisted bounded command ID set;
  • opc_console_command_duration_seconds{command_id,transport};
  • opc_console_catalog_load_total{outcome};
  • opc_console_catalog_size_bytes;
  • opc_console_reconnect_total{reason};
  • opc_console_auth_refresh_total{outcome};
  • opc_console_output_truncated_total{reason};
  • opc_console_ui_event_lag_seconds.

Local diagnostic logs are structured, redaction-safe, and disabled or bounded according to profile. A user may export a console diagnostic report that contains versions, state transitions, capability summaries, and correlation IDs, but not command payloads or credentials.

19. Testing and Evidence

19.1 Unit and Property Tests

  • grammar ambiguity and collision detection;
  • typed argument validation and binding;
  • prohibition of configuration mutations;
  • catalog and presentation compatibility;
  • terminal sanitization and Unicode width behavior;
  • history classification and redaction;
  • action idempotency and ambiguous outcomes;
  • pure console-state reducer transitions with fake time and replayable event traces;
  • queue overflow, priority-event, stream gap, and reconnect policies;
  • authorization mapping and partial-result rules.

19.2 Fuzzing and Adversarial Tests

  • malformed/oversized catalog documents;
  • deeply nested grammar and choice bombs;
  • hostile ANSI/OSC/bidirectional/Unicode output;
  • malformed gNMI/NETCONF results;
  • rapid resize, paste, completion, and cancel sequences;
  • response floods, slow streams, queue saturation, coalescing, and gap reporting;
  • login callback replay, state mismatch, issuer substitution, and token-like error payloads;
  • transport downgrade/fallback attempts;
  • disconnect at every action lifecycle boundary.

19.3 Pseudo-Terminal Conformance

The reference TUI MUST be tested through a pseudo-terminal at multiple sizes and capability profiles. The evidence declares a supported matrix covering at least a modern xterm-compatible terminal, a common Linux jump-host terminal, TERM=dumb, non-TTY output, no-color, and append-only accessibility mode. Tests verify:

  • prompt and line preservation during asynchronous events;
  • ? and tab behavior for every example command;
  • paging, filtering, no-color, resize, and cancellation;
  • editor/pager key ownership and async notification redraw;
  • secret-free history and scrollback fixtures;
  • disconnected, expired, denied, slow, empty, and large-result UX;
  • stream reconnect, gap, initial-snapshot, and truncation markers;
  • append-only accessibility and TERM=dumb behavior;
  • raw-mode/cursor/paste/mouse restoration after exit, signals, suspend/resume, broken pipe, startup failure, and a deliberate crash;
  • stable plain-text snapshots for accessibility and support use;
  • no terminal-control injection from target data.

Replayable interaction transcripts cover successful operation plus slow login, denial, reconnect, empty output, catalog refresh, output truncation, stream gap, credential expiry, and ambiguous action outcome. Tests assert semantic reducer state and structured render events in addition to terminal-byte snapshots.

19.4 CNF Experience Conformance

A CNF command module is conformant only when:

  1. every visible command has summary, contextual help, typed arguments, effect, limits, and presentation;
  2. a new operator can discover the command from the root ? tree;
  3. ordinary use requires no YANG/XPath knowledge;
  4. empty, denied, partial, slow, and oversized results remain understandable;
  5. the module passes authorization, audit, redaction, and terminal tests;
  6. no config mutation is reachable;
  7. examples execute against the CNF test fixture;
  8. command output remains useful at 80, 120, and 160 columns.

19.5 Reader and Usability Tests

Before declaring the Phase 3 operational-actions gate complete, representative packet-core operators who did not implement the commands MUST be asked to perform tasks using only login, ?, tab completion, and describe. At minimum:

  • identify whether an ePDG is healthy;
  • find active major alarms;
  • locate an IKE SA for a known peer;
  • monitor a state transition;
  • run a bounded ping;
  • recognize and safely handle an authorization denial;
  • exit without leaving credentials or a remote action ambiguous.

Observed confusion becomes a catalog/TUI defect, not operator-training debt by default.

Each phase defines its task subset and a completion/error threshold before the study begins. Failure blocks that phase's exit unless the RFC evidence records an explicitly accepted known gap. At least one usability pass MUST cover the append-only accessibility mode and one MUST cover the configured headless login path. Accessibility evidence includes a screen-reader user or qualified accessibility review rather than relying on snapshots alone.

19.6 Release Evidence

RFC 006 evidence includes:

  • catalog schema and compatibility report;
  • command-module conformance report per reference CNF;
  • authentication-provider and broker security tests;
  • terminal injection corpus results;
  • parser/catalog fuzz summaries;
  • pseudo-terminal UX snapshots;
  • authorization/audit/redaction matrix;
  • performance budgets for UI responsiveness and bounded memory;
  • known gaps and unsupported terminal/provider profiles.

20. Delivery Plan

Phase 1: Developer Preview and First-Class Read Console

  • opc-mgmt-command model, validator, registry, and testkit;
  • well-known console YANG catalog;
  • production gNMI read client and authenticated catalog discovery;
  • trusted management contexts;
  • OIDC and OpenShift OAuth login-provider interfaces with a fake broker;
  • the interactive Rust TUI delivered in the same phase;
  • root help, tab completion, editing, safe history, tables, pager, JSON, status, whoami, reconnect, and cancellation;
  • SDK-standard health, alarm, runtime, and config-application-status commands;
  • ePDG read-only vertical slice.

Phase 1 is not complete if only the catalog or client crates exist. The TUI, its conformance tests, and operator usability tasks for login, discovery, health, alarms, denial, and exit are required deliverables. This phase is a developer preview because the broker credential profile is not yet production.

Phase 2: Production Read and Monitor MVP

  • accepted and implemented RFC 003 management-user identity amendment;
  • gNMI Subscribe and streaming renderer;
  • NETCONF read fallback;
  • production access broker and management-user credential profiles;
  • credential refresh/rotation and logout;
  • role-visible catalog refresh;
  • monitor alarms and ePDG state-monitoring commands;
  • terminal and authentication fault campaigns;
  • operator usability tasks for headless login, stream gaps, reconnect, and accessible monitoring.

Phase 3: Typed Diagnostics and Operational-Actions Gate

  • opc-mgmt-action contracts;
  • NETCONF action/RPC and registered operational-service adapters;
  • bounded ping as the first probe action;
  • confirmation and ambiguous-outcome UX;
  • accepted/streaming action lifecycles;
  • selected ePDG operational actions after security review;
  • operator usability tasks for confirmation, cancellation, ambiguous outcome, and reattachment.

Phase 4: Broader CNF and TUI Experience

  • AMF, SMF, UPF, and additional ePDG command modules;
  • optional full-screen dashboards and detail panes over the same catalog;
  • signed environment distribution and approved credential-store adapters;
  • one-shot automation using the exact command IDs and engine;
  • broader cross-CNF vocabulary and usability refinement.

21. Alternatives Considered

21.1 Raw gNMI/NETCONF Tools

Rejected as the operator experience. They require paths, schemas, protocol knowledge, and separate tools, and do not recreate a discoverable persistent network-element session. They remain valuable engineering diagnostics.

21.2 Generate the Entire CLI from YANG

Rejected as the only command-design mechanism. YANG supplies types, paths, and actions but does not by itself create concise task-oriented grammar, useful tables, domain grouping, examples, or operator workflows. Generated fallback inspection may be added for developers, but curated CNF commands are required.

21.3 CNF-Specific CLI Binaries

Rejected. They duplicate authentication, protocol, safety, terminal, and authorization work and produce inconsistent operator experiences.

21.4 Remote Shell over SSH

Rejected. It creates arbitrary-code and container-access boundaries, weakens schema validation and audit, and couples operational workflows to CNF process internals.

21.5 Send Command Strings to a Generic Execute RPC

Rejected. It makes the remote parser an undocumented API, obscures typed authorization paths, encourages shell-like injection, and prevents standard gNMI/NETCONF interoperability.

21.6 Browser UI First

Rejected for this feature. Operators explicitly require the persistent terminal workflow, discoverable command tree, low-friction keyboard navigation, and jump-host compatibility. A browser UI may reuse the framework later.

21.7 OpenPacketCore-Owned Identity Store and Login Page

Rejected. The SDK integrates with the deployment's identity system. It does not become another password, MFA, recovery, and user-lifecycle authority.

22. Open Design Decisions

The following decisions must be closed before their implementation phase:

DecisionOwner/reviewMust close before
Final binary and package name (opc, opc-console, or another unambiguous name)SDK maintainersPhase 1 packaging
Catalog wire encoding and maximum production limitsManagement/schema and security reviewersPhase 1 catalog implementation
Initial full-screen views beyond the required shell, pager, help, and streaming presentationTUI owner and operator UX reviewersPhase 4 full-screen implementation; not an MVP blocker
Exact management-user X.509 and SSH credential profiles and RFC 003 amendmentSecurity and identity maintainersPhase 2 production credentials
Standalone production broker, existing-platform adapter, or bothPlatform architecture and securityPhase 2 broker implementation
Approved persistent credential stores by OS and disconnected profilePlatform securityAny phase enabling persistent refresh credentials

These decisions do not change the central architecture: trusted configurable login, a declarative CNF command catalog, typed protocol operations, and a first-class interactive TUI.

23. Acceptance Criteria

RFC 014 reaches full operational-actions implementation status when:

  1. A reference ePDG publishes a validated catalog through the well-known model.
  2. An operator can authenticate through at least one configurable human provider and reach a persistent authenticated prompt.
  3. The operator can discover and run ePDG health, alarm, IKE SA, and peer-state reads without entering a YANG/XPath path.
  4. ?, tab completion, describe, paging, resize, cancellation, safe history, structured output, reconnect, and whoami pass pseudo-terminal tests.
  5. The TUI remains responsive during slow login, catalog retrieval, reads, subscriptions, and connection loss.
  6. Catalogs and outputs cannot inject terminal controls or exceed configured resource bounds.
  7. All target operations pass authentication, NACM authorization, audit, and redaction checks.
  8. No console command or raw escape hatch can mutate configuration.
  9. A bounded ping action demonstrates typed probe execution without arbitrary remote command strings.
  10. RFC 006 evidence records authentication, catalog, CNF experience, TUI, security, fuzzing, and performance results.

24. Summary

OpenPacketCore will provide operators with a modern version of the persistent network-element shell: immediate, discoverable, keyboard-driven, and useful without knowledge of management protocol paths. CNFs define their operational vocabulary through typed declarative command modules. The SDK turns that vocabulary into consistent help, completion, authorization, protocol requests, safe presentation, and audit.

The design restores operational immediacy without restoring configuration drift, arbitrary shell access, bespoke authentication, or per-CNF UI fragmentation. The TUI is where these guarantees become a coherent operator experience, so it is designed, tested, and shipped as a first-class component from the first implementation phase.

Architecture Decision Records

This directory contains accepted and proposed architecture decisions for the OpenPacketCore SDK hardening and management-plane work.

ADRs are the durable record of architectural intent. The audit completion reports and implementation status matrix record what was validated; these ADRs record why the shape of the SDK is what it is. Proposed ADRs are included here when they gate in-progress work, but they do not authorize implementation until accepted.

Index

ADRDecision
0001Config management is secure by default, commit-confirmed, audited, and explicitly authorized.
0002Config persistence HA uses ConsensusConfigStore on the shared Openraft engine, with sealed/redacted commands, atomic authority fencing, shared authenticated transport, and exact offline legacy recovery.
0003Authoritative session HA uses one Openraft-backed store with validated identity, committed state-machine application, and envelope encryption above consensus; standalone SQLite is not HA.
0004Production identity, TLS, keys, and audit integrity are explicit SDK substrates with fail-closed adapters.
0005Runtime health, admin/probe routes, metrics, and alarms are shared SDK surfaces with production authorization and redaction.
0006Storage, security, runtime, HA, and release evidence are validated through fail-closed fault injection.
0007Operator lifecycle policy logic lives in Rust SDK crates as reusable policy engines.
0008Kubernetes operator integration is demonstrated by a Go reference harness without becoming a product CNF operator.
0009Production data-plane claims require explicit node-resource, BPF, pod-security, and fallback validation.
0010RFC 006 evidence, SBOM/VEX, provenance, bundle verification, performance baselines, and gates are first-class release inputs.
0011opc-amf-lite is the SDK vertical integration proof, not a product NF.
0012Diagnostics safety and privacy governance boundaries are structured, fail-closed, and compile-gated.
0013NGAP requires generated ASN.1 APER code; hand-written and FFI codecs are rejected.
0014rustls/tokio-only dependency policy, no gRPC stack in SDK crates, and a measured (not aspirational) MSRV.
0015Protocol codecs are proven against spec-authored byte fixtures, never only their own encoder output.
0016(proposed) tonic/prost are permitted only for opc-gnmi-server as the ADR 0014 §3 exception; core SDK crates stay gRPC-free.
0017Explicitly allowlisted Linux kernel UAPI sys crates, including the fixed-profile descriptor-only opc-fs-verity-sys, plus the narrowly scoped opc-sqlite-file-control-sys descriptor and test-VFS boundary, hold all reviewed unsafe FFI; this exception to ADR 0014 §8 does not reopen ADR 0013's rejection of foreign C codec FFI.
0018EPC and untrusted-access additions are limited to SDK-owned reusable mechanisms; product policy, deployment defaults, ePDG orchestration, and carrier-readiness claims remain product-owned.
0019Openraft is the only distributed-persistence consensus authority; domain state machines remain SDK-owned and the opc-persist migration is required before the workspace can claim a unified profile.

ADR 0001: Secure Config Management

Status

Accepted

Date

2026-06-08

Context

The SDK exposes shared configuration management primitives that downstream CNFs will use for production configuration changes. Early helper APIs made it too easy to wire allow-all authorization or treat commit-confirmed behavior as a test-only convention.

For carrier deployments, configuration writes must be explicit, authorized, recoverable, and auditable. Pending configuration must either be confirmed before its deadline or roll back to a confirmed point without silently accepting unsafe state.

Decision

Configuration management is secure by default:

  • Production-facing ConfigBus constructors require an explicit ConfigAuthorizer.
  • Allow-all construction is limited to clearly named dev/test helpers.
  • Commit-confirmed state is persisted durably with deadline metadata.
  • Expired pending commits roll back to a previous confirmed configuration.
  • Failed rollback or failed confirmation fences the bus into recovery-required state instead of allowing further writes.
  • Configuration audit records are persisted after redaction and protected by a hash chain/HMAC.

Consequences

Downstream CNFs must provide an authorization adapter rather than relying on SDK defaults. Tests can still use dev-only allow-all constructors, but production call sites are visibly different.

Rollback and recovery behavior is now part of the SDK contract. Operators can recover from failed commits, but they cannot pretend a pending or failed commit is a confirmed production state.

Evidence

  • crates/opc-config-bus/src/lib.rs
  • crates/opc-persist/src/backend.rs
  • crates/opc-persist/tests/persist.rs
  • docs/implementation-status.md

ADR 0002: Config Store Consensus HA

Status

Accepted

Date

2026-06-08

Amended 2026-07-12 for the atomic #177 migration to the workspace's shared Openraft engine.

Amended 2026-07-16 for the shared config-bus adapter and atomic named rollback points.

Context

Single-node SQLite persistence cannot support a carrier-HA configuration claim. The earlier opc-persist hardening prototype implemented its own Raft-style election, replication, read, membership, snapshot, and TCP/mTLS paths, while QuorumConfigStore supplied another majority algorithm. Keeping either path beside the workspace's Openraft authority would leave two possible answers to election, commitment, and recovery questions.

Config values also cross a sensitive boundary. Consensus must replicate the result of application encryption without acquiring the ability to obtain a key, call HKMS, or observe plaintext. Migration from the removed engine cannot infer which legacy suffix was committed: only an externally established applied state is admissible.

Decision

ConsensusConfigStore uses the exact-pinned Openraft engine exported by opc-consensus. Openraft is the sole distributed authority for configuration state. It exclusively owns election, term/vote persistence, leader authority, log matching, quorum commit, membership transitions, linearizable read barriers, log compaction, and snapshot lineage/install authority.

For the config profile, the admitted voter set is immutable within one configuration epoch. The adapter requires exact equality with the configured set and exposes no subset/superset transition; a reviewed fleet transition uses a new coordinated topology epoch rather than a second membership policy.

The SDK continues to own the deterministic config command and SQLite adapter: sealed commit application, confirmed-commit and rollback-point semantics, redacted audit finalization, durable idempotent request outcomes, bounded codecs, cluster/configuration/epoch scope, and fail-closed errors. None of those surfaces counts votes or implements a second log-repair algorithm.

The removed custom consensus modules, QuorumConfigStore, private config TCP peer/server, and standalone consensus-node binary are not compatibility authority paths and must not be reintroduced behind another constructor or feature.

opc-config-bus-consensus owns the narrow application-facing adapter. Its production RaftManagedDatastore<C> can wrap only ConsensusConfigStore and implements only ManagedDatastore<SealedConfig<C>>. The generic PersistManagedDatastore<C, S> exists for single-node/test composition and compatibility, but it does not make S a consensus authority. AMF-lite uses the shared adapter rather than maintaining a product-local copy.

Protection boundary

The production composition is:

application -> HKMS-backed encryption -> ConsensusConfigStore
            -> Openraft -> SQLite and Openraft snapshots

In API terms, EncryptingManagedDatastore is outside RaftManagedDatastore, which in turn delegates wholesale to ConsensusConfigStore. The adapter owns no provider or key handle.

The outer application layer encrypts configuration first. A successful encryption operation mints a one-shot, non-serializable claim bound to the exact envelope bytes and plaintext digest. Before proposal, the config adapter consumes that claim, validates the canonical AEAD envelope and config AAD, replaces every present audit value with the redaction marker, and finalizes the audit chain. Openraft RPCs, logs, outcomes, follower apply, replay, catch-up, SQLite state, and snapshots contain only sealed ciphertext, deterministic metadata, and redacted audit records. They never contain plaintext, a provider, a provider or key handle, or raw key material, and Openraft never calls HKMS.

This is payload-envelope protection, not full-database encryption. Openraft terms, indexes, membership, request IDs, timestamps, envelope key IDs, and other routing metadata remain visible unless a separately qualified database/volume encryption layer protects them.

Storage authority claim

Opening a pristine database creates the Openraft schema and durable config_raft_identity authority marker in one immediate SQLite transaction. The same transaction checks legacy authority first. Every standalone SQLite mutation checks that marker under the shared connection lock and fails closed after the claim, including through handles retained from before the claim or a freshly reopened backend. Safe public APIs expose neither the raw SQLite connection nor the audit key/key bytes; OS-level access to the database path remains a deployment filesystem-permission boundary.

The SQLite log adapter accepts only contiguous appends and caps each encoded entry at 16 MiB. Committed, applied, and purged floors are immutable; reads and startup require exact log or snapshot lineage and reject persisted holes, while Openraft may explicitly truncate and replace only an uncommitted suffix. The command, config wire, storage schema, and snapshot envelope have distinct revision checks. Startup rejects unknown config-owned schema objects or a manifest/digest mismatch, verifies current audit HMACs using the deployment audit-key epoch/fingerprint, and bounds retained durable outcomes to the newest 4,096 application sequences.

Config command and config-specific RPC revision 3 add named rollback-point creation to the same applied mutation as the encrypted commit. Revisions 1 and 2 remain readable only under their original semantics; a revision-1 or revision-2 command cannot claim the inline-label behavior. Exact formation and RPC checks reject mixed revisions, so this change requires a drained, coordinated stop/upgrade/start of the complete config voter set.

The snapshot root is an exact private 0700, non-symlink directory on the SQLite durable device. The adapter holds its opened directory descriptor, creates snapshot artifacts private, and rejects path/device/inode replacement before authority-affecting work. Startup verifies the referenced snapshot before a bounded directory scan removes recognized canceled staging, sidecars, approved-recovery staging, and unreferenced snapshots. Drop guards also remove staging files when async snapshot work is canceled.

The topology admits an explicit singleton or an odd voter set of 3 through 9, must contain the local stable node ID, and requires an exact peer route for every configured remote voter. Cluster, configuration, and positive epoch are persisted and validated on reopen. Reads and readiness use Openraft's linearizable barrier; a local SQLite read or listener bind is not quorum evidence.

Shared transport

opc-persist consumes only the transport-neutral ConsensusPeer and ConsensusRpcHandler ports in opc-consensus. The production mTLS implementation, connection authentication, bounded network framing, and credential lifecycle belong to the existing shared opc-session-net transport composition. opc-persist owns no second TCP listener, client, TLS configuration, certificate parser, or certificate-rotation mechanism. The transport's currently session-named server/peer types accept and implement the shared ports; naming does not give the transport config-state authority. A three-node integration forms and commits the real config Openraft store over the loopback mTLS adapter, proving this composition in process.

Forwarded mutations and read barriers carry a validated remaining caller budget. The receiver uses the lesser of that budget and its own operation cap, so routing cannot create a fresh full server timeout. Zero, oversized, or malformed budgets fail before work begins.

Certificate and trust-bundle rotation keeps the shared transport's existing responsibility and qualification status. Operators must use its trust-overlap, fresh-authentication, connection-drain, readiness, and old-trust retirement procedure. Real-mTLS tests prove that a subsequent new call/full handshake observes a renewed SVID and rejects a wrong rotated identity; they do not prove retained-connection retirement or seamless continuity merely because the config adapter uses the shared port.

Legacy admission and rollback

Normal open rejects any nonempty legacy config/consensus authority with RecoveryRequired. It never parses a legacy log as Openraft metadata and never selects a majority tail at startup.

The only legacy admission path is offline open_with_legacy_recovery with an ApprovedLegacyConfigRecovery that binds:

  • one checkpointed SQLite snapshot with no nonempty WAL;
  • its exact non-zero SHA-256 checksum;
  • the exact latest applied transaction ID and config version; and
  • DiscardUnknownAppendedSuffix, an explicit decision to discard every unprovable suffix in the target legacy database.

Before replacement, the adapter opens the source without following symlinks, binds verification and consumption to that exact descriptor, rechecks the path/device/inode and offline WAL state after staging, and hashes the complete source. It checks SQLite integrity and required tables, rejects a source already claimed by Openraft, verifies the exact chain head and complete parent/version lineage, loads and verifies every audit chain, and validates every sealed config envelope. The first retained version need not be version 1, but it has no parent and every next record names the prior transaction at version +1. The target state is replaced and the Openraft authority marker is created in one immediate target-database transaction. This atomicity is node-local; operators must still drain and coordinate the whole fleet and preserve the same authority decision across members.

Migration is one-way. Rollback to the legacy software is permitted only by stopping the fleet and restoring untouched pre-migration backups. Deleting config_raft_* tables, removing the authority marker, or copying selected Openraft-era rows into legacy storage is not a rollback procedure. Any writes accepted after migration are absent from the restored backup and require an explicit operator disposition.

Consequences

The workspace has one consensus engine for SDK-owned distributed persistence. opc-persist no longer maintains custom election, replication, membership, snapshot, retry, or TCP/TLS implementations. Config and session state retain separate deterministic schemas and adapters while sharing Openraft and the bounded authenticated transport boundary.

The implementation provides durable config authority, atomic local admission, sealed/redacted consensus state, exact legacy recovery, partition/failover tests, and linearizable reads. An AMF-lite integration composes the real config encryption wrapper, rotates provider-backed keys, exercises followers/snapshots/restart, captures the shared wire, and scans live and restarted DB/WAL/SHM, log/outcome/history rows, and snapshots for plaintext, raw-key, provider-endpoint, and opaque-handle canaries while asserting exact provider-call counts. This qualifies the three-node provider/HKMS boundary; it does not alone establish a carrier-production profile or a remote-HKMS deployment. The shared transport suite also qualifies in-process three-node real-mTLS config formation/commit and new-call SVID reload. Deployed multi-process compatibility and restart/rejoin, resource bounds, soak, seamless connection retirement/trust lifecycle, and candidate release evidence remain production qualification work under GAP-001-006.

Standalone SqliteBackend remains valid only for single-replica profiles that explicitly accept that availability model.

Evidence

  • crates/opc-consensus/
  • crates/opc-persist/src/consensus/store.rs
  • crates/opc-persist/src/consensus/raft_adapter.rs
  • crates/opc-persist/src/consensus/storage.rs
  • crates/opc-persist/src/consensus/sqlite.rs
  • crates/opc-config-bus-consensus/
  • crates/opc-persist/src/backend/ops.rs
  • crates/opc-persist/tests/consensus_openraft.rs
  • crates/opc-amf-lite/tests/config_consensus_encryption.rs
  • crates/opc-session-net/tests/consensus_transport.rs
  • docs/consensus-operator-runbook.md
  • docs/adr/0019-one-openraft-consensus-engine.md

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

ADR 0004: Security Identity, Keying, And Audit Integrity

Status

Accepted

Date

2026-06-08

Context

The SDK needs reusable production security substrates rather than bespoke per-CNF wiring. Identity, mTLS transport, key retrieval, audit redaction, and tamper evidence must be consistent across config, session, persistence, alarm, and operator-facing paths.

Decision

Production security uses explicit shared adapters:

  • opc-identity watches SPIFFE SVIDs and trust bundles.
  • opc-tls builds reloadable mTLS client/server configurations from identity material.
  • opc-key provides durable KmsKeyProvider adapters over authenticated KMS transports or local Unix-socket agents.
  • Memory key providers remain deterministic test/conformance adapters.
  • Persistence audit records redact sensitive values before storage and before hash-chain/HMAC material is calculated.
  • opc-mgmt-audit-store is the production adapter for management-operation audit: it stores only the existing bounded structured fields, maintains an authenticated retention anchor, verifies retained history on restart, and fails closed under bounded queue/acknowledgement deadlines.
  • Alarm administration uses NACM-backed authorization and durable audit sinks.

Consequences

Production deployments must supply real identity and KMS infrastructure. Unauthenticated TCP KMS and in-memory keys are not production key sources.

Security failures should fail closed and surface sanitized errors rather than leaking paths, SQL details, PEM material, keys, subscriber identifiers, or network addresses.

The local authenticated anchor detects retained-row alteration, deletion, reordering, and anchor/row disagreement. A coherent whole-database rollback is outside the evidence a local file can provide and requires a deployment-owned external monotonic checkpoint when storage anti-rollback is required.

Fixed-retention appends prune at most one previously authenticated low-water row. The new low-water must authenticate before another append can prune it; retention reconfiguration performs full verification before multi-row pruning. Normal appends therefore remain constant-work boundary checks. A SQLite data version change from an external connection activates the exceptional orphan path-row scan before append.

Evidence

  • crates/opc-identity/
  • crates/opc-tls/
  • crates/opc-key/
  • crates/opc-persist/src/backend/
  • crates/opc-mgmt-audit-store/
  • crates/opc-alarm/src/nacm_adapter.rs
  • crates/opc-alarm/src/persist_adapter.rs

ADR 0005: Runtime Observability And Admin Probes

Status

Accepted

Date

2026-06-08

Context

Production CNFs need consistent runtime health, readiness, metrics, alarm visibility, and debug/admin routes. These surfaces must be shared and redaction-safe, not reimplemented by each NF.

Decision

Runtime observability is a shared SDK surface:

  • opc-runtime owns liveness, readiness, startup, debug, and admin route semantics.
  • Production and lab admin/probe/debug endpoints require bearer token authorization.
  • /metrics exports Prometheus text through a shared SdkMetrics registry.
  • Metrics use low-cardinality, redaction-safe labels.
  • Runtime, ConfigBus, persistence, session store, NACM, and alarms report counters/gauges/histograms through the shared metrics surface.
  • Runtime failures and drain failures raise SDK-managed alarms.

Consequences

Downstream CNFs should wire the SDK runtime and metrics instead of creating incompatible health/admin conventions.

Debug endpoints are production-controlled operational surfaces. They must never expose raw configs, tokens, SQL, file paths, certificate material, subscriber IDs, or other sensitive data.

Evidence

  • crates/opc-runtime/src/admin.rs
  • crates/opc-runtime/src/health.rs
  • crates/opc-redaction/src/metrics.rs
  • crates/opc-sdk-integration/tests/observability.rs
  • docs/operator-readiness.md

ADR 0006: Fail-Closed Fault Injection Validation

Status

Accepted

Date

2026-06-08

Context

Happy-path tests are insufficient for SDK stability claims. Storage, KMS, SPIFFE, consensus, session replication, runtime, and evidence release gates all have failure modes where unsafe behavior can look like success unless tested directly.

Decision

The SDK validates production safety with explicit fault injection and chaos tests:

  • Persistence can simulate disk full, fsync/write failure, corrupt database, corrupt WAL, failed rollback target load, failed rollback point creation, and audit-chain corruption.
  • Config and session HA are tested under partitions, crashes, stale leaders, stale fences, rejoin/catch-up, split-brain healing, and partial writes.
  • SPIFFE and KMS are tested under expiry, rotation, bundle removal, timeout, and unavailability.
  • Runtime and admin routes are tested for authentication, malformed requests, timeouts, and redaction.
  • Release gates are tested for missing evidence, malformed JSON, dirty provenance, missing signatures, tampered bundles, and unsafe evidence values.

Consequences

Test-only fault hooks are acceptable when explicitly gated and named as dangerous test hooks. Production APIs should not expose fault injection knobs.

Regression tests must prefer fail-closed assertions: no publish, no partial commit, no stale promotion, no sensitive error leak, and no unsafe readiness claim.

Evidence

  • crates/opc-sdk-integration/tests/fault_injection.rs
  • crates/opc-security-testkit/
  • crates/opc-session-testkit/
  • crates/opc-evidence/tests/evidence_pipeline.rs

ADR 0007: Operator Lifecycle Rust Policy Core

Status

Accepted

Date

2026-06-08

Context

The SDK is not a product operator, but downstream CNF operators need common policy decisions for compatibility, admission, configuration apply, migration, drain, rollback, and fleet status. Those policy decisions should be reusable from Rust SDK code and Go Kubernetes operators.

Decision

Operator lifecycle policy lives in Rust SDK crates:

  • operator-lifecycle owns lifecycle phases, admission checks, compatibility matrix policy, config-apply decisions, and rollback constraints.
  • operator-controller owns deterministic conversion helpers, migration plan execution, drain client orchestration, and multi-cluster status aggregation.
  • Policy functions use structured inputs/outputs and fail closed on unknown, malformed, stale, or unsupported state.
  • Error messages are sanitized before crossing operator or webhook boundaries.

Consequences

The SDK can expose consistent policy decisions to multiple operator implementations without forcing all Kubernetes code into Rust.

Rust lifecycle crates do not deploy workloads by themselves. Product CNF operators still own reconciliation of Deployments, StatefulSets, Services, protocol-specific CRDs, and live cluster behavior.

Evidence

  • crates/operator-lifecycle/
  • crates/operator-controller/
  • crates/operator-lifecycle-cli/
  • docs/operator-readiness.md

ADR 0008: Go Reference Operator Boundary

Status

Accepted

Date

2026-06-08

Context

The original repository direction is polyglot: SDK core behavior is Rust, while Kubernetes operator integration should use Go controller-runtime, which is the first-class Kubernetes operator ecosystem. At the same time, this repository is an SDK, not an AMF/SMF/UPF product operator.

Decision

The repository includes a Go reference operator harness under operators/sdk-reference-operator.

The Go harness demonstrates:

  • CRD API versions and conversion wiring.
  • Validating webhook integration.
  • Controller reconciliation shape and status updates.
  • Kustomize/RBAC/cert-manager/manager manifests.
  • A Go-to-Rust JSON CLI bridge to operator-lifecycle-cli.

The harness is explicitly not a production CNF operator and does not encode product-specific reconciliation.

Consequences

Downstream CNF teams get a concrete Go integration pattern without importing product behavior into the SDK repository.

Reference tests use Go unit tests, fake-client controller/webhook tests, rendered Kustomize manifests, and Rust CLI contract tests. Product CNF operators must add envtest, kind, and real-cluster end-to-end tests around their own reconciliation logic.

Manager images must package both the Go manager binary and the Rust operator-lifecycle-cli, or set OPERATOR_LIFECYCLE_CLI_PATH to a valid CLI location.

Evidence

  • operators/sdk-reference-operator/
  • crates/operator-lifecycle-cli/
  • docs/operator-readiness.md
  • docs/implementation-status.md

ADR 0009: Platform Preflight Resource Contract

Status

Accepted

Date

2026-06-08

Context

Carrier CNFs often depend on CPU isolation, NUMA locality, hugepages, NIC capabilities, SR-IOV, AF_XDP/eBPF, CNI behavior, and pod-security exceptions. These assumptions cannot remain tribal knowledge or comments in deployment manifests.

Decision

Production data-plane readiness is an explicit SDK contract:

  • opc-node-resources models resource profiles and node capability reports.
  • CPU manager, topology manager, isolated/reserved CPU sets, NUMA mappings, hugepage pools, NIC capabilities, and data-plane interfaces are validated.
  • AF_XDP/eBPF artifacts require digest pinning, signer/evidence identity, program type, attach point, and allowed capability checks.
  • Pod-security exceptions must be minimal and evidence-linked.
  • Lab/dev fallback paths fail closed in production.
  • Operator admission and config-apply paths consume the preflight report.

Consequences

Production manifests must provide explicit resource profiles and node capability evidence. If evidence is absent, stale, or incompatible, the SDK policy blocks rollout instead of silently downgrading to lab behavior.

The Go reference operator projects this contract into CRD fields but does not replace product-specific operator resource management.

Evidence

  • crates/opc-node-resources/src/lib.rs
  • crates/operator-lifecycle/src/admission.rs
  • crates/operator-lifecycle/src/config_apply.rs
  • operators/sdk-reference-operator/api/

ADR 0010: Release Assurance Evidence Pipeline

Status

Accepted

This status accepts the architectural decision. It does not mean the complete pipeline is implemented; the implementation status below remains partial.

Date

2026-06-08

Context

The SDK needs release evidence that is machine-readable and fail-closed. Manual claims like "tests passed" are insufficient for conformance, supply-chain assurance, and auditability.

Decision

opc-evidence defines the SDK-owned data model and policy engine for the RFC 006 release-assurance pipeline.

It provides:

  • Source extraction for RFC 006 tags such as @spec, @req, @conformance, @gap, @security, @performance, and @test.
  • Deterministic CycloneDX SBOM generation from local Cargo manifests and lock data.
  • VEX policy result and record validation.
  • SLSA/in-toto-style provenance tied to commit, builder, input materials, output digests, and dirty/clean worktree state.
  • Bundle assembly and verification with canonical manifest signing bytes.
  • Signer/verifier traits and deterministic in-process test signing.
  • Performance baseline schema with redaction-safe environment metadata and regression status.
  • PR/release gate policy that fails closed on missing evidence, missing signatures, tampering, mismatched commits, dirty release provenance, malformed JSON, or unsafe evidence content.

Implementation status

The library primitives are implemented and tested. Signing inputs are deterministic and domain-separated, signer/verifier identities are bound to the manifest, manifest paths and digests fail closed, and GateEvaluator requires separately supplied artifacts to exactly match their signed bundle values. The signed manifest additionally binds the canonical record, gap, and waiver inputs that drive the gate, and commit identities are cross-checked without disclosure. Repository workflows still do not produce and enforce the complete RFC 006 artifact set or invoke the release policy evaluator with a production external signer/verifier. Consequently, this ADR's end-to-end pipeline decision remains only partially implemented.

Consequences

Release pipelines must treat evidence artifacts as required inputs, not as optional reports.

Real Sigstore/Cosign keyless signing remains an external signer adapter boundary. The SDK owns the signing/verifier interface and test verifier, not a hard dependency on one hosted signing provider.

Evidence

  • crates/opc-evidence/src/extract.rs
  • crates/opc-evidence/src/sbom.rs
  • crates/opc-evidence/src/vex.rs
  • crates/opc-evidence/src/provenance.rs
  • crates/opc-evidence/src/bundle.rs
  • crates/opc-evidence/src/performance.rs
  • crates/opc-evidence/src/policy.rs
  • crates/opc-evidence/tests/evidence_bundle.rs
  • crates/opc-evidence/tests/evidence_policy.rs
  • crates/opc-evidence/tests/evidence_sbom_vex.rs
  • crates/opc-evidence/tests/evidence_provenance.rs

ADR 0011: First NF Vertical Proof

Status

Accepted

Date

2026-06-08

Context

The SDK needed proof that its seams compose in a real NF-shaped control-plane slice. Toy examples can validate local APIs, but they do not prove that runtime, config, session, identity, KMS, NACM, alarms, metrics, and HA recovery work together.

Decision

opc-amf-lite is the first NF vertical integration proof.

It demonstrates:

  • Runtime startup and supervised workers.
  • Secure ConfigBus integration.
  • Consensus-backed configuration persistence.
  • Quorum session storage with read-repair behavior.
  • KMS-backed encryption paths.
  • NACM authorization and audit.
  • Alarm and metrics integration.
  • HA recovery and failure validation.

opc-amf-lite is not a product AMF. It is a reusable SDK proof slice that downstream CNFs can study when wiring their own production crates.

Consequences

The SDK can claim that its core seams compose into an NF-shaped control-plane vertical. It cannot claim complete AMF/SMF/UPF protocol coverage from this slice.

Future NF crates should follow the integration pattern but own their procedure-specific logic, protocol fidelity, and product tests.

Evidence

  • crates/opc-amf-lite/
  • crates/opc-amf-lite/README.md
  • docs/implementation-status.md
  • docs/operator-readiness.md

ADR 0012: Diagnostics Safety and Privacy Governance

Status

Accepted

Date

2026-06-08

Context

Diagnostics, support bundles, exports, and evidence files pose a high risk of leaking sensitive subscriber identifiers (SUPI, IMSI, MSISDN), secrets, cryptographic credentials, database internals, and local filesystem paths. The SDK required a structured, fail-closed diagnostics and privacy boundary to satisfy RFC 010.

Decision

Establish a clear, multi-crate boundary for diagnostics safety and privacy governance:

  1. Structured, Redacted Support Bundles:

    • Diagnostic data is collected as structured DiagnosticEntry variants.
    • Support bundles are redacted prior to serialization using redact_support_bundle.
    • The engine cleans sensitive subscriber identifiers, IPs, SPIFFE IDs, JWTs, paths, database errors, and secrets, producing a RedactionSummary.
    • Unknown or unsafe attachments fail closed in Production mode.
  2. Declarative Retention & Legal Holds:

    • RetentionPolicy schema in opc-data-governance dictates retention duration, data class, and disposal action.
    • Policies validate durational boundaries and block deletion/disposal decisions when a legal hold flag is active.
  3. Classification-Preserving Exports:

    • ExportedItem in opc-export encapsulates the payload and ExportMetadata.
    • Production validation rejects raw sensitive payloads unless they are encrypted.
  4. Analytics Minimization:

    • MinimizationPolicy in opc-privacy enforces k-anonymity cohort sizing thresholds, binning, and subscriber ID digest hashing.
    • Cohorts below the threshold or direct identifiers are rejected.
  5. Data-Governance Evidence Gating:

    • Release gates require DataGovernanceEvidenceReport validation.
    • The evaluator parses the report and scans it to ensure no absolute paths, credentials, or raw IPs are present.

Consequences

  • Diagnostic attachments and support bundles cannot silently leak raw sensitive identifiers or secrets in Production mode.
  • Downstream CNFs can safely collect support bundles and perform analytics exports without violating privacy regulations.
  • Data-governance compliance is automatically checked and enforced at release compile/gate time.

Evidence

  • crates/opc-redaction/src/support_bundle.rs
  • crates/opc-data-governance/src/retention.rs
  • crates/opc-export/src/lib.rs
  • crates/opc-privacy/src/lib.rs
  • crates/opc-evidence/src/data_governance.rs
  • crates/opc-sdk-integration/tests/privacy_governance.rs

ADR 0013: NGAP ASN.1 Strategy

Status

Accepted — amended 2026-06 with first implementation experience

Date

2026-06-11

Context

NGAP (NG Application Protocol, 3GPP TS 38.413) is required for gNodeB↔AMF and AMF↔SMF signaling. Unlike GTP-U (fixed binary headers) or PFCP (TLV IEs), NGAP is defined in ASN.1 using APER (Aligned Packed Encoding Rules). Hand-writing an APER codec is error-prone, high-maintenance, and incompatible with the SDK's goal of spec-traceable, fuzz-safe protocol code.

The SDK currently has:

  • opc-protocol — zero-copy codec framework with BorrowDecode/Encode
  • opc-proto-gtpu — GTP-U codec following the above framework
  • opc-proto-pfcp — PFCP codec (planned, TS 29.244)

NGAP is the next mandatory codec after PFCP, but its ASN.1 nature makes it structurally different from the existing binary codecs.

Decision

We will not hand-write NGAP APER parsing or code-generation.

Instead, we will evaluate and adopt a maintained Rust ASN.1 / APER toolchain that can consume the 3GPP ASN.1 modules directly. The evaluation criteria are:

  1. MSRV 1.81 compatibility — must compile on the SDK's declared MSRV.
  2. License compatibility — Apache-2.0 or MIT, no copyleft dependencies.
  3. #![forbid(unsafe_code)] — generated and runtime code must be pure safe Rust.
  4. Fuzzability — the generated codec must integrate with cargo-fuzz and tolerate hostile inputs without panics.
  5. Maintenance risk — actively maintained, responsive to security issues, ideally with existing 3GPP or telecom user base.

Options Evaluated

Option A: hampi / rasn ecosystem

  • hampi (GitHub: repnop/hampi) — ASN.1 compiler generating Rust structs with APER/UPER/OER support.
  • rasn (GitHub: XAMPPRocky/rasn) — runtime ASN.1 codec library with derive macros.

Pros: Pure Rust, no_std capable, active development, Apache-2.0. Cons: hampi's APER support is partial (v0.x); no proven 3GPP NGAP corpus yet; smaller community than protobuf alternatives. Verdict: Leading candidate. Requires a spike to compile 3GPP R18 NGAP ASN.1 modules and validate against known-good PCAPs.

Option B: Generated code from asn1-codecs (ERI framework)

The asn1-codecs family (used by some telecom OSS projects) generates Rust from ASN.1 via an intermediate representation.

Pros: Explicitly designed for telecom ASN.1 modules. Cons: Mixed maintenance status; some forks carry unsafe code; licensing unclear on some forks; heavy dependency tree. Verdict: Fallback if Option A fails the spike. Requires legal review of upstream license before adoption.

Option C: FFI to srsRAN / OAI C NGAP codec

Reuse the established C NGAP implementations from srsRAN or OpenAirInterface.

Pros: Battle-tested against live networks; spec-complete. Cons: FFI requires unsafe blocks, violating the SDK's #![forbid(unsafe_code)] invariant. Cross-compilation for musl/target environments adds complexity. Memory-safety bugs in C code become SDK security issues. Verdict: Rejected. The forbid(unsafe_code) constraint is architectural and non-negotiable for a carrier-grade CNF security substrate.

Option D: Hand-written subset

Implement only NGSetupRequest/Response and InitialUEMessage by hand and omit the rest.

Pros: Zero new dependencies; full control over decode limits and fuzzing. Cons: Maintenance nightmare on every 3GPP release; no spec-traceability to ASN.1 modules; high bug rate. Verdict: Rejected. The SDK explicitly rejected hand-written ASN.1 for NGAP at the architecture level.

Recommendation

Proceed with Option A (hampi/rasn).

Phased plan:

  1. Spike (v0.2.x follow-up): Compile 3GPP R18 NGAP ASN.1 modules with hampi/rasn, generate structs, and validate against a small corpus of known-good NGAP PDUs (extracted from 3GPP test specifications or opc-testbed fixtures).
  2. Subset crate (v0.3.0): Create opc-proto-ngap wrapping only NGSetupRequest/Response and InitialUEMessage to prove the integration pattern with opc-protocol's decode-context limits.
  3. Full message surface (v0.4.0+): Expand to the full NGAP message and IE surface required by the AMF-lite reference implementation.

Consequences

  • The SDK gains a maintainable, spec-traceable NGAP codec path.
  • Downstream NF operators must accept a generated-code dependency (acceptable given the alternative of FFI or hand-written bugs).
  • If hampi/rasn fails the spike, we fall back to Option B with a license review gate.

Implementation experience (2026-06)

The first opc-proto-ngap attempt followed the phased plan and stalled at step 1 on toolchain compatibility, not on the codec approach itself:

  • rasn (0.22 and 0.25) failed the then-declared MSRV of 1.81. Its derive implementation transitively requires uuid ^1.11, which resolves to a getrandom release whose manifest uses edition2024 — unparseable by Cargo 1.81. No pinning escape existed within rasn's requirements.
  • Investigating the failure exposed that the workspace's own dependency graph had already drifted past MSRV 1.81 through the same getrandom release (reached via uuid, tempfile, and quickcheck), i.e. the MSRV declaration no longer reflected reality independent of NGAP.
  • hampi was not pursued: no meaningful release since 2021 and its APER encoder was still marked work-in-progress then — unacceptable abandonment risk for a protocol codec.

Consequences acted on:

  • The workspace MSRV was raised to 1.88, the actual floor of the resolved dependency graph (set by time; edition2024 support needs ≥ 1.85, the icu stack ≥ 1.86). This repairs the MSRV gate and removes the blocker on Option A. See ADR 0014 for the toolchain/dependency policy.
  • The Option A spike should be re-run against rasn on the raised MSRV before any consideration of Option B (asn1-codecs, which still carries its license-review gate per the comparison above).

Evidence

  • Gap register updated: GAP-PROTO-003 now records the partially closed codec boundary.
  • docs/implementation-status.md linked.

ADR 0014: Dependency and Toolchain Policy

Status

Accepted (amended 2026-06-12: crypto-provider scope and JWT backend, point 9; amended 2026-07-24: DTLS provider admission, point 9)

Date

2026-06-11

Context

The SDK is the foundation for downstream CNFs with carrier security and audit requirements. Every dependency the workspace takes is inherited by every downstream NF, and several incidents during development showed that implicit policy does not survive contact with routine maintenance:

  • The declared MSRV silently drifted out of truth: routine lockfile updates pulled a getrandom release whose manifest requires edition2024, unparseable by the Cargo version the workspace claimed to support — and the breakage reached the graph through three independent parents (uuid, tempfile, quickcheck), one of them in the production graph.
  • An HTTP adapter was nearly built on a second client stack when the workspace already standardized on one.
  • A license gate failure appeared days after the dependency that caused it, because the gate's evidence had been captured before the dependency landed.

Decision

  1. TLS: rustls only. No openssl/native-tls anywhere in the graph, including transitively via feature defaults (disable default-features where needed). Rationale: a single auditable TLS stack and reproducible cross-compilation, with no coupling to a system OpenSSL/native-tls library (dynamic linking, version skew). This rule targets system/dynamic crypto; vendored crypto built statically from source as part of the graph (e.g. ring, aws-lc-sys) is permitted — see point 9.

  2. Async runtime: tokio only. No second runtime, no runtime-agnostic abstraction layers.

  3. No gRPC stack (tonic/prost) in SDK crates. Internal transports (e.g. session replication) use hand-specified framing over the existing tokio/rustls stack; external 3GPP interfaces are HTTP/2 (hyper) or raw protocol codecs. A future exception requires an ADR, not a Cargo.toml edit. (An ASN.1 codec dependency for NGAP per ADR 0013 is the kind of exception that warrants that process.)

  4. HTTP clients: hyper is the workspace HTTP stack. reqwest (rustls-backed, built on hyper) is tolerated in leaf adapter crates (currently opc-key-vault) but must not spread into core crates.

  5. MSRV is the measured floor of the resolved graph, not an aspiration. Currently 1.88 (set by time). The CI msrv job compiles the whole workspace (--all-targets --all-features) on exactly the declared version; a lockfile update that raises the floor must raise rust-version, this ADR's record, and the contributor docs in the same change. Raising MSRV is acceptable for a pre-1.0 SDK; lying about it is not.

  6. Licenses: Apache-2.0/MIT/BSD-family only, enforced by cargo deny with a curated allow-list; uncommon-but-permissive licenses are admitted as per-crate exceptions in deny.toml, never as global allows.

  7. Every new dependency is justified in the PR description (what it replaces, why the existing stack cannot serve, license, MSRV impact).

  8. unsafe_code = "forbid" is workspace-wide and non-negotiable, which also rules out FFI-based protocol libraries (see ADR 0013).

  9. Cryptographic providers. rustls uses the ring provider for TLS; opc-sbi's jsonwebtoken uses the aws_lc_rs backend for JWT-SVID signature verification. Both are vendored, statically-built crypto (no system OpenSSL), consistent with point 1. aws_lc_rs is chosen over jsonwebtoken's pure-Rust rust_crypto backend because the latter pulls the rsa crate, which carries RUSTSEC-2023-0071 (the "Marvin" timing sidechannel) with no fixed release available upstream. That advisory is unreachable for our verify-only (public-key) usage — the SDK never holds or decrypts with an RSA private key — but aws_lc_rs is constant-time and keeps both security gates (cargo audit, cargo deny) green without a standing advisory exception, which matters for a security SDK whose advisory surface is inherited by every downstream consumer. Future goal: migrate JWT verification to the pure-Rust rust_crypto backend once the rsa crate ships a constant-time release (its in-progress crypto-bigint migration), dropping the aws-lc-sys/cmake build step and fully satisfying the pure-Rust ideal.

    A third provider family is admitted for DTLS: the audited workspace-vendored dimpl 0.7.2 fork with its pure-Rust rust-crypto feature (RustCrypto AEAD/ECDSA/ECDH crates) implements the RFC 6083 DTLS/SCTP record layer for opc-diameter-transport. The existing stack cannot serve: rustls has no DTLS support, and FFI/OpenSSL-class bindings are barred by point 8. The production dependency is an exact, non-publishable path binding with default-features = false and features = ["rust-crypto"]; its aws-lc-rs/rcgen defaults and the rsa crate therefore stay out of the production graph. Downstream feature unification is not presented as a selectable deployment-provider contract. The fork's complete upstream test tree and both built-in provider configurations remain CI qualification inputs, and vendor/dimpl/UPSTREAM.md records the exact crates.io checksum, upstream Git revision, local patch inventory, and mandatory source gates. Peer-certificate PKI validation for DTLS is performed with the same rustls-webpki verification family as the TLS side, not by the engine. DTLS construction is not currently routed through opc-crypto-provider; the transport binds the fork's audited RustCrypto provider explicitly instead of consulting the fork's process-global default-provider slot, and its fallible constructor runs that provider's known-answer validation before it can accept traffic.

Consequences

  • Some integrations cost more to build (hand-rolled framing instead of tonic; hyper plumbing instead of convenience clients) in exchange for a dependency graph that downstream carriers can audit once and trust.
  • MSRV moves forward with the ecosystem rather than pinning old dependency lines; downstream consumers should track a recent stable toolchain.
  • scripts/publish-order.py --check and cargo deny check are the mechanical halves of this policy; this ADR is the rationale they enforce.

ADR 0015: Protocol Codec Conformance Policy

Status

Accepted

Date

2026-06-11

Context

The SDK ships wire codecs for 3GPP protocols (GTP-U, PFCP, NAS-5GS, with NGAP planned). Codec bugs are uniquely dangerous: an encoder and decoder written by the same hand are internally consistent, so round-trip tests pass perfectly while every byte on the wire is wrong for a real peer. This failure mode occurred twice during development — a scrambled PFCP header flag layout and a byte-swapped Outer Header Creation description field — and in both cases the existing test suite was green because the fixtures had been derived from the codec's own output.

Decision

Every protocol codec crate (opc-proto-*) MUST satisfy all of the following before it is merged, and CONFORMANCE.md must claim nothing the tests do not prove:

  1. Spec-authored fixtures. Conformance tests include byte fixtures hand-authored from the 3GPP specification (or captured from an independent implementation), with octet-level comments citing the spec section. Fixtures derived from this codec's own encoder do not count as conformance evidence — they detect regressions, not wire-format errors.
  2. Byte-exact round-trips. decode → encode must reproduce the input bytes exactly for every fixture, including unknown/vendor-extension elements, which must be preserved raw.
  3. Declared canonicalization. Where a typed view legitimately normalizes (zeroing spare bits, dropping forward-compatibility trailing octets that the spec requires receivers to ignore), CONFORMANCE.md must say so explicitly, and a raw byte-preserving layer must remain available for forwarding paths.
  4. Hostile-input safety. No panics on any input: checked arithmetic on all length/offset math, enforced decode limits (message length, element count, recursion depth), and negative tests for truncation, overflow, and depth bombs.
  5. Fuzzing. A fuzz target over the decode surface with a seed corpus of spec-valid messages, registered in the fuzz CI workflow. The fuzz crate must compile in CI even when fuzzing is not executed.
  6. Framework fit. Codecs implement the opc-protocol traits (BorrowDecode/OwnedDecode/Encode) and carry @spec/@req traceability tags so RFC 006 evidence tooling can index them.
  7. CONFORMANCE.md enumerates exactly which messages, elements, and fields are covered, at which 3GPP release, and what belongs outside the codec boundary.

Consequences

  • Writing a codec costs more up front: authoring fixtures from the spec is slower than round-tripping the encoder. That cost is the point — it is the only test construction that catches self-consistent wire errors.
  • Reviews of codec changes start from the fixtures: a reviewer verifies bytes against the cited spec section before reading the implementation.
  • opc-proto-gtpu, opc-proto-pfcp, and opc-proto-nas conform today and serve as the templates; future codecs (NGAP per ADR 0013) inherit the same bar.

ADR 0016: Northbound gRPC Stack Exception (gNMI)

Status

Accepted

Date

2026-06-13

Context

ADR 0014 §3 states: "No gRPC stack (tonic/prost) in SDK crates. … A future exception requires an ADR, not a Cargo.toml edit." That rule keeps the core SDK dependency graph lean and auditable: internal transports use hand-specified framing over tokio/rustls, and external 3GPP interfaces are HTTP/2 (hyper) or raw protocol codecs.

The management-plane work introduces opc-gnmi-server (see docs/design/opc-gnmi-server-spec.md). gNMI (OpenConfig) is a gRPC service: its contract is a protobuf service over HTTP/2. There is no rustls/hyper-only or hand-framed path to a conformant gNMI server — a client (gnmic, gNMIc, OpenConfig collectors) speaks gRPC and nothing else. So opc-gnmi-server cannot exist without a gRPC stack, and per ADR 0014 §3 that requires this ADR.

gNMI is a distinct dependency category from the cases ADR 0014 §3 was written for. It is a northbound management interface embedded by a CNF that chooses to expose gNMI — not an internal SDK transport and not a 3GPP data-plane codec.

Decision

Permit tonic, prost, and prost-types only for the northbound gNMI server crate, opc-gnmi-server. prost-types is included because the vendored OpenConfig gNMI proto uses standard Google protobuf types such as google.protobuf.Any. tonic-build is permitted only as that crate's build-time proto-generation dependency if the Phase-0 spike chooses build-time generation. Any future gRPC-based management crate requires an explicit ADR amendment and an update to the mechanical allow-list; this exception is not a blanket "management crates may use gRPC" policy. Specifically:

  1. Scope boundary. tonic/prost/prost-types MUST NOT appear in any core SDK crate (opc-config-bus, opc-config-model, opc-persist, opc-runtime, opc-identity, opc-tls, opc-nacm, opc-yanggen, the opc-proto-* codecs, opc-sbi, the opc-mgmt-* foundation crates, etc.). They live only in opc-gnmi-server unless this ADR is amended. ADR 0014 §3 remains in force everywhere else. Inside this SDK workspace, no other crate may depend on or re-export opc-gnmi-server; downstream CNFs outside the workspace opt in to gNMI by depending on the server crate directly.
  2. Boundary is enforced mechanically. scripts/check-management-plane-policy.py --check asserts that no crate outside the explicit allowed set directly or transitively depends on tonic/prost/prost-types/tonic-build, or on opc-gnmi-server itself. The CI job runs this gate. The initial allowed set is exactly opc-gnmi-server.
  3. One TLS stack only (ADR 0014 §1 preserved). opc-gnmi-server serves tonic over the rustls::ServerConfig produced by opc-mgmt-transport (ring provider), not tonic's own/native TLS. No openssl/native-tls enters the graph (verify tonic/hyper features with default-features = false, rustls only).
  4. Dependency hygiene (ADR 0014 §6/§7). tonic/prost/prost-types are MIT/Apache — compatible with the license gate. The PR adding them justifies them per §7 and passes cargo deny. The pinned tonic version MUST compile on the workspace MSRV (currently 1.88, ADR 0014 §5); the Phase-0 spike validates this before the version is pinned, and any MSRV bump follows the §5 process.
  5. Proto pin and generation mode. The gNMI proto is vendored at an exact tag under crates/opc-gnmi-server/proto/; the vendored files carry the upstream tag/commit in their header, and the advertised gNMI version string derives from this pin. The Phase-0 spike must choose and document exactly one generation mode:
    • build-time generation with tonic-build, which adds an explicit protoc build prerequisite and a CI check that generated output is reproducible; or
    • checked-in generated Rust, which avoids protoc in downstream builds but requires a regeneration script and a CI drift check. In either mode, generated service code is treated as part of the opc-gnmi-server boundary and does not become a shared SDK dependency.
  6. This exception does not generalize. It authorizes a gRPC server for a northbound management protocol that is gRPC by definition. It is not license to adopt gRPC for internal transports or to relax ADR 0014 §3 for core crates.

Consequences

  • A downstream CNF outside this workspace that embeds opc-gnmi-server inherits tonic/prost/prost-types. That is an explicit opt-in to gNMI; CNFs that do not expose gNMI never pull the stack.
  • The core SDK graph stays gRPC-free and auditable, exactly as ADR 0014 §3 intends; only the optional northbound server adds gRPC.
  • The mechanical gate from point 2 exists and runs in CI, so this exception's scope cannot silently erode — the same "implicit policy does not survive maintenance" lesson that motivated ADR 0014.
  • NETCONF (opc-netconf-server) is unaffected: it is XML over SSH/TLS and needs no gRPC stack.

ADR 0017: SCTP Transport Strategy and Unsafe-FFI Sys-Crate Boundary

Status

Accepted

Date

2026-06-13

Context

ADR 0014 §8 states unsafe_code = "forbid" is workspace-wide and "non-negotiable, which also rules out FFI-based protocol libraries (see ADR 0013)." ADR 0013 rejected Option C — FFI to the srsRAN/OAI C NGAP codec — because foreign C code parsing attacker-controlled bytes turns memory-safety bugs into SDK security issues.

opc-sctp is required for CNFs that terminate N2/NGAP or other SCTP interfaces. Unlike NGAP, SCTP is not a codec — it is an OS transport. Linux implements SCTP in the kernel (lksctp); a userspace program reaches it through SCTP sockets: socket(AF_INET, SOCK_STREAM|SOCK_SEQPACKET, IPPROTO_SCTP), SCTP setsockopt options, sendmsg/recvmsg with SCTP control messages, and, where necessary, thin libsctp helper calls such as bind/send/receive variants over the same kernel SCTP UAPI. Rust's std and tokio expose no SCTP socket API, so reaching kernel SCTP requires libc/UAPI FFI, which is unsafe. ADR 0014 §8 was written for protocol codec libraries and did not anticipate an OS-transport syscall surface.

The distinction is decisive:

  • ADR 0013's rejected FFI links a large foreign C parser (thousands of lines) that consumes attacker-controlled wire bytes. The attack surface is the C code itself.
  • SCTP FFI is a thin wrapper over kernel socket UAPI and optional libsctp helper functions that themselves configure or call the kernel SCTP stack. The SCTP protocol implementation is the kernel — already trusted, exactly as for TCP/UDP. This is the same category of unsafe that tokio/mio already use internally for socket I/O in the workspace. The "foreign C parsing attacker bytes" risk ADR 0013 guarded against simply is not present.

Options

  • A. Kernel SCTP behind a narrow opc-libsctp-sys sys crate. Thin libc/SCTP-UAPI FFI in one crate, including libsctp helpers only where the Linux SCTP API requires them; a safe opc-sctp wrapper above it. Linux-only.
  • B. Userspace SCTP stack (pure Rust). Reimplement the SCTP transport protocol with no FFI. Rejected: a from-scratch transport-protocol implementation is large and security-sensitive (association state machine, retransmission, multihoming, chunk bundling) and is more likely to harbor exploitable bugs than thin syscall FFI over the hardened kernel stack; no maintained pure-Rust SCTP stack exists to adopt.
  • C. Omit SCTP from the SDK. Ship no SCTP transport. Acceptable only if the first production CNF does not terminate N2/NGAP or any SCTP interface; it blocks N2-terminating CNFs.

Decision

Amend ADR 0014 §8 to permit a narrow, explicitly allowlisted unsafe exception pattern for Linux kernel UAPI sys crates, and adopt Option A when an SCTP-terminating CNF is in scope:

  1. opc-libsctp-sys provides thin FFI over Linux SCTP socket UAPI and minimal libsctp helpers where required. It is the only SCTP workspace crate permitted to contain unsafe; follow-on Linux kernel UAPI exceptions such as opc-linux-xfrm-sys, opc-linux-gtpu-sys, and opc-fs-verity-sys, and narrow reviewed FFI boundaries such as opc-sqlite-file-control-sys, must be separately and explicitly allowlisted by the same mechanical gate. The fs-verity boundary is limited to enabling and measuring the fixed version-1, SHA-256, 4096-byte-block profile through already-open file descriptors; it accepts no paths or file contents. The SQLite production boundary is limited to SQLITE_FCNTL_HAS_MOVED, SQLITE_FCNTL_VFSNAME, SQLITE_FCNTL_FILE_POINTER, and SQLITE_FCNTL_JOURNAL_POINTER: it returns only movement state or owned duplicate descriptors after authenticating the bundled Linux Unix VFS. Its opt-in test feature may register one non-default VFS that rejects unnamed temporary opens and delegates named opens to the bundled default VFS. This does not authorize borrowed-handle exposure, pathname authority, file contents, another opcode, a production VFS, or general SQLite FFI. Each allowlisted sys crate does not inherit [workspace.lints] (so the workspace-wide unsafe_code = "forbid" stays in force for every other crate); it sets its own local crate policy (unsafe_code = "allow" plus unsafe_op_in_unsafe_fn = "deny", or equivalent crate attributes) that allows unsafe only there, with a // SAFETY: comment required on every allowed unsafe token (unsafe block, unsafe fn, unsafe impl, unsafe trait, or unsafe extern block).
  2. opc-sctp (the public crate) is #![forbid(unsafe_code)] and exposes only safe async abstractions (associations, messages, events) over the sys crate, integrated with tokio::io::unix::AsyncFd (the spec's async model). Its manifest must declare the tokio features it relies on, including net, instead of relying on feature unification from unrelated workspace crates.
  3. Boundary is enforced mechanically. scripts/check-management-plane-policy.py --check token-scans OpenPacketCore workspace crate sources and asserts unsafe appears only in explicitly allowlisted sys crates (opc-libsctp-sys and later, reviewed kernel-UAPI boundaries such as opc-linux-xfrm-sys, opc-linux-gtpu-sys, and the fixed-profile descriptor-only opc-fs-verity-sys, plus the pinned file-control and test-only VFS boundary in opc-sqlite-file-control-sys); the same gate also rejects each allowed sys crate if it inherits [workspace.lints], rejects it if it lacks the required local unsafe lint policy, and requires each allowed unsafe token in that sys crate to be documented by an adjacent SAFETY: comment. The CI job runs this gate, so the exception cannot silently spread or become undocumented.
  4. ABI safety. Every C struct crossing the boundary has a struct-layout (size/alignment/offset) test. Kernel-UAPI sys crates build on their admitted targets in CI and fail explicitly elsewhere. In particular, opc-fs-verity-sys exposes its descriptor API only on Unix: Linux provides the reviewed ioctl implementation, non-Linux Unix targets compile an explicit unsupported result, and non-Unix targets are outside this crate's admitted platform surface.
  5. This exception pattern does not reopen ADR 0013. It authorizes FFI only to explicitly reviewed trusted Linux kernel UAPI boundaries such as SCTP socket/XFRM netlink calls and minimal helper calls that wrap those UAPIs. FFI that links a foreign C protocol codec (parsing attacker-controlled bytes — NGAP/NAS/etc.) remains rejected; those stay pure-Rust per ADR 0013/0015.
  6. SCTP is implemented per Option A behind this boundary, never as scattered unsafe and never as a userspace reimplementation without revisiting this ADR.

Consequences

  • The workspace gains small, auditable OpenPacketCore Linux UAPI sys crates containing unsafe; downstream carrier auditors review those explicitly allowlisted sys crates rather than a diffuse unsafe surface, and unsafe_code = "forbid" remains true everywhere else.
  • The CI gate from point 3 exists, mirroring the "policy must be mechanically enforced" lesson of ADR 0014.
  • opc-sctp uses the non-inheritance mechanism and AsyncFd model described by this ADR. Its README and tests record the current capability profile.
  • Static multihoming stays inside the same boundary: the sys crate owns the packed Linux bindx/connectx and bounded address-list UAPI, while the safe crate validates complete address sets, preserves the single-address syscall path, and exposes typed capability and kernel-active-address evidence.
  • NGAP-over-SCTP wiring (PPID 60) is separate integration work and is not authorized to use FFI for the NGAP codec itself.

ADR 0018: EPC and Untrusted-Access SDK Boundary

Status

Accepted

Date

2026-06-26

Context

The SDK is beginning a work stream that harvests reusable primitives from an ePDG-derived source packet for EPC and untrusted-access CNF use cases. Task 0.1 produced the committed inventory in docs/refactoring/epdg-sdk-harvest-inventory.md and the fixture provenance map in docs/refactoring/epdg-sdk-fixture-provenance.md. Those documents classify the source material as planning and provenance context, not as SDK-ready implementation, conformance evidence, or product claims.

This boundary matters because ePDG and EPC systems mix reusable mechanisms with deployment policy:

  • reusable protocol framing, bounded parsing, evidence schemas, resource models, redaction classes, and narrow kernel UAPI adapters can belong in a neutral SDK;
  • product-specific attach procedures, APN/realm/PLMN selection, retransmission policy, IKE/Child SA state machines, lawful-intercept workflow, charging policy, CRDs, Helm values, carrier acceptance, and deployment defaults must remain outside the SDK.

The current public SDK already states that GTP-U is applicable to LTE/EPC user plane while EPC control-plane protocols such as GTP-C, Diameter, and S1AP are not currently provided. This ADR authorizes future mechanism work for selected EPC/untrusted-access primitives without converting the SDK into an ePDG product, an EPC core, or a carrier-accepted deployment.

Decision

Adopt the following boundary for ePDG-derived EPC and untrusted-access work.

1. Source-use and provenance rule

The task 0.1 inventory and fixture provenance map are the normative inputs for the first harvest tranche. They are not copy-paste authorization. Each SDK change MUST re-author reusable behavior in SDK style and MUST keep product bytes, product tests, and product claims out of conformance evidence unless they later satisfy ADR 0015 provenance requirements.

If a source crate or directory lacks an explicit compatible license marker, code copying is blocked until source ownership is confirmed. Concepts may still be used to design independently authored SDK APIs when that does not import source implementation.

2. Mechanism is SDK-owned; policy is product-owned

The SDK MAY own product-neutral mechanisms that are reusable by multiple packet core CNFs:

  • pure Rust wire codecs and typed views that preserve unknown/raw fields;
  • bounded parser limits, hostile-input behavior, fuzz targets, and CONFORMANCE.md records required by ADR 0015;
  • transport-neutral protocol metadata, dictionaries, and peer-test utilities;
  • narrow Linux UAPI/sys boundaries plus safe wrappers, where an ADR authorizes the unsafe exception and mechanical gates enforce it;
  • redaction and regulated-data classification primitives;
  • resource/capability models and preflight validators;
  • runtime health-gate aggregation primitives;
  • simulator scaffolding and release-evidence schemas.

The product that embeds the SDK MUST own deployment and business policy:

  • ePDG attach orchestration and subscriber/session lifecycle decisions;
  • APN, DNN, realm, PLMN, PGW, AAA/HSS/CDF, and charging policy;
  • IKE SA and Child SA state machines, EAP-AKA procedure, cookie/retransmit policy, key derivation choices, and 3GPP profile enforcement;
  • XFRM SA/SPD policy, namespaces, privileges, kernel module loading, and rollout defaults;
  • readiness thresholds, drain routing, peer-selection policy, CRD/YANG/Helm shapes, lawful-intercept workflow, and carrier acceptance claims.

SDK APIs for these primitives MUST be named and documented as mechanism surfaces, not as an epdg product facade or production-ready EPC control plane.

3. Surface-specific boundary

Surface from task 0.1 inventorySDK-owned mechanismProduct-owned policy
GTPv2-C S2b control planeExperimental opc-proto-gtpv2c codec subset, IE framing, typed S2b views, raw/unknown IE preservation, hostile-input limits, fuzz, and conformance scaffolding.UDP peer lifecycle, PGW selection, APN/realm/PLMN policy, attach/session orchestration, retries, timers, and deployment readiness.
Diameter base and 3GPP dictionariesFuture opc-proto-diameter header/AVP codec, bounded grouped AVPs, dictionary metadata, base-message helpers, and transport-neutral test helpers.Realm routing, AAA/HSS/CDF business behavior, peer topology, transport operations, watchdog thresholds, and readiness policy.
Linux XFRM / IPsec installerNarrow sys crate and safe wrapper for Linux XFRM UAPI, mock/dry-run backend, capability probes, redaction-safe error/report types, and exact IKEv2 Child SA intent to XFRM request mapping.SA/SPD policy, IKE state, namespaces, privileges, key lifetime policy, kernel-module management, traffic readiness, and product rollout defaults.
Runtime health gatesGeneric gate model, status/impact aggregation, stable JSON projection, and tests for blocking/degraded/unknown/informational gates.Which gates are required, how peer health affects traffic, LI/charging/readiness thresholds, and drain/routing decisions.
Telco redaction and regulated dataIdentifier classes and redaction primitives for IMSI/SUPI, MSISDN/GPSI, IMEI/MEI, NAI, SIP URI, APN/DNN, TEID, SPI, Diameter Session-Id, LI identifiers, and delivery addresses.Lawful-intercept reveal workflow, warrant/correlation policy, retention choices, and deployment-specific support-bundle release decisions.
IPsec gateway node resourcesPure ResourceProfile and NodeCapabilityReport extensions for XFRM, UDP 500/4500, SCTP, Multus/network attachment, Linux capability, and lab-fallback validation.CRD fields, Helm values, Multus network names, privilege rendering, canonical config projection, and product admission policy.
IKEv2 codecExperimental opc-proto-ikev2 framing and typed payloads, executable typed IKE-SA profiles, product-neutral SA_INIT proposal selection, PRF-HMAC-SHA2 key derivation, AES-GCM and AES-CBC/HMAC SK/SKF protection, IKE_AUTH cleartext helpers, Child SA negotiation intent, and RFC 7383 fragment framing/reassembly mechanisms.IKE SA and EAP-AKA state machines, cookie and retransmit policy, response caching, deployment profile policy, Child SA lifecycle management, fragment queues, key custody, and carrier qualification.
EPC/ePDG testbed simulatorsSimulator mechanics for AAA/HSS, Diameter peer, PGW S2b, UE/IKE, LI MDF, and charging CDF behaviors built on SDK protocol crates with fixture provenance.ePDG smoke scenarios, deployment assertions, carrier acceptance, traffic-mix claims, and product soak policy.
Packet-core evidence packsReusable evidence schemas for protocol coverage, fixture provenance, fuzz corpus digests, redaction validation, kernel dataplane evidence, and explicit gap rows.Product conformance claims, LI/charging sign-off, carrier acceptance, and readiness release decisions.
Generic operator helpersReusable Go helper APIs for conditions, observed generation, rollout gates, workload ports, network attachments, drain coordination, metrics, and fake-client tests.Product CRDs, RBAC, cert-manager choices, LI mounts, XFRM privilege rendering, gNMI push sequence, and Helm defaults.

4. Dependency, safety, and implementation guardrails

All future work under this boundary inherits existing SDK policy:

  1. ADR 0014 remains in force: rustls only, tokio only, workspace MSRV 1.88, compatible licenses, justified dependencies, and no unauthorized gRPC stack.
  2. ADR 0015 remains in force for every opc-proto-* codec: spec-authored or independent fixtures, byte-exact decode/encode where claimed, raw preservation, hostile-input tests, fuzz targets, and honest CONFORMANCE.md coverage.
  3. ADR 0017 is the pattern for kernel UAPI exceptions. Any XFRM/IPsec sys crate MUST be narrow, mechanically checked, locally documented with adjacent SAFETY: comments, and kept below a safe public wrapper. This does not authorize FFI protocol parsers.
  4. ePDG-derived fixture bytes are parity evidence until the provenance map's intake checklist is satisfied. They may test migration compatibility, but they MUST NOT be counted as SDK conformance proof by themselves.
  5. Public APIs that can expose subscriber identifiers, key material, TEIDs, SPIs, Diameter Session-Id values, or lawful-intercept identifiers MUST include redaction-safe Debug, Display, error, metric, and evidence behavior.

5. Maturity and claim language

New crates and APIs created from this work stream start as experimental unless a separate RFC/ADR, conformance record, and product-neutral test suite justify a stronger status. Documentation MUST distinguish:

  • "SDK provides a reusable primitive/mechanism";
  • "a downstream product may compose the primitive into an ePDG/EPC function"; and
  • "a downstream product has completed carrier acceptance."

Only the first claim is an SDK claim. The other two remain product claims.

Consequences

  • The SDK can grow reusable EPC and untrusted-access mechanisms without importing ePDG product policy, product defaults, or carrier-readiness claims.
  • Future implementation tasks have a durable boundary to cite when deciding what belongs in crates/*, operators/operator-sdk-go, and test/evidence crates.
  • Reviews can reject changes that make an SDK crate choose APN/realm policy, lawful-intercept workflow, charging behavior, production privileges, or attach orchestration, even if the source product contained that logic next to reusable mechanisms.
  • The task 0.1 inventory remains the source map for this harvest tranche, while ADR 0014, ADR 0015, and ADR 0017 remain the dependency, conformance, and unsafe boundary gates.

ADR 0019: One Openraft Consensus Engine

Status

Accepted

Date

2026-07-12

Context

The SDK historically grew two distributed-persistence implementations: a custom config-store Raft-style engine in opc-persist and a custom majority-visible session coordinator. Splitting elections, voting, log matching, commitment, membership, read barriers, snapshots, and repair across SDK-owned algorithms multiplied failure modes and made qualification ambiguous. Combining Openraft with a custom majority writer in one authority path would be worse: either side could select different durable truth.

The SDK must still own domain state machines, persistence schemas, authenticated transport composition, bounded codecs, payload protection, metrics, and operator policy. Those are adapters around consensus, not reasons to implement consensus again.

Decision

Openraft is the only consensus engine permitted for SDK-owned distributed persistence authority.

  • opc-consensus exact-pins and re-exports the approved Openraft version. No domain crate imports Openraft directly.
  • Openraft exclusively owns election, term/vote state, leader authority, log matching, quorum commitment, membership transitions, linearizable read barriers, compaction, and snapshot lineage/install authority.
  • Domain adapters may implement deterministic commands and state machines, Openraft storage traits, bounded RPC encoding, authenticated peer routing, application journals/watch cursors, idempotent request outcomes, and redaction-safe status. They must not count votes, select a majority value, allocate an authoritative sequence outside client_write, or repair a distributed log through an independent algorithm.
  • Raw append, truncate, rebuild, term/vote mutation, membership mutation, and snapshot-install APIs are not production service surfaces. An offline migration may replace legacy state only under explicit bounded operator approval; it cannot run as a second live authority.
  • ConsensusSessionStore is the session adapter delivered by #127. QuorumSessionStore may remain only as a type alias to it.
  • ConsensusConfigStore is the config adapter migrated under #177. The custom config Raft modules, QuorumConfigStore, config TCP peer/server, and standalone consensus-node binary are removed rather than retained as a compatibility engine.

The shared engine also has one runtime and complete-call profile. opc-consensus owns the 2,000 ms heartbeat/AppendEntries/read-index ceiling, 5,000 ms Vote ceiling, [5,000 ms, 8,000 ms) election range, 10,000 ms snapshot/forward/read-barrier and operation ceilings, 30,000 ms listener ceilings, and the contained 1,500 ms cold-connect sub-bound. It also owns the replication payload, snapshot trigger/chunk, retained-log, and Tokio runtime choices. Session and configuration adapters select only their non-secret cluster label; they cannot silently drift to separate timing or runtime behavior.

Interim engine-source and release gate

The accepted one-engine rule applies to source selection as well as APIs. Until an official stable Openraft release contains per-campaign election-timeout resampling, the workspace exact-pins https://github.com/openpacketcore/openraft revision f607e636406b16bd0ad7925dbb631da1b7a4cd96 (signed tag opc-v0.9.24-election-resampling-1). The dependency is a full immutable rev, not a mutable branch or tag, and locked metadata must resolve only openraft/openraft-macros 0.9.24 from that revision.

Registry 0.9.24 SDK one-shot leader-loss runs happened to pass. They do not invalidate the deterministic scripted engine regression or the historical observed-leader split-vote in the multi-process qualification harness: the registry implementation reused one sampled timeout across campaigns. The forked engine resamples every campaign without adding SDK election, vote, leader-lease, or quorum logic.

A published crates.io manifest cannot preserve this git revision. Therefore the exact 26-crate transitive normal reverse-dependency closure rooted at opc-consensus, opc-session-store, and opc-persist is source-build-only and publish = false. Metadata/profile tests and scripts/publish-order.py derive and check the closure; the other 51 workspace crates keep their existing publication status. Remove the gate only when all three conditions hold:

  1. an official stable Openraft release contains the fix;
  2. the workspace uses an exact registry version and checksum; and
  3. the complete #143 profile is requalified against that revision.

This gate does not graduate the HA profile. Its machine-readable maturity stays experimental, qualification_complete stays false, and #143 remains an unresolved dependency.

Kubernetes controller leader election, gNMI master arbitration, local single-node SQLite transactions, session fencing leases, caches, and test fakes do not become Openraft concerns unless they start deciding distributed durable state authority.

Encryption and HKMS boundary

For configuration persistence the production composition is:

application -> HKMS-backed encryption -> ConsensusConfigStore
            -> Openraft -> SQLite and Openraft snapshots

The session composition follows the same outer-protection rule through its encryption or remote-sealing wrapper. Consensus commands contain already sealed envelopes. The config adapter additionally masks audit values and finalizes the audit chain before proposal. Openraft therefore persists and replicates sealed ciphertext and redacted finalized audit content, never plaintext, an HKMS/KMS provider, a provider or key handle, or raw key material. Follower apply, replay, catch-up, request outcomes, and snapshot installation do not call a provider. Reads decrypt only after crossing back through the outer protection adapter.

Provider unavailability blocks a new plaintext protection operation before client_write and can block decryption, but it does not prevent Openraft from replicating or recovering already sealed state.

The envelope marker alone is insufficient. Each adapter validates its canonical envelope/AAD representation and record-visible binding before proposal and again when persisted state is decoded. A durable authority marker fences public standalone config mutations after Openraft claims a database; each domain may impose a stricter raw-storage fence.

This is payload-envelope encryption. Unless a separate storage layer says otherwise, consensus metadata, routing fields, terms/indexes, timestamps, ownership/fence metadata, request IDs, and envelope key IDs are not full-database encrypted.

Shared transport boundary

opc-consensus owns the bounded, transport-neutral ConsensusPeer and ConsensusRpcHandler contracts. Domain crates provide handlers and consume peers; they do not provide competing sockets. The production mTLS listener and peer, live certificate authentication, framing, and connection lifecycle are owned by opc-session-net and the CNF composition. A real three-node ConsensusConfigStore integration forms Openraft and commits/linearizably reads through RemoteSessionConsensusPeer/SessionConsensusServer over mTLS, proving that config uses this shared boundary in process.

The #177 migration deliberately deletes the private opc-persist TCP/mTLS stack. It does not create another endpoint or another credential-rotation API. Certificate and trust-bundle rotation remains the shared transport's existing responsibility, including trust overlap, fresh authentication, connection drain, readiness gating, and old-trust retirement. #163 real-mTLS tests cover finite retained-connection retirement, overlapping trust, complete replacement handshakes, and old/wrong-scope trust rejection. #164/#143 retain the broader fleet production qualification gates.

Migration rule

An adapter must never reinterpret a nonempty legacy consensus log as Openraft metadata or use startup heuristics to choose a legacy tail. Pristine state may be claimed directly. Nonempty legacy authority fails closed unless the fleet is offline and an operator explicitly approves one coherent applied snapshot.

Config recovery must bind the complete source file's exact SHA-256 checksum, the exact latest applied transaction ID and config version, and an explicit DiscardUnknownAppendedSuffix disposition. The source must be checkpointed with no nonempty WAL. Integrity, required tables, audit chains, config envelopes, checksum, and chain head are verified before the target is replaced and the Openraft marker is created in one immediate SQLite transaction. Every unprovable target suffix is discarded; it is never merged or promoted.

For config authority, recovery binds the complete source file's exact SHA-256 checksum, latest applied transaction ID and config version, and an explicit DiscardUnknownAppendedSuffix disposition. The source must be checkpointed with no nonempty WAL. Integrity, required tables, audit chains, config envelopes, checksum, and chain head are verified before the target is replaced and the Openraft marker is created in one immediate SQLite transaction. Every unprovable target suffix is discarded; it is never merged or promoted.

For session authority, the operator-safe procedure is the offline, full-fleet campaign in the legacy recovery runbook. It may copy an operator-selected immutable checkpoint, but Openraft alone commits the recovery epoch and returns the fleet to service.

A local conversion transaction is not a fleet transaction. Operators must drain every old authority, preserve one coherent authority decision, convert members under a coordinated rollout, and keep untouched pre-migration backups.

Migration is one-way. Rollback to a removed engine is only a stopped-fleet restore of those pre-migration backups. Removing Openraft tables or markers, or attempting to reconstruct legacy logs from Openraft state, is prohibited.

Consequences

The SDK accepts the dependency and integration cost of Openraft once in opc-consensus and no longer maintains competing distributed-safety algorithms. Config and session tests focus on deterministic state-machine and adapter behavior; shared engine and transport qualification can exercise one set of election, replication, membership, read, and snapshot semantics.

The interim git source also makes the affected release closure intentionally source-build-only. This is a bounded distribution cost, not permission to mix registry and forked Openraft consumers or to reintroduce SDK-owned consensus logic.

#127 and #177 close the single-engine implementation transition. They do not by themselves declare either domain carrier-production ready. Recovery, restore, credential lifecycle, real-network compatibility, restart/rejoin, resource, soak, and candidate release evidence retain their domain-specific gates.

Evidence

  • crates/opc-consensus/
  • crates/opc-session-store/src/consensus/
  • crates/opc-session-store/tests/consensus_openraft.rs
  • crates/opc-persist/src/consensus/
  • crates/opc-persist/tests/consensus_openraft.rs
  • crates/opc-amf-lite/tests/config_consensus_encryption.rs
  • crates/opc-session-net/src/consensus.rs
  • crates/opc-session-net/tests/consensus_transport.rs
  • crates/opc-session-testkit/tests/qualification_multiprocess.rs
  • crates/opc-session-testkit/qualification/v2/session-ha-profile.json
  • scripts/publish-order.py
  • docs/adr/0002-config-store-consensus-ha.md
  • docs/adr/0003-session-store-quorum-replication.md
  • docs/consensus-operator-runbook.md

OPC gNMI Server Design Spec

Status

Implemented foundation, owned by opc-gnmi-server.

Scope

opc-gnmi-server is the optional northbound gNMI server for CNFs that choose to expose OpenConfig management. It is outside the core SDK dependency graph and is the only workspace crate allowed to depend on tonic, prost, prost-types, or tonic-build.

The crate owns:

  • vendored gNMI protobuf bindings and the tonic service wrapper;
  • authenticated gNMI-over-TLS listener integration;
  • Capabilities, Get, Set, and Subscribe handling;
  • OpenPacketCore commit-confirmed registered extension semantics;
  • gNMI master-arbitration enforcement;
  • schema-backed path, value, audit, metrics, and config-bus integration.

Security Contract

Production embeddings must construct GnmiServer with an explicit audit sink through new, new_with_audit, new_with_arbitration, or new_with_audit_and_arbitration. The tracing audit sink is available only through *_dev_only constructors for tests, conformance fixtures, and local development.

GnmiService::new requires an authenticated transport principal on every RPC. The unauthenticated service wrapper is crate-private and compiled only for tests. Runtime listeners must derive principals from the mTLS transport and attach them to requests before dispatch.

Set commits submit complete candidates to opc-config-bus with the running snapshot version they were built from. opc-config-bus enforces that base version for candidate-bearing requests, so a stale gNMI Set cannot overwrite an intervening commit.

Extension Semantics

The OpenPacketCore commit-confirmed extension uses the experimental registered extension ID documented in opc-gnmi-server. It is advertised only when the extension registry enables it and master arbitration is also configured.

Every commit-confirmed Begin, Confirm, or Cancel Set must carry a valid master-arbitration extension. This binds control actions to the gNMI election fence for the tenant and role, preventing a different writer from confirming or cancelling another writer's pending commit unless it wins arbitration first.

Servers with arbitration disabled reject commit-confirmed registration at construction time.

Dependency Boundary

ADR 0016 permits the gRPC stack only in opc-gnmi-server. The CI policy script must continue to enforce that:

  • no other workspace crate depends on tonic, prost, prost-types, or tonic-build;
  • no other workspace crate depends on or re-exports opc-gnmi-server;
  • all gNMI TLS serving uses the shared rustls configuration built by the OPC management transport stack.

Verification

The gNMI foundation is covered by crate tests for:

  • authenticated Capabilities, Get, Set, and Subscribe behavior;
  • Set stale-candidate rejection after intervening commits;
  • commit-confirmed timeout, confirm, cancel, malformed payload, and missing arbitration cases;
  • master-arbitration election, tenant, and role fencing;
  • listener mTLS principal derivation and max-session bounds;
  • extension payload redaction in status, metrics, and audit paths.