Commit Graph

4596 Commits

Author SHA1 Message Date
Joseph Doherty 9cddc9306b test: preserve server SDK 2.0 value security and history contracts 2026-11-10 10:39:39 -05:00
Joseph Doherty ac5e9d84f0 fix: propagate SDK request cancellation to historian calls 2026-11-10 10:39:39 -05:00
Joseph Doherty 9712e66ff4 fix: retain certificate ownership and optional alarm workflows 2026-11-10 10:37:28 -05:00
Joseph Doherty 08354231f8 fix: register compatibility aggregates once during server startup 2026-11-10 10:32:13 -05:00
Joseph Doherty 9efd7667e1 build: update HistorianGateway client packages to 0.4.0 2026-11-10 10:32:13 -05:00
Joseph Doherty b71620145b fix: preserve SDK role identity and continuation authority lifecycle 2026-11-10 10:30:18 -05:00
Joseph Doherty a5bed28765 fix: preserve baseline OPC UA aggregate algorithms under SDK 2.0 2026-11-10 10:24:19 -05:00
Joseph Doherty 96fd67695f test: verify SDK 2 driver boundaries and lifecycle contracts 2026-11-10 10:23:28 -05:00
Joseph Doherty 7314f159a0 fix: reject unsupported upstream matrix payloads per item 2026-11-10 10:21:41 -05:00
Joseph Doherty 3dcb035f89 fix: preserve history sentinels and alarm SDK metadata 2026-11-10 10:21:41 -05:00
Joseph Doherty 33c7dc4292 fix: preserve SDK 2.0 client value and lifetime contracts 2026-11-10 10:20:11 -05:00
Joseph Doherty 4400127810 fix: preserve SDK 2 driver value and credential lifetimes 2026-11-10 10:17:38 -05:00
Joseph Doherty f9a006dab4 fix: initialize SDK telemetry and preserve ByteString boundaries 2026-11-10 10:11:32 -05:00
Joseph Doherty 52a4476b21 refactor: migrate OPC UA driver and browser to SDK 2.0 2026-11-10 10:08:47 -05:00
Joseph Doherty 586ce9422d refactor: migrate client applications to OPC UA SDK 2.0 2026-11-10 10:08:47 -05:00
Joseph Doherty 9af1a824b3 refactor: migrate OPC UA server SDK 2.0 compilation and boundaries 2026-11-10 10:04:27 -05:00
Joseph Doherty d2076e75c6 build: pin OPC UA SDK 2.0.1 and migration tooling 2026-11-10 09:51:58 -05:00
Joseph Doherty d9984028b7 build: raise Extensions baseline for OPC UA SDK 2.0 2026-11-10 09:51:15 -05:00
Joseph Doherty 56250a88a4 docs: record OPC UA 2.0 migration baseline and decisions 2026-11-10 09:50:10 -05:00
Joseph Doherty 83ed880e44 build: bump xunit.v3 to 3.2.2 repo-wide, drop the four VersionOverrides (#795)
v2-ci / unit-tests (push) Has been cancelled
Central pin 1.1.0 -> 3.2.2; AdminUI/ControlPlane/Runtime/Host.Tests no longer carry VersionOverride.
The three driver IntegrationTests fixtures' Fact/Theory attributes gain the source-info ctor (xUnit3003).
No xUnit1051 fallout remained (already cleaned). Test counts unchanged.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 09:31:31 -05:00
Joseph Doherty ea4a3c9289 docs: add reviewed OPC UA SDK 2.0 upgrade plan
v2-ci / unit-tests (push) Waiting to run
2026-11-10 09:26:19 -05:00
Joseph Doherty b211911f98 test: migrate AdminUI/ControlPlane/Runtime/Host tests to xunit.v3 (#756)
v2-ci / unit-tests (push) Failing after 13m22s
Akka.TestKit.Xunit2 -> Akka.TestKit.Xunit 1.5.62 (xunit.v3 TestKit; needs xunit.v3 3.2.2, set via
VersionOverride on the four projects). Removes the xunit 2.9.3 pin. IAsyncLifetime returns ValueTask,
SqlServerFactAttribute gains the source-info ctor, and xUnit1051 call sites pass
TestContext.Current.CancellationToken. Test counts unchanged.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 08:57:33 -05:00
Joseph Doherty 2a8e2981fe feat(scripting): ScriptWorker isolation on Windows: restricted worker identity, pipe ACL, firewall + PeCacheDir installer (lmxopcua#780)
v2-ci / unit-tests (push) Has been cancelled
Owner decisions §14 #5/#6 as built (design §12.5):

- Scripting:Worker:Identity (default unset, Windows only, fail-closed validation): each worker
  starts under a restricted token derived from the host's. Every privilege but SeChangeNotify is
  removed; the restricting SIDs are NT SERVICE\OtOpcUaScriptWorker's SID + Everyone + Users +
  RESTRICTED; the token and default DACLs are host + SYSTEM + identity + RESTRICTED. The worker
  is created suspended, put in its Job Object before it runs, inherits only its stdio pipes, gets
  an ACL'd work dir as TEMP, and runs with DOTNET_EnableDiagnostics=0. The identity is granted on
  the host's window station and desktop, because USER32 init made coreclr.dll fail with
  ERROR_DLL_INIT_FAILED on windev. The logon SID is not used because the host's process DACL
  gives it PROCESS_VM_READ.
- The worker pipe on Windows always has an explicit protected DACL: host account + identity
  only. There is no owner ACE, so an elevated host's Administrators gets no access.
- The worker treats access denied on its parent-pid poll as alive; the Job Object covers that case.
- scripts/install/Install-ScriptWorkerIsolation.ps1 + ScriptWorkerIsolation.psm1 are idempotent
  and support -WhatIf, -Uninstall and -PlanOnly. They create the disabled anchor service, the
  PeCacheDir ACL (protected, no Users, key and markers host-only, pe\ Modify for the identity),
  read+execute on script-worker\, a ReadData deny on the install root, the outbound firewall
  block on the worker exe, and optionally the host-service env.
- Tests: the dry-run harness scripts/install/tests/Test-ScriptWorkerIsolation.ps1 (46
  assertions, mutation-checked); ScriptWorkerIdentityTests; WorkerIsolationTests, which cover
  the pipe ACL, token access and a real restricted worker running a script, all green on windev.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 08:46:23 -05:00
Joseph Doherty 0e96dd08a2 fix(scripting): deny System.AppContext and other process-wide mutators in the script sandbox (lmxopcua#792)
v2-ci / unit-tests (push) Waiting to run
ForbiddenTypeAnalyzer now denies System.AppContext, System.Console, GCSettings, ProfileOptimization,
EncodingProvider/CodePagesEncodingProvider, TypeDescriptor and CryptoConfig as types, and the ambient
CultureInfo.{DefaultThreadCurrentCulture,DefaultThreadCurrentUICulture,CurrentCulture,CurrentUICulture}
and TimeZoneInfo.ClearCachedData members (member deny-list now covers properties and member-access reads).
Shadow-mode tests diverge via culture-sensitive string.Compare (worker is InvariantGlobalization) instead of
AppContext.BaseDirectory, with a premise test. Mutation-proved: 21 new cases fail with the analyzer reverted.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 07:48:39 -05:00
Joseph Doherty b1eedceacf docs(scripting): §12.4 — Shadow mode as built, its tests, and the docker-dev drill with alarms (lmxopcua#780 P2)
v2-ci / unit-tests (push) Has been cancelled
Shadow on central-1/2 with all three seams: zero mismatches on ordinary VTs / Calc / alarms, counted mismatches on
exactly the divergent (value) and decimal (unsupported_type) scripts, kill -9 of the shadow workers left every
published value Good. OutOfProcess with ScriptedAlarm listed: alarms raise and clear, a runaway predicate is killed
twice and quarantined (BadInternalError) with its siblings Active and Good, kill -9 under an in-flight predicate
freezes it Active + BadResourceUnavailable and it re-evaluates in isolation. Answers A-7 (Modbus UInt16 boxes as
ushort) and A-8 (a decimal VT publishes BadNotSupported in process). Rig restored to revision e221b179.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 07:36:11 -05:00
Joseph Doherty 03686eb5d7 feat(scripting): Scripting:Runner=Shadow — in-process results published, each compiled run mirrored to the ScriptWorker pool and compared (lmxopcua#780 P2)
- ShadowScriptRunner (Core.Scripting): IScriptShadow.Mirror copies the inputs and queues the shadow run on the
  pool; compares value (structural), CLR type, status and failure class (compile / budget / threw+message /
  unsupported_type); runner-fault shadow outcomes (Unavailable, worker busy) are not compared. Bounded:
  Scripting:Shadow:MaxInFlight (256) and one per script; drops counted (behind | key_busy).
- Metrics otopcua.script.shadow.{compared,mismatch{script},dropped,inflight}; one rate-limited Warning per
  script and reason with a diff summary (Scripting:Shadow:MismatchLogsPerMinute, re-armed at the seam's deploy).
- The VirtualTag, Calculation and scripted-alarm seams recognise the shadow and keep their in-process path
  byte-for-byte (no coalescer, one-at-a-time pump, in-process engine), mirroring after each compiled run.
- ScriptRunnerSelection builds the shadow over an OutOfProcessScriptRunner whose lines carry
  ScriptRunnerRole=Shadow; Status() is null under Shadow so readiness never sees the shadow pool; the
  script-runner health entry stays Healthy and reports the shadow counters as data.
- docker-dev/docker-compose.script-runner-shadow.yml; docs/VirtualTags.md "Shadow mode".
- Tests: comparison table, per-script metric + once-per-reason log, rate limit, hung/blocking/throwing shadow
  never reaches the caller, MaxInFlight drop accounting, input copy; real-worker divergent + unsupported-type
  scripts counted against exactly those scripts, a worker-only runaway killed/quarantined there with every
  published value intact, alarm transitions identical to in process across a kill -9 of the shadow worker.
  Mutation-proved (16 mutants, 15 red; the one survivor added an assertion).

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 07:23:06 -05:00
Joseph Doherty 0796928504 feat(scripting): batch submit, exact per-item deadlines, scripted-alarm seam on the out-of-process runner (lmxopcua#780)
v2-ci / unit-tests (push) Waiting to run
- IScriptRunner.RunBatch: one EvalBatch frame per worker for every evaluation ready to run; per-item, streamed results.
  The default (InProcessScriptRunner) is RunAsync per item. Used by the Calculation pump (dequeue all, split at a
  repeated tag, publish in dequeue order), the VirtualTag seam (ScriptBatchCoalescer, no timer) and the scripted-alarm
  engine (whole dirty set). In process the pump and the engine stay one at a time, unchanged.
- Exact per-item deadlines: the worker hands work to an idle evaluation thread or defers it (EvalResults
  deferred_eval_ids); the host stops that run's clock and restarts T then G when it starts (started_eval_ids). A run
  queued behind a runaway is neither timed out nor struck and is a victim, not the culprit, at the kill.
- Scripted-alarm seam behind Scripting:Runner:Seams; unset Seams now means VirtualTag + Calculation, so alarms run out of
  process only when listed. Load still compiles each predicate (to PE bytes only) so a broken one fails the load; passes
  batch and apply in the serial order under the gate. Script faults freeze the condition BadInternalError; runner faults
  freeze it BadResourceUnavailable and re-evaluate on recovery; a dead worker never clears an alarm. IScriptRunnerProvider
  carries the runner to DriverHostActor.
- Perf harness: batch throughput (RunBatch vs RunAsync x n) and alarm-pass latency (in process / batched / serial);
  design §12.3 records macOS + windev. On windev the serial out-of-process alarm pass misses 250 ms from ~500 alarms;
  batched p99 <= 53 ms at 1,000.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 06:37:36 -05:00
Joseph Doherty 11a5c1d4e2 test: deflake load-sensitive tests (lmxopcua#769)
v2-ci / unit-tests (push) Has been cancelled
- NodeManagerLockTimeBenchmark: wall-clock budget enforced only with OTOPCUA_BENCH=1; default run keeps the state-correctness asserts and logs timings.
- OpcUaClientDriverWire host-status: signal (semaphore) wait that only accepts a non-Running state observed after the server is disposed.
- AbCip PollLatencyComparison: retry once on a fixture connection reset; p95 criterion enforced only with AB_LAT_ENFORCE=1.
- ScriptRunGuardTests: orphan scripts held by a ctx.Released signal instead of a 250 ms wall-clock spin that could finish inside the 60 ms budget under load; stuck-orphan wall-clock rule disabled in the tests that pin the strike counting.
- DependencyMuxReplayTests: split the inside-grace positive control (1 h grace) from the release test (control asserted before unregister), removing the 200 ms grace race.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 05:39:22 -05:00
Joseph Doherty 000d7f5591 fix(virtualtags): route VirtualTag outputs through the dependency mux so VT->VT reads work (lmxopcua#791)
v2-ci / unit-tests (push) Waiting to run
Proven broken first: VirtualTagToVirtualTagFlowTests (real mux + host + Roslyn
evaluator) failed on master - VT A evaluated, VT B reading A by its key never did.
VT outputs only reached their OPC UA node; the mux carried driver values only.

- VirtualTagHostActor also sends each real result (not re-asserts) and a disabled
  tag's BadOutOfService to the mux as VirtualTagOutput, keyed by the VT's UNS
  NodeId identifier {EquipmentId}/{Name}. Publish path unchanged (single source).
- DependencyMuxActor pins VT outputs (retained with no subscriber, no grace,
  immune to SourceRemoved) and replays them; VirtualTagOutputsRemoved releases
  keys that leave the deployed set.
- DraftValidator: blocking VirtualTagDependencyCycle (A->B->A, self-read, loops
  through Calculation tags, {{equip}} refs resolved); disabled VTs are not nodes.
  EquipReferenceUnresolved names the key form when {{equip}}/<name> is a VT.
- Runtime backstop: the host re-detects VT cycles per apply and does not route
  outputs of tags on one.
- AdminUI ScriptTagCatalog lists VTs by key (the bare leaf name never resolved).
- docs/VirtualTags.md documents the routing, key, retention and cycle rules.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 05:17:00 -05:00
Joseph Doherty 70050c9d7e test(opcuaclient): slow server-side subscription delete keeps the stale-BadNoSubscription grace running from delete completion (lmxopcua#764)
v2-ci / unit-tests (push) Has been cancelled
Unsubscribe paths now delete through the existing DeleteSubscriptionAsync seam so a test can hold the delete open >grace. Mutation: dropping the post-delete restamp fails both slow-delete tests.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 05:05:18 -05:00
Joseph Doherty f201f16fd4 fix(virtualtags): a disabled VirtualTag keeps its node and reads BadOutOfService (lmxopcua#787, owner decision)
v2-ci / unit-tests (push) Has been cancelled
Revises 963b0d66, which dropped a disabled VirtualTag from the runtime plan so a
bound client got BadNodeIdUnknown. Now the plan carries Enabled (part of value
equality): AddressSpaceComposer.Compose and DeploymentArtifact.BuildEquipmentVirtualTagPlans
keep the row with Enabled=false (byte-parity), the applier materialises the node as
normal, and VirtualTagHostActor spawns no VirtualTagActor for it but publishes
BadOutOfService (new OpcUaStatus constant) with a null value stamped at deploy, on
every apply. Enabled flips are node-irrelevant to the change classifier; enabled->disabled
stops the child, disabled->enabled resumes evaluation. Validator exemption and name
uniqueness unchanged. Scripted alarms keep their not-materialised convention.
Tests updated; new wire test DisabledVirtualTagWireTests. docs/VirtualTags.md updated.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 04:48:57 -05:00
Joseph Doherty 8582ce91bc perf(scripting): cut out-of-process runner host CPU; compile timeout from compile start; quiet start-failure logs (lmxopcua#789, #790, #788)
v2-ci / unit-tests (push) Has been cancelled
#789 (partial, further tuning deferred): combining inline frame writer, buffered
frame reader, one timer per evaluation for T and T+G, slot release on the reader
before the caller resumes, a non-spinning per-connection result dispatcher, and
blocking pipe I/O on Unix (both ends) so the socket never enters the socket
engine; TerminateJobObject kill on Windows. macOS host CPU at 10k evals/s K=2:
228-240 % -> 79 % (InProcess 69 %, IPC share +10 vs budget 25); windev 153 % ->
123 % (+115, still a miss). Harness reports the "vs InProcess" share, warm-cache
worker RSS, and times kill latency from the soft-timeout answer.

#790: the worker sends LoadStarted; the compile timeout is armed then, so a load
queued behind other compiles is never killed or struck.

#788: one Error with stack per down period, rate-limited Warnings with attempt
count, one Information on recovery.

Design doc §8.2 notes + §12.2 record.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 04:25:08 -05:00
Joseph Doherty 963b0d66fc fix(virtualtags): a disabled VirtualTag is no longer deployed or evaluated (lmxopcua#787)
v2-ci / unit-tests (push) Waiting to run
VirtualTag.Enabled was authorable but ignored by both runtime-plan seams.
AddressSpaceComposer.Compose and DeploymentArtifact.BuildEquipmentVirtualTagPlans
now drop disabled rows (byte-parity), so no node is materialised and no
VirtualTagActor is spawned - the same outcome as a disabled scripted alarm.
DraftValidator exempts a disabled VT from VirtualTagNoEffectiveTrigger.
Calculation tags have no Enabled flag, so no equivalent gap. docs/VirtualTags.md updated.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 03:15:22 -05:00
Joseph Doherty 1ead8f458a feat(scripting): self-contained script worker publish, docker-dev drill, §8 perf harness (lmxopcua#780 P1)
v2-ci / unit-tests (push) Waiting to run
- A self-contained Host publish (-r win-x64 --self-contained, the Windows installs) now publishes the
  ScriptWorker self-contained for the same RID into script-worker/. The launcher starts its apphost
  directly when runtimeconfig.json lists includedFrameworks. A framework-dependent worker (docker-dev)
  keeps running through the dotnet muxer.
- docker-dev/docker-compose.script-runner.yml: OutOfProcess for the central pair's VT + Calc seams.
  Live drill recorded in design §12.1 (runaway kill + quarantine, kill -9 recovery, open circuit
  with /health/ready still 200, metrics).
- ScriptRunnerPerfTests (Category=Perf, opt-in via OTOPCUA_SCRIPT_PERF=1): InProcess vs OutOfProcess
  at K=1/2/4. The accepted 5k evals/s/node gate passes on macOS and windev. The host-CPU, RSS and
  Windows latency budget misses are tracked as #789.
- Fix: the single-flight slot is released before the awaiting caller resumes. Before, a caller that
  started its next run right after the previous one returned could be refused with "previous
  evaluation still running", which the seams publish as Bad.
- Fix: the worker compiles on dedicated threads, not the thread pool. A burst of cold loads starved
  the heartbeat, and the host killed the shard (found on windev). Load-timeout queueing is #790.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 02:53:53 -05:00
Joseph Doherty f50b29abae feat(scripting): PE cache, quarantine-race fix, recovery, readiness for the out-of-process runner (lmxopcua#780 P1)
v2-ci / unit-tests (push) Failing after 1h12m10s
Second P1 slice of docs/plans/2026-11-09-out-of-process-script-runner-design.md; still opt-in behind
Scripting:Runner=OutOfProcess (InProcess unchanged).

- Compiled-PE cache (design §7, §14 #8): Scripting:Worker:PeCacheDir holds <dir>/pe/<id>.pe entries authenticated
  with HMAC-SHA256 under a per-node random key (<dir>/pe-cache.key, created by the host, 0600/0700 on Unix), handed to
  the worker in Hello. Entry id = SHA-256(compile fingerprint + kind + source); the fingerprint covers the Roslyn
  version, the Core.Scripting build (analyzer, wrapper, stack guard), the sandbox reference set and the runtime. Edited,
  truncated, other-key or renamed entries are rejected, deleted and recompiled; the worker never loads unverified bytes.
  Pruned by recency at startup (30 d / 50 000). ScriptEvaluator splits into CompileToImage + FromImage. Measured on
  40 scripts, restart to every first result: macOS 820 ms no cache / 840 ms empty / 85 ms warm;
  windev (Windows) 9.5 s / 9.4 s / 1.0 s.
- Quarantine race: WorkerConnection runs exit attribution synchronously BEFORE completing the evaluations/loads it held
  (and before Exited), and the shard passes it straight to the runner — a kill strike is in place before the killed
  key's slot is released.
- Supervision: a dead worker restarts by itself after its backoff; Unavailable outcomes carry ScriptRunOutcome.Recovery
  (-> VirtualTagEvalResult.RetryWhen) and the VT actor / Calculation driver re-evaluate once against current inputs when
  it completes. Isolation worker stops after Scripting:Worker:IsolationIdleTimeout (10 min). Keys compiling when a
  worker dies are suspects too (isolation finds a poison compile and quarantines it). Startup orphan sweep by launch
  marker: kills only a process whose marker's host is gone and whose pid + start time + image match.
- Crash-loop circuit (Scripting:Worker:CircuitOpenAfter, 2 min): runner Unavailable, one Error, evaluations answered
  at once while shards keep retrying at the cap.
- Observability (§10): otopcua.script.runner.state, worker.up/inflight/queue_depth/heap_bytes (shard),
  ipc.roundtrip + eval.duration (kind), compile{cache}, isolation.suspects, exits reason=idle. `script-runner` health
  check on the ready tier, at worst Degraded (§14 #12: degraded but still ready).

Left on #780: self-contained worker publish, installer (account, pipe ACL, firewall), docker-dev live drill,
scripted-alarm seam, the §4.5 per-seam batch submit, the §8 perf harness.

Tests: SupervisionTests, PeCacheStoreTests/PeCacheWorkerTests (real workers), ScriptRunnerHealthCheckTests, VT + Calc
recovery seam tests; 14 mutations each turned their test red.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 00:20:24 -05:00
Joseph Doherty 69a6e0b4c8 feat(scripting): out-of-process pre-emptible script runner behind Scripting:Runner (lmxopcua#780 P1)
v2-ci / unit-tests (push) Waiting to run
Opt-in (Scripting:Runner=OutOfProcess; InProcess stays the default and its code path is unchanged). Implements
the P1 slice of docs/plans/2026-11-09-out-of-process-script-runner-design.md with the owner's §14 decisions:

- Options: Scripting:Runner (InProcess | Shadow | OutOfProcess), Scripting:Runner:Seams, Scripting:Worker:*
  (Shards 2, HardGrace max(1 s,T), HeapLimitBytes 512 MiB, QuarantineAfterKills 2, UnavailableGrace 5 s,
  IsolationProbationRuns 20, PeCacheDir validated absolute + outside the content root (fail closed),
  ExecutablePath). Shadow is not implemented yet: runs InProcess with a startup Warning.
- Core.Scripting: IScriptRunner seam + InProcessScriptRunner (today's guard, the parity baseline).
- Core.Scripting.Protocol: protobuf envelopes, 4-byte LE framing (zero/oversize/truncated/garbage refused),
  Variant carrying the exact CLR type; unsupported types are explicit failures.
- ScriptWorker app: compiles in the worker, P-thread scheduler, host-driven soft cancel, fail-fast
  watchdog, heartbeats, ctx.Logger forwarding, exits on pipe EOF / parent gone, forbidden-assembly
  self-check; Akka and Grpc.Core.Api excluded from its folder. Shipped in script-worker/ (Build + Publish).
- Core.Scripting.OutOfProcess: K shards + isolation worker, soft T / hard T+G kill, attribution
  (deadline kill = one key; crash = suspects -> isolation; isolation crash = strike), kill-strike
  quarantine, backoff 250 ms -> 30 s, fail closed, cleared allow-listed env, nonce + pid handshake on a
  current-user pipe, Windows Job Object.
- VirtualTag + Calculation seams use the runner when selected; a runner-side failure (worker restarted /
  runner unavailable) publishes BadResourceUnavailable (0x80040000) — ScadaBridge#112. Script faults keep
  BadInternalError. The scripted-alarm seam stays in process (alarms last).

Tests: new ZB.MOM.WW.OtOpcUa.ScriptWorker.Tests (Category=ScriptWorker, real processes; green on macOS and
on windev/Windows), seam tests in Host/Runtime/Calculation suites; mutation-checked.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 23:33:08 -05:00
Joseph Doherty f5c30ed0ef feat(sql): authored write block — Update/Insert/Upsert/Procedure + timestamp stamp; ODBC scope + login docs (lmxopcua#781)
v2-ci / unit-tests (push) Has been cancelled
Item 2: a tag's optional `write` block picks the statement the driver builds from its own
catalog-validated identifiers (no authored SQL; values always bound):
- Update (default, unchanged back-compat UPDATE ... WHERE key; 0 rows = BadNoData)
- Insert (key+value[+stamp]; append for a WideRow topByTimestamp tag)
- Upsert: SQL Server MERGE WITH (HOLDLOCK), PostgreSQL INSERT ... ON CONFLICT, MySQL
  ON DUPLICATE KEY UPDATE (1-or-2 rows), Oracle MERGE from DUAL
- Procedure: named stored procedure resolved against the routine catalog by the catalog gate
  (INFORMATION_SCHEMA.ROUTINES / ALL_PROCEDURES), value/key/timestamp params by name (MySQL positional)
- stampTimestamp sets the tag's timestamp column to the write time (UTC)
Odbc serves Update/Insert only (shared SqlWriteSupport table). Constraint failures map to
BadEntryExists (unique) / BadOutOfRange (other integrity, data exception); everything else,
permission denied included, stays BadCommunicationError. WriteIdempotent stays false.
The parser refuses malformed blocks (strict keys/enums/param charset); DraftValidator emits
blocking SqlWriteBlockInvalid / SqlWriteModeUnsupported from the driver's own parse; the AdminUI
editor surfaces the same message.

Items 1 + 4 (docs/drivers/Sql.md): ODBC stays the ANSI SQL:2008 profile (native providers for
SQL Server/MySQL, DB2 unsupported) with an ops note on driver manager + vendor driver on every
Odbc and AdminUI host; read-only drivers require a SELECT-only login, writable drivers a login
limited to their write targets.

Tests: SqlWriteModeTests (parse, every dialect's SQL, gate, status map, SQLite end-to-end),
DraftValidatorSqlWriteBlockTests, AdminUI round-trip; live SqlWriteModesLiveTests on SQL Server,
PostgreSQL, MySQL, Oracle (seeds gain write_targets + set_target) and an Odbc write leg.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 22:59:09 -05:00
Joseph Doherty 2da58d0360 build(deps): OPC Foundation SDK 1.5.378.106 -> 1.5.378.182 (lmxopcua#782 interim hop)
v2-ci / unit-tests (push) Has been cancelled
Behaviour-neutral hop: pin the SDK AggregateManager default to our
TreatUncertainAsBad=false (SDK .182 flipped it to true), parity guard now an
explicit deliberate-divergence assertion plus a booted-server check; adapt to
obsolete FindPredefinedNode(Type)/TraceConfiguration; Microsoft.Extensions.
Logging.Abstractions 10.0.7 -> 10.0.8 (SDK floor); stall wire test's late-flush
positive control is best-effort (.176 preserves publish queue ordering).

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 22:47:58 -05:00
Joseph Doherty 42c73bfb83 fix(adminui): confirm before deleting role grants, clusters, nodes and ACLs (lmxopcua#785)
v2-ci / unit-tests (push) Waiting to run
The four Delete buttons deleted on the first click. They now open the shared
RawConfirmDialog naming the item; only Confirm deletes. Adds bUnit (test-only)
render tests: modal opens without deleting, Cancel keeps the item, Confirm deletes.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 22:22:47 -05:00
Joseph Doherty 20820bbf76 docs(review): record #761 gate D AdminUI live pass (4/5 PASS; 4 unconfirmed deletes)
v2-ci / unit-tests (push) Has been cancelled
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 22:15:17 -05:00
Joseph Doherty f717735753 test(virtual-tags): re-apply until the host has pruned the dead child (lmxopcua#769)
v2-ci / unit-tests (push) Has been cancelled
Child_is_respawned_after_unexpected_termination watched the child die, then re-applied once. The host prunes _children only when its own Terminated arrives, so a re-apply that lands first still sees vt-1 as present, spawns nothing, and the test times out at 15 s (caught 1 in 5 full Runtime.Tests runs on 2026-11-09). Re-apply inside AwaitAssert until the new RegisterInterest arrives; the assertions are unchanged. 30/30 isolated runs pass.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 21:59:35 -05:00
Joseph Doherty 850c5405d0 fix(scripting): name the script on the mirrored quarantine line; keep BadInternalError (lmxopcua#784)
v2-ci / unit-tests (push) Has been cancelled
(a) ScriptRunGuard logs its quarantine Error through the seam's script root
logger, which carries no ScriptName, so ScriptLogCompanionSink mirrored it to
the main log as "[Script] unknown emitted Error ..." and rate-limited every
quarantine on the node under one shared "unknown" key. The guard now binds the
script instance key as ScriptName (ForContext) on the quarantine Error and the
subscriber-threw Warning, so the mirror names the script and limits each one
separately. Test drives two real quarantines through the companion sink with a
1-per-window limiter: both mirrored, each named, none "unknown". Mutation-
checked: dropping the ForContext fails it.

(b) Evaluated a quarantine-specific OPC UA status (BadOutOfService) and kept
BadInternalError: it is the documented code for any script-attributable fault
on virtual tags, Calculation tags and scripted-alarm quality (and the
out-of-process runner design keeps it), BadOutOfService reads as a deliberately
disabled point, a new code is a ScadaBridge-visible contract change, and the
once-per-Good->Bad dedup means a timeout-driven quarantine would rarely show a
different code anyway. Documented in docs/VirtualTags.md ("Quarantine: what a
client sees"), with pointers from Calculation.md, ScriptedAlarms.md and the
OpcUaStatus.BadInternalError doc comment, plus where a quarantine IS visible.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 21:30:17 -05:00
Joseph Doherty 80118a01c5 fix(deploy): persist which nodes never acked on a deploy timeout (lmxopcua#784)
In the normal timeout branch (some acks landed) the deployment row was set to
TimedOut with a null FailureReason, so GET /api/deployments/{id} could not say
which node to fix; only the central log named it. The branch now persists a
reason naming the silent nodes ("Timed out waiting for acks (n/m landed). No ack
from: ..."), bounded to 20 named nodes plus a count of the rest, and still
backstopped by the 2048-char Truncate.

Tests: the reason names the silent node and not the acked one; on a 25-node
silence it names 20, counts 5 and fits the column. Mutation-checked: dropping
the SetFailureReason fails both, dropping the bound fails the second.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 21:30:17 -05:00
Joseph Doherty 43f61eba2e fix(mesh): stop reporting override-keyed clusters as REVOKED (lmxopcua#784)
The docker-dev centrals logged "Mesh PSK secret ot-mesh-psk-<cluster> ... is
absent (or blank) in the store; the key is REVOKED" for every cluster on every
contact refresh, while every gRPC dispatch and ack authenticated.

Investigated: not an auth bypass. The rig supplies each cluster's key through
MeshTransport:ClusterPsks (central side, equal to that cluster's nodes'
MeshTransport:GrpcPsk) and seeds nothing in the store. TryGetPsk/Matches consult
the override first on every call, so enforcement was on; only RefreshAsync read
the store for those clusters regardless and logged a revocation that never took
effect. RefreshAsync now skips clusters covered by a non-blank override, so the
REVOKED warning is only logged for a cluster that genuinely has no key.

Test pins it with a positive control (a cluster with no override is still read,
warned about and refused) and enforcement on the override key (wrong token
rejected). Mutation-checked: removing the skip fails the test.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 21:30:17 -05:00
Joseph Doherty d9b95dcf56 fix(virtualtags): consume VirtualTag.TimerIntervalMs; add VirtualTagNoEffectiveTrigger gate (lmxopcua#783)
v2-ci / unit-tests (push) Waiting to run
Choice: implement the timer (option a). The field was designed and promised as a
working trigger: the entity doc says ChangeTriggered and TimerIntervalMs together
realize the trigger model, the DB check constraint requires ChangeTriggered OR a
timer >= 50, the AdminUI virtual-tag modal exposes "TimerIntervalMs (optional)" and
UnsTreeService rejects a tag with neither, and the 2026-06-08 design specified
"ChangeTriggered OR TimerIntervalMs>=50". Only the runtime never honoured it (the
standalone TimerTriggerScheduler was deleted in #603), so a timer-only tag sat at
BadWaitingForInitialData forever.

- EquipmentVirtualTagPlan carries ChangeTriggered/TimerIntervalMs (in equality, so
  an edit respawns the child); both the composer and the artifact decode fill them.
- VirtualTagActor schedules a repeating tick (PreStart, cancelled in PostStop,
  clamped to the 50 ms floor). ChangeTriggered=false makes the timer the only
  trigger. A tick during an in-flight async run (#779) is dropped, not stashed, so
  slow scripts never build a backlog of runs.
- DraftValidator VirtualTagNoEffectiveTrigger (hard error, same severity as
  CalculationNoEffectiveTrigger): no timer and no change trigger with a literal
  ctx.GetTag read.

Tests: VirtualTagActorTimerTests, DraftValidatorVirtualTagTriggerTests, artifact
decode test. Mutation-checked: no scheduling, stashing ticks, no cancel in
PostStop, and removing the validator call each fail a test.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 21:27:59 -05:00
Joseph Doherty 7891b3cb54 docs(reviews): record driver fixture + H1 gates (lmxopcua#761)
v2-ci / unit-tests (push) Has been cancelled
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 21:09:48 -05:00
Joseph Doherty 7cbeb9a7cc docs(reviews): record docker-dev Wave-4 gates A/B/C/E (lmxopcua#761)
Gates A (#731), B (#732), C (#733) and E (#734) run on the local docker-dev
rig against origin/master 1b9c36f3 on 2026-11-09: all four PASS. Records the
procedure, the measured thread/fd/RSS, subscription and deploy-status
numbers per gate, plus six non-blocking observations.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 21:09:02 -05:00
Joseph Doherty 1b9c36f38d docs(plans): OPC UA SDK 2.0 upgrade spike catalogue (lmxopcua#782)
v2-ci / unit-tests (push) Waiting to run
Catalogue of what breaks moving from 1.5.378.106 to 2.0.1: restore
break (MEL 10.0.12), 42 -> 103 compile errors in the first two waves,
root-cause groups with API mappings, PeerAddress replacement for the
reflection-based remote endpoint resolver, effort and risks.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 20:32:50 -05:00
Joseph Doherty c5df43d594 fix(historian): ContinuousHistorization CommitMode code default is PerEntry, matching appsettings/docker-dev/fallback (lmxopcua#776)
v2-ci / unit-tests (push) Has been cancelled
The Lane H2 remediation made Periodic(100 ms) the options-class default, but
appsettings.json, docker-dev and Program.cs's bad-value fallback all used
PerEntry, so effective behaviour never changed and the docs contradicted the
shipped config. The options default is now PerEntry; Periodic stays opt-in
(<= CommitIntervalMs of crash loss in exchange for throughput). Program.cs now
uses ContinuousHistorizationOptions.ResolveCommitMode() so the fallback shares
the single DefaultCommitMode constant. docs/Historian.md updated.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 20:27:06 -05:00
Joseph Doherty 1ecd01d3e0 build(deps): bump ZB.MOM.WW.Auth 0.3.0 -> 0.4.1 (scadaproj#5)
v2-ci / unit-tests (push) Successful in 1h4m29s
Auth 0.4.1 (BindMode stays ServiceAccount; empty password now refused in the library too; Username canonicalised from UserNameAttribute). No API breaks, no test changes needed.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 18:29:40 -05:00