Align docs with StyleGuide and add CLAUDE.md

- Rename 16 kebab-case docs to PascalCase per StyleGuide
- Move per-language client design docs from docs/ to clients/<lang>/
  alongside their READMEs
- Add ## Related Documentation sections to 15 docs that lacked one
- Fix sentence-case violations in H3 headings (StyleGuide rule)
- Update cross-references in gateway.md, client READMEs, scripts,
  and generate-proto.ps1 helpers to follow the new paths
- Add CLAUDE.md with build/test commands, the source-update
  verification matrix, the parity-first contract, and pointers
  to MXAccess and Galaxy Repository analysis sources

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-30 10:19:22 -04:00
parent 133c83029b
commit 51a9dadf62
45 changed files with 522 additions and 134 deletions
+21 -8
View File
@@ -91,12 +91,15 @@ return ApiKeyVerificationResult.Success(new ApiKeyIdentity(
KeyId: storedKey.KeyId,
KeyPrefix: storedKey.KeyPrefix,
DisplayName: storedKey.DisplayName,
Scopes: storedKey.Scopes));
Scopes: storedKey.Scopes,
Constraints: storedKey.Constraints));
```
`ApiKeyVerificationResult` carries either an `ApiKeyIdentity` or a discriminated `ApiKeyVerificationFailure` value. The failure enum distinguishes parse errors, missing pepper, missing or revoked keys, and secret mismatch so the calling middleware can emit precise audit detail without leaking which check failed to the client.
`ApiKeyIdentity` exposes only non-secret fields (`KeyId`, `KeyPrefix`, `DisplayName`, `Scopes`) and is the type downstream authorization code consumes.
`ApiKeyIdentity` exposes only non-secret fields (`KeyId`, `KeyPrefix`,
`DisplayName`, `Scopes`, and `Constraints`) and is the type downstream
authorization code consumes.
## Storage
@@ -131,7 +134,9 @@ public SqliteConnection CreateConnection()
`SqliteAuthSchema` declares table names and the current schema version as constants. Three tables are involved:
- `api_keys` stores `key_id`, `key_prefix`, the `secret_hash` blob, `display_name`, serialized `scopes`, and the `created_utc`, `last_used_utc`, and `revoked_utc` timestamps.
- `api_keys` stores `key_id`, `key_prefix`, the `secret_hash` blob,
`display_name`, serialized `scopes`, optional serialized `constraints`, and
the `created_utc`, `last_used_utc`, and `revoked_utc` timestamps.
- `api_key_audit` is an append-only log keyed by an autoincrement `audit_id` with `key_id`, `event_type`, `remote_address`, `created_utc`, and `details` columns.
- `schema_version` carries a single row whose `version` column is matched against `SqliteAuthSchema.CurrentVersion`.
@@ -150,9 +155,10 @@ public static ApiKeyRecord Read(SqliteDataReader reader)
SecretHash: (byte[])reader["secret_hash"],
DisplayName: reader.GetString(3),
Scopes: ApiKeyScopeSerializer.Deserialize(reader.GetString(4)),
CreatedUtc: DateTimeOffset.Parse(reader.GetString(5), System.Globalization.CultureInfo.InvariantCulture),
LastUsedUtc: ReadNullableDateTimeOffset(reader, 6),
RevokedUtc: ReadNullableDateTimeOffset(reader, 7));
Constraints: ApiKeyConstraintSerializer.Deserialize(reader.IsDBNull(5) ? null : reader.GetString(5)),
CreatedUtc: DateTimeOffset.Parse(reader.GetString(6), System.Globalization.CultureInfo.InvariantCulture),
LastUsedUtc: ReadNullableDateTimeOffset(reader, 7),
RevokedUtc: ReadNullableDateTimeOffset(reader, 8));
}
```
@@ -193,8 +199,8 @@ The supported subcommands match `ApiKeyAdminCommandKind` exactly:
| Subcommand | Required options | Behaviour |
|------------|------------------|-----------|
| `init-db` | none | Runs the migrator and records an audit entry. |
| `create-key` | `--key-id`, `--display-name` | Generates a new secret, stores its peppered hash, and prints the assembled `mxgw_<keyId>_<secret>` token. |
| `list-keys` | none | Lists every stored key with its scopes and revocation state. |
| `create-key` | `--key-id`, `--display-name` | Generates a new secret, stores its peppered hash and optional constraints, and prints the assembled `mxgw_<keyId>_<secret>` token. |
| `list-keys` | none | Lists every stored key with its scopes, constraints, and revocation state. |
| `revoke-key` | `--key-id` | Sets `revoked_utc` if the key is currently active. |
| `rotate-key` | `--key-id` | Replaces the secret hash and prints the new token. |
@@ -203,11 +209,18 @@ Examples:
```bash
mxgateway apikey init-db
mxgateway apikey create-key --key-id ops.alice --display-name "Alice (ops)" --scopes read,write
mxgateway apikey create-key --key-id area1.reader --display-name "Area 1 reader" --scopes invoke:read,metadata:read --read-subtree "Area1/*" --browse-subtree "Area1/*"
mxgateway apikey list-keys --json
mxgateway apikey revoke-key --key-id ops.alice
mxgateway apikey rotate-key --key-id ops.alice
```
Constraint flags are optional. `--read-subtree`, `--write-subtree`,
`--read-tag-glob`, `--write-tag-glob`, and `--browse-subtree` are repeatable.
`--max-write-classification` accepts one integer. `--read-alarm-only` and
`--read-historized-only` are boolean flags. Existing rows with null
constraints remain fully unconstrained after migration.
Key ids are restricted by the parser to ASCII letters, digits, periods, and hyphens so they remain safe to embed in the token format and in URL paths used by administrative tooling.
## Scope Serialization
+45 -2
View File
@@ -1,6 +1,8 @@
# Gateway gRPC Authorization
The authorization subsystem enforces per-RPC scope checks against the authenticated `ApiKeyIdentity` produced by the authentication layer, so service implementations never need to repeat permission logic.
The authorization subsystem has two layers. The gRPC interceptor enforces the
verb scope required by the RPC. Service-layer constraint checks then narrow
what an authenticated API key can browse, read, or write inside the Galaxy.
## Overview
@@ -12,6 +14,8 @@ The participating types live under `src/MxGateway.Server/Security/Authorization/
- `GatewayGrpcScopeResolver` maps a request message (and, for `MxCommandRequest`, the inner `MxCommandKind`) to the scope string that must be present on the caller.
- `GatewayScopes` exposes the canonical scope constants used by the resolver and any downstream consumer.
- `GatewayRequestIdentityAccessor` and `IGatewayRequestIdentityAccessor` expose the verified identity to handlers and any service code that runs inside the call.
- `IConstraintEnforcer` applies optional API-key constraints against the
cached Galaxy hierarchy from service bodies.
- `GrpcAuthorizationServiceCollectionExtensions` wires the components into the DI container and the gRPC pipeline.
The `ApiKeyIdentity` consumed here is produced by the authentication layer; see [Authentication](./Authentication.md) for how it is built and how scopes are persisted.
@@ -21,7 +25,9 @@ The `ApiKeyIdentity` consumed here is produced by the authentication layer; see
Centralizing the policy in `GatewayGrpcAuthorizationInterceptor` produces three concrete benefits:
1. Every RPC defined in `MxAccessGatewayService` is covered by construction. A new RPC inherits the check the moment its request type is added to `GatewayGrpcScopeResolver`, instead of relying on each service method to remember to call an authorization helper.
2. The service class stays a thin translator between proto contracts and domain calls. RPC methods do not branch on identity or scope, which keeps the AGENTS.md guideline that gRPC handlers contain no policy.
2. Verb-scope policy stays centralized. Request-specific constraints still run
in service bodies because they need command payloads, item handles, and
Galaxy metadata that the interceptor should not inspect.
3. Authentication and authorization happen in one place, so the gRPC `Status` mapping is consistent. A failed key check always returns `Unauthenticated`, and a missing scope always returns `PermissionDenied` with the offending scope name.
## Interceptor Flow
@@ -131,6 +137,43 @@ private static string ResolveCommandScope(MxCommandKind kind)
Reads (`Register`, `AddItem`, `Advise`, and any other unspecified kind) fall through to `InvokeRead`, which keeps the matrix small while still separating reads from writes, secured writes, metadata lookups, event drains, and worker shutdown.
## Constraint Enforcement
`ApiKeyIdentity.Constraints` is optional. Empty constraints preserve the
previous behavior: the key is authorized only by its verb scopes. Non-empty
constraints are stored as JSON in `api_keys.constraints` and are applied by
`IConstraintEnforcer` after the interceptor succeeds.
Supported constraints are:
| Constraint | Meaning |
|------------|---------|
| `read_subtrees` | Contained-path globs allowed for read/subscription commands. |
| `write_subtrees` | Contained-path globs allowed for write commands. |
| `read_tag_globs` | Tag-address globs allowed for read/subscription commands. |
| `write_tag_globs` | Tag-address globs allowed for write commands. |
| `max_write_classification` | Maximum Galaxy attribute `security_classification` a key may write. |
| `browse_subtrees` | Contained-path globs used to filter Galaxy browse results and deploy-event counts. |
| `read_alarm_only` | Read/subscription commands must target objects with alarm-bearing attributes. |
| `read_historized_only` | Read/subscription commands must target objects with historized attributes. |
Glob matching is anchored, case-insensitive, and supports `*` and `?`.
Subtree and tag glob lists are alternatives: matching either list allows that
scope dimension. Empty lists mean unconstrained for that dimension.
The service checks read constraints for `AddItem`, `AddItem2`, `AddItemBulk`,
`SubscribeBulk`, and `AdviseItemBulk`. It checks write constraints for
`Write`, `Write2`, `WriteSecured`, and `WriteSecured2`. Successful item
registrations are tracked per session so later item-handle commands resolve
back to the original tag address. If a constrained key presents an unknown item
handle, the gateway fails closed.
Non-bulk constraint failures return gRPC `PermissionDenied`. Bulk read
commands preserve input order and return a failed `SubscribeResult` for each
denied item while still forwarding allowed items to the worker. Every denial
adds an `api_key_audit` entry with the key id, command kind, target, and
blocking constraint; secured values and raw credentials are never logged.
## Scope Catalog
`GatewayScopes` is the single source of truth for scope strings. Every entry is currently mapped by either the resolver or another security component:
+2 -2
View File
@@ -101,6 +101,6 @@ fixtures and validate deterministic wrapper expectation files.
## Related Documentation
- [Client Proto Generation](./client-proto-generation.md)
- [Client Libraries Detailed Design](./client-libraries-design.md)
- [Client Proto Generation](./ClientProtoGeneration.md)
- [Client Libraries Detailed Design](./ClientLibrariesDesign.md)
- [Protobuf Contracts](./Contracts.md)
@@ -20,15 +20,15 @@ Target client languages:
Language-specific plans:
- `docs/clients-dotnet-csharp-design.md`
- `docs/clients-golang-design.md`
- `docs/clients-rust-design.md`
- `docs/clients-python-design.md`
- `docs/clients-java-design.md`
- `clients/dotnet/DotnetClientDesign.md`
- `clients/go/GoClientDesign.md`
- `clients/rust/RustClientDesign.md`
- `clients/python/PythonClientDesign.md`
- `clients/java/JavaClientDesign.md`
Shared generation inputs:
- `docs/client-proto-generation.md`
- `docs/ClientProtoGeneration.md`
- `docs/ClientBehaviorFixtures.md`
- `docs/ClientPackaging.md`
- `clients/proto/proto-inputs.json`
@@ -433,3 +433,15 @@ Each client README should include:
- integration test instructions,
- warning that canceling a client call does not abort an in-flight MXAccess COM
call.
## Related Documentation
- [.NET 10 C# Client Detailed Design](../clients/dotnet/DotnetClientDesign.md)
- [Go Client Detailed Design](../clients/go/GoClientDesign.md)
- [Rust Client Detailed Design](../clients/rust/RustClientDesign.md)
- [Python Client Detailed Design](../clients/python/PythonClientDesign.md)
- [Java Client Detailed Design](../clients/java/JavaClientDesign.md)
- [Client Proto Generation](./ClientProtoGeneration.md)
- [Client Behavior Fixtures](./ClientBehaviorFixtures.md)
- [Client Packaging](./ClientPackaging.md)
- [Cross-Language Smoke Matrix](./CrossLanguageSmokeMatrix.md)
+4 -4
View File
@@ -2,7 +2,7 @@
This document defines the clean-checkout commands for building, packaging, and
running the official MXAccess Gateway clients. Use the tool paths and versions
in [Toolchain Links](./toolchain-links.md) when a command is missing from
in [Toolchain Links](./ToolchainLinks.md) when a command is missing from
`PATH`.
## Shared Inputs
@@ -254,7 +254,7 @@ does not abort an MXAccess COM call that is already executing on the worker STA.
## Related Documentation
- [Client Proto Generation](./client-proto-generation.md)
- [Client Libraries Detailed Design](./client-libraries-design.md)
- [Client Proto Generation](./ClientProtoGeneration.md)
- [Client Libraries Detailed Design](./ClientLibrariesDesign.md)
- [Client Behavior Fixtures](./ClientBehaviorFixtures.md)
- [Toolchain Links](./toolchain-links.md)
- [Toolchain Links](./ToolchainLinks.md)
@@ -199,8 +199,8 @@ scripts/validate-client-behavior-fixtures.ps1
## Related Documentation
- [Protobuf Contracts](./Contracts.md)
- [Client Libraries Detailed Design](./client-libraries-design.md)
- [Client Libraries Detailed Design](./ClientLibrariesDesign.md)
- [Client Packaging](./ClientPackaging.md)
- [Client Behavior Fixtures](./ClientBehaviorFixtures.md)
- [Client Libraries Implementation Plan](./implementation-plan-clients.md)
- [Client Libraries Implementation Plan](./ImplementationPlanClients.md)
- [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md)
+4 -4
View File
@@ -41,7 +41,7 @@ hand-edit generated files.
Client generation inputs are published through
`clients/proto/proto-inputs.json` and the descriptor set under
`clients/proto/descriptors/`. See
[Client Proto Generation](./client-proto-generation.md) for language-specific
[Client Proto Generation](./ClientProtoGeneration.md) for language-specific
generation inputs, output directories, and golden protobuf JSON fixtures.
## Generation
@@ -73,7 +73,7 @@ powershell -ExecutionPolicy Bypass -File scripts/publish-client-proto-inputs.ps1
## Related Documentation
- [Client Proto Generation](./client-proto-generation.md)
- [Gateway Process Detailed Design](./gateway-process-design.md)
- [MXAccess Worker Instance Detailed Design](./mxaccess-worker-instance-design.md)
- [Client Proto Generation](./ClientProtoGeneration.md)
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
- [MXAccess Worker Instance Detailed Design](./MxAccessWorkerInstanceDesign.md)
- [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md)
+2 -2
View File
@@ -94,5 +94,5 @@ gateway, the installed MXAccess worker path, and provider state.
## Related Documentation
- [Gateway Testing](./GatewayTesting.md)
- [Client Libraries Detailed Design](./client-libraries-design.md)
- [Client Proto Generation](./client-proto-generation.md)
- [Client Libraries Detailed Design](./ClientLibrariesDesign.md)
- [Client Proto Generation](./ClientProtoGeneration.md)
+1 -1
View File
@@ -291,4 +291,4 @@ Use this checklist when applying the design to another project:
## Related Documentation
- [Gateway Dashboard Detailed Design](./gateway-dashboard-design.md)
- [Gateway Dashboard Detailed Design](./GatewayDashboardDesign.md)
@@ -308,3 +308,11 @@ These are explicit post-v1 revisit items, not open blockers:
- restricted worker service account,
- production coalescing by item handle,
- command batching for high-volume tag setup.
## Related Documentation
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
- [MXAccess Worker Instance Detailed Design](./MxAccessWorkerInstanceDesign.md)
- [Authentication](./Authentication.md)
- [Authorization](./Authorization.md)
- [Galaxy Repository](./GalaxyRepository.md)
+63 -16
View File
@@ -32,13 +32,23 @@ The service is defined in
|-----|---------|
| `TestConnection` | Connectivity probe. Returns `{ ok: bool }` after a `SELECT 1`. Does not throw on SQL failure — returns `ok = false`. Always hits SQL directly so it remains a true health check. |
| `GetLastDeployTime` | Returns the cached `galaxy.time_of_last_deploy`. Served from the shared hierarchy cache; refreshed in the background. |
| `DiscoverHierarchy` | Returns the full deployed hierarchy plus every object's dynamic attributes. **Served from cache** — see [Hierarchy Cache](#hierarchy-cache). |
| `DiscoverHierarchy` | Returns one page of the deployed hierarchy plus each returned object's dynamic attributes. **Served from cache** — see [Hierarchy Cache](#hierarchy-cache). |
| `WatchDeployEvents` | **Server-streaming.** The server emits the current state immediately on subscribe (so clients can bootstrap without waiting), then emits one event per detected deploy change. See [Deploy Notifications](#deploy-notifications). |
`DiscoverHierarchy` is intentionally a single unary RPC rather than a stream:
the row set is small (thousands of objects, low tens-of-thousands of
attributes for typical Galaxies) and clients almost always want the whole tree
at once.
`DiscoverHierarchy` is a paged unary RPC. The raw request accepts `page_size`
and `page_token`; the server defaults omitted page size to 1000 objects and
caps every page at 5000 objects. Page tokens bind to the cache sequence and the
active filter set, so changing filters between pages returns `InvalidArgument`
instead of mixing snapshots. Official high-level clients preserve the older
"return the full hierarchy" behavior by looping pages internally.
The request can also slice the cached hierarchy without running new SQL. A
caller may choose one root (`root_gobject_id`, `root_tag_name`, or
`root_contained_path`) and may combine that with `max_depth`, category ids,
template-chain substring filters, an anchored case-insensitive tag-name glob,
alarm-only, historized-only, and `include_attributes = false` for a skeleton
tree. All filters are applied with AND semantics, and `total_object_count`
reports the post-filter count.
## Hierarchy Cache
@@ -56,12 +66,14 @@ Refresh strategy is **deploy-time gated**:
3. If the deploy timestamp is unchanged, the heavy hierarchy + attributes
queries are **skipped**. The cache simply marks `LastSuccessAt`.
4. If the deploy timestamp changed (or no data has loaded yet), the cache
pulls hierarchy + attributes, materializes a `DiscoverHierarchyReply`
once, replaces the entry atomically, and publishes a deploy event.
pulls hierarchy + attributes, materializes a Galaxy object list plus a
dashboard summary once, replaces the entry atomically, and publishes a
deploy event.
Materializing the reply at refresh time means subsequent `DiscoverHierarchy`
calls return a pre-built proto message — no per-request projection, no
per-request allocations beyond the gRPC serializer's frame.
Materializing objects and dashboard summaries at refresh time means subsequent
`DiscoverHierarchy` calls page over an immutable object list. The dashboard
uses the precomputed summary and does not rescan raw SQL rowsets on each
snapshot.
When SQL is unreachable, the cache retains the previous data and flips
`Status` to `Stale` (or `Unavailable` if no data was ever loaded). A
@@ -110,7 +122,7 @@ Typical client pattern:
3. If sequence skipped a number, treat it as a dropped event and refresh.
```
### Reply Shape
### Reply shape
```proto
message GalaxyObject {
@@ -139,9 +151,32 @@ message GalaxyAttribute {
bool is_historized = 10;
bool is_alarm = 11;
}
message DiscoverHierarchyRequest {
int32 page_size = 1; // omitted/0 uses the server default of 1000
string page_token = 2; // opaque token returned by the previous page
oneof root {
int32 root_gobject_id = 3;
string root_tag_name = 4;
string root_contained_path = 5;
}
google.protobuf.Int32Value max_depth = 6;
repeated int32 category_ids = 7;
repeated string template_chain_contains = 8;
string tag_name_glob = 9;
optional bool include_attributes = 10;
bool alarm_bearing_only = 11;
bool historized_only = 12;
}
message DiscoverHierarchyReply {
repeated GalaxyObject objects = 1;
string next_page_token = 2;
int32 total_object_count = 3;
}
```
### Contained Name vs Tag Name
### Contained name vs tag name
Galaxy objects carry two names. `tag_name` is globally unique and is what
MXAccess expects in `AddItem`. `contained_name` is the human-readable name
@@ -150,7 +185,7 @@ both: clients display `browse_name` to users and pass `tag_name` (or
`full_tag_reference`) into MXAccess subscriptions. When `contained_name` is
empty (top-level objects), `browse_name` falls back to `tag_name`.
### Data Types
### Data types
`mx_data_type` is returned as the raw Galaxy integer rather than mapped to a
language-neutral enum. The gateway makes no assumption about the client's
@@ -176,7 +211,8 @@ GalaxyHierarchyRefreshService (BackgroundService)
-> GalaxyRepository.GetLastDeployTimeAsync (cheap, every tick)
-> GalaxyRepository.GetHierarchyAsync (only on deploy change)
-> GalaxyRepository.GetAttributesAsync (only on deploy change)
-> GalaxyProtoMapper.MapObject (materialize DiscoverHierarchyReply once)
-> GalaxyProtoMapper.MapObject (materialize GalaxyObject list once)
-> DashboardGalaxySummary (precompute dashboard counts once)
-> IGalaxyDeployNotifier.Publish (only on deploy change)
```
@@ -189,8 +225,9 @@ Component breakdown:
recursive CTEs and pick the most-derived attribute override per object.
- `GalaxyHierarchyCache`
(`src/MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs`) holds the most
recent immutable `GalaxyHierarchyCacheEntry` (rows + materialized proto
reply + counts + status). All gRPC clients share the same entry.
recent immutable `GalaxyHierarchyCacheEntry` (materialized objects +
precomputed dashboard summary + counts + status). All gRPC clients share the
same entry.
- `GalaxyHierarchyRefreshService`
(`src/MxGateway.Server/Galaxy/GalaxyHierarchyRefreshService.cs`) is a
hosted `BackgroundService` that drives `RefreshAsync` on the configured
@@ -220,6 +257,11 @@ Security`), but production deployments that use SQL authentication should set
the override via environment variable rather than committing credentials to
`appsettings.json`.
The dashboard parses this connection string and displays only non-secret
fields: server, database, integrated security, encrypt, and trust-server-
certificate. It never displays user id, password, access token, or arbitrary
unparsed connection string text.
## Authorization
All four Galaxy RPCs (including `WatchDeployEvents`) require the
@@ -228,6 +270,11 @@ privilege to `MxCommandKind.GetSessionState` or `MxCommandKind.GetWorkerInfo`.
The mapping lives in `GatewayGrpcScopeResolver`; see
[Authorization](./Authorization.md) for the full scope catalog.
API keys can also carry `browse_subtrees` constraints. `DiscoverHierarchy`
intersects those contained-path globs with the caller's request filters.
`WatchDeployEvents` still emits deploy notifications, but its object and
attribute counts are scoped to the caller's browsable subtrees.
A request without an API key returns `Unauthenticated`. A request with a key
that lacks `metadata:read` returns `PermissionDenied` with the missing scope
embedded in the status detail.
+9 -3
View File
@@ -35,6 +35,8 @@ paths, timeouts, queue sizes, enum values, or protocol values are invalid.
"DefaultCommandTimeoutSeconds": 30,
"MaxSessions": 64,
"MaxPendingCommandsPerSession": 128,
"DefaultLeaseSeconds": 1800,
"LeaseSweepIntervalSeconds": 30,
"AllowMultipleEventSubscribers": false
},
"Events": {
@@ -52,7 +54,8 @@ paths, timeouts, queue sizes, enum values, or protocol values are invalid.
"ShowTagValues": false
},
"Protocol": {
"WorkerProtocolVersion": 1
"WorkerProtocolVersion": 1,
"MaxGrpcMessageBytes": 16777216
},
"Galaxy": {
"ConnectionString": "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;",
@@ -107,6 +110,8 @@ to avoid accidental large allocations from malformed or oversized frames.
| `MxGateway:Sessions:DefaultCommandTimeoutSeconds` | `30` | Default timeout used while the gateway waits for a worker command reply when an open-session request does not provide a positive command timeout. |
| `MxGateway:Sessions:MaxSessions` | `64` | Maximum number of concurrently open gateway sessions. Session opens reserve a slot atomically before worker creation. |
| `MxGateway:Sessions:MaxPendingCommandsPerSession` | `128` | Maximum number of pending worker commands for one session. Excess commands fail fast instead of queueing indefinitely. |
| `MxGateway:Sessions:DefaultLeaseSeconds` | `1800` | Initial session lease and refresh duration. Unary client activity extends the lease by this duration. |
| `MxGateway:Sessions:LeaseSweepIntervalSeconds` | `30` | Hosted monitor interval for closing expired leases. Active event-stream subscribers keep a session from expiring while the stream remains attached. |
| `MxGateway:Sessions:AllowMultipleEventSubscribers` | `false` | Controls whether multiple `StreamEvents` subscribers may attach to one session. `true` is rejected until event fan-out is implemented. |
All numeric session options must be greater than zero. The current event stream
@@ -146,6 +151,7 @@ and `RecentSessionLimit` must be greater than or equal to zero.
| Option | Default | Description |
|--------|---------|-------------|
| `MxGateway:Protocol:WorkerProtocolVersion` | `1` | Worker IPC protocol version expected by the gateway and worker. This must match `GatewayContractInfo.WorkerProtocolVersion`. |
| `MxGateway:Protocol:MaxGrpcMessageBytes` | `16777216` | Public gRPC max send and receive message size in bytes. The same default is used by official clients. The validator allows values from `1024` through `268435456`. |
The protocol option is exposed for diagnostics and explicit deployment
configuration, not for compatibility negotiation. A mismatch fails validation
@@ -164,8 +170,8 @@ behavior.
## Related Documentation
- [Gateway Process Detailed Design](./gateway-process-design.md)
- [Gateway Dashboard Detailed Design](./gateway-dashboard-design.md)
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
- [Gateway Dashboard Detailed Design](./GatewayDashboardDesign.md)
- [Worker Process Launcher](./WorkerProcessLauncher.md)
- [Worker Frame Protocol](./WorkerFrameProtocol.md)
- [Galaxy Repository Browse](./GalaxyRepository.md)
@@ -165,7 +165,7 @@ counts and rates instead.
## Pages
### Dashboard Home
### Dashboard home
Show top-level status:
@@ -184,7 +184,7 @@ Show top-level status:
Use Bootstrap cards for individual metric summaries. Keep the layout compact
and operational.
### Sessions Page
### Sessions page
Show active and recent sessions in a table:
@@ -203,7 +203,7 @@ Show active and recent sessions in a table:
Rows should link to session details.
### Session Details Page
### Session details page
Show:
@@ -219,7 +219,7 @@ Show:
For v1, details should be read-only unless an explicit admin action design is
added.
### Workers Page
### Workers page
Show:
@@ -235,7 +235,7 @@ Show:
- event queue depth,
- restart/kill reason if terminal.
### Events Page
### Events page
Show aggregate event diagnostics:
@@ -249,7 +249,7 @@ Show aggregate event diagnostics:
Do not display full tag values by default. If value display is later added, make
it opt-in and redacted.
### Settings Page
### Settings page
Show read-only effective configuration:
@@ -390,3 +390,13 @@ The first dashboard slice implements:
11. periodic realtime refresh through Blazor Server.
12. route-mapping tests, disabled-dashboard tests, auth tests, and snapshot
projection/redaction tests.
## Related Documentation
- [Dashboard Interface Design](./DashboardInterfaceDesign.md)
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
- [Authentication](./Authentication.md)
- [Authorization](./Authorization.md)
- [Sessions](./Sessions.md)
- [Metrics](./Metrics.md)
- [Diagnostics](./Diagnostics.md)
@@ -314,7 +314,7 @@ Faulted
-> Closed
```
### State Rules
### State rules
- `Creating`: session id and in-memory state exist, but no worker has launched.
- `StartingWorker`: worker process launch is in progress.
@@ -432,7 +432,7 @@ Recommended size limits:
- reject zero-length payloads,
- reject payloads larger than configured maximum before allocation.
### Envelope Rules
### Envelope rules
Every message uses `WorkerEnvelope`:
@@ -489,7 +489,7 @@ and nonce, waits for `WorkerReady`, and only then exposes `Ready` state. The
read loop starts after readiness so the handshake has a single owner for its
ordered frames.
### Read Loop
### Read loop
The read loop:
@@ -505,7 +505,7 @@ The read loop:
If the pipe closes while the session is not closing, fault the session.
### Write Loop
### Write loop
The write loop serializes all writes to the pipe. No other code should write to
the pipe directly.
@@ -972,3 +972,17 @@ The first gateway slice should implement:
17. Basic structured logs.
This proves the process model before the full command surface is implemented.
## Related Documentation
- [MXAccess Worker Instance Detailed Design](./MxAccessWorkerInstanceDesign.md)
- [Worker Frame Protocol](./WorkerFrameProtocol.md)
- [Worker Process Launcher](./WorkerProcessLauncher.md)
- [Gateway Configuration](./GatewayConfiguration.md)
- [Sessions](./Sessions.md)
- [gRPC](./Grpc.md)
- [Authentication](./Authentication.md)
- [Authorization](./Authorization.md)
- [Metrics](./Metrics.md)
- [Diagnostics](./Diagnostics.md)
- [Gateway Testing](./GatewayTesting.md)
+2 -2
View File
@@ -161,6 +161,6 @@ dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj
- [Cross-Language Smoke Matrix](./CrossLanguageSmokeMatrix.md)
- [Parity Fixture Matrix](./ParityFixtureMatrix.md)
- [Gateway Process Design](./gateway-process-design.md)
- [Gateway Process Design](./GatewayProcessDesign.md)
- [Worker Frame Protocol](./WorkerFrameProtocol.md)
- [MXAccess Worker Instance Detailed Design](./mxaccess-worker-instance-design.md)
- [MXAccess Worker Instance Detailed Design](./MxAccessWorkerInstanceDesign.md)
+7 -2
View File
@@ -31,6 +31,11 @@ A second gRPC service, `GalaxyRepositoryGrpcService`, is mapped alongside it. It
`MxAccessGatewayService` derives from the generated `MxAccessGateway.MxAccessGatewayBase` and implements every RPC declared in `mxaccess_gateway.proto`. The proto contract itself is documented in [Contracts](./Contracts.md); this section covers only what the server-side handler does on top of that contract.
Public gRPC send and receive message sizes are configured from
`MxGateway:Protocol:MaxGrpcMessageBytes` (default 16 MiB). Official clients use
the same default so paged Galaxy browse replies and larger MXAccess payloads
fail consistently instead of depending on language-specific gRPC defaults.
### `OpenSession`
`OpenSession` validates the request, asks `ISessionManager` to open a session under the caller's identity, and returns a reply that advertises both protocol versions and the capabilities the gateway supports. Capability strings are static because the gateway has a fixed feature set per build; clients use them as a forward-compatibility hint rather than runtime negotiation.
@@ -211,7 +216,7 @@ if (!writer.TryWrite(publicEvent))
Under `FailFast` the session is faulted so subsequent commands return `FailedPrecondition`; the client must reopen. Under the default policy only the stream is dropped and the session continues to accept commands, leaving recovery to the client (typically a fresh `StreamEvents` call with an updated `AfterWorkerSequence`). Either way, the consumer side observes `StatusCode.ResourceExhausted` via the `EventQueueOverflow` mapping above.
### Cancellation and Cleanup
### Cancellation and cleanup
The handler creates a linked cancellation token (`streamCts`) so that completing the consumer (client disconnect, error, or graceful end-of-stream) also cancels the producer. The `finally` block cancels the source, disposes the subscriber slot, awaits the producer (swallowing the expected cancellation), and emits `StreamDisconnected("Detached")` so dashboards see the disconnection regardless of cause.
@@ -233,4 +238,4 @@ Because the interceptor runs before any handler, `MxAccessGatewayService` can sa
- [Contracts](./Contracts.md)
- [Sessions](./Sessions.md)
- [Authorization](./Authorization.md)
- [Gateway Process Design](./gateway-process-design.md)
- [Gateway Process Design](./GatewayProcessDesign.md)
@@ -5,13 +5,13 @@ first slice is stable enough to generate contracts and run smoke tests.
Primary designs:
- `docs/client-libraries-design.md`
- `docs/clients-dotnet-csharp-design.md`
- `docs/clients-golang-design.md`
- `docs/clients-rust-design.md`
- `docs/clients-python-design.md`
- `docs/clients-java-design.md`
- `docs/toolchain-links.md`
- `docs/ClientLibrariesDesign.md`
- `clients/dotnet/DotnetClientDesign.md`
- `clients/go/GoClientDesign.md`
- `clients/rust/RustClientDesign.md`
- `clients/python/PythonClientDesign.md`
- `clients/java/JavaClientDesign.md`
- `docs/ToolchainLinks.md`
## Shared Milestone: client-contracts-and-fixtures
@@ -382,6 +382,15 @@ Deliverables:
Acceptance criteria:
- new developer can build each client from a clean checkout using
`docs/toolchain-links.md`,
`docs/ToolchainLinks.md`,
- generated code command is documented for every language.
## Related Documentation
- [Implementation Plan Index](./ImplementationPlanIndex.md)
- [Client Libraries Detailed Design](./ClientLibrariesDesign.md)
- [Client Proto Generation](./ClientProtoGeneration.md)
- [Client Behavior Fixtures](./ClientBehaviorFixtures.md)
- [Client Packaging](./ClientPackaging.md)
- [Cross-Language Smoke Matrix](./CrossLanguageSmokeMatrix.md)
@@ -6,10 +6,10 @@ streaming, metrics, dashboard, tests, and operational hooks.
Primary designs:
- `docs/gateway-process-design.md`
- `docs/gateway-dashboard-design.md`
- `docs/design-decisions.md`
- `docs/toolchain-links.md`
- `docs/GatewayProcessDesign.md`
- `docs/GatewayDashboardDesign.md`
- `docs/DesignDecisions.md`
- `docs/ToolchainLinks.md`
## Milestone: gateway-foundation
@@ -509,3 +509,17 @@ Acceptance criteria:
- worker exits,
- artifacts stay in temp directories.
## Related Documentation
- [Implementation Plan Index](./ImplementationPlanIndex.md)
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
- [Gateway Configuration](./GatewayConfiguration.md)
- [Sessions](./Sessions.md)
- [gRPC](./Grpc.md)
- [Authentication](./Authentication.md)
- [Authorization](./Authorization.md)
- [Gateway Dashboard Detailed Design](./GatewayDashboardDesign.md)
- [Gateway Testing](./GatewayTesting.md)
- [Metrics](./Metrics.md)
- [Diagnostics](./Diagnostics.md)
@@ -17,9 +17,9 @@ Implementation order:
Detailed plans:
- `docs/implementation-plan-gateway.md`
- `docs/implementation-plan-mxaccess-worker.md`
- `docs/implementation-plan-clients.md`
- `docs/ImplementationPlanGateway.md`
- `docs/ImplementationPlanMxAccessWorker.md`
- `docs/ImplementationPlanClients.md`
## Gitea Milestones
@@ -91,10 +91,17 @@ Every implementation issue should meet this baseline:
## Toolchain
Use `docs/toolchain-links.md` for installed compiler/runtime paths. If a new
Use `docs/ToolchainLinks.md` for installed compiler/runtime paths. If a new
terminal cannot find a recently installed tool, refresh PATH:
```powershell
$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
```
## Related Documentation
- [Gateway Implementation Plan](./ImplementationPlanGateway.md)
- [MXAccess Worker Implementation Plan](./ImplementationPlanMxAccessWorker.md)
- [Client Libraries Implementation Plan](./ImplementationPlanClients.md)
- [Toolchain Links](./ToolchainLinks.md)
@@ -7,9 +7,9 @@ and shutdown.
Primary designs:
- `docs/mxaccess-worker-instance-design.md`
- `docs/design-decisions.md`
- `docs/toolchain-links.md`
- `docs/MxAccessWorkerInstanceDesign.md`
- `docs/DesignDecisions.md`
- `docs/ToolchainLinks.md`
- `C:\Users\dohertj2\Desktop\mxaccess\docs\MXAccess-Public-API.md`
## Milestone: mxaccess-worker-foundation
@@ -29,7 +29,7 @@ Deliverables:
- reference generated worker contracts,
- reference `ArchestrA.MXAccess.dll`,
- create `src/MxGateway.Worker.Tests`,
- document MSBuild command from `docs/toolchain-links.md`.
- document MSBuild command from `docs/ToolchainLinks.md`.
Acceptance criteria:
@@ -453,3 +453,14 @@ Acceptance criteria:
- each public method has planned parity fixture or documented gap,
- gateway results preserve HRESULT/status/value/event shape.
## Related Documentation
- [Implementation Plan Index](./ImplementationPlanIndex.md)
- [MXAccess Worker Instance Detailed Design](./MxAccessWorkerInstanceDesign.md)
- [Worker Bootstrap](./WorkerBootstrap.md)
- [Worker STA](./WorkerSta.md)
- [Worker Conversion](./WorkerConversion.md)
- [Worker Frame Protocol](./WorkerFrameProtocol.md)
- [Worker Process Launcher](./WorkerProcessLauncher.md)
- [Parity Fixture Matrix](./ParityFixtureMatrix.md)
+3 -3
View File
@@ -64,7 +64,7 @@ _eventStreamSendLatencyHistogram = _meter.CreateHistogram<double>("mxgateway.eve
| `mxgateway.commands.duration` | `method`, optional `category` | Command round-trip time. The `category` tag is added on failure so success and failure latencies stay distinguishable. |
| `mxgateway.events.stream_send.duration` | `family` | Time spent writing each public event to the gRPC response stream in `MxAccessGatewayService.StreamEvents`. |
### Observable Gauges
### Observable gauges
Observable gauges are pull-based; the `Meter` invokes the supplied callback whenever a listener samples it. Each callback re-acquires `_syncRoot` so the gauge value matches the snapshot taken at the same instant.
@@ -201,10 +201,10 @@ metrics.RecordEventStreamSend(publicEvent.Family.ToString(), stopwatch.Elapsed);
## Dashboard Consumption
`Dashboard/DashboardSnapshotService.cs` calls `_metrics.GetSnapshot()` once per `GetSnapshot` invocation and projects it into the dashboard transport types together with the session registry view. The dashboard receives a single, internally consistent snapshot per tick rather than reading individual counters at separate times. See [Gateway Dashboard Design](./gateway-dashboard-design.md) and [Dashboard Interface Design](./DashboardInterfaceDesign.md) for the projection rules and wire format.
`Dashboard/DashboardSnapshotService.cs` calls `_metrics.GetSnapshot()` once per `GetSnapshot` invocation and projects it into the dashboard transport types together with the session registry view. The dashboard receives a single, internally consistent snapshot per tick rather than reading individual counters at separate times. See [Gateway Dashboard Design](./GatewayDashboardDesign.md) and [Dashboard Interface Design](./DashboardInterfaceDesign.md) for the projection rules and wire format.
## Related Documentation
- [Gateway Dashboard Design](./gateway-dashboard-design.md)
- [Gateway Dashboard Design](./GatewayDashboardDesign.md)
- [Dashboard Interface Design](./DashboardInterfaceDesign.md)
- [Sessions](./Sessions.md)
@@ -36,7 +36,7 @@ installation:
dotnet msbuild src\MxGateway.Worker\MxGateway.Worker.csproj /restore /p:Configuration=Debug /p:Platform=x86
```
`docs/toolchain-links.md` records the Visual Studio MSBuild executable for
`docs/ToolchainLinks.md` records the Visual Studio MSBuild executable for
classic .NET Framework and COM interop builds:
```powershell
@@ -836,3 +836,13 @@ The first worker slice should implement:
This slice proves the worker can preserve the core MXAccess requirements:
single-process isolation, STA ownership, message pumping, command execution,
and event delivery.
## Related Documentation
- [Worker Bootstrap](./WorkerBootstrap.md)
- [Worker STA](./WorkerSta.md)
- [Worker Conversion](./WorkerConversion.md)
- [Worker Frame Protocol](./WorkerFrameProtocol.md)
- [Worker Process Launcher](./WorkerProcessLauncher.md)
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
- [Design Decisions](./DesignDecisions.md)
+1 -1
View File
@@ -98,5 +98,5 @@ normal unit tests only validate the repository fixture shape.
## Related Documentation
- [Gateway Testing](./GatewayTesting.md)
- [MXAccess Worker Instance Detailed Design](./mxaccess-worker-instance-design.md)
- [MXAccess Worker Instance Detailed Design](./MxAccessWorkerInstanceDesign.md)
- [Protobuf Contracts](./Contracts.md)
+3 -3
View File
@@ -178,9 +178,9 @@ The order — fault, deregister, dispose, release slot, record metric, log, reth
While `Ready`, callers reach the worker through `SessionManager.InvokeAsync` or `ReadEventsAsync`. Both delegate to `GatewaySession`, which checks the state under lock and updates `LastClientActivityAt` on every invocation. `GatewaySession` also exposes typed bulk helpers (`AddItemBulkAsync`, `SubscribeBulkAsync`, etc.) that wrap `WorkerCommand` round-trips and translate non-`Ok` `ProtocolStatus` replies into `SessionManagerException` with `SessionNotReady`.
Event streaming uses `AttachEventSubscriber` which returns a disposable lease. When `allowMultipleSubscribers` is false the second attach throws `EventSubscriberAlreadyActive`; this prevents two gRPC streams from racing on the same worker event channel.
Event streaming uses `AttachEventSubscriber` which returns a disposable lease. When `allowMultipleSubscribers` is false the second attach throws `EventSubscriberAlreadyActive`; this prevents two gRPC streams from racing on the same worker event channel. Active event subscribers keep the session lease from expiring until the stream is disposed.
`ExtendLease` and `IsLeaseExpired` cooperate with `SessionManager.CloseExpiredLeasesAsync`, which iterates a registry snapshot and closes any session whose lease has expired with `LeaseExpiredReason`.
Sessions open with `MxGateway:Sessions:DefaultLeaseSeconds` (default 1800) added to the open timestamp. Unary client activity refreshes the lease by the same duration. `ExtendLease` and `IsLeaseExpired` cooperate with `SessionManager.CloseExpiredLeasesAsync`, which iterates a registry snapshot and closes any session whose lease has expired with `LeaseExpiredReason`. `SessionLeaseMonitorHostedService` runs that sweep every `MxGateway:Sessions:LeaseSweepIntervalSeconds` seconds (default 30).
### Close
@@ -266,6 +266,6 @@ The registry must be a singleton because its `ConcurrentDictionary` is the sourc
## Related Documentation
- [Gateway Process Design](./gateway-process-design.md)
- [Gateway Process Design](./GatewayProcessDesign.md)
- [Gateway Configuration](./GatewayConfiguration.md)
- [Worker Process Launcher](./WorkerProcessLauncher.md)
@@ -170,3 +170,9 @@ These checks passed after installation:
- `javac` compile of a temporary Java class.
- Python imports for `grpc`, `grpc_tools`, and `pytest`.
## Related Documentation
- [Implementation Plan Index](./ImplementationPlanIndex.md)
- [Client Proto Generation](./ClientProtoGeneration.md)
- [Client Packaging](./ClientPackaging.md)
+1 -1
View File
@@ -257,6 +257,6 @@ The exception is distinct from `COMException` so the worker's command pipeline c
## Related Documentation
- [MXAccess Worker Instance Design](./mxaccess-worker-instance-design.md)
- [MXAccess Worker Instance Design](./MxAccessWorkerInstanceDesign.md)
- [Contracts](./Contracts.md)
- [Worker STA Threading](./WorkerSta.md)
+1 -1
View File
@@ -50,5 +50,5 @@ dotnet build src/MxGateway.Server/MxGateway.Server.csproj
## Related Documentation
- [Gateway Process Detailed Design](./gateway-process-design.md)
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
- [Protobuf Contracts](./Contracts.md)
+1 -1
View File
@@ -71,5 +71,5 @@ dotnet build src/MxGateway.Server/MxGateway.Server.csproj
## Related Documentation
- [Gateway Process Detailed Design](./gateway-process-design.md)
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
- [Worker Frame Protocol](./WorkerFrameProtocol.md)
+1 -1
View File
@@ -152,6 +152,6 @@ finally
## Related Documentation
- [MXAccess worker instance design](./mxaccess-worker-instance-design.md)
- [MXAccess worker instance design](./MxAccessWorkerInstanceDesign.md)
- [Worker conversion](./WorkerConversion.md)
- [Worker bootstrap](./WorkerBootstrap.md)
-213
View File
@@ -1,213 +0,0 @@
# .NET 10 C# Client Detailed Design
## Purpose
Provide an idiomatic .NET 10 C# client library for MXAccess Gateway, plus a test
CLI and unit tests. This client is for modern .NET callers and must not load
MXAccess COM.
Follow the [C# Style Guide](./style-guides/CSharpStyleGuide.md) for
handwritten code and the [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md)
for generated contract inputs.
## Projects
Recommended layout:
```text
clients/dotnet/
MxGateway.Client.sln
MxGateway.Client/
MxGateway.Client.csproj
GatewayClient.cs
MxGatewaySession.cs
MxGatewayClientOptions.cs
Authentication/
Conversion/
Errors/
Generated/
MxGateway.Client.Cli/
MxGateway.Client.Cli.csproj
Program.cs
Commands/
MxGateway.Client.Tests/
MxGateway.Client.Tests.csproj
MxGateway.Client.IntegrationTests/
MxGateway.Client.IntegrationTests.csproj
```
Target framework:
```xml
<TargetFramework>net10.0</TargetFramework>
```
The scaffold uses a project reference to
`src/MxGateway.Contracts/MxGateway.Contracts.csproj` for generated protobuf and
gRPC types. `clients/dotnet/generated` remains reserved for client-local
generator output if the .NET client later needs to decouple from the contracts
project.
Expected packages:
- `Grpc.Net.Client`
- `Google.Protobuf`
- `Grpc.Tools` for generation
- `Microsoft.Extensions.Logging.Abstractions`
- `System.CommandLine` or similar for CLI
- test framework: xUnit or NUnit
## Library API
Suggested public types:
```csharp
public sealed class MxGatewayClient : IAsyncDisposable
{
public static MxGatewayClient Create(MxGatewayClientOptions options);
public Task<MxGatewaySession> OpenSessionAsync(
OpenSessionOptions? options = null,
CancellationToken cancellationToken = default);
public Task<MxCommandReply> InvokeAsync(
MxCommandRequest request,
CancellationToken cancellationToken = default);
}
public sealed class MxGatewaySession : IAsyncDisposable
{
public string SessionId { get; }
public Task<int> RegisterAsync(string clientName, CancellationToken ct = default);
public Task UnregisterAsync(int serverHandle, CancellationToken ct = default);
public Task<int> AddItemAsync(int serverHandle, string item, CancellationToken ct = default);
public Task<int> AddItem2Async(int serverHandle, string item, string context, CancellationToken ct = default);
public Task AdviseAsync(int serverHandle, int itemHandle, CancellationToken ct = default);
public Task UnAdviseAsync(int serverHandle, int itemHandle, CancellationToken ct = default);
public Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(int serverHandle, IReadOnlyList<string> tagAddresses, CancellationToken ct = default);
public Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(int serverHandle, IReadOnlyList<int> itemHandles, CancellationToken ct = default);
public Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(int serverHandle, IReadOnlyList<int> itemHandles, CancellationToken ct = default);
public Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(int serverHandle, IReadOnlyList<int> itemHandles, CancellationToken ct = default);
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(int serverHandle, IReadOnlyList<string> tagAddresses, CancellationToken ct = default);
public Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(int serverHandle, IReadOnlyList<int> itemHandles, CancellationToken ct = default);
public Task WriteAsync(int serverHandle, int itemHandle, MxValue value, int userId, CancellationToken ct = default);
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken ct = default);
public Task CloseAsync(CancellationToken ct = default);
}
```
Generated protobuf types should remain available under a generated namespace.
Handwritten wrappers should not hide raw replies.
## Options
```csharp
public sealed class MxGatewayClientOptions
{
public required Uri Endpoint { get; init; }
public required string ApiKey { get; init; }
public bool UseTls { get; init; }
public string? CaCertificatePath { get; init; }
public string? ServerNameOverride { get; init; }
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(10);
public TimeSpan DefaultCallTimeout { get; init; } = TimeSpan.FromSeconds(30);
public MxGatewayClientRetryOptions Retry { get; init; } = new();
public ILoggerFactory? LoggerFactory { get; init; }
}
```
The .NET client applies a bounded Polly retry policy only to idempotent calls:
`CloseSession` and diagnostic `Invoke` commands such as `Ping`,
`GetSessionState`, and `GetWorkerInfo`. It does not retry `OpenSession`, event
streams, writes, secured writes, authentication, registration, item management,
or subscription changes because those calls can partially succeed in MXAccess.
API key may be loaded from `MXGATEWAY_API_KEY` by the CLI, not implicitly by the
library constructor unless a helper explicitly says it does that.
## Auth Interceptor
Use a gRPC call credentials/interceptor layer to attach:
```text
authorization: Bearer <api key>
```
The interceptor must redact the key in logs and exceptions.
## Streaming
Expose `StreamEventsAsync` as `IAsyncEnumerable<MxEvent>`. On cancellation,
cancel the gRPC stream and surface `OperationCanceledException` only when the
caller initiated cancellation.
Do not reorder events.
## Error Handling
Recommended exceptions:
```csharp
MxGatewayException
MxGatewayAuthenticationException
MxGatewayAuthorizationException
MxGatewaySessionException
MxGatewayWorkerException
MxGatewayCommandException
MxAccessException
```
For command replies that include MXAccess HRESULT/status, prefer returning the
reply and exposing helper methods:
```csharp
reply.EnsureProtocolSuccess();
reply.EnsureMxAccessSuccess();
```
## Test CLI
Project: `MxGateway.Client.Cli`.
Command examples:
```powershell
mxgw-dotnet version
mxgw-dotnet smoke --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt
mxgw-dotnet stream-events --session-id <id> --json
mxgw-dotnet write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123
```
The CLI should use `System.CommandLine` or a similarly testable parser. JSON
output should be deterministic and redact API keys.
## Unit Tests
Use an in-process fake gRPC service with `Grpc.AspNetCore.Server` test host or
mock the generated client behind an internal interface.
Required tests:
- auth metadata is attached,
- API key is redacted,
- options build plaintext and TLS channels correctly,
- `RegisterAsync` builds the right command payload,
- `AddItem2Async` includes context,
- `WriteAsync` converts scalar and array values,
- command reply status helpers preserve MXAccess HRESULT,
- `StreamEventsAsync` yields ordered events,
- stream cancellation disposes the call,
- CLI parsing and JSON output.
## Integration Tests
Use xUnit traits or categories. Skip unless:
```text
MXGATEWAY_INTEGRATION=1
MXGATEWAY_ENDPOINT=<endpoint>
MXGATEWAY_API_KEY=<key>
MXGATEWAY_TEST_ITEM=<item>
```
Integration smoke should open, register, add, advise, stream for bounded time,
and close.
-178
View File
@@ -1,178 +0,0 @@
# Go Client Detailed Design
## Purpose
Provide an idiomatic Go client module for MXAccess Gateway, plus a test CLI and
unit tests. The Go client should be suitable for services and command-line
automation.
Follow the [Go Style Guide](./style-guides/GoStyleGuide.md) for handwritten
code and the [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md) for
generated contract inputs.
## Module Layout
Recommended layout:
```text
clients/go/
go.mod
mxgateway/
client.go
session.go
options.go
auth.go
values.go
errors.go
internal/generated/
mxaccess_gateway.pb.go
mxaccess_gateway_grpc.pb.go
cmd/mxgw-go/
main.go
tests/
```
Generated code should come from `protoc` plus:
- `protoc-gen-go`
- `protoc-gen-go-grpc`
## Library API
Suggested API:
```go
type Client struct {
// owns grpc.ClientConn
}
type Options struct {
Endpoint string
APIKey string
Plaintext bool
CACertFile string
ServerNameOverride string
DialTimeout time.Duration
CallTimeout time.Duration
}
func Dial(ctx context.Context, opts Options) (*Client, error)
func (c *Client) OpenSession(ctx context.Context, opts OpenSessionOptions) (*Session, error)
func (c *Client) Invoke(ctx context.Context, req *pb.MxCommandRequest) (*pb.MxCommandReply, error)
func (c *Client) Close() error
```
Session:
```go
type Session struct {
ID string
}
func (s *Session) Register(ctx context.Context, clientName string) (int32, error)
func (s *Session) Unregister(ctx context.Context, serverHandle int32) error
func (s *Session) AddItem(ctx context.Context, serverHandle int32, item string) (int32, error)
func (s *Session) AddItem2(ctx context.Context, serverHandle int32, item, context string) (int32, error)
func (s *Session) Advise(ctx context.Context, serverHandle, itemHandle int32) error
func (s *Session) AddItemBulk(ctx context.Context, serverHandle int32, tagAddresses []string) ([]*pb.SubscribeResult, error)
func (s *Session) AdviseItemBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*pb.SubscribeResult, error)
func (s *Session) RemoveItemBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*pb.SubscribeResult, error)
func (s *Session) UnAdviseItemBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*pb.SubscribeResult, error)
func (s *Session) SubscribeBulk(ctx context.Context, serverHandle int32, tagAddresses []string) ([]*pb.SubscribeResult, error)
func (s *Session) UnsubscribeBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*pb.SubscribeResult, error)
func (s *Session) Write(ctx context.Context, serverHandle, itemHandle int32, value Value, userID int32) error
func (s *Session) Events(ctx context.Context) (<-chan EventResult, error)
func (s *Session) Close(ctx context.Context) error
```
## Authentication
Use a unary and stream interceptor to attach:
```text
authorization: Bearer <api key>
```
The interceptor should use `metadata.AppendToOutgoingContext` or call options.
Do not print API keys in errors.
## TLS
Support:
- `credentials/insecure` for local plaintext,
- `credentials.NewClientTLSFromFile`,
- custom `tls.Config` for advanced callers.
## Streaming
`Events(ctx)` should return a receive channel of:
```go
type EventResult struct {
Event *pb.MxEvent
Err error
}
```
The receive goroutine exits on stream end, context cancellation, or error. The
channel should be closed exactly once. Do not reorder events.
## Error Handling
Expose typed errors:
```go
type GatewayError struct { ... }
type CommandError struct { ... }
type MxAccessError struct { ... }
```
Use `errors.Is` / `errors.As` support. Preserve raw protobuf replies on command
errors.
## Test CLI
Binary: `mxgw-go`.
Recommended commands:
```text
mxgw-go version
mxgw-go smoke --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --item TestChildObject.TestInt
mxgw-go write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123
mxgw-go stream-events --session-id <id> --json
```
Recommended CLI library:
- standard `flag` for minimalism, or
- Cobra if subcommand ergonomics matter.
## Unit Tests
Use `bufconn` for in-memory gRPC tests.
Required tests:
- auth interceptor on unary calls,
- auth interceptor on streaming calls,
- plaintext and TLS dial options,
- command helper request construction,
- value conversion,
- status conversion,
- typed error wrapping,
- stream channel closes on cancellation,
- late stream error propagation,
- CLI JSON redaction.
## Integration Tests
Use Go build tags or environment skip:
```text
MXGATEWAY_INTEGRATION=1
```
Integration test should run `OpenSession`, `Register`, `AddItem`, `Advise`,
bounded `StreamEvents`, and `CloseSession`.
-212
View File
@@ -1,212 +0,0 @@
# Java Client Detailed Design
## Purpose
Provide a Java client library for MXAccess Gateway, plus a test CLI and unit
tests. The Java client should work for JVM services and operator tooling.
Follow the [Java Style Guide](./style-guides/JavaStyleGuide.md) for handwritten
code and the [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md) for
generated contract inputs.
## Build Layout
Recommended Gradle multi-project layout:
```text
clients/java/
settings.gradle
build.gradle
src/main/generated/
mxgateway-client/
build.gradle
src/main/java/com/dohertylan/mxgateway/client/
src/test/java/com/dohertylan/mxgateway/client/
mxgateway-cli/
build.gradle
src/main/java/com/dohertylan/mxgateway/cli/
```
Alternative Maven layout is acceptable if the repo standardizes on Maven.
Target Java:
- Java 21 recommended.
- The Gradle scaffold uses the Java 21 toolchain for compilation and tests.
Expected dependencies:
- `grpc-netty-shaded`
- `grpc-protobuf`
- `grpc-stub`
- `protobuf-java`
- `picocli`
- `junit-jupiter`
- `mockito` if needed
## Library API
Suggested API:
```java
public final class MxGatewayClient implements AutoCloseable {
public static MxGatewayClient connect(MxGatewayClientOptions options);
public MxGatewaySession openSession(OpenSessionOptions options);
public MxCommandReply invoke(MxCommandRequest request);
public CompletableFuture<MxCommandReply> invokeAsync(MxCommandRequest request);
public void close();
}
public final class MxGatewaySession implements AutoCloseable {
public String sessionId();
public int register(String clientName);
public void unregister(int serverHandle);
public int addItem(int serverHandle, String item);
public int addItem2(int serverHandle, String item, String context);
public void advise(int serverHandle, int itemHandle);
public List<SubscribeResult> addItemBulk(int serverHandle, List<String> tagAddresses);
public List<SubscribeResult> adviseItemBulk(int serverHandle, List<Integer> itemHandles);
public List<SubscribeResult> removeItemBulk(int serverHandle, List<Integer> itemHandles);
public List<SubscribeResult> unAdviseItemBulk(int serverHandle, List<Integer> itemHandles);
public List<SubscribeResult> subscribeBulk(int serverHandle, List<String> tagAddresses);
public List<SubscribeResult> unsubscribeBulk(int serverHandle, List<Integer> itemHandles);
public void write(int serverHandle, int itemHandle, MxValue value, int userId);
public Iterator<MxEvent> streamEvents();
public void streamEventsAsync(StreamObserver<MxEvent> observer);
public void close();
}
```
Expose generated protobuf classes for callers that need raw access.
## Options
```java
public final class MxGatewayClientOptions {
URI endpoint;
String apiKey;
boolean plaintext;
Path caCertificatePath;
String serverNameOverride;
Duration connectTimeout;
Duration callTimeout;
}
```
## Authentication
Use a gRPC `ClientInterceptor` to attach:
```text
authorization: Bearer <api key>
```
Redact API keys in `toString`, logs, and CLI output.
## TLS
Support:
- plaintext for local development,
- TLS with default JVM trust store,
- custom CA certificate file,
- server name override for test environments.
## Streaming
Support both:
- blocking iterator for simple CLIs,
- async `StreamObserver` for services.
Do not reorder events. Stream cancellation should call `ClientCall.cancel`.
## Error Handling
Recommended exceptions:
```java
MxGatewayException
MxGatewayAuthenticationException
MxGatewayAuthorizationException
MxGatewaySessionException
MxGatewayWorkerException
MxGatewayCommandException
MxAccessException
```
`MxGatewayCommandException` should carry the raw command reply when available.
## Test CLI
Binary wrapper name:
```text
mxgw-java
```
Use `picocli`.
Commands:
```text
mxgw-java version
mxgw-java smoke --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --item TestChildObject.TestInt
mxgw-java stream-events --session-id <id> --json
mxgw-java write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123
```
JSON output can use Jackson or protobuf JSON formatting. Keep it deterministic.
## Unit Tests
Use JUnit 5.
Use `InProcessServerBuilder` and `InProcessChannelBuilder` for fake gRPC tests.
Required tests:
- auth interceptor attaches metadata,
- key redaction,
- plaintext and TLS channel setup,
- request construction helpers,
- value conversion,
- status/error mapping,
- blocking event stream iteration,
- async stream observer cancellation,
- CLI parsing,
- JSON output.
## Integration Tests
Skip unless:
```text
MXGATEWAY_INTEGRATION=1
```
Use JUnit assumptions. Integration flow should open, register, add, advise,
stream for bounded time, and close.
## Packaging
Publish library and CLI separately:
- `mxgateway-client` jar,
- `mxgateway-cli` runnable distribution.
Generated protobuf code should be produced during the build from shared proto
files and should not be hand-edited.
## Current Build
Run the Java scaffold checks from `clients/java`:
```powershell
gradle test
```
The `mxgateway-client` project generates the gateway and worker protobuf/gRPC
bindings into `src/main/generated`, compiles the generated contracts, and runs
JUnit 5 tests. The `mxgateway-cli` project builds a Picocli-based `mxgw-java`
entry point for later command implementation.
-197
View File
@@ -1,197 +0,0 @@
# Python Client Detailed Design
## Purpose
Provide an async Python client package for MXAccess Gateway, plus a test CLI and
unit tests. The Python client should be useful for automation, diagnostics, and
test harnesses.
Follow the [Python Style Guide](./style-guides/PythonStyleGuide.md) for
handwritten code and the [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md)
for generated contract inputs.
## Package Layout
Recommended layout:
```text
clients/python/
pyproject.toml
src/mxgateway/
__init__.py
client.py
session.py
options.py
auth.py
values.py
errors.py
generated/
src/mxgateway_cli/
__main__.py
commands.py
tests/
```
Expected dependencies:
- `grpcio`
- `grpcio-tools`
- `protobuf`
- `click` or `typer`
- `pytest`
- `pytest-asyncio`
## Library API
Use async-first API. A sync wrapper can be added later if needed.
Suggested API:
```python
client = await GatewayClient.connect(
endpoint="localhost:5000",
api_key=api_key,
plaintext=True,
)
session = await client.open_session()
server = await session.register("python-client")
item = await session.add_item(server, "TestChildObject.TestInt")
await session.advise(server, item)
async for event in session.stream_events():
...
await session.close()
await client.close()
```
Classes:
```python
class GatewayClient:
@classmethod
async def connect(cls, options: ClientOptions) -> "GatewayClient": ...
async def open_session(self, options: OpenSessionOptions | None = None) -> "Session": ...
async def invoke(self, request: MxCommandRequest) -> MxCommandReply: ...
async def close(self) -> None: ...
class Session:
session_id: str
async def register(self, client_name: str) -> int: ...
async def add_item(self, server_handle: int, item: str) -> int: ...
async def add_item2(self, server_handle: int, item: str, context: str) -> int: ...
async def advise(self, server_handle: int, item_handle: int) -> None: ...
async def add_item_bulk(self, server_handle: int, tag_addresses: Sequence[str]) -> list[SubscribeResult]: ...
async def advise_item_bulk(self, server_handle: int, item_handles: Sequence[int]) -> list[SubscribeResult]: ...
async def remove_item_bulk(self, server_handle: int, item_handles: Sequence[int]) -> list[SubscribeResult]: ...
async def unadvise_item_bulk(self, server_handle: int, item_handles: Sequence[int]) -> list[SubscribeResult]: ...
async def subscribe_bulk(self, server_handle: int, tag_addresses: Sequence[str]) -> list[SubscribeResult]: ...
async def unsubscribe_bulk(self, server_handle: int, item_handles: Sequence[int]) -> list[SubscribeResult]: ...
async def write(self, server_handle: int, item_handle: int, value: MxValueInput, user_id: int = 0) -> None: ...
async def stream_events(self) -> AsyncIterator[MxEvent]: ...
async def close(self) -> None: ...
```
## Authentication
Use gRPC metadata:
```python
metadata = (("authorization", f"Bearer {api_key}"),)
```
Provide a metadata helper that all unary and streaming calls use. Redact API
keys in exceptions and CLI output.
## TLS
Support:
- insecure channel for local development,
- TLS channel with default roots,
- custom root certificate file.
## Streaming
Expose `stream_events` as an async iterator. Canceling the task should cancel
the gRPC stream.
Do not hide stream errors. Convert common auth/session errors into typed
exceptions.
## Error Handling
Define typed exceptions:
```python
MxGatewayError
MxGatewayTransportError
MxGatewayAuthenticationError
MxGatewayAuthorizationError
MxGatewaySessionError
MxGatewayWorkerError
MxGatewayCommandError
MxAccessError
```
`MxGatewayCommandError` should include the raw protobuf reply when available.
## Test CLI
Entry point:
```text
mxgw-py
```
Recommended commands:
```text
mxgw-py version
mxgw-py smoke --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --item TestChildObject.TestInt
mxgw-py stream-events --session-id <id> --json
mxgw-py write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123
```
Use `click` or `typer`. JSON output should be stable for test automation.
## Unit Tests
Use `pytest` and `pytest-asyncio`.
Use fake generated stubs or an in-process test gRPC server where practical.
Required tests:
- API key metadata injection,
- API key redaction,
- insecure and TLS channel option construction,
- request construction for method helpers,
- value conversion from Python values,
- status/error mapping,
- async event iteration,
- stream cancellation,
- CLI parsing,
- JSON output.
## Integration Tests
Skip unless:
```text
MXGATEWAY_INTEGRATION=1
```
Use bounded smoke flow and always attempt `close_session` in `finally`.
## Packaging
Use `pyproject.toml`. Publishable package name should be stable, for example:
```text
mxaccess-gateway-client
```
Generated protobuf code should be regenerated through a documented command, not
edited by hand.
-189
View File
@@ -1,189 +0,0 @@
# Rust Client Detailed Design
## Purpose
Provide an async Rust client crate for MXAccess Gateway, plus a test CLI and
unit tests. The Rust client should use `tonic` and `tokio`.
Follow the [Rust Style Guide](./style-guides/RustStyleGuide.md) for handwritten
code and the [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md) for
generated contract inputs.
## Crate Layout
Recommended layout:
```text
clients/rust/
Cargo.toml
build.rs
crates/
mxgateway-client/
src/lib.rs
src/client.rs
src/session.rs
src/options.rs
src/auth.rs
src/value.rs
src/error.rs
src/generated/
mxgw-cli/
src/main.rs
tests/
```
Expected dependencies:
- `tonic`
- `prost`
- `prost-types`
- `tokio`
- `tokio-stream`
- `thiserror`
- `clap`
- `serde`
- `serde_json`
- `tracing`
## Library API
Suggested API:
```rust
pub struct GatewayClient { /* tonic channel + generated client */ }
pub struct ClientOptions {
pub endpoint: String,
pub api_key: String,
pub plaintext: bool,
pub ca_file: Option<PathBuf>,
pub server_name_override: Option<String>,
pub connect_timeout: Duration,
pub call_timeout: Duration,
}
impl GatewayClient {
pub async fn connect(options: ClientOptions) -> Result<Self, Error>;
pub async fn open_session(&self, options: OpenSessionOptions) -> Result<Session, Error>;
pub async fn invoke(&self, request: MxCommandRequest) -> Result<MxCommandReply, Error>;
}
```
Session:
```rust
pub struct Session {
pub id: String,
}
impl Session {
pub async fn register(&self, client_name: &str) -> Result<i32, Error>;
pub async fn add_item(&self, server_handle: i32, item: &str) -> Result<i32, Error>;
pub async fn add_item2(&self, server_handle: i32, item: &str, context: &str) -> Result<i32, Error>;
pub async fn advise(&self, server_handle: i32, item_handle: i32) -> Result<(), Error>;
pub async fn add_item_bulk(&self, server_handle: i32, tag_addresses: Vec<String>) -> Result<Vec<SubscribeResult>, Error>;
pub async fn advise_item_bulk(&self, server_handle: i32, item_handles: Vec<i32>) -> Result<Vec<SubscribeResult>, Error>;
pub async fn remove_item_bulk(&self, server_handle: i32, item_handles: Vec<i32>) -> Result<Vec<SubscribeResult>, Error>;
pub async fn un_advise_item_bulk(&self, server_handle: i32, item_handles: Vec<i32>) -> Result<Vec<SubscribeResult>, Error>;
pub async fn subscribe_bulk(&self, server_handle: i32, tag_addresses: Vec<String>) -> Result<Vec<SubscribeResult>, Error>;
pub async fn unsubscribe_bulk(&self, server_handle: i32, item_handles: Vec<i32>) -> Result<Vec<SubscribeResult>, Error>;
pub async fn write(&self, server_handle: i32, item_handle: i32, value: MxValue, user_id: i32) -> Result<(), Error>;
pub async fn events(&self) -> Result<impl Stream<Item = Result<MxEvent, Error>>, Error>;
pub async fn close(&self) -> Result<(), Error>;
}
```
## Authentication
Use a `tonic` interceptor or request extension layer to add:
```text
authorization: Bearer <api key>
```
Use `SecretString` or equivalent if a dependency is acceptable. Always redact
API keys in `Debug` output.
## TLS
Support:
- plaintext channel for local development,
- native or rustls TLS depending on project preference,
- custom CA file,
- domain override.
## Streaming
Expose event streams as a `Stream<Item = Result<MxEvent, Error>>`. Dropping the
stream should cancel the underlying gRPC stream.
Do not buffer unboundedly in the client. If a helper channel is used, make it
bounded.
## Error Handling
Use `thiserror`:
```rust
pub enum Error {
Transport(tonic::transport::Error),
Status(tonic::Status),
Authentication(String),
Authorization(String),
Session(SessionError),
Worker(WorkerError),
Command(CommandError),
MxAccess(MxAccessError),
Timeout,
Cancelled,
}
```
Preserve raw command replies in `CommandError` where applicable.
## Test CLI
Binary: `mxgw`.
Use `clap` derive.
Commands:
```text
mxgw version
mxgw smoke --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt
mxgw stream-events --session-id <id> --json
mxgw write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123
```
JSON output should use `serde_json`.
## Unit Tests
Use a fake `tonic` server started on a local ephemeral port, or abstract the
generated client behind a trait for unit tests.
Required tests:
- generated client compiles from proto,
- auth metadata injection,
- TLS/plaintext endpoint construction,
- value conversion,
- command request construction,
- error mapping from `tonic::Status`,
- event stream order,
- stream cancellation,
- CLI parsing,
- JSON redaction.
## Integration Tests
Skip unless:
```text
MXGATEWAY_INTEGRATION=1
```
Use `tokio::test`. Run bounded smoke flow and ensure `CloseSession` is attempted
with `drop` fallback docs, but do not rely on `Drop` for async close.