Commit Graph

2842 Commits

Author SHA1 Message Date
Joseph Doherty 5962cc3d41 docs(scripting): T starts at the run's start but is wall clock from there; document the late-notice, ceiling and counter rules (#827 review F4)
The proto comment, VirtualTags.md and CLAUDE.md said the soft deadline
measures the script's own execution and that starvation never counts. Only
the time before the run STARTS is excluded; from the start it is wall-clock
time, as in process, so a GC pause or a starved / suspended worker during
the run still counts. A mid-run freeze test is not added: it races the
script's own return against the watchdog's notice; documented as not covered.
Also documents F2 (late notice cancels and counts), F3 (progress ceiling,
killed_unresponsive) and F5 (answer = stalled | retrigger).
2026-11-11 02:26:28 -05:00
Joseph Doherty f50209082e fix(scripting): count re-triggers answered 'worker unresponsive' (#827 review F5)
otopcua.script.ipc.timeouts gains the tag answer = stalled | retrigger: a
later trigger refused while the key's stalled run is still unanswered was
counted nowhere (not a script refusal, so not ScriptRunGuard's counter).
KeyState carries its Kind for the tag. Test: the stalled-worker test listens
for the instrument and sees one stalled and one retrigger (before: only
stalled).
2026-11-11 02:25:17 -05:00
Joseph Doherty 3f5fdd1d90 fix(scripting): host progress ceiling kills a worker that heartbeats but never answers (#827 review F3)
With the worker soft clock the host's per-run timer is only the transport
deadline and the kill G is armed by the worker's soft notice, so a worker
whose watchdog never sends one (the design treats the worker as untrusted)
held the run, the key's slot and the tag's BadResourceUnavailable forever;
a deferred run had no host clock at all.

WorkerConnection now tracks evaluation progress (any Results frame, or the
submit that made the worker busy again). The heartbeat monitor kills the
worker when evaluations are outstanding and nothing progressed for
3 x the largest outstanding T + G plus the silence limit:
WorkerExitReason.KilledUnresponsive (metric reason killed_unresponsive), no
culprit, attributed like a heartbeat kill (suspects, no strike on a shard).
A legitimate run has answered or been killed by T + G.

Test seams: ProgressCeilingOverride, DropSoftNoticesForTest. Tests: a worker
whose notices are dropped (G = 60 s, so its own fail-fast cannot end it) is
killed KilledUnresponsive after the 1.5 s ceiling and the run is released
(with the check disabled the test times out); positive control: 20 queued
150 ms runs behind one thread under a 1 s ceiling are all answered.
2026-11-11 02:24:01 -05:00
Joseph Doherty c5dffbd8b8 fix(scripting): soft notice resumes the caller on the dispatcher; a late notice still cancels and counts (#827 review F1, F2)
F1: PendingEval.SoftExpired runs on the pipe reader and completed the
caller's outcome inline (production outcomes have no async continuations, by
design, because results resume on the ResultDispatcher). The caller's code
(the runner's Cancel write, the seam's publish) therefore ran on the reader,
which could hold up heartbeats (a false silence kill) and, with blocking I/O,
write to the pipe from the reader. The soft outcome is now posted to the
dispatcher like a result. Test: a 1.5 s continuation on a soft timeout with a
300 ms silence limit resumes on ScriptWorker[t].Results and the worker lives
(before: it resumed on the Reader).

F2: after a transport timeout ("worker unresponsive"), a late soft notice
armed G but nobody sent Cancel, so a polling script could not end itself and
the whole worker was killed at notice + G with a kill strike. PendingEval now
exposes LateSoftExpiry (asynchronous continuations, cancelled when the run
answers); the runner sends the Cancel and counts the timeout
(NoteTimeout) from it. A genuinely slow script whose notice is delayed past
T + G is thus still a timeout. Test: stall past T + G, thaw, a polling
script overruns T: the worker survives G and the key is quarantined for
timeouts, not a hard deadline (before: the worker was killed).
2026-11-11 02:21:45 -05:00
Joseph Doherty 30c6c3a0bf test(scriptworker): tight-heartbeat tests warm the worker before arming the 3-interval silence kill (#827 audit)
A_burst_of_cold_compiles_does_not_starve_the_heartbeat (50 ms heartbeat) and
A_caller_that_blocks_after_its_result_does_not_starve_the_reader (100 ms)
launched their worker under the tight silence kill, so its cold start (launch,
first compile, Roslyn JIT) had to stay within 150/300 ms of silence. Under 28
busy processes on 14 cores that failed 2 in 10 runs in the WARM-UP, killed
for ~165 ms of silence, and replacement workers were killed the same way.

New test seam: WorkerLaunchSpec.HeartbeatSilenceLimit (runner internal ctor
heartbeatSilenceLimit) and WorkerConnection.SilenceLimit. The tests launch
with a 30 s limit, warm, then tighten to three intervals for the phase they
are about; the assertions on that phase are unchanged.
2026-11-11 01:59:11 -05:00
Joseph Doherty f7ca2c5826 fix(scripting): out-of-process soft deadline measures the script's own run, not the pipe or a starved worker (#827)
The soft deadline T was armed on the host at submit, so the pipe round trip
and the time a starved worker waits to be scheduled counted against the
script: under load a trivial script reported 'script timed out', in Shadow
mode that was a script mismatch, and at T + G the host killed the worker and
struck the key.

- Protocol (additive, negotiated): Hello/Ready.worker_soft_clock and
  EvalResults.soft_expired_eval_ids. protocol_version stays 1; a worker that
  does not echo the flag keeps the pre-#827 host clock.
- Worker: the watchdog runs each run's soft clock from the moment an
  evaluation thread starts it and reports the crossing (woken early for
  budgets shorter than its 50 ms poll). It still never cancels by itself;
  its fail-fast is anchored at the soft notice + G + margin.
- Host (PendingEval): the soft notice answers the caller and arms the kill G
  later. The host's own timer becomes a transport deadline, T + G from submit
  (restarted at a deferred run's start); passing it with no result and no
  notice yields TransportTimedOut -> Unavailable 'script worker unresponsive'
  (BadResourceUnavailable, recovery = the run's completion), no strike, no
  kill, no Cancel. A re-trigger refused by that still-unanswered run is
  Unavailable too, not 'previous run still running'. Counted as
  otopcua.script.ipc.timeouts{kind}.
- Frame writer: coalescing now carries soft_expired_eval_ids (it silently
  dropped them); the host processes soft notices before results in a frame.
- Shadow: unresponsive is shadow_unavailable (Unavailable), never a mismatch.

Tests: WorkerSoftClockTests suspends the real worker (SIGSTOP via libc kill /
NtSuspendProcess) - a stall past T still returns 7; a stall past T + G is a
runner fault with nothing struck; a late-starting runaway is still timed out
at T from its start and killed G after; Shadow stays match /
shadow_unavailable; both clocks enforce T and G; PendingEval state machine.
2026-11-11 01:38:46 -05:00
Joseph Doherty dea5b84af0 test(runtime): late-burst SourceRemoved test no longer races the 5 s PostStop budget (#828)
The test parks the removed child in PostStop (ShutdownGate) to hold the
retired-but-not-terminated window open, but PostStop abandons a shutdown after
DefaultShutdownTimeout (5 s). Under a loaded suite run more than 5 s can pass
between the stop and the host processing the stale burst; the child then
terminates, the host forgets it on Terminated, and the burst sent AS it is
forwarded and re-retained. Reproduced deterministically with a 6 s sleep
before the burst. Not a production path: a terminated actor cannot send.

DriverHostActor gains a childShutdownTimeout test seam (last parameter, passed
to DriverInstanceActor.Props). The test raises it to 5 min, asserts the child
was really stopped (ShutdownCount) before the burst, checks the premise as a
positive control (no Terminated by the time the fence has passed), and expects
Terminated once the gate is released. With an 8 s sleep before the burst the
fixed test passes.
2026-11-11 01:38:46 -05:00
Joseph Doherty 5bbe1e3c7a fix(heidenhain): resolve the probe's secret resolver lazily — eager resolution crash-looped central nodes at boot (#803)
v2-ci / unit-tests (push) Successful in 1h9m7s
The Test-Connect probe set is resolved inside the AdminOperations Akka starter; resolving
ISecretResolver there walks AddZbSecrets -> IAuditWriter -> ActorAuditWriter, whose ctor needs
AuditWriterActorKey (registered later in the same builder). Found by the docker-dev live gate.
Regression test: resolving the probe set never resolves ISecretResolver (fails before the fix).
Also: TagRowProblem doc comment re-attached; Heidenhain credential help text names the
secret:heidenhain/ and env:OTOPCUA_HEIDENHAIN_ namespaces.
2026-11-11 00:27:37 -05:00
Joseph Doherty 7af9c40e41 Merge feat/803-heidenhain-lsv2 2026-11-11 00:03:43 -05:00
Joseph Doherty 54f58c583c fix(heidenhain): refuse literal credentials and writable/mistyped tags at the save path (#803 review)
- Review 1: RawTreeService Create/UpdateDriver and Create/UpdateDevice refuse a Heidenhain
  config whose password fields are not blank / secret:heidenhain/... / env:OTOPCUA_HEIDENHAIN_...
  (DriverConfigSaveGate over HeidenhainOptionsParser.DescribeCredentialProblems: top level,
  Access, Devices[], case-insensitive keys; the value is never echoed), so a literal password
  never reaches the draft DB. The driver and device modals show the reason and disable Save.
- Review 8: CreateTag/UpdateTag/ImportTags (tag modal, manual entry, CSV import, browse commit)
  refuse a ReadWrite Heidenhain tag or a Tag.DataType that disagrees with what the driver
  publishes (HeidenhainTagRules: catalog type, Float64 => Double, or the binding's dataType).
  The tag modal and manual-entry grid fix the access level at Read and derive the data type from
  the address (RawTagRowDefaults). New deploy gate HeidenhainTagDataTypeMismatch.
- docs/drivers/Heidenhain.md: credential namespace convention + why, the save-path and deploy
  enforcement, optional-role credential behaviour, BadConfigurationError, backoff/ordering notes.

Tests: HeidenhainSavePathTests (service save path; red with the gates disabled) and new
DraftValidatorHeidenhainTests cases.
2026-11-10 23:54:32 -05:00
Joseph Doherty f7a3588b24 fix(heidenhain): namespaced credentials, optional-role credential isolation, loop robustness (#803 review)
- Credential exfiltration (review 2): password references are scoped to
  secret:heidenhain/<name> and env:OTOPCUA_HEIDENHAIN_<NAME> only. Enforced by
  HeidenhainOptionsParser.TryParseCredentialReference (parser, probe, deploy gate) AND again at
  resolve time in HeidenhainCredentials (fail closed without consulting the store or the
  environment), so Test Connect can no longer send env:ConnectionStrings__ConfigDb or another
  subsystem's secret to an arbitrary Host. The probe also refuses an unsendable password.
- Optional-role credentials (review 6): an unresolvable/NUL/non-Latin-1/>247-byte DNC or DATA
  credential never sends a login and never fails the session; the reads needing that role are
  BadConfigurationError (capability ConfigurationError) and INSPECT telemetry continues. A
  policy refusal inside AcquireAsync is per-tag, not a session teardown.
- Start after DisposeAsync is a no-op (review 3); initial subscription push and Publish are
  ordered under one lock (review 4); JSON-null sections are normalised and the acquisition loop
  catches every fault (review 5); backoff resets only after a session published a poll batch,
  with clamped delays (review 7).

Tests: HeidenhainReviewFixTests + namespace/normalisation tests in HeidenhainConfigTests;
each was confirmed red against the reverted fix.
2026-11-10 23:49:48 -05:00
Joseph Doherty b89f458065 Merge fix/808-uns-folder-scope 2026-11-10 23:44:32 -05:00
Joseph Doherty e39a125069 fix(acl): close the #808 review findings — reference variables, owner folder chain, NUL keys, unknown ids (#808)
Review of af5c9744:
1. UNS reference variables (s=Area/Line/Equip/Eff) of a moved or renamed
   equipment stayed readable/writable under the OLD names' grants: they now
   resolve as ReferenceVariable and are authorized at their split path only
   while it equals their live equipment folder's placement (live chain ==
   generation chain) + one name; otherwise denied (Read and Write).
2. TryPlaceScriptedCondition now also requires the owner folder's live chain
   to match the generation (AuthorizeScriptedCondition gains ownerFolderChain),
   so a moved equipment's alarm/children/commands follow its folder.
3. Identifiers containing NUL are denied before the decision cache, keeping
   client ids disjoint from the NUL-prefixed resolved-scope keys (a client
   could otherwise poison a real folder's cached verdict for its key shard).
4. An id no live node holds is Unknown: placed at [\0unknown] so only
   Cluster / Namespace-uns grants decide it (a removed opaque id can no longer
   split onto a same-named narrow grant; narrow grantees get no NodeDeleted for
   removed UNS folders/vtags/conditions — documented); 4-segment
   reference-shaped ids keep their split.
5. A wire test drives placements from DeploymentArtifact.ParseDataPlaneAuthority.

Baseline updates: #797 owner tests (unit + wire) and Sdk2RepeatedAlarmAuthority
now install folder scopes; Sdk2HistoryAuthority's UNS alias used the RAW path
as its UNS id (never minted in production) and now carries EqA/HistoryAlias.
Each new guard was falsified (reverting it fails its test).
2026-11-10 23:44:11 -05:00
Joseph Doherty 8a0f0df05f Merge feat/804-mtconnect-parity 2026-11-10 23:24:27 -05:00
Joseph Doherty 8fb03d6a72 docs(heidenhain): driver page, README/probe index rows, drivers rule note (#803)
docs/drivers/Heidenhain.md: read-only boundary + allowlist table, transport, config,
credential references, address grammar (fixed catalog, PLC element/byte-count rules,
DATA types incl. FF Boolean), quality table, lifecycle/health/comms loss, probe, tests,
hardware-gated items and deliberate exclusions.

Also: InitializeAsync clears surviving subscription handles (the actor re-subscribes
after every successful init, so a kept handle would double-publish).
2026-11-10 23:23:25 -05:00
Joseph Doherty 3641d56154 feat(heidenhain): AdminUI driver/device forms, typed tag editor, CSV + browse-commit (#803)
- HeidenhainDriverForm over HeidenhainDriverFormModel (JsonObject bag preserves Devices and
  unknown keys; no enum fields; Validate asks HeidenhainOptionsParser) + DriverConfigFormMap.
- HeidenhainDeviceForm/Model (Host, Port 19000, optional per-device password references)
  + DeviceFormMap. Literal passwords are refused in both forms.
- RawDriverTypeDialog entry; HeidenhainTagConfigEditor over HeidenhainTagConfigModel, whose
  Validate is the driver's own TryFromTagConfig; catalog leaves derive dataType + isArray;
  registered in TagConfigEditorMap + TagConfigValidator.
- CsvColumnMap (HhAddress/HhDataType/HhIsArray) and RawBrowseCommitMapper branch.
- Guards: DriverPageJsonConverterTests external-serializer entry, TagConfigNonEnumParityTests
  runtime map + corpus + freshly-authored rows, TagConfigRoundTrip, HeidenhainAuthoringTests.
2026-11-10 23:17:26 -05:00
Joseph Doherty af5c9744b7 fix(acl): place opaque-id UNS folders, virtual tags and scripted conditions for every operation (#808)
UNS Area/Line/Equipment folders are minted under their opaque entity ids
(s=UA-/UL-/EQ-...), virtual tags under s=EQ-.../Name and scripted conditions
(+ #629 children) under s=<ScriptedAlarmId>[/...], while every UNS grant is
planted at a NAME path. DataPlaneAuthorizer split those ids, so with
Security:Acl:Enabled a narrow (Area/Line/Equipment) grant could not Browse,
Read, subscribe or HistoryRead them, and Server-object delivery never reached
it — so a narrow key never held the EventId Ack/Confirm/AddComment need.

- The generation carries UnsFolderScopes (folderId -> path + id chain), built
  by DeploymentArtifact from the same AclScopePathResolver as the grants; ids
  two entity kinds share are dropped; plumbed through IDataPlaneAuthoritySink.
- OtOpcUaNodeManager.ResolveUnsAclSubject reports live facts (folder id
  ancestry / scripted owner binding); attached by the Authorizer setter.
- Evaluate places the subject against the CURRENT generation before the
  decision cache, which now caches the walk keyed on the resolved scope.
  Folders require live chain == generation chain (a moved equipment is denied
  under old and new line until the rebuild). Owned nodes (vtags, conditions,
  children) get owner path + a reserved segment, so Tag grants and their
  Browse transit never expose them. Conditions share TryPlaceScriptedCondition
  with the #797 command gate.
- Server-object visibility of a scripted condition is decided on the
  condition alone, so a moved alarm cannot reach its old owner there.

Tests: UnsFolderScopeWireTests (opaque ids, end-to-end browse -> EventId ->
Acknowledge on an Equipment grant; sibling denied; Tag-only grant; moved alarm
and moved equipment stale windows). Rig tests now install folder scopes
explicitly (ServerEventRig.FolderScopes); the #797 recreate test deliberately
asserts the moved alarm is invisible to the Cluster observer in the window.
2026-11-10 23:12:46 -05:00
Joseph Doherty 2b45694f91 feat(heidenhain): read-only, credential and endpoint deploy gates + TagConfig inspector (#803)
- HeidenhainDeployChecks (wired into DraftValidator): HeidenhainTagWritable refuses a
  ReadWrite access level on any Heidenhain tag, so no node is ever advertised writable;
  HeidenhainCredentialPersisted refuses a literal password in DriverConfig/DeviceConfig
  (only blank / secret: / env: references; the value is never echoed);
  HeidenhainConfigInvalid for out-of-range settings or a tagged device without an endpoint.
- EquipmentTagConfigInspector maps Heidenhain to HeidenhainTagDefinitionFactory.Inspect.
- Driver validates only devices that carry tags (an unconfigured default Device1 no longer
  faults the driver).
2026-11-10 23:10:36 -05:00
Joseph Doherty d6392dae17 fix(mtconnect): close Java-driver parity gaps found by the #804 review
- Lift an Agent's MTConnectError body under a non-2xx status (cppagent 2.x serves
  OUT_OF_RANGE as HTTP 400, NO_DEVICE as 404) into the refusal-marked
  InvalidDataException, reading at most 64 KiB. The 400 used to be a transport
  fault, so the OUT_OF_RANGE re-baseline never ran against a real 2.x Agent.
- Run the instanceId restart test on every re-baseline snapshot: on 2.x a restart
  is first seen there (the old cursor gets a 400), and was adopted silently.
- Report the stream Degraded after 3 consecutive re-baselines with no /sample
  chunk (it read Healthy for ever); LastSuccessfulRead keeps advancing.
- Prohibit DTDs on the /probe and write-answer parses; bound the streams walk to
  64 levels (unbounded recursion = uncatchable StackOverflowException).
- Skip structural elements under <Components>/<Devices> instead of failing the probe.
- Never serve a non-finite float as Good; serve integer-valued decimals on
  integral tags losslessly (a real fraction stays BadOutOfRange).
- Merge sibling browse containers that resolve to one folder name.

Unit 678 -> 714, integration 18 -> 20 (live fixture); every gap test failed first.
2026-11-10 23:08:42 -05:00
Joseph Doherty d50ee809e3 feat(heidenhain): native read-only LSV2 driver + in-process simulator (#803)
Native LSV2 TCP transport (default port 19000; no Java/Python/vendor SDK), ported from
the ignitionoee v0.20.3 reference driver:

- Lsv2ReadOnlyPolicy: payload-aware outbound allowlist (A_LG INSPECT/DNC/DATA, R_VR, R_PR,
  R_RI 22-28/30-34/51, bounded R_MB, R_DP), applied in Lsv2Codec.Frame before any byte is
  written; no public raw-command API.
- Lsv2Client: serialized transactions, one monotonic deadline per transaction, EOF/oversize/
  unexpected/trickle close the client; Close never waits on the gate.
- Lsv2Data / HeidenhainBindingCodec: reference decoders, R_PR byte-capacity + element-index
  rules, next-area cap, DATA Boolean 00/01/FF, strict non-truncating coercion.
- HeidenhainSession: per-batch wire cache, refusal suppression, provisional DNC denial retry,
  alarm cursor (57 = end, overflow = BadOutOfRange), capability strings.
- HeidenhainDeviceRuntime: one loop per control, generation-guarded publication, all-node
  BadNoCommunication on comms loss, bounded exponential reconnect.
- HeidenhainDriver: cached ReadAsync, push subscriptions deduped on (value,status), static
  catalog discovery (no connect), host status, diagnostics, skipped-tag report. No IWritable.
- Probe (INSPECT login + version) and factory registered in DriverFactoryBootstrap (probe on
  admin nodes too); DriverTypeNames.Heidenhain.

Tests: 159 unit tests against an in-process LSV2 simulator incl. the Java fixtures' captured
iTNC 530 replies as golden vectors; env-gated LiveIntegration category for real hardware.
2026-11-10 23:08:30 -05:00
Joseph Doherty d52a3b1c64 fix(client): Client.Shared EndpointSelector uses the shared OpcUaEndpointUrlRewriter (#812)
Client.Shared now project-references Driver.OpcUaClient.Contracts (Core.Abstractions
only, no NuGet/SDK; pinned by ContractsIsDependencyLightTests) and delegates the
host/port rewrite to OpcUaEndpointUrlRewriter, deleting its own copy.

The two copies differed; reconciled toward the client so its pinned behaviour is
unchanged (EndpointSelectorRewriteParityTests, written green against the old copy):
- no-op rewrite (same host case-insensitively + same port) now returns the advertised
  string verbatim in the shared rewriter too (it re-serialised: added a trailing slash
  and case-folded a case-only host difference);
- a real rewrite serialises via AbsoluteUri, not Uri.ToString(), which un-escaped the
  advertised path (/x%20y became /x y) in the driver + browser;
- unusable URLs: the rewriter still falls back to the configured URL (driver/browser);
  the client keeps its loud failure as call-site validation (null -> InvalidOperation,
  unparseable -> UriFormatException).
2026-11-10 22:46:51 -05:00
Joseph Doherty 047491d90e Merge fix/797-scripted-alarm-equipment-scope
v2-ci / unit-tests (push) Has been cancelled
2026-11-10 21:42:18 -05:00
Joseph Doherty 4620e542e8 fix(acl): require the live scripted-alarm owner to match the generation's deployed owner (#797)
Review of 3d1b86b4: OpcUaPublishActor installs a new authority generation
before applying its address space, and an apply that throws keeps the old one.
A scripted alarm moved from equipment A to B therefore stayed commandable with
A's grant under B's generation (briefly before the rebuild, or indefinitely
after a failed apply).

The generation now carries ScriptedAlarmOwners (ScriptedAlarmId -> EquipmentId,
decoded by ParseDataPlaneAuthority from the artifact's ScriptedAlarms rows, the
same rows the composition materialises from). AuthorizeScriptedCondition denies
unless the node manager's live binding names the same owner as the generation:
a moved alarm is denied under the old owner's, the new owner's and a Cluster
grant until the address space re-binds it; an alarm the generation no longer
deploys is denied. Install/apply order unchanged.

Tests: window (a) generation moved / binding stale => both owners and Cluster
denied, then rebuild under the new owner => new owner allowed, old denied;
window (b) apply failed after install => denied across clock and further
generations, then a successful rebuild restores the new owner only; undeployed
alarm denied; artifact decode of the owner map.
2026-11-10 21:42:03 -05:00
Joseph Doherty 3d1b86b451 fix(acl): authorize scripted alarm commands at their owning equipment's scope (#797)
Scripted condition NodeIds are opaque ScriptedAlarmIds, so splitting them as a
topology path could never match an Equipment grant: a narrow equipment grant was
denied while a Cluster grant passed.

- OtOpcUaNodeManager binds each LIVE scripted condition instance to the
  EquipmentId it was materialised under (only when it hangs under that folder);
  dropped on remove / drop-recreate / equipment-subtree removal / rebuild; a
  same-id re-materialise under another equipment re-parents and re-binds.
- DataPlaneAuthoritySnapshot carries EquipmentScopePaths (EquipmentId ->
  [uns, Area, Line, Equipment]) decoded from the artifact by the same
  AclScopePathResolver call that plants Equipment grants.
- DataPlaneAuthorizer.AuthorizeScriptedCondition re-reads key, grant and
  generation per call (uncached) and evaluates at the owner path; unbound,
  replaced or unplaceable owners deny under every grant. No id-split or
  realm-root fallback.
- Native conditions (RawPath) and the ACL-off role gate are unchanged.
  NodeIds and AlarmCommand.AlarmId are unchanged.

Sdk2OpaqueScriptedScopeBaselineTests deliberately flipped: it pinned the denial
and now pins owner-grant allow / sibling deny / cluster allow. Follow-up #808
(folder ids + Server-object event visibility for narrow UNS grants).
2026-11-10 21:23:57 -05:00
Joseph Doherty 378962eeb1 fix(opcuaclient-browser): keep configured host/port behind NAT (#806)
The AdminUI OPC UA browse picker connected to the EndpointUrl the server
advertised in GetEndpoints verbatim, so behind NAT / a container port
publish it dialled the server's private hostname + internal port and
failed DNS where the runtime driver (which rewrites per #676) connected.

- Move the rewrite to the shared OpcUaEndpointUrlRewriter (.Contracts,
  pure Uri logic, no SDK dependency) and the policy+mode selection to
  OpcUaEndpointSelection (.Session leaf). Driver and browser both select
  through it, so the browse connects exactly where the driver does.
- Policy/mode filter, server certificate and strict browse PKI
  (no auto-accept) are unchanged.
- Unit tests: rewrite (host/port differ, same, IPv6 both sides, path
  kept, port-less, scheme mismatch untouched), browser selection seam.
- Live NAT tests (Fixture=opc-plc-nat, OPCUA_NAT_ENDPOINT): browse via a
  published port with a premise check that the advertised host:port
  differs and does not resolve, nsu= round-trip, unreachable endpoint
  fails cleanly, SignAndEncrypt reaches the server and is refused
  BadCertificateUntrusted. Verified red without the rewrite.

Refs #806
2026-11-10 21:00:04 -05:00
Joseph Doherty 19610c6812 fix: share SDK2 host disposal completion and close unadopted servers 2026-11-10 17:06:27 -05:00
Joseph Doherty 67bc61236a Merge remote-tracking branch 'origin/master' into codex/opcua-sdk-2 2026-11-10 14:40:51 -05:00
Joseph Doherty 230cef4ef0 build: adopt HistorianGateway 1.0 TLS options
v2-ci / unit-tests (push) Has been cancelled
2026-11-10 14:40:30 -05:00
Joseph Doherty cd60e469a1 docs: remove stale SDK 1 peer and calculator descriptions 2026-11-10 14:02:47 -05:00
Joseph Doherty ef8c2d1919 docs: clarify aggregate registration and SDK identity ownership 2026-11-10 12:48:41 -05:00
Joseph Doherty bd3143b7d7 fix(client): use default certificate lifetime without rotating persisted keys 2026-11-10 12:26:54 -05:00
Joseph Doherty 41e65f6a22 fix(opcua): restore alarm component reference after SDK creation 2026-11-10 12:14:35 -05:00
Joseph Doherty a21104d27c fix(opcua): scope application audit receive permissions to validation 2026-11-10 11:41:31 -05:00
Joseph Doherty 09ac0e58cb fix(opcua): preserve repeated alarm methods and owned write reverts 2026-11-10 11:04:53 -05:00
Joseph Doherty 6aaccb332f fix: initialize real OPC UA client certificate configuration 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 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 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 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