Commit Graph

906 Commits

Author SHA1 Message Date
Joseph Doherty 8257a0333e docs(SEC-40): correct the hub-mapping comment now that AlarmsHub uses its own policy 2026-08-20 09:30:45 -04:00
Joseph Doherty 2ef7b93761 fix(SEC-41, SEC-40): re-resolve dashboard tag grants; gate the alarm feed on a real role
Committed together because the two changes share the same doc paragraphs (the ACL
section of GatewayDashboardDesign.md, the GroupToTag / AllowAnonymousLocalhost rows
of GatewayConfiguration.md, and CLAUDE.md's authentication paragraph), and splitting
them would leave one of the two describing behaviour that does not yet exist.

SEC-41 — tag grants no longer trusted from login-stamped claims
---------------------------------------------------------------
DashboardSessionAcl read the zb:dashboardtag claims stamped into the cookie at
login. GatewayOptions binds once at startup, so applying a GroupToTag edit needs a
restart — but the restart does not clear the auth cookie (data-protection keys
persist), so a browser returning afterwards re-authenticated with tag claims
resolved under the OLD map. A revoked grant therefore survived on the cookie seam
for the sliding 8-hour idle life, renewable indefinitely by activity, while the
hub-token seam had no such gap because its mint re-resolves.

CanViewSession now re-resolves the grant on every subscribe from the caller's
mxgateway:ldap_group claims through DashboardGroupTagMapping.MapGroupsToTags
against the currently bound map — the same one call HubTokenService.Issue makes.

Fallback (documented on the class and in the design doc): a principal carrying NO
group claims falls back to its stamped tag claims. That arm exists for the
hub-token principal — HubTokenService.Validate rehydrates roles and tags but not
groups, so re-resolution already happened at mint, one lifetime ago at worst. It
cannot re-open the cookie hole: DashboardAuthenticator stamps group and tag claims
from one group list, so a cookie principal holding a tag claim always holds the
groups it came from, and a login yielding no groups yields no tags. A principal
with neither kind of claim stays an empty-grant Viewer.

SEC-40 — the alarm feed requires a real authenticated dashboard role
--------------------------------------------------------------------
AlarmsHub sat behind HubClientsPolicy, which the read-only environment bypasses
satisfy, so an anonymous-localhost caller or a Mode=Disabled deployment received
galaxy-wide alarm metadata — alarm reference (a full tag path), state, severity,
category, area, acknowledging operator — for the whole subscription expression.

Decided: tighten the gate, keep the scope. New AuthenticatedDashboardRoleRequirement
+ its handler, and a MxGateway.Dashboard.Alarms policy built from it. Because
DashboardAuthorizationHandler is typed to DashboardAuthorizationRequirement, it is
never invoked for the new type, so neither Authentication:Mode=Disabled nor
anonymous localhost can satisfy the policy — the same structural device the mounted
Secrets UI already relies on. The policy keeps both schemes so external hub clients
can still authenticate with a bearer token, and DisableLogin auto-login is
unaffected (it mints a genuinely authenticated both-roles principal).

The /alarms page carries the same policy, not just the hub: it reads the same
central monitor in process, so gating only the hub would have left the disclosure
one navigation away. The side rail hides the Alarms link for callers the policy
denies, matching the Secrets link precedent. AlarmsHubPublisher is unchanged and
stays unfiltered by design — audience is decided at hub admission, and the monitor
carries no per-session provenance to narrow by.

Alarm metadata deliberately remains galaxy-wide for authenticated Viewers: an
AlarmFeedMessage has no session id or owning key for the SEC-25 tag intersection to
work against, and equating a visibility tag with an alarm-reference prefix would
turn GroupToTag into the data-access constraint it explicitly is not.

Tests
-----
DashboardSessionAclTests: the SEC-41 regression — a cookie-style principal whose
stale tag claim is no longer granted by any of its groups under the current map is
denied; plus the allowing direction (including with no stamped tag at all, which
proves the decision comes from the groups), the ignore-case corner, group claims
that map to nothing, and the hub-token fallback asserted against an EMPTY map so
dropping the fallback would fail loudly.

AlarmFeedAuthorizationTests (new): both handlers run together in the most permissive
environment (Mode=Disabled + loopback + AllowAnonymousLocalhost) do not satisfy the
alarm requirement, with the bypassable requirement as the control so the negative
cannot pass because the bypass was misconfigured; authenticated Viewer/Admin do
satisfy it; an unauthenticated role claim does not; the registered policy carries
the non-bypassable requirement and both schemes; and both readers — the hub type's
[Authorize] and the /alarms endpoint metadata — carry it.

SecretsNavRenderTests: registers the new policy (an unregistered policy name throws
rather than denying in an AuthorizeView) and gains the Alarms-link negative/control
pair.

Verified: dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx (zero warnings);
filtered run of the touched classes — 92 passed, 0 failed.
2026-08-20 09:29:44 -04:00
Joseph Doherty b177636d7a fix(GWC-38): deny Galaxy browse when the ambient identity is absent under API-key auth
GatewayBrowseScopeProvider resolved `identityAccessor.Current?.EffectiveConstraints
?? ApiKeyConstraints.Empty`, so an absent ambient identity produced an empty
BrowseSubtrees list — which the shared GalaxyRepository library reads as "no
scoping". The control's fail state was therefore open: any future path reaching the
library's galaxy service without GatewayGrpcAuthorizationInterceptor having pushed
the identity would silently hand a browse-scoped key the full hierarchy, with
nothing failing and nothing logged. A constraint-narrowing control's absence is
invisible in normal operation, which is what made the honest "safe only because the
global interceptor runs first" comment insufficient on its own.

The provider now distinguishes the two states the old expression collapsed:

- authenticated key with no BrowseSubtrees constraint -> empty list, unscoped
  (unchanged, and now asserted in its own test rather than implied);
- no ambient identity with Authentication:Mode=ApiKey -> log a warning naming the
  broken ordering invariant and throw RpcException(PermissionDenied);
- no ambient identity with Authentication:Mode=Disabled -> unscoped, because
  nothing authenticates there so no key and no constraints exist to scope by.

The registration-order comment at GatewayApplication.cs now says the ordering is
still required but no longer silently trusted.

Tests: GatewayBrowseScopeProviderTests gains the PermissionDenied case and the
Disabled-mode case alongside the unconstrained-identity one.
GalaxyRepositoryHostWiringTests phase 1 now pushes an identity carrying no
BrowseSubtrees instead of pushing none — that, not an absent identity, is
production's unscoped case.

Docs: docs/Authorization.md's constraint section states the two-state distinction
and both fail behaviours under browse_subtrees.
2026-08-20 09:28:54 -04:00
Joseph Doherty f4919ba97c docs(archreview): cycle-3 architecture review (2026-08-19 @ 8b3e3b3)
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m19s
ci / java (push) Successful in 2m25s
ci / portable (push) Successful in 8m21s
Six domain reports + overall synthesis. All 47 cycle-2 Done claims verified
at HEAD with zero false Dones and zero regressions; 34 new findings
(0 Critical, 0 High, 4 Medium, 25 Low, 5 Info): GWC-31..38, WRK-32..34,
IPC-33..39, SEC-37..41, CLI-46..50, TST-31..36.
2026-08-20 09:10:12 -04:00
Joseph Doherty 8b3e3b3b45 Merge feat/sweep-reread-tag-summary: fca978d sweep re-read (17 collateral rationale deletions restored across 10 files) + DashboardGroupTagMapping summary now states the short-name GroupToTag key constraint
ci / windows-x86 (push) Successful in 1m15s
ci / nightly-windev (push) Has been skipped
ci / java (push) Successful in 2m16s
ci / portable (push) Successful in 8m12s
2026-08-18 09:39:12 -04:00
Joseph Doherty a8a8147a61 chore(plans): record sweep re-read + GroupToTag summary follow-ups as closed
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m20s
ci / java (push) Successful in 2m11s
ci / portable (push) Successful in 35m1s
2026-08-18 09:36:17 -04:00
Joseph Doherty f82dac1906 docs(src): restore substantive rationale prose the fca978d marker sweep deleted
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Failing after 37s
ci / java (push) Successful in 2m11s
ci / portable (push) Successful in 19m36s
Targeted re-read of the 203-file fca978d sweep (docs(src): add missing
XML docs and strip tracking-ID comments): a mechanical pre-pass narrowed
1,383 deletions to 68 files / 816 residual prose lines, and a judged
review of every one found 17 collateral deletions across 10 files —
rationale prose deleted alongside resolved markers with no equivalent
surviving anywhere in the tree. Restored (markers stay stripped, per the
sweep's intent):

- SessionManager: the three metrics-accounting invariants (kill-path
  gauge decrement safety, shutdown kill-fallback registry guard vs
  double bookkeeping, SessionClosed-not-SessionRemoved on failed close)
- SessionManagerTests: the matching accounting expectation note and the
  reason-string propagation pins (test summary + FakeWorkerClient.LastKillReason)
- MxAccessGatewayService.AcknowledgeAlarm: the routing remarks (GUID vs
  Provider!Group.Tag vs InvalidRequest; session-less via IGatewayAlarmService)
  — inheritdoc resolves to nothing (proto-generated base is undocumented)
- HubTokenService.Validate: why the hollow-token guard exists
  (non-empty AuthenticationType alone satisfies IsAuthenticated)
- DashboardSessionAdminService: why both broad catches exist (keep raw
  teardown exceptions out of Blazor's error boundary), Close + Kill paths
- WorkerPipeSession.RunAsync: why the factory result throws instead of
  NREing (unambiguous failure; finally-block Dispose can't no-op)
- LmxSubtagAlarmSource: Advise idempotency; Write is always unsecured
  (user id 0), never WriteSecured semantics
- WnWrapAlarmConsumer: the v1-prefix path is what WIN-911-style code uses
- DashboardSnapshotPublisherTests: what the 10ms slack absorbs
  (Task.Delay's coarse Windows timer quantum)
- DashboardBrowseAndAlarmModelTests: why the label text is pinned, not
  just the CSS class

Everything else flagged verified benign: inheritdoc replacements resolve
to equal-or-richer interface docs, or the substance survives relocated.
NonWindows slnx 0W/0E; touched gateway test classes 65/65.
2026-08-18 09:33:59 -04:00
Joseph Doherty 4b6f9077bf docs(dashboard): surface the short-name GroupToTag key constraint in the class summary
The consequence that full-DN keys can never match (the shared LDAP
provider pre-strips groups to short RDN names) was documented only in
MapGroupsToTags's lookup comment; the class-level <summary> now states
it and points there, mirroring DashboardGroupRoleMapping.
2026-08-18 09:20:46 -04:00
Joseph Doherty 21903710e7 Merge feat/followups-tickets: worker suite first fully green on windev (WorkerPipeSessionTests harness fix), check-codegen Check 4 runs on Windows (.exe banner + PS5.1 quoting), Gradle 9 checkGeneratedClean revived, push-driven alarms truncation banner, settings recent-limit rows, Server-0xx doc-resolution audit (two regressions re-fixed), ack leg answered as unavailable-by-configuration, full-DN group-key docs corrected
ci / java (push) Successful in 3m3s
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m23s
ci / portable (push) Successful in 12m13s
2026-08-18 07:33:34 -04:00
Joseph Doherty f4b974eec1 chore(plan): followups-and-tickets as-built record; prior follow-ups closed
ci / windows-x86 (push) Successful in 1m30s
ci / nightly-windev (push) Has been skipped
ci / java (push) Successful in 3m38s
ci / portable (push) Successful in 11m12s
2026-08-18 07:29:15 -04:00
Joseph Doherty fb68bdb699 test(dashboard): settings render test proves label-value pairing
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m30s
ci / java (push) Successful in 3m3s
ci / portable (push) Successful in 12m36s
2026-08-18 07:25:57 -04:00
Joseph Doherty 8ae0c2f3d3 fix(codegen): PS5.1-safe quoting for native-exe args in generate-proto probes
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m26s
ci / java (push) Successful in 3m29s
ci / portable (push) Successful in 11m43s
2026-08-18 07:25:42 -04:00
Joseph Doherty 753d070535 style(worker-tests): field ordering in FakeRuntimeSession
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m31s
ci / java (push) Successful in 3m32s
ci / portable (push) Successful in 23m6s
refreshStaActivityOnCapture was declared between DispatchAsync and the property that
wraps it, rather than with the other instance fields. Moved up to the field block per the
member ordering in docs/style-guides/CSharpStyleGuide.md. Declaration move only — no
behavior, no other edits.
2026-08-18 07:24:39 -04:00
Joseph Doherty db7b1db947 docs(dashboard): GroupToRole/GroupToTag keys are short names — full-DN keys never match the pre-stripped groups
Follow-up to the Server-040 comment restoration (d3ac527). The restored
comment in DashboardGroupRoleMapping records that the shared
ZB.MOM.WW.Auth.Ldap provider hands the mapper groups already stripped to
short RDN names, so a full-DN GroupToRole *key* can never match. Three
doc sites still told operators the opposite, and the tag mapper carried
the same consequence with no comment at all.

- docs/GatewayConfiguration.md: GroupToRole and GroupToTag rows both said
  keys may be 'short CN or full DN — leading-RDN match'. Corrected to
  short group names only, with the reason (pre-stripped groups) and a
  note that the mapper's full-string-then-RDN fallback only matters to
  non-library callers of the IGroupRoleMapper<string> seam.
- docs/GatewayDashboardDesign.md: the login-flow bullet said the user's
  memberOf '(or short CN)' is matched against GroupToRole; it now
  describes what actually happens — groups arrive short and are matched
  against short-name keys.
- DashboardGroupTagMapping: short comment at the lookup pointing at the
  DashboardGroupRoleMapping comment, since it reuses ExtractFirstRdnValue
  and inherits the same full-DN-key-unsupported consequence.

Both doc rows are pre-existing on main — they predate this branch and
were not introduced by the Server-040 work; only their incorrectness was
surfaced by it. Comment/doc-only; no logic changes.
2026-08-18 07:23:59 -04:00
Joseph Doherty 6b5c737b04 docs(toolchain): windev protoc-gen-go-grpc updated to pinned 1.6.2
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m24s
ci / java (push) Successful in 2m54s
ci / portable (push) Failing after 6m48s
2026-08-18 07:23:37 -04:00
Joseph Doherty 1605f542cc docs(probe): align RPC-doc hedging with the findings doc; attribute rig-state inventory 2026-08-18 07:23:33 -04:00
Joseph Doherty 5417222f44 chore(plan): tasks 1-8 completed with review outcomes; task 9 in progress
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m23s
ci / java (push) Successful in 2m38s
ci / portable (push) Successful in 15m28s
2026-08-18 07:13:46 -04:00
Joseph Doherty aaeb86b132 fix(worker-tests): decouple FakeRuntimeSession dispatch safety-net from the inspection window
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m13s
ci / java (push) Successful in 2m26s
ci / portable (push) Failing after 4m56s
The BlockDispatch branch waited 5 real seconds and then proceeded regardless — it does not
branch on the wait's result. The long-in-flight test's inspection loop is bounded by
elapsed time and a frame floor, so on a loaded box (the documented 4-5x slowdown class) it
can plausibly outrun that 5 s. When it does, the reply is emitted mid-window,
AssertNotWorkerFault waves it past, and the reply leg then waits for a reply already gone
by — failing at the 20 s cancellation with no message, on exactly the loaded-box run the
widened windows exist to survive.

The wait is a pure safety net: nothing asserts on it firing, and every test that blocks
dispatch releases it explicitly (ReleaseDispatch, or a WorkerShutdown envelope, both of
which Set the event) — none reaches the timeout on a healthy run. Named it
BlockedDispatchSafetyNet and raised it to 30 s, above any window a test opens and above the
20 s cancellation those tests arm, so a wedged test always fails on its own token with its
own message. Dispose still releases the wait, so teardown never waits on it either.

Inline rationale in the test now states the decoupling and what a close pairing would cost,
rather than asserting the window stays inside a 5 s ceiling. Nothing else changed.
2026-08-18 07:08:47 -04:00
Joseph Doherty bc22792a36 docs(probe): close superseded unblock bullet, hedge the summary, evidence gobject_id, ack-caveat in RPC docs 2026-08-18 07:03:20 -04:00
Joseph Doherty 462850afb7 fix(worker-tests): doc-comment placement + watchdog-window headroom in long-in-flight test
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Failing after 17s
ci / java (push) Successful in 2m38s
ci / portable (push) Successful in 8m34s
Three review findings on 7da52b6:

1. The AssertNotFault doc block landed between the predicate-overload ReadUntilAsync's
   doc block and the helper, so the compiler attached the merged block to the helper and
   ReadUntilAsync lost its docs entirely. Helper and its docs moved above ReadUntilAsync,
   whose docs are back where they belong.

2. The compressed watchdog windows (50 ms grace, 100 ms ceiling) reintroduced the
   load-sensitivity the fix removed, one layer down.
   ReportWatchdogFaultIfNeededAsync measures staleness AFTER the heartbeat frame is
   written and flushed over the real named pipe, so a beat whose pipe I/O outlasts the
   ceiling faults a healthy session no matter how fresh the captured activity was. At
   100 ms that is a plausible stall on a loaded box, and this is the one test asserting
   the watchdog NEVER fires. Widened to a 200 ms grace and a 1 s ceiling — still two
   orders of magnitude under the 75 s production default.

   The inspection loop is now bounded by a 2 s window (twice the ceiling, so a fake whose
   activity stopped advancing still accumulates past it and faults) with a 30-frame floor,
   rather than a fixed 30 frames that no longer outran the wider ceiling. The floor keeps
   a window that saw almost no beats from passing as a clean one. Two seconds stays well
   inside FakeRuntimeSession's 5 s dispatch-block ceiling, so the command is still in
   flight for the whole window.

3. AssertNotFault renamed AssertNotWorkerFault, matching the WorkerFault body case it
   tests.

Scenario intent unchanged: long in-flight command, pump refreshing, zero fault frames,
reply delivered. Test-only.
2026-08-18 07:01:16 -04:00
Joseph Doherty d1ae43d0d3 docs(probe): ack-leg third attempt — unavailable by configuration, GUID leg still assumed 2026-08-18 06:56:58 -04:00
Joseph Doherty f57a6ae5ff fix(dashboard): alarms feed doc row + monitor-contract comment + mid-truncation attach test
The In-process page feeds table still described the alarms page's subscription as
provider status only, contradicting the three passages updated in 7b6dfba.

The loop's catch-all comment (and the cadence bullet that repeated it) claimed the
monitor completes a subscriber's stream on restart. It does not: ClearCache pushes
snapshot_status(false) through the still-open channel, and a subscriber is only
completed-with-error on a failed TryWrite, or removed by its own disposal.

The SnapshotStatus arm's priming claim had no test behind it — every push test
attached to an untruncated feed and pushed the edge itself. ScriptedAlarmFeed now
replays an optional open sequence, and a new test attaches to a feed already
primed provider_status then snapshot_status(truncated) and asserts the banner
comes up with no edge pushed after render.
2026-08-18 06:49:20 -04:00
Joseph Doherty d3ac52758c docs(reviews): re-verify doc-only Server-0xx resolutions; re-fix regressions
Audited every Server-0xx finding whose Resolution described a
documentation-only or comment-only change, and spot-checked the doc
sub-claims of otherwise test-backed resolutions. 20 entries annotated
in place (append-only; no historical resolution text rewritten).

Two corrections had not survived and are re-applied:

Server-040: the MapGroupsToRoles lookup-precedence comment moved intact
into DashboardGroupRoleMapping (792e3f9) and was then deleted wholesale
by fca978d, a sweep meant only to strip (Server-NNN) tracking markers.
That also removed a later, substantive paragraph recording that the
shared ZB.MOM.WW.Auth.Ldap provider pre-strips groups to short RDN
names, so a full-DN GroupToRole key is unsupported. Both paragraphs
restored, minus the tracking IDs.

Server-009: the WAL / busy_timeout note vanished when the Storage
section of docs/Authentication.md was rewritten to delegate
connection-factory detail to ZB.MOM.WW.Auth.ApiKeys. The behavior is
still live in the library (confirmed against 0.2.1), so the fix is
prose-only.

Server-011/014/022/023 are annotated as moot rather than regressed:
the IAlarmRpcDispatcher trio was deleted in dc9c0c9 and no stale
'not yet wired' / 'PR A.6/A.7' prose survives in Server source.
Server-038's documented v1 ACL gap was later closed by
IDashboardSessionAcl, so its remarks are current.

Comment/doc-only; no logic changes.
2026-08-18 06:48:45 -04:00
Joseph Doherty 7da52b65b7 fix(worker-tests): model the STA pump at heartbeat capture — WorkerPipeSessionTests long-in-flight repro
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m11s
ci / java (push) Successful in 2m14s
ci / portable (push) Successful in 8m33s
RunAsync_LongInFlightCommandThatKeepsPumping_DoesNotFaultAndDeliversReply failed
deterministically on the Windows box with a StaHung fault whose command_method was
empty and whose staleness was 233 ms — i.e. a fault raised with no command in flight,
before the scenario under test began. FakeRuntimeSession stamps LastStaActivityUtc once,
at construction, and the test only started refreshing it after the blocked dispatch
signalled. Everything between those two points — handshake, STA init, the first
heartbeat — captured a snapshot already stale past the compressed 50 ms grace, with no
correlation id for the watchdog to suppress on, so the watchdog correctly reported the
fake as hung.

The harness, not the product, was wrong: StaRuntime.ThreadMain calls MarkActivity() on
every WaitForWorkOrMessages iteration, so a live worker is never captured stale, idle or
busy. Model that where it belongs — FakeRuntimeSession.RefreshStaActivityOnCapture (opt
in, default off) stamps activity at each CaptureHeartbeat and leaves the rest of the
snapshot alone — and arm it before RunAsync so the first beat is covered. The test-owned
refresh loop goes away with it; a thread-pool loop racing a compressed grace could not
have held the invariant anyway.

Scenario intent is unchanged and slightly stronger: the command still blocks in dispatch
across 30 heartbeats (~600 ms, many multiples of the 100 ms stuck ceiling), no frame may
be a fault, and the reply must still arrive. The reply leg is now fault-checked too
(previously it skipped frames blindly), and the pump keeps running across the release, as
it does in production while the reply is marshalled off the STA. Fault assertions now
report the category and diagnostic message instead of a bare body-case mismatch.

Test-only change; no product code, frame protocol, or STA rule touched.
2026-08-18 06:46:08 -04:00
Joseph Doherty 7b6dfba654 feat(dashboard): alarms page consumes snapshot_status feed frame for the truncation banner
The truncated-snapshot caveat moved from poll-only to push-driven. The page
already held an in-process alarm-feed subscription for the provider badge; it
now also handles the feed's snapshot_status frame, so a capped provider fetch is
caveated when the monitor decides it rather than up to three seconds later.

The poll's assignment stays as the reconcile baseline — both sources read the
same monitor verdict, and the frame is consumed, never synthesized page-side.
StreamAsync primes every subscriber with a snapshot_status frame at open, so a
page attaching mid-truncation needs no priming logic of its own; the loop is
renamed StatusFeedLoopAsync because it now feeds two indicators, not one.
2026-08-18 06:43:23 -04:00
Joseph Doherty 2b1efb5e50 docs(auth): make Authentication.md examples runnable as written 2026-08-18 06:40:11 -04:00
Joseph Doherty a390fe16e2 feat(dashboard): settings page shows RecentFaultLimit and RecentSessionLimit 2026-08-18 06:39:32 -04:00
Joseph Doherty c94c4d43c4 fix(codegen): normalize .exe off plugin version banners so Check 4 runs on Windows 2026-08-18 06:39:00 -04:00
Joseph Doherty df45cb4a37 fix(java-client): checkGeneratedClean via ProviderFactory.exec — Project.exec is gone in Gradle 9 2026-08-18 06:38:35 -04:00
Joseph Doherty fd941c249e docs(plan): followups-and-tickets — 10 tasks closing every recorded follow-up
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m21s
ci / java (push) Successful in 2m11s
ci / portable (push) Successful in 8m53s
2026-08-18 06:36:47 -04:00
Joseph Doherty 45058d57a5 Merge feat/followup-closeout: feed-level snapshot_status truncation frame on StreamAlarms rolled out to all five clients, ShowTagValues gating all three dashboard seams, both-direction codegen Check 3, GroupToTag/UntaggedSessionVisibility settings rows, dashboard_tags on ApiKeysPage, wnwrap probe answers (@COUNT records-in-reply, re-raise GUID replacement, ack leg re-blocked)
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m20s
ci / java (push) Successful in 2m15s
ci / portable (push) Successful in 8m8s
2026-08-18 06:26:30 -04:00
Joseph Doherty f2a422b02f docs(auth): canonical scope strings in the two remaining stale lists
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m18s
ci / java (push) Successful in 2m58s
ci / portable (push) Successful in 8m49s
Both lists predate the scope rename and would mislead anyone creating a key:
CLAUDE.md's Authentication section still named the pre-rename scopes, and
docs/Authentication.md's ops.alice example passed 'read,write', which
GatewayScopes.ValidateScopes rejects outright. Same defect family as the
Build/Test/Run sample fixed in a5f843c.

Recorded as a follow-up: code review finding Server-012 claims it fixed the two
CLAUDE.md lists on 2026-05-18, but neither correction was present — a Resolved
finding is not re-examined, so the sibling Server-0xx doc resolutions want a
spot-check for the same pattern.
2026-08-18 06:13:52 -04:00
Joseph Doherty a5f843cbfb chore(plan): close the record — prior follow-ups annotated, closeout as-built noted
The prior plan's "Follow-ups recorded, not started" block described pre-branch
behavior; every item is now closed, narrowed, or restated with its evidence, so
the block no longer misleads a reader who lands on it first. The stale Rust-guard
bullet is corrected in place rather than deleted: Check 3 always existed, and
saying so is the only way the reader learns what the real (one-directional) gap was.

Also fixes the CLAUDE.md apikey sample, which named a verb the parser has never
accepted ('create'; only 'create-key' exists, no alias), omitted the required
--key-id, and listed non-canonical scope strings that GatewayScopes now rejects
at create time — the sample could not have run.
2026-08-18 06:12:31 -04:00
Joseph Doherty c3c603f169 docs(alarms): describe the real StreamAlarms open sequence, and close TST-16
The final integration review's non-blocker reservations, all documentation
or comment truth except one test arm.

The alarm feed opens provider_status -> snapshot_status -> cached
active_alarm -> snapshot_complete, which is what GatewayAlarmMonitor has
done since the snapshot_status frame landed. Two places still described
the old order: docs/Grpc.md said provider_status arrived *after* the
initial snapshot, contradicting its own snapshot_status section two
paragraphs down, and AlarmFeedMessage's leading proto comment named
neither status frame at all. Both now state the sequence the monitor
emits, so a client author reading either one gets the frame order right.

The proto comment change flows through the generated trees (Contracts,
Go, Java) and the client descriptor set; the Rust vendored copy stays
byte-identical to canonical. Python's generator does not carry proto
comments into its output, so it has no delta.

AlarmsHubPublisherTests' valueless-payload case covered snapshot_complete
and provider_status but not snapshot_status, leaving the newest arm
unpinned against the redaction switch that must ignore it. Added.

WnWrapAlarmConsumer's ack comment led with the 2026-05-01 reading that
-55 tracks the 8-arg overload, then refuted itself six lines later with
the 2026-08-18 probe. It now leads with the observation labelled as
narrower than it reads -- mirroring the correction already in
docs/AlarmClientDiscovery.md -- so the block argues one thing: the 6-arg
call site stays for parity, and rc semantics are per the probe. A
paragraph orphaned by an earlier splice is rewrapped. Comment interior
only; the file compiles on Windows.

TST-16 gets a dated closure note rather than a rewrite: the flag it
called dead was implemented 2026-08-18. GatewayDashboardDesign's /browse
paragraph gains the failed-read carve-out GatewayConfiguration already
documented, so the two agree that a failed read keeps its - placeholder.
2026-08-18 06:09:06 -04:00
Joseph Doherty a95e510015 chore(plan): Task 8 verification-green at 90331b6 — residuals recorded 2026-08-18 05:56:45 -04:00
Joseph Doherty 47d74032e6 chore(plan): Task 7 complete — probe retry answered Q2 and the re-raise leg, ack leg re-blocked with evidence 2026-08-18 05:48:10 -04:00
Joseph Doherty 540a1188aa docs(alarms): stop describing the wnwrap ack as working, and say the re-raise replaces the record
Follow-up to 90331b6. Three comment/prose corrections, no behaviour change.

AcknowledgeByName's comment still said the 6-arg overload "works and reaches the
alarm-history path correctly", which the same commit's own findings contradict in
three other places. It now says what was observed: rc=0 means accepted, not
applied — the 2026-08-18 probe acked a live alarm six ways and the snapshot, the
OPERATOR_NAME field, and the extension's .Acked attribute all stayed put. The -55
tracks the consumer, not the overload. Subscribe's comment gets the same
treatment: "lets AlarmAckByName succeed" becomes "return rc=0".

AlarmProbeFindings.md said the re-raise arrives as "a separate record" alongside
the returned one, which reads as coexistence and is wrong. The snapshot carries
one record per tag: the cap=1024 replies bracketing the re-raise are both
elementCount=3 (one per TestMachine_00{1,2,3}) at an identical 1613 bytes, and the
old GUID is absent from the later one. The re-raise replaces the record, so a
single poll spanning it sees the Clear and the Raise together.

Worker diff verified strictly comment-only; builds x86 on windev, 0W/0E.
2026-08-18 05:46:35 -04:00
Joseph Doherty 90331b622c docs(alarms): second wnwrap probe attempt — secured-write verb unblocked the rig, answering @COUNT and the re-raise GUID leg
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m23s
ci / java (push) Successful in 2m25s
ci / portable (push) Successful in 24m49s
AuthenticateUser("Administrator", "") + WriteSecured raises the alarm UDAs that
plain Write could not touch (SecurityError detail=1008), so the 2026-08-17 blocker
was the verb, exactly as that run's own Unblocking list predicted.

Two of the three open questions are now observed rather than assumed:

- ALARM_RECORDS/@COUNT reports the records in the reply, not the total active
  count. With three alarms active it read 1 at cap 1 and 2 at cap 2. There is no
  exact truncation signal to switch to, so IsTruncatedFetch's conservative rule is
  the design rather than a placeholder — behaviour unchanged, only the comments.
- Clear-then-re-raise mints a new GUID; the ALM->RTN leg keeps its GUID
  (reconfirming the 2026-05-01 capture). ComputeTransitions already reads the
  re-raise correctly as one instance ending and another beginning.

The acknowledge leg stays unobserved for a narrower reason: every wnwrap ack
surface is inert on this rig. AlarmAckByName returns 0 from the ack-only consumer
and -55 from the SetXmlAlarmQuery-applied one, for both the 6-arg and 8-arg forms,
and neither the snapshot STATE, OPERATOR_NAME, nor the extension's own .Acked
attribute moves. That corrects AlarmClientDiscovery.md, which read the zero return
as a working ack.

Comment- and prose-only; no behaviour change. The three throwaway probes ran from
the windev CI clone and were deleted; that clone is a clean tree at ab3ff16.
2026-08-18 05:40:11 -04:00
Joseph Doherty ad30adca2f chore(plan): Task 2 complete — five-client snapshot_status rollout approved 2026-08-18 05:33:25 -04:00
Joseph Doherty 4d4f443c27 fix(clients): review nits — alphabetize the Java CLI alarm imports, cover the Rust snapshot-status render
The Java import block's AlarmFeedMessage already sorted after
AlarmProviderStatus before c748361; adding AlarmSnapshotStatus widened the
gap. Order all three alphabetically.

The Rust CLI tests the sibling ProviderStatus render path but not the new
SnapshotStatus arm, so the summary string and the JSON shape were both
uncovered. Add the matching test over alarm_feed_message_summary and
alarm_feed_message_to_json.
2026-08-18 05:33:00 -04:00
Joseph Doherty c7483615cf chore(clients): roll out feed-level snapshot_status to all five clients
Task 1 added AlarmSnapshotStatus and AlarmFeedMessage.snapshot_status = 5.
Carry it downstream from the canonical Contracts protos:

- Rust vendored protos under clients/rust/protos, refreshed byte-identical
  (build.rs falls back to them for out-of-repo tarball builds)
- client descriptor set (protoc 34.1 pin)
- Go (protoc-gen-go v1.36.11 / protoc-gen-go-grpc 1.6.2)
- Python (grpcio-tools 1.80.0 pin)
- Java (gradle generateProto)

.NET needs no regeneration: the client compiles against the Contracts
Generated/ output committed with the proto change.

The hand-written CLI feed renderers switch on the payload oneof, so codegen
alone does not carry the arm. Add snapshot-status to the .NET, Go, Rust, and
Java renderers; the .NET and Go renderers were also missing provider-status,
which has been on the wire since the provider-mode work, so add it there too.
Java's renderer is an exhaustive switch expression and did not compile until
the new case landed. The Python CLI renders generic protobuf-JSON and needs
no change.

Each client README gains a paragraph on the feed-level frame next to its
existing from_truncated_snapshot paragraph: it arrives at stream open after
provider_status and before the cached active_alarm frames, then on every
verdict change including the clearing frame a monitor restart emits, so a
live consumer can track set completeness without polling QueryActiveAlarms.

GatewayDashboardDesign: list the two new payload cases the AlarmsHub forwards,
and — separately — record the GroupToRole / GroupToTag / UntaggedSessionVisibility
rows the settings page already renders but the bullet list omitted.
2026-08-18 05:26:06 -04:00
Joseph Doherty d13144a9d7 chore(plan): Task 3 complete — ShowTagValues coverage approved across all three seams 2026-08-18 05:21:45 -04:00
Joseph Doherty 155c9d619f chore(plan): Task 6 complete — dashboard_tags display and create input approved 2026-08-18 05:21:21 -04:00
Joseph Doherty 51b0a8f55f chore(plan): Task 1 complete — feed-level snapshot_status approved 2026-08-18 05:21:11 -04:00
Joseph Doherty e50ec493cb docs(config): ShowTagValues /browse clause — redaction applies to successful reads only 2026-08-18 05:20:25 -04:00
Joseph Doherty 312e072070 docs(proto): spell out the monitor-restart clearing emission on AlarmSnapshotStatus 2026-08-18 05:19:31 -04:00
Joseph Doherty fab600d3b0 fix(dashboard): de-duplicate dashboard_tags typed into the API-key create form
The form split tags with the shared ParseList and attached the result verbatim,
so "team-a, TEAM-A" persisted as two entries and the constraints column read
dashboard_tags=[team-a, TEAM-A] — one grant reported as two on the page whose
job is to show what a key was granted. Enforcement never saw it (a session holds
its tags in a case-insensitive set), which is exactly why the display was the
only place it could surface.

De-duplicates ordinal-ignore-case at the attach point only, first spelling
winning, matching ApiKeyAdminCommandLineParser.ParseDashboardTags. ParseList is
untouched: the five glob lists are matched literally, so near-duplicates there
are not necessarily the same rule and must survive verbatim — pinned by a test.

The help text claimed to mirror the CLI flag; it now claims only the shared
separators and the dedupe, since the form still drops an empty segment silently
where the CLI hard-fails. A browser form has no exit code to fail with, so that
difference stays, and Authorization.md now records it.
2026-08-18 05:19:02 -04:00
Joseph Doherty 1ea6f60ea2 fix(dashboard): keep the '-' placeholder on failed /browse reads instead of '[redacted]' 2026-08-18 05:18:47 -04:00
Joseph Doherty bf7b44c2d7 chore(plan): Task 5 complete — settings rows approved with comparer fix 2026-08-18 05:15:37 -04:00
Joseph Doherty ff16c69d59 fix(config): propagate GroupToTag's comparer into the effective-config projection 2026-08-18 05:14:58 -04:00