diff --git a/design/followups.md b/design/followups.md
index 6abccba..1066b93 100644
--- a/design/followups.md
+++ b/design/followups.md
@@ -46,7 +46,11 @@ move to `## Resolved` with a date + commit hash.
**Resolves when:** F19-F26 are all closed and the four DoD bullets above pass.
-**Cumulative execution log.** F19 + F23 (`ed17c07`); F24 (`7611d9e`); F20 (`9dfd193`); F22 (`43c10a1`); F21 (`5f98558`); F25 step 1 (`25dbd8d`); F25 step 2 (`a2b8989`); F25 step 3 (`c4bf0a0`); F25 step 4 (`1e59249`); F25 step 5 (`9b8133f`); F25 step 6 (`321b796`); F25 step 7 landed in this commit:
+**Cumulative execution log.** F19 + F23 (`ed17c07`); F24 (`7611d9e`); F20 (`9dfd193`); F22 (`43c10a1`); F21 (`5f98558`); F25 step 1 (`25dbd8d`); F25 step 2 (`a2b8989`); F25 step 3 (`c4bf0a0`); F25 step 4 (`1e59249`); F25 step 5 (`9b8133f`); F25 step 6 (`321b796`); F25 step 7 (`1b1ee1e`); F26 step 1 landed in this commit:
+- F26 step 1: `mxaccess::AsbTransport` — bridges F25's `AsbClient` into the M0 `Transport` trait. Generic over `T: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static` (the same bounds AsbClient takes). `Transport::capabilities()` returns the ASB-specific flags per `design/60-roadmap.md` M5: `buffered_subscribe = false`, `activate_suspend = false`, `operation_complete_frame = false`. `Transport::kind()` returns `TransportKind::Asb`. `AsbTransport::new(client)` / `into_client()` / `client_mut()` for transport↔client conversion. New deps: `mxaccess` now path-deps `mxaccess-asb` + `mxaccess-asb-nettcp`. Compile-time `Send + Sync + 'static` assertion guards the trait-bound contract. 2 new tests: kind == Asb; capabilities all false. **Stubbed for F26 step 2:** `Session::connect_asb` constructor that owns the full TCP-open + preamble + DH handshake orchestration, plus operation routing that maps ASB types (`ItemStatus`, `RuntimeValue`) back to `mxaccess` types (`MxStatus`, `DataChange`, `MxValue`). Stubbed for F26 step 3: subscription routing — `Session::subscribe` on ASB maps to a `CreateSubscription` + `AddMonitoredItems` + `Publish`-callback pipeline; F25 subscription operations themselves are not yet implemented.
+
+**Earlier slices:**
+- F25 step 7 (commit `1b1ee1e`):
- F25 step 7: Disconnect operation (closes the connection lifecycle: Connect → ops → Disconnect → End → close). New `build_disconnect_request_body(data, iv)` mirrors `AsbContracts.cs:109-114` (``) — same payload shape as AuthenticateMe but under a different wrapper element. New `client::disconnect()` builds a fresh encrypted authentication-data blob via F23's `create_authentication_data` (encrypts `local_pub || remote_pub` under the derived AES key with a fresh IV), wraps it, and sends one-way + signed (regular HMAC, no force). 2 new tests: `disconnect_request_carries_data_and_iv_under_correct_wrapper` (checks wrapper element name + Data/IV byte ordering), and end-to-end `disconnect_writes_signed_one_way_envelope` via `tokio::io::duplex` peer that verifies the encoded SizedEnvelope contains the disconnectIn action string. With Disconnect landed, `AsbClient` now covers the full session lifecycle: `send_preamble().await? → connect().await? → register_items()/read()/keep_alive()/unregister_items() → disconnect().await? → send_end().await?`.
**Earlier slices:**
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 650505a..4197118 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -339,6 +339,8 @@ version = "0.0.0"
dependencies = [
"async-trait",
"futures-util",
+ "mxaccess-asb",
+ "mxaccess-asb-nettcp",
"mxaccess-callback",
"mxaccess-codec",
"mxaccess-galaxy",
diff --git a/rust/crates/mxaccess/Cargo.toml b/rust/crates/mxaccess/Cargo.toml
index 3aeefba..98515ed 100644
--- a/rust/crates/mxaccess/Cargo.toml
+++ b/rust/crates/mxaccess/Cargo.toml
@@ -14,6 +14,8 @@ mxaccess-callback = { path = "../mxaccess-callback" }
mxaccess-galaxy = { path = "../mxaccess-galaxy" }
mxaccess-nmx = { path = "../mxaccess-nmx" }
mxaccess-rpc = { path = "../mxaccess-rpc" }
+mxaccess-asb = { path = "../mxaccess-asb" }
+mxaccess-asb-nettcp = { path = "../mxaccess-asb-nettcp" }
thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
diff --git a/rust/crates/mxaccess/src/lib.rs b/rust/crates/mxaccess/src/lib.rs
index 3326742..66f1b64 100644
--- a/rust/crates/mxaccess/src/lib.rs
+++ b/rust/crates/mxaccess/src/lib.rs
@@ -30,6 +30,9 @@ pub use mxaccess_codec::{
// ---- Public types --------------------------------------------------------
pub mod session;
+pub mod transport_asb;
+
+pub use transport_asb::AsbTransport;
pub use mxaccess_galaxy::{GalaxyTagMetadata, Resolver, ResolverError};
pub use mxaccess_nmx::WriteValue;
diff --git a/rust/crates/mxaccess/src/transport_asb.rs b/rust/crates/mxaccess/src/transport_asb.rs
new file mode 100644
index 0000000..50e033e
--- /dev/null
+++ b/rust/crates/mxaccess/src/transport_asb.rs
@@ -0,0 +1,129 @@
+//! `AsbTransport` — bridges the F25 `mxaccess_asb::AsbClient` into the
+//! `mxaccess::Transport` trait + `Session` API.
+//!
+//! Per `design/60-roadmap.md` M5, the ASB transport surfaces:
+//!
+//! * **No `subscribe_buffered`** — ASB has no proven equivalent of NMX's
+//! buffered-batch DataUpdate frame; consumers calling
+//! `Session::subscribe_buffered` over ASB get
+//! `Error::Unsupported(Capability::BufferedSubscribe)`.
+//! * **No `Activate` / `Suspend`** — these are NMX `INmxService2`
+//! primitives without an ASB analogue.
+//! * **No `OperationComplete` outside the proven write-completion frame**
+//! — ASB doesn't surface a generic completion-frame channel.
+//!
+//! ## Scope of this iteration (F26 step 1)
+//!
+//! Implements:
+//! * [`AsbTransport`] struct that owns an [`AsbClient`] over an
+//! `AsyncRead + AsyncWrite + Unpin + Send` transport.
+//! * [`Transport`] trait impl returning the capability flags above.
+//! * [`AsbTransport::new`] constructor.
+//!
+//! Stubbed for next F26 iteration:
+//! * `Session::connect_asb` constructor — wires `AsbTransport` into a
+//! `Session`. Needs a thin shim that owns the AsbClient + delegates
+//! `register_items`/`read`/`write`/`subscribe` to the corresponding
+//! client method, mapping ASB result types (`ItemStatus`,
+//! `RuntimeValue`) back to `mxaccess` types (`MxStatus`,
+//! `DataChange`, `MxValue`).
+//! * Subscription routing — `Session::subscribe` on ASB maps to a
+//! `CreateSubscription` + `AddMonitoredItems` + `Publish`-callback
+//! pipeline; the F25 subscription operations are not yet wired up.
+
+use mxaccess_asb::AsbClient;
+use tokio::io::{AsyncRead, AsyncWrite};
+
+use crate::{Transport, TransportCapabilities, TransportKind};
+
+/// `Transport` implementation for the ASB (`net.tcp` + binary-message-
+/// encoder) data plane. Owns the underlying [`AsbClient`].
+pub struct AsbTransport {
+ client: AsbClient,
+}
+
+impl AsbTransport {
+ /// Build a transport from an already-constructed [`AsbClient`].
+ /// The client should typically have completed
+ /// `send_preamble().await? -> connect().await?` before being
+ /// wrapped — the F26 next-step `Session::connect_asb` will own that
+ /// orchestration.
+ pub fn new(client: AsbClient) -> Self {
+ Self { client }
+ }
+
+ /// Surface the inner client. M5 / F26 step 2 wires concrete
+ /// operations through here.
+ pub fn client_mut(&mut self) -> &mut AsbClient {
+ &mut self.client
+ }
+
+ /// Consume the transport and return the inner client. Useful when
+ /// the caller wants to issue raw IASBIDataV2 operations directly
+ /// before / after the Session-level orchestration kicks in.
+ pub fn into_client(self) -> AsbClient {
+ self.client
+ }
+}
+
+/// Compile-time only: `AsbTransport` must be `Send + Sync + 'static`
+/// (the `Transport` trait bound). Sync is provided by `AsbClient`'s
+/// internal lack of interior mutability over non-Sync types — the
+/// `AsyncRead + AsyncWrite + Unpin + Send` transport is the only
+/// non-trivial constraint, and Tokio's `TcpStream` satisfies it.
+const _: fn() = || {
+ fn assert_send_sync() {}
+ assert_send_sync::>();
+};
+
+impl Transport for AsbTransport {
+ fn capabilities(&self) -> TransportCapabilities {
+ TransportCapabilities {
+ // ASB has no proven buffered-batch DataUpdate equivalent.
+ buffered_subscribe: false,
+ // Activate/Suspend are NMX `INmxService2` primitives.
+ activate_suspend: false,
+ // No generic completion-frame channel on ASB.
+ operation_complete_frame: false,
+ }
+ }
+
+ fn kind(&self) -> TransportKind {
+ TransportKind::Asb
+ }
+}
+
+#[cfg(test)]
+#[allow(
+ clippy::unwrap_used,
+ clippy::expect_used,
+ clippy::panic,
+ clippy::indexing_slicing
+)]
+mod tests {
+ use super::*;
+ use mxaccess_asb_nettcp::auth::{AsbAuthenticator, CryptoParameters};
+
+ fn make_authenticator() -> AsbAuthenticator {
+ AsbAuthenticator::new("test-passphrase", &CryptoParameters::defaults(), [0u8; 16]).unwrap()
+ }
+
+ #[test]
+ fn asb_transport_kind_is_asb() {
+ let (client_end, _peer) = tokio::io::duplex(64);
+ let client = AsbClient::new(client_end, make_authenticator(), "test://x/y");
+ let transport = AsbTransport::new(client);
+ assert_eq!(transport.kind(), TransportKind::Asb);
+ }
+
+ #[test]
+ fn asb_transport_capabilities_disable_buffered_and_activate_suspend() {
+ let (client_end, _peer) = tokio::io::duplex(64);
+ let client = AsbClient::new(client_end, make_authenticator(), "test://x/y");
+ let transport = AsbTransport::new(client);
+ let caps = transport.capabilities();
+ assert!(!caps.buffered_subscribe);
+ assert!(!caps.activate_suspend);
+ assert!(!caps.operation_complete_frame);
+ }
+}