Phase 6: Introduce ConnectionConfig and move SpiceClient¶
Prompt¶
Before executing any step, back brief the operator. This is
the final phase of the crate extraction plan. Unlike Phases
3-5 (which were mechanical file moves), Phase 6 changes an
API boundary: SpiceClient::new switches from taking ryll's
Config to taking a new ConnectionConfig struct defined in
the protocol crate. The invariant is that all 99 tests pass
and ryll's behaviour is unchanged.
I prefer one commit per logical change.
Situation¶
After Phases 1-5, ryll/src/protocol/ contains only:
ryll/src/protocol/
├── mod.rs # 3 lines: pub mod client; pub use client::SpiceClient;
└── client.rs # 250 lines: SpiceClient + SpiceCaVerifier + TLS setup
SpiceClient stayed behind because it imports
crate::config::Config at
client.rs:17. Config
is ryll's application-level struct that bundles CLI parsing,
.vv file parsing, and SPICE connection parameters.
The research pass found:
- SpiceClient uses 5 of Config's 6 fields:
host,port,tls_port,ca_cert,password. The 6th (host_subject) is#[allow(dead_code)]and unused. - Config is already minimal — all 6 fields are SPICE
connection parameters. No ryll-specific app concerns (CLI
args, headless mode, USB disks, etc.) are mixed in. The
broader
Argsstruct holds those. SoConnectionConfigwill be structurally identical toConfig, just defined in the protocol crate instead of ryll. - Single instantiation site:
app.rs:1924 —
SpiceClient::new(config)?;insiderun_connection(). - Single import site:
app.rs:24 —
use crate::protocol::SpiceClient;. - Three new deps needed in the protocol crate's
Cargo.tomlwhenclient.rsmoves: socket2 = { version = "0.6", features = ["all"] }— TCP keepalive (matching spice-gtk behaviour).webpki-roots = "0.26"— system CA root certificates for TLS.rustls-pemfile = "2"— PEM parsing for inline CA certs from .vv files.- After Phase 6,
ryll/src/protocol/is deleted entirely.
Mission and problem statement¶
- Define a
ConnectionConfigstruct inshakenfist-spice-protocolcontaining the 6 fieldsSpiceClientneeds (per Decision #4 in the master plan). - Change
SpiceClient::newto takeConnectionConfiginstead ofConfig. - Move
client.rsfrom ryll into the protocol crate. - Add a
From<&Config> for ConnectionConfigadapter in ryll'sconfig.rsso the single call site can bridge between ryll'sConfig(which owns CLI parsing) and the protocol crate'sConnectionConfig(which owns connection parameters). - Delete
ryll/src/protocol/entirely.
After this phase:
SpiceClientandConnectionConfiglive inshakenfist-spice-protocol.- ryll has no
protocol/module at all. - ryll imports
SpiceClientandConnectionConfigfromshakenfist_spice_protocol. - ryll's
Config(inconfig.rs) has animpl From<&Config> for ConnectionConfigadapter. - All 99 tests pass. Behaviour is unchanged.
- The crate is still at version
0.0.0.
Approach¶
Two commits:
- API refactor commit (no cross-crate file move). While
client.rsstays in ryll: - Add
ConnectionConfigstruct to the protocol crate'slib.rs. - Change
client.rstouse shakenfist_spice_protocol::ConnectionConfig;instead ofuse crate::config::Config;. - Add
impl From<&Config> for ConnectionConfigin ryll'sconfig.rs. - Update the call site in
app.rsto buildConnectionConfigfromConfigbefore passing toSpiceClient::new. -
Verify all tests pass. This commit proves the API boundary change works before any files move.
-
Move commit (pure mechanical, same pattern as Phases 3-5). Move
client.rsinto the protocol crate: git mv ryll/src/protocol/client.rsshakenfist-spice-protocol/src/client.rs.- Delete
ryll/src/protocol/mod.rsand the now-empty directory. - Remove
mod protocol;fromryll/src/main.rs. - Add
pub mod client;andpub use client::SpiceClient;to the protocol crate'slib.rs. - Add
socket2,webpki-roots,rustls-pemfileto the protocol crate'sCargo.toml. -
Update
app.rsto importSpiceClientfromshakenfist_spice_protocolinstead ofcrate::protocol. -
Status update commit: mark Phase 6 complete.
Pre-flight verification¶
- Working tree is clean.
- All 99 tests pass (80 ryll + 2 compression + 17 usbredir + 0 protocol).
cargo clippy --workspace --all-targets -- -D warningspasses.ryll/src/protocol/contains exactlyclient.rsandmod.rs(2 files).- Confirm
Configstruct fields inryll/src/config.rsmatch the research:host,port,tls_port,password,ca_cert,host_subject. - Confirm
SpiceClient::newtakesconfig: Configatclient.rs:114. - Confirm single
SpiceClient::new(config)call site atapp.rs:1924.
ConnectionConfig design¶
/// SPICE server connection parameters. This is the narrow
/// type that `SpiceClient` needs to establish a connection;
/// it deliberately excludes application-level concerns like
/// CLI parsing, .vv file handling, and session settings.
#[derive(Debug, Clone, Default)]
pub struct ConnectionConfig {
pub host: String,
pub port: u16,
pub tls_port: Option<u16>,
pub password: Option<String>,
/// PEM-encoded CA certificate for TLS. When present,
/// hostname verification is relaxed (SPICE servers
/// commonly use self-signed certs without SAN extensions).
pub ca_cert: Option<String>,
/// Expected certificate subject. Currently informational
/// only — SPICE servers commonly omit SAN extensions, so
/// subject matching is not enforced.
pub host_subject: Option<String>,
}
Not #[non_exhaustive]. Unlike DecompressedImage (which
represents decoder output and may gain metadata fields),
ConnectionConfig is a user-facing configuration struct where
struct-literal construction (ConnectionConfig { host, port,
..Default::default() }) is the natural ergonomic pattern. Any
new fields will be Option<T> with a default of None, which
works fine with existing struct literals via
..Default::default(). If #[non_exhaustive] is wanted later,
it can be added in the 0.1.0 release — but the team should
be aware that adding it is a breaking change (it breaks all
external struct literal construction).
ryll's adapter¶
In ryll/src/config.rs:
use shakenfist_spice_protocol::ConnectionConfig;
impl From<&Config> for ConnectionConfig {
fn from(c: &Config) -> Self {
ConnectionConfig {
host: c.host.clone(),
port: c.port,
tls_port: c.tls_port,
password: c.password.clone(),
ca_cert: c.ca_cert.clone(),
host_subject: c.host_subject.clone(),
}
}
}
In ryll/src/app.rs, the call site changes from:
to:
(Or equivalently let client = SpiceClient::new((&config).into())?;
but the explicit form is more readable.)
Execution¶
Step 1: API refactor (ConnectionConfig + SpiceClient signature change)¶
-
Add the
ConnectionConfigstruct definition toshakenfist-spice-protocol/src/lib.rs, after the existing re-exports. Addpub use ConnectionConfig;alongside the existing re-exports if defined inlib.rs, or define it in a newconnection.rsfile withpub mod connection;+pub use connection::ConnectionConfig;inlib.rs. (Thelib.rsapproach is simpler for a single struct; defer to aconnection.rsfile if the struct grows.) -
In
ryll/src/protocol/client.rs: - Change
use crate::config::Config;touse shakenfist_spice_protocol::ConnectionConfig;. - Change
pub fn new(config: Config) -> Result<Self>topub fn new(config: ConnectionConfig) -> Result<Self>. - The
configfield type in theSpiceClientstruct changes fromConfigtoConnectionConfig. -
All field accesses (
self.config.host,self.config.port, etc.) stay the same becauseConnectionConfighas the same field names. -
In
ryll/src/config.rs: - Add
use shakenfist_spice_protocol::ConnectionConfig; -
Add
impl From<&Config> for ConnectionConfig(the adapter shown above). -
In
ryll/src/app.rs: - Add
use shakenfist_spice_protocol::ConnectionConfig; -
At the
SpiceClient::new(config)?;call site, change toSpiceClient::new(ConnectionConfig::from(&config))?;. -
Verify locally:
cargo fmt --all --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspace— all 99 tests pass.-
pre-commit run --all-files -
Commit.
Commit message:
Introduce ConnectionConfig for SpiceClient.
Step 2: Move client.rs into the protocol crate¶
git mv ryll/src/protocol/client.rs shakenfist-spice-protocol/src/client.rs.- Delete
ryll/src/protocol/mod.rs(git rm). - Remove
mod protocol;fromryll/src/main.rs. - In
shakenfist-spice-protocol/src/lib.rs: - Add
pub mod client;. - Add
pub use client::SpiceClient;. - In the moved
client.rs, update the import: use shakenfist_spice_protocol::ConnectionConfig;becomesuse crate::ConnectionConfig;(since client.rs is now inside the protocol crate).- The
use shakenfist_spice_protocol::{...}imports forChannelType,SpiceErrorbecomeuse crate::{...}. - The
use shakenfist_spice_protocol::link::{...}becomesuse crate::link::{...}. - Add three new deps to
shakenfist-spice-protocol/Cargo.toml: - In
ryll/src/app.rs: - Change
use crate::protocol::SpiceClient;touse shakenfist_spice_protocol::SpiceClient;. - Verify locally (same suite as Step 1, plus check that
ryll/src/protocol/is gone andcargo build -p shakenfist-spice-protocolsucceeds). - Commit.
Commit message:
Move SpiceClient into shakenfist-spice-protocol.
Step 3: Mark Phase 6 complete¶
Update master plan execution table and record discoveries.
Open questions¶
-
Should
ConnectionConfigbe#[non_exhaustive]? Decision for this phase: no. Struct-literal construction with..Default::default()is the natural pattern for a config struct.#[non_exhaustive]would force every external construction through a constructor or mutation, adding friction. FutureOption<T>fields work fine with the existing struct literal pattern. Revisit at0.1.0if desired. See the "ConnectionConfig design" section above for the full rationale. -
Should ryll's
Configbe renamed now thatConnectionConfigexists? The master plan (Decision #4) suggested considering a rename toAppConfigorClientConfig. SinceConfigis used in 5+ places in ryll and the name is unambiguous within ryll's scope (it's the onlyConfigtype), renaming is churn for no real gain. Defer unless confusion arises. -
Should
host_subjectbe included in ConnectionConfig even though SpiceClient doesn't use it? Yes — Decision #4 in the master plan already includes it. It's part of the .vv file spec, other clients may want it, and having it in ConnectionConfig means a future SpiceClient enhancement can start using it without a breaking change. -
Where does
ConnectionConfiglive —lib.rsor a newconnection.rs? For a single struct with no methods beyondDebug/Clone/Defaultderives,lib.rsis fine. If it grows a builder or validation logic, split it out then.
Administration and logistics¶
Success criteria¶
ryll/src/protocol/no longer exists.shakenfist-spice-protocol/src/client.rscontainsSpiceClient(moved from ryll).shakenfist-spice-protocol/src/lib.rsexportsConnectionConfig,SpiceClient, plus the existing constants/link/logging/messages modules.ryll/src/config.rshasimpl From<&Config> for ConnectionConfig.ryll/src/app.rsimportsSpiceClientandConnectionConfigfromshakenfist_spice_protocoland constructsConnectionConfig::from(&config)at the call site.mod protocol;is gone fromryll/src/main.rs.- All 99 tests pass (80 ryll + 2 compression + 17 usbredir + 0 protocol).
cargo build -p shakenfist-spice-protocolsucceeds.- CI is green.
- The crate's
Cargo.tomlstill hasversion = "0.0.0".
Future work¶
- Publish all three extracted crates as
0.1.0— the master plan's existing future-work item. With Phase 6 done, all three crates have their final API shape (modulo the API polish items flagged in Phases 3-4). - Publish ryll itself to crates.io — the separate future-work item from the ryll reservation commit.
- Consider a builder pattern for ConnectionConfig if the field count grows or if callers want fluent construction. Defer until a real consumer (kerbside) wants it.
Bugs fixed during this work¶
(None expected.)
Discoveries during execution¶
- Doc-test added test count from 99 to 100. The
ConnectionConfigstruct has a doc example in lib.rs with a runnable code block. This becomes a doc-test, adding 1 to the total test count: 80 ryll + 2 compression + 17 usbredir + 0 protocol lib tests + 1 protocol doc-test = -
This was not predicted by the plan (which said "99") but is correct and expected behaviour.
-
No surprises during the file move.
client.rsregistered as 98% rename (2% from theshakenfist_spice_protocol::*→crate::*import change).git log --followtraces it back toryll/src/protocol/client.rs. -
Three new deps added cleanly.
socket2,webpki-roots, andrustls-pemfilewere already in ryll's Cargo.toml with compatible versions. Adding them to the protocol crate required no version negotiation. -
ryll/src/protocol/deleted entirely. After Phase 6, ryll has noprotocol/module at all. Themod protocol;declaration inmain.rsis gone.app.rsimportsSpiceClientdirectly fromshakenfist_spice_protocol. -
The
From<&Config> for ConnectionConfigadapter is trivial — 6 field clones with no logic. This confirms the research finding that ryll'sConfigwas already minimal. IfConnectionConfigever diverges fromConfig(e.g. adds aproxyfield that Config doesn't have), the adapter is the one place to update.
Back brief¶
Before executing, confirm:
- Two-commit sequence: (Step 1) API refactor inside ryll + ConnectionConfig in protocol crate, (Step 2) move client.rs into protocol crate.
ConnectionConfigis NOT#[non_exhaustive].- Three new deps (
socket2,webpki-roots,rustls-pemfile) are added to the protocol crate in Step 2 (when client.rs moves), not Step 1. ryll/src/protocol/is deleted entirely in Step 2.- No
version =qualifier on path-deps. - Test count stays at 99.