feat(clients): CLI-04 typed single-item command parity (+CLI-30 unregister) — 4/5 clients
Every parity-critical single-item MXAccess command now has a typed session helper instead of only a raw-Invoke escape hatch. Added per client: - Phase 1: AdviseSupervisory, WriteSecured, WriteSecured2, AuthenticateUser, ArchestrAUserToId - Phase 2: AddBufferedItem, SetBufferedUpdateInterval, Suspend, Activate - CLI-30: Unregister (Rust + .NET; Go/Python already had it) Each wraps the existing raw-command machinery (no new wire surface) and runs the same MXAccess-level reply validation (hresult < 0 + MxStatusProxy). MXAccess parity preserved: WriteSecured before AuthenticateUser+AdviseSupervisory surfaces the native failure unchanged (not pre-validated/reordered). Credentials (AuthenticateUser password, WriteSecured payloads) route through each client's secret-redaction seam and never reach logs/exceptions/ToString/Debug/Display; each suite asserts a distinctive credential is absent from surfaced errors. New CLI subcommands source credentials via flag/env, never echoed. - .NET: 21 helpers (validated + Raw), CLI subcommands, multi-secret CLI redactor. Build clean (0 warn), 102 passed. - Go: 9 helpers + *Raw variants, redactSecrets seam, promoted CLI advise-supervisory to typed. gofmt/vet/build/test clean. - Rust: 10 helpers incl. unregister; verified ensure_mxaccess_success runs on secured paths; error.rs credential scrub. fmt/check/test/clippy clean. - Python: 9 async helpers, redact_secret seam + _invoke_redacted, CLI commands. 145 passed. - Shared doc: ClientLibrariesDesign "Typed Command Parity" section. Java client typed parity is batched to windev (no local JRE); CLI-04 + CLI-30 stay open until it lands. Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
This commit is contained in:
@@ -21,11 +21,10 @@ use serde_json::Value;
|
||||
use zb_mom_ww_mxgateway_client::galaxy::{BrowseChildrenOptions, LazyBrowseNode};
|
||||
use zb_mom_ww_mxgateway_client::generated::galaxy_repository::v1::DeployEvent;
|
||||
use zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::{
|
||||
alarm_feed_message, AcknowledgeAlarmRequest, AdviseSupervisoryCommand, AlarmFeedMessage,
|
||||
CloseSessionRequest, MxCommand, MxCommandKind, MxCommandRequest, MxEvent, MxEventFamily,
|
||||
MxValue as ProtoMxValue, OpenSessionRequest, PingCommand, StreamAlarmsRequest,
|
||||
StreamEventsRequest, Write2BulkEntry, WriteBulkEntry, WriteSecured2BulkEntry,
|
||||
WriteSecuredBulkEntry,
|
||||
alarm_feed_message, AcknowledgeAlarmRequest, AlarmFeedMessage, CloseSessionRequest, MxCommand,
|
||||
MxCommandKind, MxCommandRequest, MxEvent, MxEventFamily, MxValue as ProtoMxValue,
|
||||
OpenSessionRequest, PingCommand, StreamAlarmsRequest, StreamEventsRequest, Write2BulkEntry,
|
||||
WriteBulkEntry, WriteSecured2BulkEntry, WriteSecuredBulkEntry,
|
||||
};
|
||||
use zb_mom_ww_mxgateway_client::{
|
||||
next_correlation_id, ApiKey, ClientOptions, Error, EventItem, GalaxyClient, GatewayClient,
|
||||
@@ -118,6 +117,67 @@ enum Command {
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Release a `ServerHandle` (and the items advised under it) via
|
||||
/// MXAccess `Unregister`.
|
||||
Unregister {
|
||||
#[command(flatten)]
|
||||
connection: ConnectionArgs,
|
||||
#[arg(long)]
|
||||
session_id: String,
|
||||
#[arg(long)]
|
||||
server_handle: i32,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Resolve an MXAccess user id from a credential via `AuthenticateUser`.
|
||||
/// The password is read from `--password` or, if omitted, from the
|
||||
/// environment variable named by `--password-env`; it is never echoed to
|
||||
/// stdout/stderr.
|
||||
AuthenticateUser {
|
||||
#[command(flatten)]
|
||||
connection: ConnectionArgs,
|
||||
#[arg(long)]
|
||||
session_id: String,
|
||||
#[arg(long)]
|
||||
server_handle: i32,
|
||||
#[arg(long)]
|
||||
verify_user: String,
|
||||
/// Verifier password. Prefer `--password-env` so the secret never
|
||||
/// appears in the process command line.
|
||||
#[arg(long)]
|
||||
password: Option<String>,
|
||||
/// Name of the environment variable holding the verifier password.
|
||||
/// Used only when `--password` is not supplied.
|
||||
#[arg(long, default_value = "MXGATEWAY_VERIFY_PASSWORD")]
|
||||
password_env: String,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Single credential-verified write via MXAccess `WriteSecured`.
|
||||
///
|
||||
/// Parity note: this fails natively unless the session has first run
|
||||
/// `authenticate-user` + `advise-supervisory` and the item carries a
|
||||
/// value-bearing body — the native failure is surfaced, not hidden.
|
||||
WriteSecured {
|
||||
#[command(flatten)]
|
||||
connection: ConnectionArgs,
|
||||
#[arg(long)]
|
||||
session_id: String,
|
||||
#[arg(long)]
|
||||
server_handle: i32,
|
||||
#[arg(long)]
|
||||
item_handle: i32,
|
||||
#[arg(long, value_enum)]
|
||||
value_type: CliValueType,
|
||||
#[arg(long)]
|
||||
value: String,
|
||||
#[arg(long, default_value_t = 0)]
|
||||
current_user_id: i32,
|
||||
#[arg(long, default_value_t = 0)]
|
||||
verifier_user_id: i32,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
SubscribeBulk {
|
||||
#[command(flatten)]
|
||||
connection: ConnectionArgs,
|
||||
@@ -669,18 +729,69 @@ async fn dispatch(command: Command) -> Result<(), Error> {
|
||||
} => {
|
||||
let session = session_for(connection, session_id).await?;
|
||||
session
|
||||
.invoke(
|
||||
MxCommandKind::AdviseSupervisory,
|
||||
zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::mx_command::Payload::AdviseSupervisory(
|
||||
AdviseSupervisoryCommand {
|
||||
server_handle,
|
||||
item_handle,
|
||||
},
|
||||
),
|
||||
)
|
||||
.advise_supervisory(server_handle, item_handle)
|
||||
.await?;
|
||||
print_ok("advise-supervisory", json);
|
||||
}
|
||||
Command::Unregister {
|
||||
connection,
|
||||
session_id,
|
||||
server_handle,
|
||||
json,
|
||||
} => {
|
||||
let session = session_for(connection, session_id).await?;
|
||||
session.unregister(server_handle).await?;
|
||||
print_ok("unregister", json);
|
||||
}
|
||||
Command::AuthenticateUser {
|
||||
connection,
|
||||
session_id,
|
||||
server_handle,
|
||||
verify_user,
|
||||
password,
|
||||
password_env,
|
||||
json,
|
||||
} => {
|
||||
// Resolve the credential from --password or the named env var.
|
||||
// The password is passed straight to the typed helper and is never
|
||||
// echoed to stdout/stderr or embedded in an error message.
|
||||
let verify_user_password = password
|
||||
.or_else(|| env::var(&password_env).ok())
|
||||
.ok_or_else(|| Error::InvalidArgument {
|
||||
name: "password".to_owned(),
|
||||
detail: format!(
|
||||
"supply --password or set the environment variable `{password_env}`"
|
||||
),
|
||||
})?;
|
||||
let session = session_for(connection, session_id).await?;
|
||||
let user_id = session
|
||||
.authenticate_user(server_handle, &verify_user, &verify_user_password)
|
||||
.await?;
|
||||
print_handle("userId", user_id, json);
|
||||
}
|
||||
Command::WriteSecured {
|
||||
connection,
|
||||
session_id,
|
||||
server_handle,
|
||||
item_handle,
|
||||
value_type,
|
||||
value,
|
||||
current_user_id,
|
||||
verifier_user_id,
|
||||
json,
|
||||
} => {
|
||||
let session = session_for(connection, session_id).await?;
|
||||
session
|
||||
.write_secured(
|
||||
server_handle,
|
||||
item_handle,
|
||||
current_user_id,
|
||||
verifier_user_id,
|
||||
parse_value(value_type, &value)?,
|
||||
)
|
||||
.await?;
|
||||
print_ok("write-secured", json);
|
||||
}
|
||||
Command::SubscribeBulk {
|
||||
connection,
|
||||
session_id,
|
||||
@@ -2489,6 +2600,59 @@ mod tests {
|
||||
assert_eq!(value["workerProtocolVersion"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_authenticate_user_command_with_password_env() {
|
||||
let parsed = Cli::try_parse_from([
|
||||
"mxgw",
|
||||
"authenticate-user",
|
||||
"--session-id",
|
||||
"session-1",
|
||||
"--server-handle",
|
||||
"7",
|
||||
"--verify-user",
|
||||
"verifier",
|
||||
"--password-env",
|
||||
"MY_PW_VAR",
|
||||
]);
|
||||
assert!(parsed.is_ok(), "parse failed: {parsed:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_write_secured_command() {
|
||||
let parsed = Cli::try_parse_from([
|
||||
"mxgw",
|
||||
"write-secured",
|
||||
"--session-id",
|
||||
"session-1",
|
||||
"--server-handle",
|
||||
"12",
|
||||
"--item-handle",
|
||||
"34",
|
||||
"--value-type",
|
||||
"int32",
|
||||
"--value",
|
||||
"5",
|
||||
"--current-user-id",
|
||||
"1",
|
||||
"--verifier-user-id",
|
||||
"2",
|
||||
]);
|
||||
assert!(parsed.is_ok(), "parse failed: {parsed:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_unregister_command() {
|
||||
let parsed = Cli::try_parse_from([
|
||||
"mxgw",
|
||||
"unregister",
|
||||
"--session-id",
|
||||
"session-1",
|
||||
"--server-handle",
|
||||
"12",
|
||||
]);
|
||||
assert!(parsed.is_ok(), "parse failed: {parsed:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_stream_alarms_command() {
|
||||
let parsed = Cli::try_parse_from([
|
||||
|
||||
Reference in New Issue
Block a user