fix(client/rust): handle provider_status arm (build break); real system-roots TLS; design doc (Client.Rust-030..032)
This commit is contained in:
Generated
+67
@@ -207,6 +207,22 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
@@ -597,6 +613,12 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -796,6 +818,18 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.1"
|
||||
@@ -816,6 +850,38 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
@@ -1056,6 +1122,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"prost",
|
||||
"rustls-native-certs",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
|
||||
@@ -37,7 +37,7 @@ serde_json = "1.0.145"
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "sync", "time"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["net"] }
|
||||
tonic = { version = "0.13.1", features = ["transport", "tls-ring"] }
|
||||
tonic = { version = "0.13.1", features = ["transport", "tls-ring", "tls-native-roots"] }
|
||||
tonic-build = "0.13.1"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+13
-5
@@ -81,11 +81,19 @@ cargo run -p mxgw-cli -- smoke --endpoint https://mxgateway.example.local:5001 -
|
||||
The gateway can auto-generate its own self-signed certificate (it has no PKI).
|
||||
Unlike the other clients, the Rust client is **not** lenient: tonic 0.13.1
|
||||
exposes no public hook to inject a custom certificate verifier, so TLS over Rust
|
||||
is pin-only. A TLS connection requires either `--ca-file` /
|
||||
`ClientOptions::with_ca_file(...)` to pin a CA (export the gateway's self-signed
|
||||
certificate and pin it), or `--require-certificate-validation` /
|
||||
`with_require_certificate_validation(true)` to verify against the system trust
|
||||
roots. TLS with neither set fails `connect` with a clear, actionable error rather
|
||||
cannot accept an *arbitrary* self-signed certificate. A TLS connection requires
|
||||
one of two trust paths:
|
||||
|
||||
- `--ca-file` / `ClientOptions::with_ca_file(...)` to pin a CA (export the
|
||||
gateway's self-signed certificate and pin it). This is the path for a
|
||||
self-signed gateway.
|
||||
- `--require-certificate-validation` / `with_require_certificate_validation(true)`
|
||||
to verify against the operating system's trust roots (`tls-native-roots`). This
|
||||
only succeeds for a certificate that chains to a root the host already trusts —
|
||||
i.e. a gateway fronted by a publicly- or enterprise-CA-issued certificate, not a
|
||||
bare self-signed one.
|
||||
|
||||
TLS with neither set fails `connect` with a clear, actionable error rather
|
||||
than accepting the certificate. See
|
||||
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).
|
||||
|
||||
|
||||
@@ -162,12 +162,73 @@ impl GatewayClient {
|
||||
|
||||
`stream_alarms` opens with one `active_alarm` per currently-active alarm
|
||||
(the ConditionRefresh snapshot), then a single `snapshot_complete`, then a
|
||||
`transition` for every subsequent raise / acknowledge / clear. The feed is
|
||||
served by the gateway's always-on alarm monitor — no worker session is
|
||||
opened — so any number of clients may attach. Dropping the stream cancels
|
||||
the gRPC call cooperatively. `acknowledge_alarm` is idempotent at the
|
||||
MxAccess layer; the returned `AcknowledgeAlarmReply` carries the native
|
||||
MxStatus from the worker.
|
||||
`transition` for every subsequent raise / acknowledge / clear. A fourth
|
||||
`provider_status` oneof case (`AlarmProviderStatus`: `mode`, `degraded`,
|
||||
`reason`, `since`) is emitted once on stream open and again on every
|
||||
failover/failback so late joiners learn the current alarm-provider mode.
|
||||
The CLI renders all four cases in both its one-line summary and its
|
||||
protobuf-JSON output (`alarm_feed_message_summary` /
|
||||
`alarm_feed_message_to_json`). The feed is served by the gateway's always-on
|
||||
alarm monitor — no worker session is opened — so any number of clients may
|
||||
attach. Dropping the stream cancels the gRPC call cooperatively.
|
||||
`acknowledge_alarm` is idempotent at the MxAccess layer; the returned
|
||||
`AcknowledgeAlarmReply` carries the native MxStatus from the worker.
|
||||
|
||||
## Galaxy Repository
|
||||
|
||||
`GalaxyClient` is a session-less metadata client (requires the
|
||||
`metadata:read` API-key scope). Alongside `test_connection`,
|
||||
`get_last_deploy_time`, `discover_hierarchy`, and `watch_deploy_events`, it
|
||||
exposes a lazy hierarchy walker built on the `BrowseChildren` RPC:
|
||||
|
||||
```rust
|
||||
impl GalaxyClient {
|
||||
pub async fn browse(&mut self, options: Option<BrowseChildrenOptions>) -> Result<Vec<LazyBrowseNode>, Error>;
|
||||
pub async fn browse_children_raw(&mut self, request: BrowseChildrenRequest) -> Result<BrowseChildrenReply, Error>;
|
||||
}
|
||||
|
||||
pub struct BrowseChildrenOptions {
|
||||
pub category_ids: Vec<i32>,
|
||||
pub template_chain_contains: Vec<String>,
|
||||
pub tag_name_glob: Option<String>,
|
||||
pub include_attributes: Option<bool>,
|
||||
pub alarm_bearing_only: bool,
|
||||
pub historized_only: bool,
|
||||
}
|
||||
|
||||
impl LazyBrowseNode {
|
||||
pub fn object(&self) -> &GalaxyObject;
|
||||
pub fn has_children_hint(&self) -> bool;
|
||||
pub async fn children(&self) -> Vec<LazyBrowseNode>;
|
||||
pub async fn is_expanded(&self) -> bool;
|
||||
pub async fn expand(&self) -> Result<(), Error>;
|
||||
}
|
||||
```
|
||||
|
||||
- `browse(options)` returns the root objects as `LazyBrowseNode`s. The
|
||||
supplied `BrowseChildrenOptions` filter is captured and reused when any
|
||||
returned node is expanded, so a single filter set scopes the entire walk.
|
||||
- `BrowseChildrenOptions` mirrors the request-level filters on the wire and
|
||||
combines them with **AND**: a child appears only when it satisfies every
|
||||
populated criterion (`category_ids` membership, every
|
||||
`template_chain_contains` substring, the `tag_name_glob`, plus the
|
||||
`alarm_bearing_only` / `historized_only` flags). `include_attributes` is a
|
||||
tri-state (`None` = server default). Empty/`None` fields impose no
|
||||
restriction. See
|
||||
[Galaxy Repository — BrowseChildren](../../docs/GalaxyRepository.md#browsechildren)
|
||||
for the wire-level semantics.
|
||||
- `LazyBrowseNode` is cheap to clone — clones share state through an internal
|
||||
`Arc`, so expanding one clone makes the children visible to every clone.
|
||||
`has_children_hint()` exposes the server's `child_has_children` hint so a UI
|
||||
can draw an expand affordance without issuing an RPC. `expand()` is
|
||||
idempotent: the first call issues a paged `BrowseChildren` walk (page size
|
||||
500) under an async mutex held across the await, sets the `is_expanded`
|
||||
flag, and caches the children; subsequent calls are no-ops and re-hit
|
||||
nothing. The internal paged loop guards against a server returning a
|
||||
repeated `next_page_token` by failing with `Error::InvalidArgument` rather
|
||||
than looping forever.
|
||||
- `browse_children_raw` issues a single `BrowseChildren` RPC and returns the
|
||||
raw reply for callers that want to drive paging themselves.
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -200,13 +261,20 @@ Rust client is therefore **pin-only** — it requires either:
|
||||
- `ClientOptions::with_ca_file(...)` to pin a CA (the supported path for the
|
||||
gateway's self-signed certificate; export the certificate and pin it), or
|
||||
- `ClientOptions::with_require_certificate_validation(true)` to verify against the
|
||||
system trust roots.
|
||||
operating system's trust roots. This enables the `tonic` `tls-native-roots`
|
||||
feature and calls `ClientTlsConfig::with_native_roots()`, so the handshake
|
||||
validates a certificate that chains to a root the host already trusts. It does
|
||||
**not** accept a bare self-signed gateway certificate — that still needs
|
||||
`with_ca_file`.
|
||||
|
||||
With TLS enabled (`with_plaintext(false)`), no pinned CA, and certificate
|
||||
validation not required, `GatewayClient::connect` rejects the connection with a
|
||||
clear, actionable error pointing at `with_ca_file` /
|
||||
`require_certificate_validation` rather than silently accepting the certificate.
|
||||
The CLI exposes `--ca-file` and `--require-certificate-validation`.
|
||||
`build_tls_config` computes the trust posture with the pure `tls_trust_decision`
|
||||
helper (`None` / `PinnedCa` / `SystemRoots` / `RejectNoCa`) so the posture is
|
||||
unit-testable without a live handshake. With TLS enabled (`with_plaintext(false)`),
|
||||
no pinned CA, and certificate validation not required (`RejectNoCa`),
|
||||
`GatewayClient::connect` rejects the connection with a clear, actionable error
|
||||
pointing at `with_ca_file` / `require_certificate_validation` rather than building
|
||||
a config with zero trust anchors. The CLI exposes `--ca-file` and
|
||||
`--require-certificate-validation`.
|
||||
|
||||
## Streaming
|
||||
|
||||
|
||||
@@ -1726,7 +1726,7 @@ fn event_value_to_json(value: &ProtoMxValue) -> Value {
|
||||
}
|
||||
|
||||
/// Render a streamed [`AlarmFeedMessage`] as a terse one-line summary that
|
||||
/// distinguishes the three `payload` oneof cases.
|
||||
/// distinguishes the four `payload` oneof cases.
|
||||
fn alarm_feed_message_summary(message: &AlarmFeedMessage) -> String {
|
||||
match &message.payload {
|
||||
Some(alarm_feed_message::Payload::ActiveAlarm(snapshot)) => {
|
||||
@@ -1746,6 +1746,14 @@ fn alarm_feed_message_summary(message: &AlarmFeedMessage) -> String {
|
||||
AlarmEnumName::transition_kind(transition.transition_kind)
|
||||
)
|
||||
}
|
||||
Some(alarm_feed_message::Payload::ProviderStatus(status)) => {
|
||||
format!(
|
||||
"provider-status mode={} degraded={} reason={:?}",
|
||||
AlarmEnumName::provider_mode(status.mode),
|
||||
status.degraded,
|
||||
status.reason
|
||||
)
|
||||
}
|
||||
None => "(empty)".to_owned(),
|
||||
}
|
||||
}
|
||||
@@ -1784,6 +1792,17 @@ fn alarm_feed_message_to_json(message: &AlarmFeedMessage) -> Value {
|
||||
"description": transition.description,
|
||||
}
|
||||
}),
|
||||
Some(alarm_feed_message::Payload::ProviderStatus(status)) => json!({
|
||||
"providerStatus": {
|
||||
"mode": AlarmEnumName::provider_mode(status.mode),
|
||||
"degraded": status.degraded,
|
||||
"reason": status.reason,
|
||||
"since": status.since.as_ref().map(|ts| json!({
|
||||
"seconds": ts.seconds,
|
||||
"nanos": ts.nanos,
|
||||
})),
|
||||
}
|
||||
}),
|
||||
None => Value::Null,
|
||||
}
|
||||
}
|
||||
@@ -1806,6 +1825,13 @@ impl AlarmEnumName {
|
||||
.map(|kind| kind.as_str_name().to_owned())
|
||||
.unwrap_or_else(|_| value.to_string())
|
||||
}
|
||||
|
||||
fn provider_mode(value: i32) -> String {
|
||||
use zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::AlarmProviderMode;
|
||||
AlarmProviderMode::try_from(value)
|
||||
.map(|mode| mode.as_str_name().to_owned())
|
||||
.unwrap_or_else(|_| value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Render an [`AcknowledgeAlarmReply`] as a terse line or a JSON document.
|
||||
@@ -2165,4 +2191,40 @@ mod tests {
|
||||
assert_eq!(frac.seconds, utc.seconds);
|
||||
assert_eq!(frac.nanos, 250_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alarm_feed_provider_status_renders_in_summary_and_json() {
|
||||
use zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::{
|
||||
alarm_feed_message, AlarmFeedMessage, AlarmProviderMode, AlarmProviderStatus,
|
||||
};
|
||||
|
||||
let message = AlarmFeedMessage {
|
||||
payload: Some(alarm_feed_message::Payload::ProviderStatus(
|
||||
AlarmProviderStatus {
|
||||
mode: AlarmProviderMode::Subtag as i32,
|
||||
degraded: true,
|
||||
reason: "alarmmgr unavailable".to_owned(),
|
||||
since: Some(prost_types::Timestamp {
|
||||
seconds: 1_777_995_000,
|
||||
nanos: 0,
|
||||
}),
|
||||
},
|
||||
)),
|
||||
};
|
||||
|
||||
let summary = super::alarm_feed_message_summary(&message);
|
||||
assert!(summary.contains("provider-status"), "summary: {summary}");
|
||||
assert!(
|
||||
summary.contains("ALARM_PROVIDER_MODE_SUBTAG"),
|
||||
"summary: {summary}"
|
||||
);
|
||||
assert!(summary.contains("degraded=true"), "summary: {summary}");
|
||||
|
||||
let value = super::alarm_feed_message_to_json(&message);
|
||||
let provider = &value["providerStatus"];
|
||||
assert_eq!(provider["mode"], "ALARM_PROVIDER_MODE_SUBTAG");
|
||||
assert_eq!(provider["degraded"], true);
|
||||
assert_eq!(provider["reason"], "alarmmgr unavailable");
|
||||
assert_eq!(provider["since"]["seconds"], 1_777_995_000_i64);
|
||||
}
|
||||
}
|
||||
|
||||
+141
-44
@@ -74,16 +74,22 @@ impl ClientOptions {
|
||||
}
|
||||
|
||||
/// Require TLS certificate verification even without a pinned CA. Default
|
||||
/// false: the gateway's self-signed certificate is accepted (internal-tool
|
||||
/// posture). Setting a CA file always verifies.
|
||||
/// false. Setting a CA file always verifies against that CA.
|
||||
///
|
||||
/// Note for Rust: tonic 0.13's `ClientTlsConfig` exposes no hook for a
|
||||
/// custom rustls verifier, so the Rust client cannot accept an arbitrary
|
||||
/// custom rustls verifier, so the Rust client cannot accept an *arbitrary*
|
||||
/// self-signed certificate the way the other clients do. With the default
|
||||
/// (false) and no pinned CA, [`crate::client::GatewayClient::connect`]
|
||||
/// rejects the TLS connection and asks for a CA file. Either pin a CA via
|
||||
/// [`ClientOptions::with_ca_file`] (the supported lenient path on Rust) or
|
||||
/// set this `true` to verify against the system trust roots.
|
||||
/// rejects the TLS connection and asks for a CA file. There are two
|
||||
/// supported TLS paths:
|
||||
///
|
||||
/// - Pin the gateway certificate with [`ClientOptions::with_ca_file`] (the
|
||||
/// lenient pin-only path; works for a self-signed gateway cert).
|
||||
/// - Set this `true` to verify against the operating system's trust roots
|
||||
/// (`tls-native-roots`). This only succeeds for a certificate that chains
|
||||
/// to a root the host already trusts, so it is for gateways fronted by a
|
||||
/// publicly- or enterprise-CA-issued certificate, not a bare self-signed
|
||||
/// one.
|
||||
pub fn with_require_certificate_validation(mut self, require: bool) -> Self {
|
||||
self.require_certificate_validation = require;
|
||||
self
|
||||
@@ -175,26 +181,63 @@ impl ClientOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the TLS handshake gets its trust anchors for a given set of options.
|
||||
/// Computed by [`tls_trust_decision`] and applied by [`build_tls_config`];
|
||||
/// split out so the trust posture is unit-testable without a live handshake.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum TlsTrustDecision {
|
||||
/// Plaintext transport — no TLS, no trust anchors.
|
||||
None,
|
||||
/// Validate against the CA pinned with [`ClientOptions::with_ca_file`].
|
||||
PinnedCa,
|
||||
/// Validate against the operating system's trust roots
|
||||
/// (`require_certificate_validation == true`, no pinned CA).
|
||||
SystemRoots,
|
||||
/// Reject up front: TLS requested with neither a pinned CA nor strict
|
||||
/// verification (the Rust pin-only lenient default).
|
||||
RejectNoCa,
|
||||
}
|
||||
|
||||
/// Decide the TLS trust posture from `options` without touching the filesystem
|
||||
/// or the network.
|
||||
pub(crate) fn tls_trust_decision(options: &ClientOptions) -> TlsTrustDecision {
|
||||
if options.plaintext() {
|
||||
TlsTrustDecision::None
|
||||
} else if options.ca_file().is_some() {
|
||||
TlsTrustDecision::PinnedCa
|
||||
} else if options.require_certificate_validation() {
|
||||
TlsTrustDecision::SystemRoots
|
||||
} else {
|
||||
TlsTrustDecision::RejectNoCa
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the [`ClientTlsConfig`] for a non-plaintext connection described by
|
||||
/// `options`, applying the lenient-default guard that is the **Rust
|
||||
/// pin-only exception**.
|
||||
///
|
||||
/// Returns `Ok(None)` when `options.plaintext()` is `true` (no TLS needed).
|
||||
/// Returns `Ok(Some(tls))` when a valid TLS config can be assembled.
|
||||
/// Returns `Ok(Some(tls))` when a valid TLS config can be assembled — either
|
||||
/// pinned to the CA from [`ClientOptions::with_ca_file`], or, when
|
||||
/// `require_certificate_validation` is set with no pinned CA, verifying against
|
||||
/// the operating system's trust roots (`tls-native-roots`).
|
||||
/// Returns `Err(Error::InvalidEndpoint)` when TLS is requested but no pinned
|
||||
/// CA was provided and `require_certificate_validation` is `false`.
|
||||
///
|
||||
/// # Why this guard exists
|
||||
/// # Why the no-CA guard exists
|
||||
///
|
||||
/// `tonic` 0.13's `ClientTlsConfig` builds its rustls verifier inside a
|
||||
/// crate-private connector and exposes no hook for a custom
|
||||
/// `ServerCertVerifier`. The Rust client therefore cannot accept an arbitrary
|
||||
/// `ServerCertVerifier`. The Rust client therefore cannot accept an *arbitrary*
|
||||
/// self-signed certificate the way the other language clients do. Rather than
|
||||
/// silently falling back to system-root verification (which always fails
|
||||
/// against a self-signed gateway certificate), we reject the configuration
|
||||
/// early with an actionable error.
|
||||
/// silently falling back to a verifier with no trust anchors (which rejects
|
||||
/// every certificate with a confusing handshake error), the lenient default
|
||||
/// rejects the configuration early with an actionable error. The strict opt-in
|
||||
/// instead loads the system trust roots so a certificate chaining to an
|
||||
/// already-trusted root validates.
|
||||
pub(crate) fn build_tls_config(options: &ClientOptions) -> Result<Option<ClientTlsConfig>, Error> {
|
||||
if options.plaintext() {
|
||||
let decision = tls_trust_decision(options);
|
||||
if decision == TlsTrustDecision::None {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -202,37 +245,46 @@ pub(crate) fn build_tls_config(options: &ClientOptions) -> Result<Option<ClientT
|
||||
if let Some(server_name) = options.server_name_override() {
|
||||
tls = tls.domain_name(server_name.to_owned());
|
||||
}
|
||||
if let Some(ca_file) = options.ca_file() {
|
||||
let certificate = fs::read(ca_file).map_err(|source| Error::InvalidEndpoint {
|
||||
endpoint: options.endpoint().to_owned(),
|
||||
detail: format!("failed to read CA file {}: {source}", ca_file.display()),
|
||||
})?;
|
||||
tls = tls.ca_certificate(Certificate::from_pem(certificate));
|
||||
} else if !options.require_certificate_validation() {
|
||||
// Lenient-default fallback (Rust pin-only exception): tonic
|
||||
// 0.13's `ClientTlsConfig` builds its rustls verifier inside a
|
||||
// crate-private connector and exposes no hook for a custom
|
||||
// `ServerCertVerifier`, so — unlike the other clients — the
|
||||
// Rust client cannot accept an arbitrary self-signed cert. Pin
|
||||
// the gateway's CA instead, or opt into strict verification
|
||||
// against the system trust roots. We reject here rather than
|
||||
// silently verifying against system roots (which would fail a
|
||||
// self-signed gateway with a confusing handshake error).
|
||||
//
|
||||
// Note: a server-name override affects SNI (the hostname sent
|
||||
// in the TLS ClientHello) but does NOT pin trust. Overriding
|
||||
// the server name alone does not bypass certificate validation.
|
||||
return Err(Error::InvalidEndpoint {
|
||||
endpoint: options.endpoint().to_owned(),
|
||||
detail: "TLS requested without a pinned CA. The Rust client cannot accept an \
|
||||
arbitrary self-signed certificate (tonic 0.13 exposes no custom \
|
||||
rustls verifier). Pin the gateway certificate with \
|
||||
ClientOptions::with_ca_file, or call \
|
||||
ClientOptions::with_require_certificate_validation(true) to verify \
|
||||
against the system trust roots. Note: a server-name override \
|
||||
affects SNI but does not pin trust."
|
||||
.to_owned(),
|
||||
});
|
||||
match decision {
|
||||
TlsTrustDecision::PinnedCa => {
|
||||
let ca_file = options.ca_file().expect("PinnedCa implies a CA file");
|
||||
let certificate = fs::read(ca_file).map_err(|source| Error::InvalidEndpoint {
|
||||
endpoint: options.endpoint().to_owned(),
|
||||
detail: format!("failed to read CA file {}: {source}", ca_file.display()),
|
||||
})?;
|
||||
tls = tls.ca_certificate(Certificate::from_pem(certificate));
|
||||
}
|
||||
TlsTrustDecision::SystemRoots => {
|
||||
// Strict opt-in with no pinned CA: verify against the OS trust
|
||||
// store. Without this the bare `ClientTlsConfig` carries zero
|
||||
// trust anchors and rejects every certificate, so the documented
|
||||
// "verify against the system trust roots" behaviour would be
|
||||
// unreachable. Only a certificate chaining to an already-trusted
|
||||
// root validates — a bare self-signed gateway cert still needs
|
||||
// `with_ca_file`.
|
||||
tls = tls.with_native_roots();
|
||||
}
|
||||
TlsTrustDecision::RejectNoCa => {
|
||||
// Lenient-default fallback (Rust pin-only exception): the Rust
|
||||
// client cannot accept an arbitrary self-signed cert. Pin the
|
||||
// gateway's CA, or opt into strict verification against the
|
||||
// system trust roots.
|
||||
//
|
||||
// Note: a server-name override affects SNI (the hostname sent in
|
||||
// the TLS ClientHello) but does NOT pin trust.
|
||||
return Err(Error::InvalidEndpoint {
|
||||
endpoint: options.endpoint().to_owned(),
|
||||
detail: "TLS requested without a pinned CA. The Rust client cannot accept an \
|
||||
arbitrary self-signed certificate (tonic 0.13 exposes no custom \
|
||||
rustls verifier). Pin the gateway certificate with \
|
||||
ClientOptions::with_ca_file, or call \
|
||||
ClientOptions::with_require_certificate_validation(true) to verify \
|
||||
against the system trust roots. Note: a server-name override \
|
||||
affects SNI but does not pin trust."
|
||||
.to_owned(),
|
||||
});
|
||||
}
|
||||
TlsTrustDecision::None => unreachable!("handled above"),
|
||||
}
|
||||
Ok(Some(tls))
|
||||
}
|
||||
@@ -269,6 +321,8 @@ mod tests {
|
||||
use super::ClientOptions;
|
||||
use crate::auth::ApiKey;
|
||||
|
||||
use super::{build_tls_config, tls_trust_decision, TlsTrustDecision};
|
||||
|
||||
#[test]
|
||||
fn debug_redacts_api_key() {
|
||||
let options =
|
||||
@@ -279,4 +333,47 @@ mod tests {
|
||||
assert!(debug.contains("<redacted>"));
|
||||
assert!(!debug.contains("mxgw_secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plaintext_needs_no_tls() {
|
||||
let options = ClientOptions::new("http://127.0.0.1:5000").with_plaintext(true);
|
||||
assert_eq!(tls_trust_decision(&options), TlsTrustDecision::None);
|
||||
assert!(build_tls_config(&options).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pinned_ca_uses_pinned_trust() {
|
||||
let options = ClientOptions::new("https://127.0.0.1:5000")
|
||||
.with_plaintext(false)
|
||||
.with_ca_file("/some/ca.pem");
|
||||
assert_eq!(tls_trust_decision(&options), TlsTrustDecision::PinnedCa);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_without_ca_uses_system_roots() {
|
||||
// Regression for Client.Rust-031: strict verification with no pinned CA
|
||||
// must verify against the system trust roots, not produce a config with
|
||||
// zero trust anchors. The trust decision proves roots are consulted; the
|
||||
// build then succeeds (no no-CA guard error) and emits a config.
|
||||
let options = ClientOptions::new("https://127.0.0.1:5000")
|
||||
.with_plaintext(false)
|
||||
.with_require_certificate_validation(true);
|
||||
|
||||
assert_eq!(
|
||||
tls_trust_decision(&options),
|
||||
TlsTrustDecision::SystemRoots,
|
||||
"strict-no-CA must request the system trust roots"
|
||||
);
|
||||
assert!(
|
||||
build_tls_config(&options).unwrap().is_some(),
|
||||
"strict-no-CA must build a usable TLS config"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lenient_without_ca_is_rejected() {
|
||||
let options = ClientOptions::new("https://127.0.0.1:5000").with_plaintext(false);
|
||||
assert_eq!(tls_trust_decision(&options), TlsTrustDecision::RejectNoCa);
|
||||
assert!(build_tls_config(&options).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|---|---|
|
||||
| Module | `clients/rust` |
|
||||
| Reviewer | Claude Code |
|
||||
| Review date | 2026-05-24 |
|
||||
| Commit reviewed | `42b0037` |
|
||||
| Review date | 2026-06-15 |
|
||||
| Commit reviewed | `410acc9` |
|
||||
| Status | Re-reviewed |
|
||||
| Open findings | 0 |
|
||||
|
||||
@@ -96,6 +96,25 @@ under review does not address them.
|
||||
| 9 | Testing coverage | Issue found: zero tests cover `stream_alarms` on `GatewayClient`, the new bulk read/write SDK methods, or the `BenchReadBulk` flow; the fake gateway's `stream_alarms` impl drops the sender immediately (Client.Rust-024). |
|
||||
| 10 | Documentation & comments | Issue found: `.cargo/config.toml`'s comment promises "Release builds are unaffected" but the `link-arg=/STACK:8388608` setting is unconditional under `cfg(windows)` and only applies to the MSVC linker (Client.Rust-027). |
|
||||
|
||||
### 2026-06-15 re-review (commit 410acc9)
|
||||
|
||||
Re-review pass at `410acc9`. The diff against `42b0037` (`git diff 42b0037..HEAD -- clients/rust/`) covers: Cargo metadata + Gitea alternative-registry config (`Cargo.toml`, `.cargo/config.toml`, README install section); a `[registries.dohertj2-gitea]` index entry and `publish = ["dohertj2-gitea"]` with `mxgw-cli` set `publish = false`; the resolution work for Client.Rust-022..029 (malformed-reply `Result` plumbing, `next_correlation_id` re-export, clippy fixes, `read_bulk<S: AsRef<str>>`); a **new** Galaxy lazy-browse walker (`browse`, `browse_children_raw`, `browse_children_inner`, `BrowseChildrenOptions`, `LazyBrowseNode`) with six unit tests; a **new** TLS pin-only guard (`build_tls_config` + `ClientOptions::with_require_certificate_validation` + `--require-certificate-validation` CLI flag) with a new `tests/tls.rs`; and the alarm-provider-fallback proto surface (`AlarmFeedMessage.provider_status`, added contracts-side in `1d85db7`).
|
||||
|
||||
`cargo fmt --check` is clean. `cargo check -p zb-mom-ww-mxgateway-client`, `cargo test -p zb-mom-ww-mxgateway-client` (24 lib + integration, 4 proto-fixture, 4 tls — all pass), and the library half of the workspace are clean. **`cargo clippy --workspace --all-targets -- -D warnings` and `cargo check --workspace` both FAIL at HEAD** — not on a lint but on a hard `E0004` compile error: the `mxgw-cli` binary's two `match &message.payload` blocks (`crates/mxgw-cli/src/main.rs:1731,1757`) are non-exhaustive after the proto added `AlarmFeedMessage.payload::ProviderStatus` (Client.Rust-030). The library crate compiles and all its tests pass; the break is confined to the CLI binary. No committed registry tokens — `.cargo/config.toml` carries only the sparse index URL; the README documents the token living in `~/.cargo/credentials.toml`.
|
||||
|
||||
| # | Category | Result |
|
||||
|---|---|---|
|
||||
| 1 | Correctness & logic bugs | Issue found: `mxgw-cli` fails to compile at HEAD — non-exhaustive `AlarmFeedMessage.payload` match missing the new `ProviderStatus` arm (Client.Rust-030). The library `read_bulk`/galaxy-walker/TLS-guard logic is correct and tested. |
|
||||
| 2 | mxaccessgw conventions | Issue found: `cargo clippy --workspace --all-targets -- -D warnings` / `cargo check --workspace` do not pass — CLAUDE.md mandates they do (Client.Rust-030). The prior 029 clippy regressions are resolved; this is a new build break from the alarm-provider proto change. |
|
||||
| 3 | Concurrency & thread safety | No issues found — `LazyBrowseNode` shares state via `Arc<…AsyncMutex<…>>`; `expand()` holds the mutex across the `browse_children_inner` await so concurrent expanders serialize and the idempotency check is race-free. `CORRELATION_SEQUENCE` is still `AtomicU64`/`Relaxed`. No `unsafe`. |
|
||||
| 4 | Error handling & resilience | Issue found: the strict TLS path (`require_certificate_validation(true)` with no CA) builds a `ClientTlsConfig` with zero trust roots (no `tls-native-roots`/`tls-webpki-roots` feature, no `.with_*_roots()` call), so it cannot validate any certificate — contradicting the documented "verify against the system trust roots" behaviour (Client.Rust-031). The galaxy page-token loop has a correct repeated-token guard. |
|
||||
| 5 | Security | No issues found in the registry/secret surface — `.cargo/config.toml` holds only the sparse index URL, no token; README puts the Bearer token in `~/.cargo/credentials.toml` (uncommitted). (See Client.Rust-031 for the strict-TLS validation gap, classified under error handling.) |
|
||||
| 6 | Performance & resource management | No issues found — `read_bulk` is now borrow-based (`&[S]`), the bench loop reuses `tags_ref` (Client.Rust-026 resolved). The walker clones the `GalaxyClient` channel handle per node, which is the intended cheap `Channel` clone. |
|
||||
| 7 | Design-document adherence | Issue found: `RustClientDesign.md` is not updated for the new Galaxy lazy-browse SDK surface (`browse` / `browse_children_raw` / `LazyBrowseNode` / `BrowseChildrenOptions`); CLAUDE.md requires docs to change with the source (Client.Rust-032). The TLS pin-only section pre-dates this diff but repeats the inaccurate "system trust roots" claim (cross-referenced from Client.Rust-031). |
|
||||
| 8 | Code organization & conventions | No issues found — Cargo metadata (name/version/license/repository/keywords/categories) is well-formed; `publish = ["dohertj2-gitea"]` on the library and `publish = false` on `mxgw-cli` is the right split. `license = "Proprietary"` is non-SPDX but cargo accepts it and it is a deliberate closed-source marker. |
|
||||
| 9 | Testing coverage | No issues found in the new surface — the walker has six unit tests (roots, expand, idempotency, NotFound, multi-page, filter-forwarding) and TLS has four. Gap noted: `tls_with_require_certificate_validation_does_not_short_circuit` connects to a dead address, so it only asserts the guard does not fire and never exercises a real handshake — which is why the no-trust-roots defect in Client.Rust-031 is not caught by a test. |
|
||||
| 10 | Documentation & comments | Issue found: the `alarm_feed_message_summary` / `alarm_feed_message_to_json` doc comments still say "three `payload` oneof cases" (`main.rs:1729,1755`) although the proto now has four; folded into Client.Rust-030's fix. The TLS doc inaccuracy is Client.Rust-031. |
|
||||
|
||||
## Findings
|
||||
|
||||
### Client.Rust-001
|
||||
@@ -687,3 +706,59 @@ The third error (`BulkReplyKind` enum-variant-names) is also touched by the diff
|
||||
**Recommendation:** Re-apply Client.Rust-001 (add doc comments on `with_max_grpc_message_bytes` / `max_grpc_message_bytes` in `options.rs`), Client.Rust-002 (drop the `Bulk` suffix from `BulkReplyKind`'s variants so they become `AddItem` / `AdviseItem` / …, or add a narrowly-scoped `#[allow(clippy::enum_variant_names)]` with a reason comment), and Client.Rust-012 (replace `last_deploy.lock().unwrap().clone()` with `*last_deploy.lock().unwrap()` in `galaxy.rs:282`). Verify with `cargo clippy --workspace --all-targets -- -D warnings`. Consider adding a pre-commit / CI gate so the next reviewer never has to discover the regression by running clippy.
|
||||
|
||||
**Resolution:** 2026-05-24 — Re-applied all three resolutions. `clients/rust/src/options.rs` now has `///` doc comments on `with_max_grpc_message_bytes` and `max_grpc_message_bytes`. `clients/rust/src/galaxy.rs:282` uses `*self.state.last_deploy.lock().unwrap()` instead of `.clone()`. `clients/rust/src/session.rs`'s `BulkReplyKind` variants are renamed to `AddItem` / `AdviseItem` / `RemoveItem` / `UnAdviseItem` / `Subscribe` / `Unsubscribe` (no shared `Bulk` suffix), with the call sites in `add_item_bulk` / `advise_item_bulk` / `remove_item_bulk` / `un_advise_item_bulk` / `subscribe_bulk` / `unsubscribe_bulk` updated accordingly. The sibling `BulkWriteReplyKind` already had non-suffix-sharing variants (`Write` / `Write2` / `WriteSecured` / `WriteSecured2`) and required no rename. `cargo clippy --workspace --all-targets -- -D warnings` is clean at HEAD.
|
||||
|
||||
### Client.Rust-030
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Location | `clients/rust/crates/mxgw-cli/src/main.rs:1731,1757` (origin: `src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto:909-924`, added in commit `1d85db7`) |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** The `mxgw-cli` binary does not compile at HEAD `410acc9`. `cargo check --workspace`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo build --workspace`, and `cargo test --workspace` all fail with a hard `E0004` (non-exhaustive patterns), so the entire documented Rust build/test/clippy workflow that CLAUDE.md mandates is broken:
|
||||
|
||||
```
|
||||
error[E0004]: non-exhaustive patterns: `&Some(...alarm_feed_message::Payload::ProviderStatus(_))` not covered
|
||||
--> crates/mxgw-cli/src/main.rs:1731:11
|
||||
error[E0004]: non-exhaustive patterns: `&Some(...alarm_feed_message::Payload::ProviderStatus(_))` not covered
|
||||
--> crates/mxgw-cli/src/main.rs:1757:11
|
||||
```
|
||||
|
||||
The alarm-provider-fallback contract change (`1d85db7`, within the reviewed range) added a fourth `AlarmFeedMessage.payload` oneof arm — `AlarmProviderStatus provider_status = 4`. tonic-build regenerates the Rust enum with the new `ProviderStatus` variant, but `alarm_feed_message_summary` (`main.rs:1731`) and `alarm_feed_message_to_json` (`main.rs:1757`) each `match &message.payload` exhaustively over only `ActiveAlarm` / `SnapshotComplete` / `Transition` / `None` with no wildcard arm. Because they are exhaustive matches on a now-larger enum, the binary fails to compile rather than silently mishandling the new variant. The library crate (`zb-mom-ww-mxgateway-client`) itself compiles cleanly and all 32 of its tests pass; the break is confined to the CLI — but the CLI is the cross-language e2e matrix driver, so the whole `clients/rust` workspace is unbuildable and no Rust e2e smoke can run against the gateway at this commit. This is the alarm-surface gap the review request asked to check: the `ProviderStatus` payload is unhandled in the only place the Rust client renders the alarm feed.
|
||||
|
||||
**Recommendation:** Add a `Some(alarm_feed_message::Payload::ProviderStatus(status))` arm to both `alarm_feed_message_summary` and `alarm_feed_message_to_json` (render the provider-status fields — mode, degraded/provenance, reference — consistent with how the .NET/Go/Java/Python CLIs serialise it so the cross-language parity matcher recognises the payload). While there, update the two doc comments that still say "three `payload` oneof cases" (`main.rs:1729,1755`) to four. Verify with `cargo clippy --workspace --all-targets -- -D warnings` and `cargo test --workspace`. Consider a CI gate so a contract change that adds a oneof arm cannot leave the Rust CLI unbuildable again.
|
||||
|
||||
**Resolution:** 2026-06-15 — Root cause confirmed: the contract's new fourth `AlarmFeedMessage.payload` oneof arm (`AlarmProviderStatus provider_status`, proto fields `mode`/`degraded`/`reason`/`since`) left both `match &message.payload` blocks non-exhaustive (`E0004`). Added a `Some(alarm_feed_message::Payload::ProviderStatus(status))` arm to both `alarm_feed_message_summary` (`mode`/`degraded`/`reason` one-liner) and `alarm_feed_message_to_json` (a `providerStatus` object with `mode`/`degraded`/`reason`/`since`), added an `AlarmEnumName::provider_mode` enum-name helper consistent with the existing `condition_state`/`transition_kind` renderers, and updated the summary doc comment to "four payload oneof cases". No `_ => {}` wildcard. Test: `alarm_feed_provider_status_renders_in_summary_and_json` (in `crates/mxgw-cli/src/main.rs`). All four cargo commands now pass.
|
||||
|
||||
### Client.Rust-031
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Location | `clients/rust/src/options.rs:196-240` (`build_tls_config`); `clients/rust/Cargo.toml:40` (tonic features); docs: `clients/rust/src/options.rs:76-101`, `clients/rust/README.md` (TLS trust section), `clients/rust/crates/mxgw-cli/src/main.rs:429-431`, `clients/rust/RustClientDesign.md:202` |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** The new strict-verification escape hatch does not do what it documents. `build_tls_config` only configures trust roots when a CA file is pinned: with `require_certificate_validation(true)` and no `ca_file`, it returns a bare `ClientTlsConfig::new()` and never calls `.with_native_roots()`, `.with_webpki_roots()`, or `.with_enabled_roots()`. The crate also enables only `tonic` feature `tls-ring` (`Cargo.toml:40`) — neither `tls-native-roots` nor `tls-webpki-roots` is on, so even if the code wanted to enable system roots the methods are feature-gated out. A `ClientTlsConfig` with zero trust anchors rejects every server certificate during the rustls handshake, so the strict path cannot connect to any TLS gateway — not even one whose certificate is genuinely chained to a system root. Yet `with_require_certificate_validation`'s doc comment (`options.rs:80-89`), the README "TLS trust (pin-only)" section, the `--require-certificate-validation` CLI flag help (`main.rs:429-431`), and `RustClientDesign.md:202` all tell the user this option will "verify against the system trust roots." The documented behaviour is unreachable; the only working TLS path is CA pinning (`with_ca_file`).
|
||||
|
||||
This is masked by the tests: `tls_with_require_certificate_validation_does_not_short_circuit` (`tests/tls.rs`) dials a dead address (`https://127.0.0.1:1`) and only asserts the no-CA guard error does *not* fire — it never reaches a handshake, so the absent-roots defect is invisible to the suite.
|
||||
|
||||
**Recommendation:** Either (a) make the strict path actually load system roots — add the `tls-native-roots` (and/or `tls-webpki-roots`) feature to the `tonic` dependency and call `tls = tls.with_native_roots()` (or `.with_enabled_roots()`) in the `require_certificate_validation == true && ca_file.is_none()` branch of `build_tls_config` — and add a test that pins a self-signed cert as a CA and asserts a system-root-only connection to that same server is *rejected* (proving roots are actually consulted); or (b) if loading system roots is intentionally out of scope for v1, correct every doc site (the `with_require_certificate_validation` doc comment, README, CLI flag help, and `RustClientDesign.md`) to state that the strict flag does not currently enable any trust roots and that CA pinning is the only supported TLS path. Option (a) is the better fix because the flag otherwise has no working effect.
|
||||
|
||||
**Resolution:** 2026-06-15 — Took option (a). Root cause confirmed: strict-on/no-CA returned a bare `ClientTlsConfig::new()` with zero trust anchors and the crate only enabled tonic `tls-ring`, so the documented "verify against the system trust roots" path could never validate any certificate. Added `tls-native-roots` to the `tonic` features in `Cargo.toml` and refactored `build_tls_config` to compute the trust posture via a new pure `tls_trust_decision` helper returning `TlsTrustDecision::{None,PinnedCa,SystemRoots,RejectNoCa}`; the `SystemRoots` branch now calls `ClientTlsConfig::with_native_roots()` so a cert chaining to an OS-trusted root validates. Corrected every doc site to state the strict flag verifies against OS roots (not a bare self-signed cert, which still needs `with_ca_file`): the `with_require_certificate_validation` doc comment and `build_tls_config` docs (`options.rs`), README "TLS trust" section, and `RustClientDesign.md` "Trust posture"; the CLI flag help was already accurate. TDD: added failing-first unit tests then the fix — `strict_without_ca_uses_system_roots`, `lenient_without_ca_is_rejected`, `pinned_ca_uses_pinned_trust`, `plaintext_needs_no_tls` (in `src/options.rs`). All four cargo commands pass.
|
||||
|
||||
### Client.Rust-032
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Location | `clients/rust/RustClientDesign.md`; surface in `clients/rust/src/galaxy.rs:281-379` |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** The diff under review adds substantial new public Galaxy SDK surface — `GalaxyClient::browse`, `GalaxyClient::browse_children_raw`, the `BrowseChildrenOptions` filter struct, and the `LazyBrowseNode` lazy walker (`object`, `has_children_hint`, `children`, `is_expanded`, `expand`) — none of which is described in `RustClientDesign.md`. The README was updated with a "Browsing lazily" / "High-level walker" section, but CLAUDE.md requires the design docs to change in the same change as the public API. A reader consulting the detailed design to understand the Galaxy client surface will not learn that lazy browsing, sibling pagination, the `child_has_children` hint, or the idempotent `expand` contract exist.
|
||||
|
||||
**Recommendation:** Add a "Lazy browse" subsection to the Galaxy section of `RustClientDesign.md` enumerating `browse`, `browse_children_raw`, `BrowseChildrenOptions` (its filter fields and AND semantics), and `LazyBrowseNode` (the `Arc`-shared clone semantics, the idempotent single-RPC `expand`, the `has_children_hint`, and the internal paged `BrowseChildren` loop with its repeated-page-token guard). Cross-reference `docs/GalaxyRepository.md#browsechildren` for the wire-level request/filter semantics the README already links.
|
||||
|
||||
**Resolution:** 2026-06-15 — Confirmed by inspection that `RustClientDesign.md` had no Galaxy library-API coverage at all. Added a new "Galaxy Repository" section documenting `browse`, `browse_children_raw`, the `BrowseChildrenOptions` filter struct (all six fields, AND combination semantics, `include_attributes` tri-state), and `LazyBrowseNode` (`Arc`-shared clone semantics, `has_children_hint`, the idempotent single-RPC `expand` under an async mutex with page size 500, and the repeated-page-token `Error::InvalidArgument` guard), cross-referencing `docs/GalaxyRepository.md#browsechildren`. Also noted the fourth alarm `provider_status` oneof case in the Alarms section while resolving Client.Rust-030. Doc-only change verified by inspection; design-doc anchor target confirmed present.
|
||||
|
||||
Reference in New Issue
Block a user