Commit Graph

192 Commits

Author SHA1 Message Date
Joseph Doherty 5415e6566f feat(management): Admin-gated actor dispatch for cert-trust commands (arch-review C4)
Made CommunicationService.TrustServerCertAsync/RemoveServerCertAsync/ListServerCertsAsync virtual to support the test seam (mirrors existing virtual WriteTagAsync).

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:29:57 -04:00
Joseph Doherty 722063638b feat(management): actor dispatch for BrowseNode/SearchAddressSpace/VerifyEndpoint — CLI parity (arch-review C4)
Made CommunicationService.BrowseNodeAsync/SearchAddressSpaceAsync/VerifyEndpointAsync virtual to support the test seam (mirrors existing virtual WriteTagAsync).

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:27:48 -04:00
Joseph Doherty 51cff07753 fix(management): reconcile area role gate to Designer|Deployer across actor and all three docs (arch-review C6)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:23:57 -04:00
Joseph Doherty 7af36fd5dc feat(security): wire LoginThrottle into management Basic-Auth and /auth/login|token — password-spray guard (arch-review P1/UA5)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:16:40 -04:00
Joseph Doherty d4d1732a9e fix(management): elide DatabaseConnection ConnectionString from List/Get (arch-review C3)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:15:55 -04:00
Joseph Doherty ecf8ac1b7d fix(management): elide ExternalSystem AuthConfiguration from responses and audit (arch-review C3)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:14:04 -04:00
Joseph Doherty 3a1980cb4c fix(management): elide DataConnection config secrets from responses and audit (arch-review C3)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:12:29 -04:00
Joseph Doherty 6c9de9f7ab feat(management): any-of role gates; restrict ListSecuredWrites to Operator/Verifier/Admin (arch-review UA1)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:03:51 -04:00
Joseph Doherty 15f5bc412c docs(inbound-api): dual-key API-key rotation procedure (create -> migrate -> disable -> delete)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 04:36:47 -04:00
Joseph Doherty 0ee9975111 feat(notifications): OAuth2 authority/scope management + CLI + UI surfaces
Additive UpdateSmtpConfigCommand params (OAuth2Authority/OAuth2Scope),
preserve-on-null handler application, CLI --oauth2-authority/--oauth2-scope
on smtp update, Central UI text inputs shown only for the OAuth2 auth type,
and the Component-NotificationService doc paragraph with M365 defaults.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 04:30:28 -04:00
Joseph Doherty 2e7be1e852 fix(esg): structured JSON AuthConfiguration (matches entity doc + UI placeholders) with legacy colon fallback — keys containing ':' no longer misparse
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 04:30:28 -04:00
Joseph Doherty b5e80d4c00 feat(inbound-api): machine-readable error codes per spec (incl. SITE_UNREACHABLE) + 415 for non-JSON bodies
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 04:30:28 -04:00
Joseph Doherty eb7a913d22 feat(esg): {param} path-template substitution with consumed-param removal — implements the spec'd /recipes/{id} form
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 04:14:36 -04:00
Joseph Doherty 178ae35308 feat(notifications): SMS notifications retry under SmsConfiguration.MaxRetries/RetryDelay (SMTP policy remains the Email + fallback policy) — wires the dead fields
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 04:07:44 -04:00
Joseph Doherty f0dd016207 feat(esg): TimeoutSeconds in ExternalSystem UI form + Component-ExternalSystemGateway doc sync — closes the spec-drift gap
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 04:07:44 -04:00
Joseph Doherty 899af63b2a docs(inbound-api): DB row is authoritative for method scripts — update broken-save warning + Direct SQL Warning for the revision-checked cache
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 04:07:44 -04:00
Joseph Doherty 153b82c880 docs(delmia): document at-least-once failover semantics + DelmiaRecipeDownload idempotency requirement
Manual verification (task step 3) skipped: this worktree cannot reach a
live environment to fetch the deployed DelmiaRecipeDownload script via
`scadabridge api-method get`. Documents only the mandated contract per
PLAN-06 Task 23; verifying the deployed script against it is deferred to
whoever has live environment access.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 03:52:04 -04:00
Joseph Doherty 839baa7880 fix(notifications): SMS adapter short-circuits on first transient — kills duplicate amplification and bounds the dispatch sweep
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 03:49:18 -04:00
Joseph Doherty 8e5631075d docs: sync Transport/TemplateEngine/DeploymentManager specs + CLAUDE.md with the arch-review fix wave (plan 05)
#05-T25 (terminal doc sweep + deferred-decision records). Component-Transport:
scripts/ dir marked not-implemented; transported-entity list gains LockedInDerived
/ script cadence-timeout / real AreaName / native-alarm-sources (round-trip-guard
verified); LineDiffer input cap (T22); rename call-site limitation made load-bearing;
preview→apply no-optimistic-concurrency window + session cap recorded. Component-
TemplateEngine: alarm-override granularity corrected (only TriggerConfig+PriorityLevel
instance-overridable, NOT Description/OnTriggerScript). Component-DeploymentManager:
OperationLockManager per-node invariant (routing rejection owned by plans 01/07) +
post-success audit isolation (T17). CLAUDE.md #24 blurb + Transport bullet updated
(trust gate, severity split, ArtifactDiff identity fix, T8 fidelity fixes, T14
plan-06 handoff). Component-ScriptAnalysis fifth-call-site + deny-list already synced
in T20/T21. No ../scadaproj/CLAUDE.md change needed — no wire-relationship/stack shift.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 03:22:34 -04:00
Joseph Doherty 523e670579 fix(security): bundle import runs the script trust gate — forbidden-API scripts rejected at import review, not at runtime
#05-T20. Bundle import is now the fifth ScriptTrustValidator delegating call
site. RunSemanticValidationAsync gains a Pass 0 that runs ScriptTrustValidator
.FindViolations over every non-Skip template / shared / ApiMethod script body
before name resolution; any violation is a HARD error for all kinds (a trust
verdict is authoritative, not the false-positive-prone name heuristic) and
short-circuits so it is never masked by a Pass-1 name error. DetectBlockersAsync
runs the same gate for preview parity, emitting entity-qualified Blocker rows.
Transport gains a direct ScriptAnalysis project reference. Docs: Component-
ScriptAnalysis (fifth call site) + Component-Transport updated.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 02:18:12 -04:00
Joseph Doherty c60347c5e7 fix(transport): blocker heuristic no longer hard-blocks valid imports — local declarations excluded, denylist extended, template-script findings downgraded to warnings
#05-T19. Import name-resolution findings are now split by origin: template-script
(and attribute-default-expression) references become non-blocking advisory
warnings (ConflictKind.Warning + ImportResult.Warnings) since the design-time
deploy gate re-validates authoritatively; ApiMethod-script references stay hard
errors (SemanticValidationException / ConflictKind.Blocker) as they have no
downstream gate. Names declared locally in a body (Roslyn-parsed local functions
/ methods) are excluded so a script's own helper isn't flagged; KnownNonReferenceNames
extended with common BCL/LINQ/SQL surface. DetectBlockersAsync and
RunSemanticValidationAsync Pass 1 apply the identical split so preview and apply
agree. Rollback/hard-fail tests retargeted to ApiMethods (the still-hard-failing
vehicle). CentralUI preview renders the new Warning badge/advisory.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 02:12:26 -04:00
Joseph Doherty e4136cb920 fix(transport): transport template NativeAlarmSources — imports no longer amputate native alarm mirrors nor dangle instance overrides
New TemplateNativeAlarmSourceDto + init-only TemplateDto.NativeAlarmSources (empty
default, additive — IsInherited placeholders carried for the collision detector).
Exported in ToBundleContent, consumed in FromBundleContent + BuildTemplate, and a
new SyncTemplateNativeAlarmSourcesAsync runs the Overwrite add/update/delete child
sync with per-change audit rows. ArtifactDiff.CompareTemplate now DiffChildren over
native sources so Preview reports a NativeAlarmSources field change. Proven by an
end-to-end flatten test: an instance native-alarm-source override that dangled
pre-fix now resolves against the re-imported template source.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-09 16:59:37 -04:00
Joseph Doherty 0302694f9a fix(template-engine): native alarm sources participate in the revision hash — staleness detection restored (deliberate one-time stale flip for affected instances)
TemplateEngine-011: ResolvedNativeAlarmSource bindings were absent from the
flattened-config revision hash, so edits to a native alarm source binding
(source reference, connection, condition filter, locked state) went undetected
by staleness detection. Add a HashableNativeAlarmSource record and fold the
sorted, null-when-empty NativeAlarmSources collection into HashableConfiguration
in its alphabetical slot. WhenWritingNull keeps every native-source-free config
byte-identical to its pre-migration hash (pinned literal test proves it), so the
migration is surgical: only native-source-bearing instances flip stale once
after upgrade; redeploy clears it — deliberate.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-09 16:46:45 -04:00
Joseph Doherty 57f7f65772 fix(security): enforce template locks on native alarm source overrides at flatten and management-command level
Add IsLocked to ResolvedNativeAlarmSource; FlatteningService skips overrides on a
locked source (mirrors attribute/alarm lock rules); ManagementActor SetInstanceNativeAlarmSourceOverride
flattens the instance and rejects an override on a locked source (and rejects a
canonical name that does not resolve, preventing dangling overrides).

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-09 16:28:52 -04:00
Joseph Doherty 854a6a8c0b perf(deployment): staleness/comparison paths no longer Roslyn-compile every script — deploy gate remains the authoritative compile
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-09 15:49:55 -04:00
Joseph Doherty d19ab93d39 fix(security): deny Environment/GC and ADO.NET provider namespaces; close reflection-gateway list (GetTypes/EntryPoint/Declared*/DynamicInvoke)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-09 15:49:55 -04:00
Joseph Doherty 7f1b59ff84 docs: reconcile design docs with shipped code (enums, Sms, per-channel retention, SiteCalls schema, conventions)
PLAN-04 Task 24. Component-Commons: AuditChannel gains SecuredWrite (5), AuditKind full
15-value list (+SecuredWrite*/ReconciliationAbandoned), NotificationType Email/Sms (Teams
evaluated+dropped), AuditLogRow persistence-only-projection deviation note. ConfigDB:
Type discriminator Email/Sms, data-layer conventions (UTC DateTime + Utc converter, MSSQL-
only repos/fixtures, AuditLog clustered-key + P3/P4/P6 tracked follow-ups). SiteCallAudit:
schema table rewritten to shipped columns (Channel/Target/AuditStatus-Status/SourceNode/
HttpStatus/IngestedAtUtc; no Kind/TargetSummary/provenance) + stored-vs-tracking-view note.
AuditLog: per-channel retention config lists all 5 channels incl SecuredWrite. Also fixed
two stale 'Teams planned' refs surfaced by the verification grep (HighLevelReqs, CentralUI).
2026-07-09 10:03:13 -04:00
Joseph Doherty 6567216e14 perf(notification-outbox): bounded parallel delivery within a dispatch sweep (default 4) 2026-07-09 09:41:48 -04:00
Joseph Doherty bfd2cc7e6f fix(audit-log): reconciliation pulls fail over to site NodeB when NodeA is unreachable
SiteEntry gains an additive FallbackGrpcEndpoint; SiteEnumerator now picks NodeA
else NodeB as primary (a NodeB-only site is no longer skipped) and carries a
distinct NodeB as the fallback. Both GrpcPull{AuditEvents,SiteCalls} clients dial
the primary first and, on a transport fault (Unavailable/DeadlineExceeded/
Cancelled/HttpRequestException/SocketException/OperationCanceled), fail over to
NodeB once before collapsing to empty — a mapping/unexpected fault does NOT fail
over. Keeps the reconciliation loss-recovery net available during a NodeA outage.
2026-07-09 08:57:06 -04:00
Joseph Doherty 0385942c3f feat(audit-log): permanently-abandoned reconciliation rows leave a durable ReconciliationAbandoned audit record
When a pulled AuditEvent fails to insert on every retry up to the permanent-
abandon threshold, the reconciliation actor now writes ONE synthetic
ReconciliationAbandoned row (new AuditKind) alongside the Critical log line —
fresh EventId, Status=Failed, channel preserved from the lost row, Extra carrying
the abandoned EventId + source site + final error — via the same
ScadaBridgeAuditEventFactory the ingest path uses. Best-effort in its own
try/catch so it never blocks the cursor twice. The permanent loss is now
queryable in the Audit Log, not only in a rotating log file.
2026-07-09 08:50:44 -04:00
Joseph Doherty 20098c6108 fix(site-call-audit): composite keyset cursor eliminates the single-timestamp reconciliation pin; pinned state now a published event
Reconciliation cursor becomes composite (UpdatedAtUtc, TrackedOperationId);
IPullSiteCallsClient/GrpcPullSiteCallsClient forward the Task-15 after_id keyset
(additive param, null preserves the legacy inclusive >= contract). A burst
sharing one exact UpdatedAtUtc now drains via the id tiebreak instead of pinning
forever. A legacy site that ignores after_id is latched + published as
SiteCallReconciliationPinnedChanged on the EventStream (transition-only),
replacing the prior silent log line.
2026-07-09 08:44:07 -04:00
Joseph Doherty 2c45c3238b feat(site-call-audit): relay Retry/Discard emit operator-identity audit rows
Additive RequestedBy on Retry/DiscardSiteCallRequest, plumbed from the Central UI.
On an Applied relay, SiteCallAuditActor emits one best-effort CachedResolve central
direct-write row (Submitted/Discarded) with the operator as Actor. Gated on an
injected ICentralAuditWriter (null in existing tests → no mirror read, unchanged);
the site remains the source of truth for the state change.
2026-07-09 08:21:41 -04:00
Joseph Doherty 50bb1ef8ab feat(notification-outbox): operator Retry/Discard emit audit rows with operator identity
Additive RequestedBy on Retry/DiscardNotificationRequest, plumbed from the Central
UI. RetryAsync emits a NotifyDeliver Submitted row (records who un-parked); Discard
stamps the operator on the Terminal row. Best-effort — audit failure never aborts
the action.
2026-07-09 08:12:47 -04:00
Joseph Doherty 6b06d1efcb fix(site-call-audit): same-rank freshness tiebreaker — retrying calls no longer freeze RetryCount/LastError at first write
Within an equal NON-terminal rank the newest UpdatedAtUtc wins, unfreezing the
Attempted-phase RetryCount/LastError/HttpStatus. Terminal ranks (>=3) are
excluded from the tiebreaker so terminal immutability is preserved (Delivered
never overwrites Parked); equal stamps stay idempotent, lower rank stays a no-op.
2026-07-09 07:56:39 -04:00
Joseph Doherty 5d0fcd4e66 perf(audit-log): bound GetExecutionTreeAsync edge scan to a root-anchored time window (partition elimination) 2026-07-09 07:10:35 -04:00
Joseph Doherty eb69b93947 docs(audit-log): document append-only enforcement honestly (CI guard default; DB roles are optional DBA hardening; purger now has CREATE TABLE + scoped DELETE) 2026-07-09 07:03:10 -04:00
Joseph Doherty 14c4df54f9 feat(audit-log): surface partition-purge failure as health event + counter 2026-07-09 06:49:28 -04:00
Joseph Doherty 74aa872c07 feat(audit-log): daily site SQLite retention purge job (closes unbounded site DB growth)
SiteAuditRetentionService (site-only IHostedService) ticks PurgeExpiredAsync on
InitialDelay(5m)/PurgeInterval(24h, clamped >=1m), cutoff = UtcNow - RetentionDays.
Registered site-only in AddAuditLogHealthMetricsBridge; options bound from
AuditLog:SiteRetention. Per-tick failures swallowed. Added a test-only
PurgeIntervalOverride (mirrors SiteCallAuditOptions) so the ms-cadence tick test
can bypass the 1-min production clamp. Doc updated. (PLAN-04 Task 3, S1/U2)
2026-07-09 06:22:19 -04:00
Joseph Doherty 1db0c32115 docs(requirements): sync SiteRuntime + DCL specs with hardening changes (C3/C4/C5 + S1-S6, P2/P4/P6, UA1/UA4 behavior notes) 2026-07-09 02:13:13 -04:00
Joseph Doherty 846279f6a8 chore(communication): low-severity cleanup — eviction counting, shared ingest timeout, split app heartbeat interval, doc notes 2026-07-08 21:54:42 -04:00
Joseph Doherty 0321ad0c20 feat(store-and-forward): peer-join full-buffer resync — standby anti-entropy for the S&F buffer 2026-07-08 21:43:11 -04:00
Joseph Doherty 4bdd5f0e4a fix(notifications): Notify.Send enqueues unbounded (maxRetries 0) and defers to sweep — no 30s script-thread block, no stranding 2026-07-08 20:56:09 -04:00
Joseph Doherty c3a8576863 fix(notifications): park corrupt buffered notification payloads instead of silently discarding as delivered (supersedes StoreAndForward-018) 2026-07-08 20:48:16 -04:00
Joseph Doherty 41ed57421d fix(host): wire S&F active-node delivery gate on site nodes (SelfIsPrimary); align S&F doc standby-passive wording 2026-07-08 17:48:31 -04:00
Joseph Doherty dc7a613dae docs(cleanup): sync Traefik + Host docs to oldest-member active-node semantics + DB-gated /health/active; TLS roadmap note
Renames stale ActiveNodeHealthCheck references to OldestNodeActiveHealthCheck
(Task 7 rename) across the requirements + components doc sets, corrects
leader→oldest-member wording and the embedded IsActiveNode snippet, and adds an
explicit 'production TLS profile not yet implemented' roadmap note.

deploy/wonder-app-vd03 NodeName overlay edits deferred: that production deploy
artifact is not tracked in this repo (same as Tasks 16 & 20).
2026-07-08 17:09:34 -04:00
Joseph Doherty 1f1dbd916d docs(host): REQ-HOST-6 documents the hand-rolled HOCON bootstrap; drop unused Akka.Hosting packages 2026-07-08 16:59:19 -04:00
Joseph Doherty 48e97fee01 feat(host): exit process on unexpected ActorSystem termination — completes the down-if-alone recovery loop
WhenTerminated watchdog calls IHostApplicationLifetime.StopApplication() when
the ActorSystem dies outside StopAsync (SBR self-down + run-coordinated-shutdown-when-down),
so the service supervisor restarts the node as a fresh incarnation. Optional
ctor param keeps every existing construction site compiling.

Plan's deploy/wonder-app-vd03/install.ps1 service-recovery-actions edit is
skipped: that production deploy artifact is not tracked in this repo (Task 16
skipped its appsettings.Central.json for the same reason).
2026-07-08 16:56:24 -04:00
Joseph Doherty c73b7faa11 feat(health): metrics-stale signal + status-transition timestamps; spec now matches heartbeat-liveness code 2026-07-08 16:21:30 -04:00
Joseph Doherty 099728f05a fix(transport): wire template inheritance edges on bundle import (C3) — derived templates no longer land as roots 2026-07-08 15:17:47 -04:00
Joseph Doherty 7231c2044b feat(host): fail-fast DisableLogin environment guard at composition root 2026-07-08 14:52:00 -04:00