fix(gateway): resolve 2026-06-18 array-write review findings

- Server-057: extend []-suffix normalization to AddItemBulk/AddBufferedItem so bulk-added
  array tags bind write-capable handles (authz check, worker bind, and registration kept
  consistent); update gateway.md + client READMEs. Tests: AddItemBulk/AddBufferedItem wiring.
- Server-058: assert []-fallback-resolved bare array names are still denied when out of
  read/write scope and that MaxWriteClassification is enforced on suffixed array registrations.
- Contracts-023/024/025: round-trip + field-19 descriptor pin for MxSparseArray; document
  MxSparseArray in docs/Contracts.md; enumerate it in the protocol-version-3 test summary.
- Tests-040: add wiring tests for the six uncovered sparse-write arms (WriteSecured, Write2,
  WriteSecured2, Write2Bulk, WriteSecuredBulk, WriteSecured2Bulk).

dotnet build + targeted tests green (184 passed).
This commit is contained in:
Joseph Doherty
2026-06-18 10:58:42 -04:00
parent 6c853b43af
commit 2671639250
10 changed files with 718 additions and 30 deletions
@@ -987,6 +987,19 @@ public sealed class GatewaySession
break;
case MxCommand.PayloadOneofCase.AddItem2:
command.AddItem2.ItemDefinition = NormalizeAddress(command.AddItem2.ItemDefinition);
break;
case MxCommand.PayloadOneofCase.AddBufferedItem:
command.AddBufferedItem.ItemDefinition = NormalizeAddress(command.AddBufferedItem.ItemDefinition);
break;
case MxCommand.PayloadOneofCase.AddItemBulk:
// Normalize each bare array address in place so the worker binds a write-capable handle
// for every array tag in the batch (the same IsArray-gated rewrite the single-add path
// applies). Scalar addresses pass through unchanged.
for (int i = 0; i < command.AddItemBulk.TagAddresses.Count; i++)
{
command.AddItemBulk.TagAddresses[i] = NormalizeAddress(command.AddItemBulk.TagAddresses[i]);
}
break;
case MxCommand.PayloadOneofCase.Write:
ExpandValue(command.Write.Value);
@@ -1089,9 +1102,16 @@ public sealed class GatewaySession
TrackItem(command.AddItem2.ServerHandle, reply.AddItem2.ItemHandle, NormalizeAddress(command.AddItem2.ItemDefinition));
break;
case MxCommandKind.AddBufferedItem when reply.AddBufferedItem is not null:
TrackItem(command.AddBufferedItem.ServerHandle, reply.AddBufferedItem.ItemHandle, command.AddBufferedItem.ItemDefinition);
// The reply carries no address, so tracking keys off the command's ItemDefinition;
// re-apply the array-suffix normalization (the tracking copy is a separate, un-mutated
// instance from the one forwarded at the InvokeAsync choke point) so the registration
// matches the write-capable handle the worker bound.
TrackItem(command.AddBufferedItem.ServerHandle, reply.AddBufferedItem.ItemHandle, NormalizeAddress(command.AddBufferedItem.ItemDefinition));
break;
case MxCommandKind.AddItemBulk when reply.AddItemBulk is not null:
// The worker echoes back the (already-normalized) address it bound in each
// SubscribeResult.TagAddress, so TrackBulkItems stores the suffixed array address
// without re-normalizing here.
TrackBulkItems(reply.AddItemBulk);
break;
case MxCommandKind.SubscribeBulk when reply.SubscribeBulk is not null: