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:
Joseph Doherty
2026-07-09 16:41:43 -04:00
parent 4090a478c8
commit bde042b4d4
21 changed files with 3496 additions and 97 deletions
@@ -842,6 +842,525 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
/// <summary>
/// Unregisters a previously registered client from the MXAccess session
/// (MXAccess <c>Unregister</c>), releasing its ServerHandle.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task UnregisterAsync(
int serverHandle,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await UnregisterRawAsync(serverHandle, cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Unregisters a previously registered client without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> UnregisterRawAsync(
int serverHandle,
CancellationToken cancellationToken = default)
{
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.Unregister,
Unregister = new UnregisterCommand { ServerHandle = serverHandle },
},
cancellationToken);
}
/// <summary>
/// Subscribes to supervisory events for an item (MXAccess <c>AdviseSupervisory</c>).
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task AdviseSupervisoryAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await AdviseSupervisoryRawAsync(serverHandle, itemHandle, cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Subscribes to supervisory events for an item without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> AdviseSupervisoryRawAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.AdviseSupervisory,
AdviseSupervisory = new AdviseSupervisoryCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
},
cancellationToken);
}
/// <summary>
/// Adds a buffered item to the MXAccess session (MXAccess <c>AddBufferedItem</c>),
/// returning an ItemHandle.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemDefinition">The item tag address.</param>
/// <param name="itemContext">Additional context for the item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The item handle assigned to the new buffered item.</returns>
public async Task<int> AddBufferedItemAsync(
int serverHandle,
string itemDefinition,
string itemContext,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await AddBufferedItemRawAsync(
serverHandle,
itemDefinition,
itemContext,
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.AddBufferedItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
}
/// <summary>
/// Adds a buffered item to the MXAccess session without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemDefinition">The item tag address.</param>
/// <param name="itemContext">Additional context for the item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> AddBufferedItemRawAsync(
int serverHandle,
string itemDefinition,
string itemContext,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.AddBufferedItem,
AddBufferedItem = new AddBufferedItemCommand
{
ServerHandle = serverHandle,
ItemDefinition = itemDefinition,
ItemContext = itemContext ?? string.Empty,
},
},
cancellationToken);
}
/// <summary>
/// Sets the buffered-item update interval on the MXAccess session
/// (MXAccess <c>SetBufferedUpdateInterval</c>).
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="updateIntervalMilliseconds">The buffered update interval, in milliseconds.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task SetBufferedUpdateIntervalAsync(
int serverHandle,
int updateIntervalMilliseconds,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await SetBufferedUpdateIntervalRawAsync(
serverHandle,
updateIntervalMilliseconds,
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Sets the buffered-item update interval without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="updateIntervalMilliseconds">The buffered update interval, in milliseconds.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> SetBufferedUpdateIntervalRawAsync(
int serverHandle,
int updateIntervalMilliseconds,
CancellationToken cancellationToken = default)
{
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.SetBufferedUpdateInterval,
SetBufferedUpdateInterval = new SetBufferedUpdateIntervalCommand
{
ServerHandle = serverHandle,
UpdateIntervalMilliseconds = updateIntervalMilliseconds,
},
},
cancellationToken);
}
/// <summary>
/// Suspends updates for an item (MXAccess <c>Suspend</c>).
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The item's MXSTATUS_PROXY as reported by the worker, or <c>null</c> if the reply omitted it.</returns>
public async Task<MxStatusProxy?> SuspendAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await SuspendRawAsync(serverHandle, itemHandle, cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.Suspend?.Status;
}
/// <summary>
/// Suspends updates for an item without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> SuspendRawAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.Suspend,
Suspend = new SuspendCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
},
cancellationToken);
}
/// <summary>
/// Resumes updates for a suspended item (MXAccess <c>Activate</c>).
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The item's MXSTATUS_PROXY as reported by the worker, or <c>null</c> if the reply omitted it.</returns>
public async Task<MxStatusProxy?> ActivateAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await ActivateRawAsync(serverHandle, itemHandle, cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.Activate?.Status;
}
/// <summary>
/// Resumes updates for a suspended item without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> ActivateRawAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.Activate,
Activate = new ActivateCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
},
cancellationToken);
}
/// <summary>
/// Writes a secured value to an item on the MXAccess server (MXAccess <c>WriteSecured</c>).
/// </summary>
/// <remarks>
/// MXAccess parity: <c>WriteSecured</c> fails when it is issued before a value-bearing
/// NMX body or before a prior <c>AuthenticateUser</c> + <c>AdviseSupervisory</c>. That
/// native failure is surfaced unchanged — the client does not pre-validate or reorder it.
/// The <paramref name="value"/> is credential-sensitive and must never reach logs; the
/// client mirrors the single-item WriteSecured redaction contract.
/// </remarks>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="value">The secured value to write.</param>
/// <param name="currentUserId">The current operator user id.</param>
/// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task WriteSecuredAsync(
int serverHandle,
int itemHandle,
MxValue value,
int currentUserId,
int verifierUserId,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await WriteSecuredRawAsync(
serverHandle,
itemHandle,
value,
currentUserId,
verifierUserId,
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Writes a secured value to an item without error checking. See
/// <see cref="WriteSecuredAsync"/> for the parity and redaction contract.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="value">The secured value to write.</param>
/// <param name="currentUserId">The current operator user id.</param>
/// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> WriteSecuredRawAsync(
int serverHandle,
int itemHandle,
MxValue value,
int currentUserId,
int verifierUserId,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(value);
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.WriteSecured,
WriteSecured = new WriteSecuredCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
CurrentUserId = currentUserId,
VerifierUserId = verifierUserId,
Value = value,
},
},
cancellationToken);
}
/// <summary>
/// Writes a secured value and timestamp to an item (MXAccess <c>WriteSecured2</c>).
/// </summary>
/// <remarks>
/// Same parity and redaction contract as <see cref="WriteSecuredAsync"/>: the native
/// failure that occurs before a value-bearing NMX body or a prior authenticate is
/// surfaced unchanged, and the credential-sensitive <paramref name="value"/> must never
/// reach logs.
/// </remarks>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="value">The secured value to write.</param>
/// <param name="timestampValue">The timestamp to write with the value.</param>
/// <param name="currentUserId">The current operator user id.</param>
/// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task WriteSecured2Async(
int serverHandle,
int itemHandle,
MxValue value,
MxValue timestampValue,
int currentUserId,
int verifierUserId,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await WriteSecured2RawAsync(
serverHandle,
itemHandle,
value,
timestampValue,
currentUserId,
verifierUserId,
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Writes a secured value and timestamp to an item without error checking. See
/// <see cref="WriteSecured2Async"/> for the parity and redaction contract.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="value">The secured value to write.</param>
/// <param name="timestampValue">The timestamp to write with the value.</param>
/// <param name="currentUserId">The current operator user id.</param>
/// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> WriteSecured2RawAsync(
int serverHandle,
int itemHandle,
MxValue value,
MxValue timestampValue,
int currentUserId,
int verifierUserId,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(value);
ArgumentNullException.ThrowIfNull(timestampValue);
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.WriteSecured2,
WriteSecured2 = new WriteSecured2Command
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
CurrentUserId = currentUserId,
VerifierUserId = verifierUserId,
Value = value,
TimestampValue = timestampValue,
},
},
cancellationToken);
}
/// <summary>
/// Authenticates an MXAccess verify-user (MXAccess <c>AuthenticateUser</c>), returning
/// the resolved user id used by <c>WriteSecured</c> / <c>WriteSecured2</c>.
/// </summary>
/// <remarks>
/// The <paramref name="verifyUserPassword"/> is a raw MXAccess credential. It is never
/// logged and never placed on the exception path: gateway/MXAccess failures surface only
/// reply-derived diagnostics (kind, HRESULT, MXSTATUS_PROXY), never the request payload.
/// </remarks>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="verifyUser">The user to verify.</param>
/// <param name="verifyUserPassword">The verify-user credential. Never logged.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The authenticated user id.</returns>
public async Task<int> AuthenticateUserAsync(
int serverHandle,
string verifyUser,
string verifyUserPassword,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await AuthenticateUserRawAsync(
serverHandle,
verifyUser,
verifyUserPassword,
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.AuthenticateUser?.UserId ?? reply.ReturnValue.Int32Value;
}
/// <summary>
/// Authenticates an MXAccess verify-user without error checking. See
/// <see cref="AuthenticateUserAsync"/> for the credential-handling contract.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="verifyUser">The user to verify.</param>
/// <param name="verifyUserPassword">The verify-user credential. Never logged.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> AuthenticateUserRawAsync(
int serverHandle,
string verifyUser,
string verifyUserPassword,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(verifyUser);
ArgumentNullException.ThrowIfNull(verifyUserPassword);
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.AuthenticateUser,
AuthenticateUser = new AuthenticateUserCommand
{
ServerHandle = serverHandle,
VerifyUser = verifyUser,
VerifyUserPassword = verifyUserPassword,
},
},
cancellationToken);
}
/// <summary>
/// Resolves an ArchestrA user GUID to its MXAccess user id
/// (MXAccess <c>ArchestrAUserToId</c>).
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="userIdGuid">The ArchestrA user GUID to resolve.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The resolved MXAccess user id.</returns>
public async Task<int> ArchestraUserToIdAsync(
int serverHandle,
string userIdGuid,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await ArchestraUserToIdRawAsync(serverHandle, userIdGuid, cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.ArchestraUserToId?.UserId ?? reply.ReturnValue.Int32Value;
}
/// <summary>
/// Resolves an ArchestrA user GUID to its MXAccess user id without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="userIdGuid">The ArchestrA user GUID to resolve.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> ArchestraUserToIdRawAsync(
int serverHandle,
string userIdGuid,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(userIdGuid);
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.ArchestraUserToId,
ArchestraUserToId = new ArchestrAUserToIdCommand
{
ServerHandle = serverHandle,
UserIdGuid = userIdGuid,
},
},
cancellationToken);
}
/// <summary>
/// Invokes an MXAccess command on this session.
/// </summary>