fix(archreview): gateway core P0 remediation (GWC-01/02/03, TST-02, TST-12)

Interlocking changes across the gateway server (shared GatewaySession.cs /
SessionManager.cs), committed together:

- GWC-01 (Critical): alarm monitor now attaches as an internal
  (non-counted) distributor subscriber instead of a second raw drain of the
  single worker event channel; WorkerClient._events -> SingleReader with a
  claimed-once guard so a future dual-consumer regression throws loudly.
- GWC-02 (High): faulted sessions are swept in CloseExpiredLeasesAsync
  (IsFaultedReapable + FaultedReason); new FaultedGraceSeconds (default 0).
- GWC-03 (High): configurable MaxSparseArrayLength (default 1_000_000)
  enforced before allocation.
- TST-02 (High, security): StreamEvents attach now enforces the opening key
  id -> PermissionDenied on owner mismatch.
- TST-12 (Medium): CLAUDE.md retention-defaults sentence corrected.

Verified: NonWindows build clean; targeted tests 135/135 on macOS, plus
WorkerClientTests 18/18 on the Windows host.
This commit is contained in:
Joseph Doherty
2026-07-09 05:51:57 -04:00
parent 31eec41456
commit 20392cf246
30 changed files with 632 additions and 48 deletions
@@ -27,4 +27,14 @@ public sealed class EventOptions
/// bounds the buffer).
/// </summary>
public double ReplayRetentionSeconds { get; init; } = 300;
/// <summary>
/// Gets the maximum <c>total_length</c> a sparse-array write may declare before the
/// gateway rejects it with <c>InvalidArgument</c>, enforced in
/// <see cref="Sessions.SparseArrayExpander"/> before the full array is materialized.
/// Guards against a single write forcing a multi-GB allocation. The default of
/// 1,000,000 elements is far above any realistic MXAccess array write yet well below
/// the frame-size ceiling.
/// </summary>
public int MaxSparseArrayLength { get; init; } = 1_000_000;
}
@@ -181,6 +181,10 @@ public sealed class GatewayOptionsValidator : OptionsValidatorBase<GatewayOption
options.DetachGraceSeconds,
"MxGateway:Sessions:DetachGraceSeconds must be zero or greater (0 disables detach-grace retention).",
builder);
AddIfNegative(
options.FaultedGraceSeconds,
"MxGateway:Sessions:FaultedGraceSeconds must be zero or greater (0 reaps a faulted session on the next sweep).",
builder);
AddIfNegative(
options.WorkerReadyWaitTimeoutMs,
"MxGateway:Sessions:WorkerReadyWaitTimeoutMs must be greater than or equal to zero.",
@@ -214,6 +218,10 @@ public sealed class GatewayOptionsValidator : OptionsValidatorBase<GatewayOption
builder.RequireThat(
options.ReplayRetentionSeconds >= 0,
"MxGateway:Events:ReplayRetentionSeconds must be greater than or equal to zero.");
builder.RequireThat(
options.MaxSparseArrayLength >= 1 && options.MaxSparseArrayLength <= Array.MaxLength,
$"MxGateway:Events:MaxSparseArrayLength must be between 1 and {Array.MaxLength}.");
}
private static void ValidateDashboard(DashboardOptions options, ValidationBuilder builder)
@@ -45,6 +45,18 @@ public sealed class SessionOptions
/// </remarks>
public int DetachGraceSeconds { get; init; } = 30;
/// <summary>
/// Gets the grace period, in seconds, that a faulted session is retained before the
/// lease monitor reaps it (killing its worker and freeing the session slot). A faulted
/// session is otherwise permanently unusable — every command fails the readiness check —
/// yet without this sweep it pins a session slot and a live x86 worker until its normal
/// lease expires (up to <see cref="DefaultLeaseSeconds"/>). A value of <c>0</c> (the
/// default) reaps a faulted session on the next sweep cycle, bounding the blast radius; a
/// positive value keeps the faulted session observable via <c>GetSessionStatus</c> for the
/// grace window before it is reclaimed. Must be greater than or equal to zero.
/// </summary>
public int FaultedGraceSeconds { get; init; }
/// <summary>
/// Gets a value indicating whether multiple event subscribers are allowed per session.
/// </summary>