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:
+21
-8
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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)
|
||||
+15
-4
@@ -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
@@ -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)
|
||||
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user