Commit Graph

3734 Commits

Author SHA1 Message Date
Joseph Doherty 027c423502 docs(plans): R3-04 execution notes — T6-T12 deviations recorded, T13 marked pending (live gate deferred to orchestrator) 2026-08-20 10:33:07 -04:00
Joseph Doherty 985de0b6ad chore(docker-dev): site-b-2 is Cluster:Roles-only, site-a-2 pins a miscased Cluster:Roles entry (archreview 01/S-12, 01/C-9)
T11: site-b-2 drops OTOPCUA_ROLES entirely — Cluster:Roles is its sole role source, the rig's
only node that can exhibit the two-role-authority split (01/S-12). Expected to fail every
deploy on a pre-R3-04 build.

T12: site-a-2 pins Cluster__Roles__0="Driver" (capital D) as the standing 01/C-9 case-mismatch
pin, keeping cluster-SITE-A verbatim. Pre-fix this un-elects the SITE-A Primary.

Combined into one commit (deviation from the plan's two separate T11/T12 commits): the header
comment block documenting all six nodes' role sources interleaves both nodes on adjacent lines,
so a clean split was not practical. Recorded in the plan's Execution notes.

Compose edits only — the rig is NOT brought up as part of this change.
2026-08-20 10:32:18 -04:00
Joseph Doherty 3f8d5815b3 fix(runtime): mux adapter forwards the dependency's real status code (05/STAB-19) 2026-08-20 10:30:14 -04:00
Joseph Doherty b8d2d50006 docs: one role authority — precedence, normalization, and the overlay invariant (R3-04) 2026-08-20 10:29:37 -04:00
Joseph Doherty b725788954 docs(plans): R3-01 execution notes (T1-T9) - filter gotcha, T4/T5 fixture scope, observed cookie-challenge signature 2026-08-20 10:28:55 -04:00
Joseph Doherty ae97b32c82 test(runtime): comms-loss → identical-value recovery contract + silent-resubscribe control (03/S15) 2026-08-20 10:27:48 -04:00
Joseph Doherty b8911eaa14 docs(plans): mark R3-01 T9 done; T10 remains pending (live gate, orchestrator-run) 2026-08-20 10:27:45 -04:00
Joseph Doherty a222421374 docs: mesh gRPC ingress is AllowAnonymous; the PSK interceptors are the gate (R3-01)
New docs/security.md section "Mesh gRPC ingress is AllowAnonymous by design"
(archreview 03/S14): what the cookie FallbackPolicy covers, why it can block a
correctly-PSK'd call before the interceptor's verdict matters, why the
AllowAnonymous opt-out is safe (all five interceptors fail-closed), the
affected-endpoint table (fused-vs-driver-only distinction), and the two
standing guards (MeshGrpcAnonymousMetadataTests, MeshGrpcMapConventionTests)
plus the auth-stack/driver-only boundary tests.

One cross-reference line each in docs/Configuration.md (§MeshTransport,
§ConfigSource/ConfigServe) and docs/Telemetry.md, so an operator reading
about the listener ports meets the constraint where they'd look for it.
2026-08-20 10:27:40 -04:00
Joseph Doherty 2b7100cf19 fix(runtime): route a reprovision by incarnation, not driver id (review F2)
A ReprovisionRequested in flight from an old incarnation could reach the NEW child
after HandleRestartDriver swapped it in. In Connecting that child DOES handle
InitializeRequested, so it started a second concurrent InitializeAsync, racing the
#527 dispose-before-overwrite.

Seen RED: InitializeCount == 2 on a child held in Connecting by a gated init
(an already-connected child swallows InitializeRequested and passes vacuously).
2026-08-20 10:27:02 -04:00
Joseph Doherty 659d5f51e4 test(integration): a Cluster:Roles-only node wires the driver stack 2026-08-20 10:26:34 -04:00
Joseph Doherty 4dde5cb8bf docs(plans): mark R3-01 T7 and T8 done 2026-08-20 10:25:12 -04:00
Joseph Doherty f3832aa48c test(host): a mesh gRPC map added outside MeshGrpcEndpoints fails the build
MeshGrpcMapConventionTests - source-scan of Program.cs (solution-root walk,
following FlushLoggingConventionTests.cs's idiom): zero direct
MapGrpcService</MapZbLocalDbSync( occurrences, exactly one
MapOtOpcUaMeshGrpcServices( call.

Complements the endpoint-metadata census (T1-T3): the census catches a map
added to MeshGrpcEndpoints without the AllowAnonymous convention, but cannot
see a map that bypasses the helper and goes straight into Program.cs - that
shape never reaches MapOtOpcUaMeshGrpcServices at all. Verified red against
the pre-T1 Program.cs (five inline map sites, zero mapper calls) and green
against the current extracted form.
2026-08-20 10:25:02 -04:00
Joseph Doherty edd01fcc34 fix(runtime): a changed alarm set re-subscribes instead of latching (review F1)
The alarm subscribe gated on 'a handle exists', which is correct for an idempotent
re-push and wrong for a deploy that widens/narrows the set. Combined with the
spawn-time desired set, a respawning deploy could latch the PREVIOUS revision's
alarm refs for the life of the connection; sourceNodeIds-filtering drivers
(OpcUaClient/AbCip/Focas) would never deliver the new alarm.

Seen RED: LastSubscribedAlarmRefs stayed [alarmX] after a deploy widening to
[alarmX, alarmY].
2026-08-20 10:24:52 -04:00
Joseph Doherty 82ad98e1a9 chore(docker-dev): parameterise Security__Auth__DisableLogin on the central pair
Security__Auth__DisableLogin: "${OTOPCUA_DISABLE_LOGIN:-true}" on central-1
and central-2 - byte-identical default behaviour, verified via `docker
compose config` both unset (resolves "true") and with
OTOPCUA_DISABLE_LOGIN=false (resolves "false").

DisableLogin's always-succeeding handler, registered under the cookie scheme
name, masks the AdminUI cookie FallbackPolicy entirely - which is exactly how
03/S14 (the FallbackPolicy blocking every mesh gRPC call on a login-enabled
admin node) survived seven phases of prior live gates that all ran with the
default. The production-faithful mode is now a one-variable flip instead of
an edit-and-remember-to-revert; header comment names the flip and the LDAP
users it needs (archreview 03/S14, R3-01 T8).
2026-08-20 10:24:49 -04:00
Joseph Doherty e2497c908d test(cluster): guard that role reads live only in the authority (archreview theme 4) 2026-08-20 10:21:36 -04:00
Joseph Doherty 9bd3366b28 docs(plans): mark R3-01 T6 done 2026-08-20 10:21:01 -04:00
Joseph Doherty 306ea1aecf test(host): driver-only nodes carry no authorization middleware (03/S14 exemption pinned)
MeshGrpcDriverOnlyPipelineTests - the driver-role shape (no AddOtOpcUaAuth, no
UseAuthentication/UseAuthorization at all) behaves identically to the fused
shape's T4 positive/negative tests for NodeCommandService and
TelemetryStreamService: a correctly-keyed call succeeds, a no-key call gets
the interceptor's PermissionDenied + detail string.

Also asserts the STRUCTURAL reason it is unaffected - no
IAuthorizationPolicyProvider registered at all - so a future "register auth
everywhere for consistency" change fails this test before it ever reaches a
live site outage. Confirmed (by temporarily swapping in the pre-T2/T3 mapper)
that this class passes identically on both builds: driver-only nodes were
never exposed to 03/S14 in the first place, with or without
.AllowAnonymous().
2026-08-20 10:20:56 -04:00
Joseph Doherty e475f7b4ab Merge branch 'fix/r3-05-acl-hot-path-expiry' — R3-05: bounded ConfigDb API-key lookup (deadline + TTL) and clock-driven key-expiry revalidation of live monitored items (archreview 03/S18, 03/S19) 2026-08-20 10:19:13 -04:00
Joseph Doherty 62515f3279 fix(security): pin the cached-key expiry guard, meter cache_hit after it, and close two wiring gaps (review fixes) 2026-08-20 10:18:52 -04:00
Joseph Doherty 7d98ef9332 feat(runtime): audit refs still BadNoCommunication after a reconnect (dedup-trap tripwire) 2026-08-20 10:16:01 -04:00
Joseph Doherty 353ed6bb58 docs(plans): mark R3-01 T5 done 2026-08-20 10:15:03 -04:00
Joseph Doherty 8471db4b1d test(host): no-PSK mesh calls report the interceptor's verdict, not a cookie 401/redirect
Extends MeshGrpcAuthStackTests (T5): a no-bearer or wrong-bearer call to
DeploymentArtifactService.Fetch, TelemetryStreamService.Subscribe and
NodeCommandService.Execute fails with the INTERCEPTOR's detail string
("Config-serve authentication failed.", "Telemetry stream authentication
failed.", "Node command authentication failed."), asserted alongside the
status code rather than in place of it - the vacuity trap the plan calls out
(ConfigServeAuthInterceptor throws Unauthenticated, and Grpc.Net.Client can
map an HTTP 401 to the same status, so status alone would not discriminate a
broken build).

Verified red-then-green against the T1 (pre-AllowAnonymous) mapper: all 6 new
theory cases fail there with StatusCode.Internal ("HTTP/2 server reset the
stream, PROTOCOL_ERROR") instead of Unauthenticated/PermissionDenied - the
cookie challenge breaking the h2c stream before any gRPC trailers are
written, never a clean auth denial. That is the exact signature the live
gate (T10) should grep for; recorded in the class remarks.
2026-08-20 10:14:55 -04:00
Joseph Doherty 4380e2e78a fix(cluster): fail start when a late config layer changes Cluster:Roles (role overlays may not set it) 2026-08-20 10:11:25 -04:00
Joseph Doherty c1ad9e24d2 docs(archreview): R3-03 execution notes — final verification + deviation summary 2026-08-20 10:11:16 -04:00
Joseph Doherty 3314a1f98d feat(host): warn when Cluster:Roles and OTOPCUA_ROLES disagree; boot banner names the winning source 2026-08-20 10:08:03 -04:00
Joseph Doherty 815423bd88 docs(plans): restore literal UTF-8 em-dashes in R3-01 tasks.json (cosmetic) 2026-08-20 10:07:20 -04:00
Joseph Doherty 12c4028474 docs: restart re-provisioning + per-connection watchdog grace (R3-03) 2026-08-20 10:07:20 -04:00
Joseph Doherty 97f4ea0c9e fix(runtime): comms loss marks raw + UNS value nodes BadNoCommunication, value retained (archreview 03/S15) 2026-08-20 10:07:12 -04:00
Joseph Doherty af9d3a69ac docs(plans): mark R3-01 T4 done 2026-08-20 10:06:55 -04:00
Joseph Doherty 458c84c34f test(host): mesh gRPC boundary under the production auth stack (FallbackPolicy live)
MeshGrpcAuthStackTests - the CI-shaped guard 03/S14 asked for. A real
AddOtOpcUaAuth(config) + UseAuthentication/UseAuthorization pipeline (cookie
scheme, DisableLogin=false, EnvironmentName=Production) with the production
interceptor set, driving DeploymentArtifactService.Fetch,
CentralCommandService.ReportApply and NodeCommandService.Execute through
MapOtOpcUaMeshGrpcServices.

Verified red-then-green by hand: with the T1 (pre-AllowAnonymous) mapper
swapped in, all three positive calls fail with
"HTTP/2 server reset the stream, PROTOCOL_ERROR" (RpcException/Internal) -
the cookie challenge breaking prior-knowledge h2c, not a clean auth denial.
With the current mapper (T2/T3) all three succeed. That asymmetry is the only
thing distinguishing the two builds; every other assertion here (cookie
handler identity, the falsifiability control) is unchanged by the fix and
proves nothing on its own.

Falsifiability control: LocalDbSyncService is mapped directly (bypassing
MapOtOpcUaMeshGrpcServices) with no .AllowAnonymous(), and a correctly-shaped
call to it is refused - proving the fixture's auth stack is genuinely live,
not accidentally permissive.
2026-08-20 10:06:47 -04:00
Joseph Doherty 14094e3a18 docs(archreview): file R3-05 residuals R1/R2 (Gitea #562/#563); execution notes 2026-08-20 10:06:00 -04:00
Joseph Doherty 5c7e5725a5 test(runtime): first supervision-restart test — child re-initialises + re-subscribes (archreview 03/U6)
Crash seam: the planned null-Snapshot OnDataChange (no SupervisorStrategy fallback needed).
Seen RED with T6 reverted: InitializeCount 1, not 2 — with a preceding ShutdownCount>=1
assertion proving the restart really happened, so the red is not ambiguous with 'never crashed'.
2026-08-20 10:05:45 -04:00
Joseph Doherty e46c1db5e5 fix(cluster): gossiped-role predicates compare case-insensitively — a miscased peer no longer un-elects the Primary 2026-08-20 10:04:16 -04:00
Joseph Doherty a9b592aeb8 docs(security): correct revocation latency (activation vs open session) + S18/S19 knobs 2026-08-20 10:04:03 -04:00
Joseph Doherty 04019e9414 feat(runtime): publish actor routes status-only batches to the sink 2026-08-20 10:03:25 -04:00
Joseph Doherty dfab7f614d test(integration): key expiry stops a live subscription over the wire, with no deploy 2026-08-20 10:00:52 -04:00
Joseph Doherty ec5b48bfcd feat(opcua): IOpcUaAddressSpaceSink.WriteStatuses — status-only batch write retaining the node value (03/S15) 2026-08-20 10:00:47 -04:00
Joseph Doherty 2b9a397b2d fix(runtime): re-provision a driver child after a supervision restart (archreview 03/S6, S16) 2026-08-20 09:56:05 -04:00
Joseph Doherty 6874a562f3 test+docs(mqtt): end-to-end Bad-then-identical-replay recovery 2026-08-20 09:55:11 -04:00
Joseph Doherty a4f12bccef fix(host): run monitored-item revalidation on a low-frequency timer (archreview 03/S19) 2026-08-20 09:55:01 -04:00
Joseph Doherty d97e441979 test(runtime): operator restart re-subscribes the respawned child (archreview 03/S16, U6)
Seen RED against a T3-reverted build: all 4 failed, Created[1].SubscribeCount == 0
on the headline case. 20x isolation run green after the fix was restored.
2026-08-20 09:53:38 -04:00
Joseph Doherty b7d20d14a8 fix(mqtt/sparkplug): initial push on subscribe + BadNoCommunication on connection loss 2026-08-20 09:52:15 -04:00
Joseph Doherty 1759f60476 docs(plans): mark R3-01 T3 done 2026-08-20 09:51:43 -04:00
Joseph Doherty e3cf363703 fix(host): LocalDb sync endpoint opts out of the cookie fallback via an empty-prefix anon group
MapZbLocalDbSync returns IEndpointRouteBuilder, not an IEndpointConventionBuilder,
so .AllowAnonymous() cannot be chained onto it the way the other four mesh maps
are (T2). app.MapGroup(string.Empty).AllowAnonymous().MapZbLocalDbSync() carries
the same convention while leaving the gRPC route pattern byte-identical -
verified directly (LocalDbSync_AllowAnonymousGroup_LeavesTheRoutePatternByteIdentical).

The census's RealMethodEndpoints filter had to move off a "route pattern starts
with /" heuristic to an exact-route-pattern-set match: inside the empty-prefix
group, grpc-dotnet's own "Unimplemented service"/"Unimplemented method" fallback
endpoints also gain a leading "/", so the heuristic let them back in and
over-counted (caught by DriverOnly_WithASyncPort_AlsoMapsTheLocalDbSyncEndpoint
and LocalDbSync_AllowAnonymousGroup_LeavesTheRoutePatternByteIdentical going red
against the naive filter before this fix).

All five mesh gRPC endpoints now carry IAllowAnonymous (archreview 03/S14, T3).
2026-08-20 09:51:33 -04:00
Joseph Doherty bcec88ef70 fix(cluster): all four role-scoped validators read the authority — 'Driver' no longer skips every driver rule (archreview 01/C-9) 2026-08-20 09:51:23 -04:00
Joseph Doherty b09f78396f test(opcua): pin clock-driven expiry in the revalidation pass; correct the only-trigger remark 2026-08-20 09:50:39 -04:00
Joseph Doherty ac2f8dc801 fix(mqtt): stale observed refs BadNoCommunication on connection loss so the identical replay recovers (05/STAB-18 shape a) 2026-08-20 09:46:56 -04:00
Joseph Doherty bd80ff96ca feat(security): AclRevalidationTimer — clock-driven monitored-item revalidation (archreview 03/S19) 2026-08-20 09:46:56 -04:00
Joseph Doherty 2f90156408 docs(plans): mark R3-01 T2 done 2026-08-20 09:46:40 -04:00
Joseph Doherty b05ebd6353 fix(host): mesh gRPC endpoints opt out of the AdminUI cookie FallbackPolicy (archreview 03/S14)
.AllowAnonymous() on DeploymentArtifactGrpcService, CentralCommandGrpcService,
TelemetryStreamGrpcService and NodeCommandGrpcService — the four MapGrpcService
maps that are chainable. On a login-enabled admin node the global cookie
FallbackPolicy (RequireAuthenticatedUser) applies to every endpoint without
explicit authorization metadata, gRPC included; the PSK bearer interceptors run
INSIDE the endpoint invocation, strictly after that middleware, so a
correctly-keyed node call was challenged before the interceptor's verdict could
matter. Safe because all four interceptors fail closed on an unset key.

The census now asserts IAllowAnonymous on all four (red before this change,
green after) plus a scope guard proving the opt-out never leaks onto an
unrelated endpoint. MapZbLocalDbSync (T3) is not chainable the same way and is
asserted absent here on purpose, so this test cannot start passing for the
wrong reason once T3 lands.
2026-08-20 09:46:30 -04:00