Merge origin/main with local pending work and update AGENTS.md references

- Resolve 14 conflicts from popping local stash on top of origin's
  eed1e88 + 8d3352f doc-comment additions (11 mechanical, plus
  version.rs, DashboardAuthenticatorTests.cs, DashboardGalaxyProjector.cs)
- Fix 4 test files that used AGENTS.md as the repo-root sentinel
  (now use CLAUDE.md, since AGENTS.md was removed in 4731ab5)
- Redirect 10 doc citations from AGENTS.md to the matching gateway.md
  sections (Value Model, Status Model, Security, STA Worker Thread
  Model, gRPC Layer rule, cancellation rule)

Verified: solution build clean, x86 worker build clean, 266/266
gateway tests passing, 121/121 worker tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-30 14:13:33 -04:00
parent 8d3352f2c6
commit ddad573b75
101 changed files with 6053 additions and 621 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ what an authenticated API key can browse, read, or write inside the Galaxy.
## Overview
Authorization runs as a single gRPC server interceptor registered for every call on the gateway. It pulls the authenticated identity for the current request, derives the scope that the request type requires, and either lets the call continue or fails the call with a gRPC status. The pipeline keeps service classes free of cross-cutting checks, which matches the AGENTS.md "thin gRPC layer" rule that service handlers translate between contracts and domain code without owning policy.
Authorization runs as a single gRPC server interceptor registered for every call on the gateway. It pulls the authenticated identity for the current request, derives the scope that the request type requires, and either lets the call continue or fails the call with a gRPC status. The pipeline keeps service classes free of cross-cutting checks, which matches the `gateway.md` "thin gRPC layer" rule that service handlers translate between contracts and domain code without owning policy.
The participating types live under `src/MxGateway.Server/Security/Authorization/`:
+2 -2
View File
@@ -4,7 +4,7 @@ The diagnostics subsystem provides structured logging, credential redaction, and
## Goals
The subsystem exists to satisfy two security rules from `AGENTS.md`: never log passwords or raw credential values for `AuthenticateUser`, `WriteSecured`, or related secured operations, and never log full MXAccess values by default. Code paths that touch credentials or tag values must therefore route through `GatewayLogRedactor` rather than emitting them directly.
The subsystem exists to satisfy two security rules from `gateway.md`: never log passwords or raw credential values for `AuthenticateUser`, `WriteSecured`, or related secured operations, and never log full MXAccess values by default. Code paths that touch credentials or tag values must therefore route through `GatewayLogRedactor` rather than emitting them directly.
A second goal is parity-test diagnosability. Because MXAccess sessions, workers, correlation ids, and command methods are the units of comparison, every log entry produced inside a request scope must carry those identifiers without each call site having to format them.
@@ -80,7 +80,7 @@ public static bool IsCredentialBearingCommand(string? commandMethod)
}
```
The names match the MXAccess command list in `AGENTS.md` exactly. `Write` and `Write2` are not in the set because their payloads are tag values, not credentials, and are governed by the `valueLoggingEnabled` flag described below.
The names match the MXAccess command list in `gateway.md` exactly. `Write` and `Write2` are not in the set because their payloads are tag values, not credentials, and are governed by the `valueLoggingEnabled` flag described below.
### API key redaction
+1 -1
View File
@@ -4,7 +4,7 @@ The gRPC service layer is the public entry point for client traffic. It is inten
## Layer Responsibilities
The architecture rule (from `AGENTS.md`) is that the gRPC layer must "validate request, find session, call the session worker client, map worker replies to public replies, and stream events". Anything else — caching, retries, worker process lifetime, event ordering — lives behind `ISessionManager` and the worker client. Keeping the layer thin lets the same session/worker code be reused by future transports (for example, an in-process host or an alternate IPC) without having to re-derive validation or mapping rules.
The architecture rule (from `gateway.md`) is that the gRPC layer must "validate request, find session, call the session worker client, map worker replies to public replies, and stream events". Anything else — caching, retries, worker process lifetime, event ordering — lives behind `ISessionManager` and the worker client. Keeping the layer thin lets the same session/worker code be reused by future transports (for example, an in-process host or an alternate IPC) without having to re-derive validation or mapping rules.
The layer is composed of four collaborators:
+1 -1
View File
@@ -177,7 +177,7 @@ private void Write(
### What the redactor redacts and why
`AGENTS.md` "Security And Logging" requires that the worker never log raw credential values for `AuthenticateUser`, `WriteSecured`, or related secured operations. The bootstrap nonce is also a credential: anyone who reads it can impersonate the worker to the gateway pipe. `WorkerLogRedactor` enforces this by replacing values whose field name contains any of these substrings (case-insensitive) with the literal `[redacted]`:
`gateway.md` "Security" requires that the worker never log raw credential values for `AuthenticateUser`, `WriteSecured`, or related secured operations. The bootstrap nonce is also a credential: anyone who reads it can impersonate the worker to the gateway pipe. `WorkerLogRedactor` enforces this by replacing values whose field name contains any of these substrings (case-insensitive) with the literal `[redacted]`:
```csharp
private static readonly string[] SensitiveFieldNameParts =
+3 -3
View File
@@ -4,7 +4,7 @@ The conversion layer in `MxGateway.Worker.Conversion` projects COM `VARIANT` pay
## Overview
`AGENTS.md` (section "Value And Status Rules") requires that the wire format use a value union capable of representing COM `VARIANT` values and arrays, that lossy conversions retain both the typed projection and raw diagnostic metadata, and that `MXSTATUS_PROXY` arrays never collapse to a single success flag. The types in `src/MxGateway.Worker/Conversion/` are the worker-side enforcement of those rules.
`gateway.md` (sections "Value Model" and "Status Model") requires that the wire format use a value union capable of representing COM `VARIANT` values and arrays, that lossy conversions retain both the typed projection and raw diagnostic metadata, and that `MXSTATUS_PROXY` arrays never collapse to a single success flag. The types in `src/MxGateway.Worker/Conversion/` are the worker-side enforcement of those rules.
The layer is split into three concerns:
@@ -90,7 +90,7 @@ MxDataType elementDataType = ResolveArrayElementDataType(elementType, expectedEl
mxArray.ElementDataType = elementDataType;
```
When the element type cannot be classified, `ConvertArray` does not throw. It downgrades the result to `MxDataType.Unknown`, records the original expected type in `RawElementDataType`, and serializes each element via `ConvertRawArray` as a UTF-8 byte string. This satisfies the AGENTS.md requirement to keep both the best typed projection and the raw diagnostic metadata.
When the element type cannot be classified, `ConvertArray` does not throw. It downgrades the result to `MxDataType.Unknown`, records the original expected type in `RawElementDataType`, and serializes each element via `ConvertRawArray` as a UTF-8 byte string. This satisfies the `gateway.md` requirement to keep both the best typed projection and the raw diagnostic metadata.
```csharp
default:
@@ -201,7 +201,7 @@ foreach (object? status in statuses)
### Why arrays are not collapsed
A single MXAccess command (notably `Read`, `Write`, and event callbacks) can return one status per item handle. AGENTS.md requires that the wire format represent each entry independently, because collapsing them to a Boolean success flag hides partial failures: a 50-item write where one item fails would be indistinguishable from a 50-item write where every item failed. Preserving the array per-position lets clients correlate each `MxStatusProxy` with its item handle and `MxValue`.
A single MXAccess command (notably `Read`, `Write`, and event callbacks) can return one status per item handle. `gateway.md` requires that the wire format represent each entry independently, because collapsing them to a Boolean success flag hides partial failures: a 50-item write where one item fails would be indistinguishable from a 50-item write where every item failed. Preserving the array per-position lets clients correlate each `MxStatusProxy` with its item handle and `MxValue`.
### Completion-only status fallback
+3 -3
View File
@@ -4,7 +4,7 @@ The worker STA runtime owns the dedicated single-threaded apartment thread that
## Why an STA Is Required
The installed MXAccess interop assembly declares an `Apartment` threading model (see `AGENTS.md` under "Worker Rules"). COM objects with that model must be created and called on a thread initialized as a single-threaded apartment, and any callbacks the object raises (event sink calls) are delivered through the thread's Windows message queue. A plain blocking queue is not sufficient: the STA loop must pump Windows messages so that the COM marshaler can deliver event invocations on the same thread that holds the object. Because of that constraint, every MXAccess operation in the worker is funneled through the types in `src/MxGateway.Worker/Sta/`.
The installed MXAccess interop assembly declares an `Apartment` threading model (see `gateway.md` under "STA Worker Thread Model"). COM objects with that model must be created and called on a thread initialized as a single-threaded apartment, and any callbacks the object raises (event sink calls) are delivered through the thread's Windows message queue. A plain blocking queue is not sufficient: the STA loop must pump Windows messages so that the COM marshaler can deliver event invocations on the same thread that holds the object. Because of that constraint, every MXAccess operation in the worker is funneled through the types in `src/MxGateway.Worker/Sta/`.
## Types
@@ -131,11 +131,11 @@ finally
`SetCurrentCommand` records the in-flight `CorrelationId` so `PopulateHeartbeat` can publish both `PendingCommandCount` and `CurrentCommandCorrelationId` on the worker heartbeat. Exceptions are converted through `HResultConverter` so the IPC reply still carries a structured `ProtocolStatus`, an HRESULT, and a diagnostic message instead of an unhandled fault. `NormalizeReply` backfills `SessionId`, `CorrelationId`, `Kind`, and a default `ProtocolStatusCode.Ok` so executors can return minimal replies without restating the envelope.
`CancelQueuedCommand` walks the queue and completes a single matching entry with `ProtocolStatusCode.Canceled`. It cannot abort a command already running on the STA: per `AGENTS.md`, "Canceling a gRPC call should stop waiting in the gateway, but it cannot safely abort an in-flight COM call on the STA. Hard cancellation means killing the worker process."
`CancelQueuedCommand` walks the queue and completes a single matching entry with `ProtocolStatusCode.Canceled`. It cannot abort a command already running on the STA: per `gateway.md`, "Canceling a gRPC call should stop waiting in the gateway, but it cannot safely abort an in-flight COM call on the STA. Hard cancellation means killing the worker process."
## Why the STA Loop Cannot Block on I/O
`AGENTS.md` states explicitly: "Do not block the STA on pipe writes, gRPC calls, or slow consumers. Event handlers should convert event args, enqueue outbound events, and return to pumping messages." The STA thread is the only thread that can service COM event callbacks, so any work that blocks it stalls every event the MXAccess object would otherwise deliver. The runtime keeps to that rule by giving the STA only two responsibilities inside `ThreadMain`: executing already-queued work items and pumping messages. Outbound event delivery and pipe writes happen on threads that observe the queues populated from the STA, never on the STA itself.
`gateway.md` states explicitly: "Do not block the STA on pipe writes, gRPC calls, or slow consumers. Event handlers should convert event args, enqueue outbound events, and return to pumping messages." The STA thread is the only thread that can service COM event callbacks, so any work that blocks it stalls every event the MXAccess object would otherwise deliver. The runtime keeps to that rule by giving the STA only two responsibilities inside `ThreadMain`: executing already-queued work items and pumping messages. Outbound event delivery and pipe writes happen on threads that observe the queues populated from the STA, never on the STA itself.
## Shutdown Sequence