sec(hailo): expose --tls-ca / mTLS flags on the embed CLI (iter 188)

Symmetric with iter-187 bench plumbing — adds the same TLS knobs to
`ruvector-hailo-embed` so ops can drive a one-shot embed against a
TLS-configured worker without having to build a custom client. All
flags `#[cfg(feature = "tls")]` so the no-tls build stays clean.

Same partial-config + orphan-flag refusals as iter-187:
  - --tls-domain / --tls-client-cert / --tls-client-key without
    --tls-ca → loud error
  - --tls-client-cert without --tls-client-key (or vice versa) →
    loud error
  - missing CA file → fs error surfaced with full path

Smoke-tested on the workstation:

  $ ruvector-hailo-embed --workers 100.77.59.83:50051 --tls-domain example.com --text hello
  Error: "--tls-domain / --tls-client-cert / --tls-client-key require --tls-ca"

  $ ruvector-hailo-embed --workers 100.77.59.83:50051 --tls-ca /nonexistent/ca.pem --text hello
  Error: "--tls-ca: transport error to <tls>: read ca pem at /nonexistent/ca.pem: No such file or directory (os error 2)"

  $ ruvector-hailo-embed --workers 100.77.59.83:50051 --text "iter 188 smoke test"
  {"text":"iter 188 smoke test","dim":384,"latency_us":433538,"vec_head":[...]}

Pi plaintext bench regression (c=4 b=1, 8 s × 3):

  iter-187: 68.5, 68.7, 66.7 → mean 68.0/sec, p50=56-59 ms
  iter-188: 70.3, 69.0, 67.9 → mean 69.1/sec, p50=55-57 ms

  Δ throughput: +1.6% (within tailnet noise; embed CLI changes don't
                touch the bench code path)

The TLS server-side path is now fully callable from both client tools
in this repo. Pi-side cert generation + systemd unit wiring (the
actual end-to-end TLS smoke against cognitum-v0) remains the deferred
ops follow-up.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruvnet 2026-05-03 18:25:28 -04:00
parent 840d276592
commit 168051bc1e

View file

@ -69,6 +69,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// texts are embedded in order and the binary exits. Repeat the
// flag to embed multiple texts in one invocation.
let mut inline_texts: Vec<String> = Vec::new();
// Iter 188 — symmetric TLS plumbing (mirror of iter-187 bench
// additions). Lets ops drive a single embed against a TLS-enabled
// worker without building a custom client. All flags
// `#[cfg(feature = "tls")]` so the no-tls build is unchanged.
#[cfg(feature = "tls")]
let mut tls_ca: Option<String> = None;
#[cfg(feature = "tls")]
let mut tls_domain: Option<String> = None;
#[cfg(feature = "tls")]
let mut tls_client_cert: Option<String> = None;
#[cfg(feature = "tls")]
let mut tls_client_key: Option<String> = None;
let mut i = 1;
while i < args.len() {
@ -130,6 +142,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
i += 2;
}
#[cfg(feature = "tls")]
"--tls-ca" => { tls_ca = args.get(i + 1).cloned(); i += 2; }
#[cfg(feature = "tls")]
"--tls-domain" => { tls_domain = args.get(i + 1).cloned(); i += 2; }
#[cfg(feature = "tls")]
"--tls-client-cert" => { tls_client_cert = args.get(i + 1).cloned(); i += 2; }
#[cfg(feature = "tls")]
"--tls-client-key" => { tls_client_key = args.get(i + 1).cloned(); i += 2; }
"--help" | "-h" => { print_help(); return Ok(()); }
"--version" | "-V" => {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
@ -194,6 +214,55 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
// Iter 188 — TLS transport when --tls-ca is set; mirrors iter-187
// bench plumbing. Same partial-config + orphan-flag refusals so a
// misconfigured invocation surfaces an early error instead of a
// silent plaintext downgrade.
#[cfg(feature = "tls")]
let transport: Arc<dyn ruvector_hailo_cluster::transport::EmbeddingTransport + Send + Sync> = {
if let Some(ca_path) = tls_ca.as_deref() {
let addr0 = workers.first().map(|w| w.address.clone()).unwrap_or_default();
let domain = tls_domain.clone().unwrap_or_else(|| {
ruvector_hailo_cluster::tls::domain_from_address(&addr0).to_string()
});
let mut tls = ruvector_hailo_cluster::tls::TlsClient::from_pem_files(ca_path, &domain)
.map_err(|e| format!("--tls-ca: {}", e))?;
match (tls_client_cert.as_deref(), tls_client_key.as_deref()) {
(Some(c), Some(k)) => {
tls = tls.with_client_identity(c, k)
.map_err(|e| format!("--tls-client-cert/--tls-client-key: {}", e))?;
if !quiet {
eprintln!("ruvector-hailo-embed: mTLS client identity attached");
}
}
(Some(_), None) | (None, Some(_)) => {
return Err(
"--tls-client-cert and --tls-client-key must both be set or both unset".into(),
);
}
(None, None) => {}
}
if !quiet {
eprintln!(
"ruvector-hailo-embed: TLS enabled ca={} domain={}",
ca_path, domain
);
}
Arc::new(GrpcTransport::with_tls(
std::time::Duration::from_secs(5),
std::time::Duration::from_secs(2),
tls,
)?)
} else {
if tls_domain.is_some() || tls_client_cert.is_some() || tls_client_key.is_some() {
return Err(
"--tls-domain / --tls-client-cert / --tls-client-key require --tls-ca".into(),
);
}
Arc::new(GrpcTransport::new()?)
}
};
#[cfg(not(feature = "tls"))]
let transport: Arc<dyn ruvector_hailo_cluster::transport::EmbeddingTransport + Send + Sync> =
Arc::new(GrpcTransport::new()?);
@ -601,6 +670,18 @@ OPTIONS:
text and exit (skips stdin). Repeat
the flag to embed multiple texts
in one invocation.
--tls-ca <path> Enable HTTPS by trusting the PEM CA
bundle at <path>. Without this the
embed CLI dials plaintext gRPC.
(Requires --features tls.)
--tls-domain <name> SNI / SAN value to assert against the
server cert. Defaults to the hostname
half of the first worker address.
--tls-client-cert <path> mTLS client cert (PEM). Pair with
--tls-client-key.
--tls-client-key <path> mTLS client private key (PEM). Both
cert and key must be set or both
unset.
--help, -h Print this help and exit.
--version, -V Print the binary name + version and exit.