diff --git a/async/examples/con1.rs b/async/examples/con1.rs
index b1e3b88e74f38733fb11af2e7167a2fb39bc91d7..3350507454a87774156745de249e8c28ec9ccd53 100644
--- a/async/examples/con1.rs
+++ b/async/examples/con1.rs
@@ -136,6 +136,12 @@ async fn run(args: &Args) -> Result<()> {
     info!("running main");
     trace!("tracing main");
 
+    let (cmd, wantpty) = if args.cmd.is_empty() {
+        (None, true)
+    } else {
+        (Some(args.cmd.join(" ")), false)
+    };
+
     // Connect to a peer
     let mut stream = TcpStream::connect((args.host.as_str(), args.port)).await?;
 
@@ -145,21 +151,22 @@ async fn run(args: &Args) -> Result<()> {
     let tx = vec![0; 3000];
     let tx = Box::leak(Box::new(tx)).as_mut_slice();
 
-    // cli is a Behaviour
-    let mut cli = door_async::CmdlineClient::new(args.username.as_ref().unwrap());
+    // app is a Behaviour
+    let mut app = door_async::CmdlineClient::new(args.username.as_ref().unwrap());
     for i in &args.identityfile {
-        cli.add_authkey(read_key(&i).with_context(|| format!("loading key {i}"))?);
+        app.add_authkey(read_key(&i).with_context(|| format!("loading key {i}"))?);
     }
 
-    let mut door = SSHClient::new(work, tx)?;
-    let mut s = door.socket();
+    let mut cli = SSHClient::new(work, tx)?;
+    let mut s = cli.socket();
+
 
     moro::async_scope!(|scope| {
         scope.spawn(tokio::io::copy_bidirectional(&mut stream, &mut s));
 
         scope.spawn(async {
             loop {
-                let ev = door.progress(&mut cli, |ev| {
+                let ev = cli.progress(&mut app, |ev| {
                     trace!("progress event {ev:?}");
                     let e = match ev {
                         Event::CliAuthed => Some(Event::CliAuthed),
@@ -172,41 +179,34 @@ async fn run(args: &Args) -> Result<()> {
                     Some(Event::CliAuthed) => {
                         let mut raw_pty_guard = None;
                         info!("Opening a new session channel");
-                        let (cmd, pty) = if args.cmd.is_empty() {
-                            (None, true)
-                        } else {
-                            (Some(args.cmd.join(" ")), false)
-                        };
-                        let (mut io, mut err) = if pty {
+                        let (mut io, mut errpair) = if wantpty {
                             raw_pty_guard = Some(raw_pty()?);
-                            let io = door.open_client_session_pty(cmd.as_deref()).await
+                            let io = cli.open_session_pty(cmd.as_deref()).await
                                 .context("Opening session")?;
                             (io, None)
                         } else {
-                            let (io, err) = door.open_client_session_nopty(cmd.as_deref()).await
+                            let (io, err) = cli.open_session_nopty(cmd.as_deref()).await
                                 .context("Opening session")?;
-                            (io, Some(err))
+                            let errpair = (err, door_async::stderr()?);
+                            (io, Some(errpair))
                         };
+
                         let mut i = door_async::stdin()?;
                         let mut o = door_async::stdout()?;
-                        let mut e = if err.is_some() {
-                            Some(door_async::stderr()?)
-                        } else {
-                            None
-                        };
                         let mut io2 = io.clone();
                         scope.spawn(async move {
                             moro::async_scope!(|scope| {
                                 scope.spawn(tokio::io::copy(&mut io, &mut o));
                                 scope.spawn(tokio::io::copy(&mut i, &mut io2));
-                                if let Some(ref mut err) = err {
-                                    scope.spawn(tokio::io::copy(err, e.as_mut().unwrap()));
+                                if let Some(ref mut ep) = errpair {
+                                    let (err, e) = ep;
+                                    scope.spawn(tokio::io::copy(err, e));
                                 }
                             }).await;
                             drop(raw_pty_guard);
                             Ok::<_, anyhow::Error>(())
                         });
-                        // TODO: handle channel completion
+                        // TODO: handle channel completion or open failure
                     }
                     Some(_) => unreachable!(),
                     None => {},
diff --git a/async/src/client.rs b/async/src/client.rs
index 63030291db19fa436f5d0c51d0e00bf7ea179ba0..d84a64cc77718d0eb1ab781e7f433a756b5847f8 100644
--- a/async/src/client.rs
+++ b/async/src/client.rs
@@ -58,7 +58,7 @@ impl<'a> SSHClient<'a> {
 
     // TODO: return a Channel object that gives events like WinChange or exit status
     // TODO: move to SimpleClient or something?
-    pub async fn open_client_session_nopty(&mut self, exec: Option<&str>)
+    pub async fn open_session_nopty(&mut self, exec: Option<&str>)
     -> Result<(ChanInOut<'a>, ChanExtIn<'a>)> {
         let chan = self.door.with_runner(|runner| {
             runner.open_client_session(exec, None)
@@ -69,7 +69,7 @@ impl<'a> SSHClient<'a> {
         Ok((cstd, cerr))
     }
 
-    pub async fn open_client_session_pty(&mut self, exec: Option<&str>)
+    pub async fn open_session_pty(&mut self, exec: Option<&str>)
     -> Result<ChanInOut<'a>> {
 
         // XXX error handling
diff --git a/sshproto/src/behaviour.rs b/sshproto/src/behaviour.rs
index ec34231ccfc93db4983b17824be47f46617793be..3f4336f61ebbb669abdd3c5c5981a3e95b821aa7 100644
--- a/sshproto/src/behaviour.rs
+++ b/sshproto/src/behaviour.rs
@@ -20,6 +20,7 @@ use conn::RespPackets;
 use sshwire::TextString;
 
 // TODO: "Bh" is an ugly abbreviation. Naming is hard.
+// How about SSHApp instead? CliApp, ServApp?
 
 // TODO: probably want a special Result here. They probably all want
 // Result, it can return an error or other options like Disconnect?
@@ -30,9 +31,6 @@ pub enum BhError {
     Fail,
 }
 
-#[cfg(feature = "tokio-queue")]
-pub type ReplyChannel = bhtokio::ReplyChannel;
-
 // TODO: once async functions in traits work with no_std, this can all be reworked
 // to probably have Behaviour as a trait not a struct.
 //  Tracking Issue for static async fn in traits
@@ -70,6 +68,7 @@ impl<'a> Behaviour<'a> {
     pub(crate) fn client(&mut self) -> Result<CliBehaviour> {
         self.inner.client()
     }
+
     pub(crate) fn server(&mut self) -> Result<ServBehaviour> {
         self.inner.server()
     }
diff --git a/sshproto/src/block_behaviour.rs b/sshproto/src/block_behaviour.rs
index 86240f1f579aa3579c39b7159550c048a1d3a58d..99d0271ef2a476c2f21304d2f28310a4e9310560 100644
--- a/sshproto/src/block_behaviour.rs
+++ b/sshproto/src/block_behaviour.rs
@@ -87,9 +87,17 @@ pub trait BlockCliBehaviour {
 pub trait BlockServBehaviour {
     fn hostkeys(&self) -> BhResult<&[&sign::SignKey]>;
 
+    fn have_auth_password(&self, username: &str) -> bool;
+    fn have_auth_pubkey(&self, username: &str) -> bool;
+
     // fn authmethods(&self) -> [AuthMethod];
 
     fn auth_password(&self, user: &str, password: &str) -> bool;
 
+    /// Returns whether a session channel can be opened
+    fn open_session(&self) -> BhResult<bool>;
+
+    fn open_tcp_forwarded(&self, ) -> BhResult<bool>;
 
+    fn open_tcp_direct(&self) -> BhResult<bool>;
 }
diff --git a/sshproto/src/error.rs b/sshproto/src/error.rs
index 421761033f20aa3176705553d2d6daa73ae6f166..4b719793e061b5fd66bfc11bd6a14f0d6e86ff66 100644
--- a/sshproto/src/error.rs
+++ b/sshproto/src/error.rs
@@ -23,7 +23,7 @@ pub enum Error {
     /// Input buffer ran out
     RanOut,
 
-    /// Not a UTF8 string
+    /// Not a UTF-8 string
     BadString,
 
     /// Not a valid SSH ASCII string
diff --git a/sshproto/src/packets.rs b/sshproto/src/packets.rs
index 89e2405dc63d9730fb9fb3eb43c709469fa21168..1cba6087e413b6812af00bcbb856ad42338e6b90 100644
--- a/sshproto/src/packets.rs
+++ b/sshproto/src/packets.rs
@@ -1,5 +1,7 @@
-//! SSH protocol packets. A [`Packet`] can be encoded/decoded to the
-//! SSH Binary Packet Protocol using [`crate::sshwire`].
+//! SSH protocol packets.
+//!
+//! A [`Packet`] can be encoded/decoded to the
+//! SSH Binary Packet Protocol using [`sshwire`].
 
 use core::borrow::BorrowMut;
 use core::cell::Cell;
diff --git a/sshproto/src/sshnames.rs b/sshproto/src/sshnames.rs
index 38296bc48d7197f889240304025f58fc07cb7a30..992a6e483b8a07252fb950e4f0ce2c98d0c40638 100644
--- a/sshproto/src/sshnames.rs
+++ b/sshproto/src/sshnames.rs
@@ -1,8 +1,9 @@
-//! Named SSH algorithms, methods and extensions. This module also serves as
-//! an index of SSH specifications.
-
+//! Named SSH algorithms, methods and extensions.
+//!
 //! Some identifiers are also listed directly in `packet.rs` derive attributes.
-//! Packet numbers are listed in `packet.rs`.
+//! Packet numbers are listed in `packets.rs`.
+//!
+//! This module also serves as index of SSH specifications.
 
 /// [RFC8731](https://tools.ietf.org/html/rfc8731)
 pub const SSH_NAME_CURVE25519: &str = "curve25519-sha256";
@@ -52,3 +53,9 @@ pub const SSH_AUTHMETHOD_INTERACTIVE: &str = "keyboard-interactive";
 
 /// [RFC4254](https://tools.ietf.org/html/rfc4254)
 pub const SSH_EXTENDED_DATA_STDERR: u32 = 1;
+
+/// [RFC4254](https://tools.ietf.org/html/rfc4254)
+pub const SSH_OPEN_ADMINISTRATIVELY_PROHIBITED: u32 = 1;
+pub const SSH_OPEN_CONNECT_FAILED: u32 = 2;
+pub const SSH_OPEN_UNKNOWN_CHANNEL_TYPE: u32 = 3;
+pub const SSH_OPEN_RESOURCE_SHORTAGE: u32 = 4;
diff --git a/sshproto/src/sshwire.rs b/sshproto/src/sshwire.rs
index e24e0cd83687eb3690632817eba1293007581ea5..421f885d9984cdc1a262f34e5340d5d22df23bcb 100644
--- a/sshproto/src/sshwire.rs
+++ b/sshproto/src/sshwire.rs
@@ -58,6 +58,7 @@ pub trait SSHDecodeEnum<'de>: Sized {
 }
 
 /// A subset of [`Error`] for `SSHEncode` and `SSHDecode`.
+///
 /// Compiled code size is very sensitive to the size of this
 /// enum so we avoid unused elements.
 #[derive(Debug)]
@@ -239,7 +240,7 @@ pub fn hash_mpint(hash_ctx: &mut dyn digest::DynDigest, m: &[u8]) {
 
 ///////////////////////////////////////////////
 
-/// A SSH style binary string. Serialized as 32 bit length followed by the bytes
+/// A SSH style binary string. Serialized as `u32` length followed by the bytes
 /// of the slice.
 /// Application API
 #[derive(Clone,PartialEq)]
@@ -275,12 +276,13 @@ impl<'de> SSHDecode<'de> for BinString<'de> {
 }
 
 /// A text string that may be presented to a user or used
-/// for things such as a password, username, exec command, tcp hostname, etc.
+/// for things such as a password, username, exec command, TCP hostname, etc.
+///
 /// The SSH protocol defines it to be UTF-8, though
-/// in some applications it could be treated as ascii-only.
+/// in some applications it could be treated as ASCII-only.
 /// The library treats it as an opaque `&[u8]`, leaving
-/// decoding to the `Behaviour`.
-
+/// decoding to the [`Behaviour`].
+///
 /// Note that SSH protocol identifiers in `Packet` etc
 /// are `&str` rather than `TextString`, and always defined as ASCII.
 /// Application API
@@ -288,8 +290,8 @@ impl<'de> SSHDecode<'de> for BinString<'de> {
 pub struct TextString<'a>(pub &'a [u8]);
 
 impl<'a> TextString<'a> {
-    /// Returns the utf8 decoded string, using [`core::str::from_utf8`]
-    /// Don't call this if you are avoiding including utf8 routines in
+    /// Returns the UTF-8 decoded string, using [`core::str::from_utf8`]
+    /// Don't call this if you are avoiding including UTF-8 routines in
     /// the binary.
     pub fn as_str(&self) -> Result<&'a str> {
         core::str::from_utf8(self.0).map_err(|_| Error::BadString)
@@ -484,7 +486,7 @@ impl<'de> SSHDecode<'de> for u32 {
     }
 }
 
-/// Decodes a SSH name string. Must be ascii
+/// Decodes a SSH name string. Must be ASCII
 /// without control characters. RFC4251 section 6.
 pub fn try_as_ascii<'a>(t: &'a [u8]) -> WireResult<&'a AsciiStr> {
     let n = t.as_ascii_str().map_err(|_| WireError::BadName)?;