Commit Graph

1323 Commits

Author SHA1 Message Date
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 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 418d0bbb87 Merge fix/810-uncertain-conformance 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 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 f17ff1e643 docs(mtconnect): Java reference parity matrix + live validation record (#804) 2026-11-10 23:08:42 -05:00
Joseph Doherty 87b0c18fb3 test(historian): Uncertain/partial-data conformance datasets for PercentGood/PercentBad (#810)
QualityMixConformance (SqlServer + QuestDB): tag Q mixes Good/Uncertain/Bad
regions across eight 10 s intervals (two all-Uncertain - Mantis 0006166 0%/0% -
one through an Uncertain bound, plus Uncertain bounds from a sloped non-Good end
and from a hold into an outage); tag QP has data starting and ending inside
intervals. PercentGood/PercentBad are asserted against hand-derived Part 13
DurationGood/DurationBad, the oracle is checked against the same hand values,
and Count/Average/TimeAverage(2)/Total(2)/Start/End/Delta/Interpolative are
compared with the #796 oracle forward, reverse and truncated.

Teeth: with SdkAggregateOracle.Run answering PercentGood from the baseline
(Uncertain as Good), 8 tests fail per backend; all pass with the fixed oracle.

Records a new SDK divergence: with an Uncertain first sample the baseline
TimeAverage/Total integrate the pre-data span as a ramp from 0; ours covers
only the data (Part 13) - pinned by hand.
2026-11-10 22:58:09 -05:00
Joseph Doherty 88c05cc01a docs(opcuaclient): document the shared endpoint-URL rewrite and how to run the NAT tests (#814)
docs/drivers/OpcUaClient.md, docs/plans/2026-05-28-driver-browsers-design.md and
docs/Client.CLI.md: one OpcUaEndpointUrlRewriter for driver, AdminUI browser and
Client CLI/UI; configured host+port win, advertised path kept for opc.tcp; policy/mode
selection and certificate validation unchanged; NAT test recipe against the #813 fixture.
2026-11-10 22:53:01 -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 da4e27003e docs: record completed Linux OPC UA lifecycle diagnostic
v2-ci / unit-tests (pull_request) Waiting to run
2026-11-10 20:28:11 -05:00
Joseph Doherty 1e4b017f6e docs: retain verified Linux rehearsal and diagnostic gate
v2-ci / unit-tests (pull_request) Failing after 1h6m11s
2026-11-10 18:25:37 -05:00
Joseph Doherty b18f97236a docs: record reviewed private temp mount controls
v2-ci / unit-tests (pull_request) Has been cancelled
2026-11-10 18:18:59 -05:00
Joseph Doherty 351edb0a86 docs: retain read-only Linux rehearsal startup failure 2026-11-10 18:13:00 -05:00
Joseph Doherty 626cb4f7f1 docs: record final late-slope controls before artifact preparation
v2-ci / unit-tests (pull_request) Has been cancelled
2026-11-10 18:03:13 -05:00
Joseph Doherty 7023285a3e docs: retain late-slope guard control receipts 2026-11-10 17:54:43 -05:00
Joseph Doherty fbf73436be docs: record reviewed native import reproduction
v2-ci / unit-tests (pull_request) Waiting to run
2026-11-10 17:42:40 -05:00
Joseph Doherty 041189aa6d docs: close SDK2 ownership evidence review
v2-ci / unit-tests (pull_request) Has been cancelled
2026-11-10 17:35:54 -05:00
Joseph Doherty 327bbdaab9 docs: record reviewed SDK2 ownership regressions and diagnostic design 2026-11-10 17:33:06 -05:00
Joseph Doherty ce21f5db09 Merge master SDK1 ownership repair with SDK2 disposal adaptation 2026-11-10 17:17:00 -05:00
Joseph Doherty efb9197a0f docs: record reviewed SDK1 terminal disposal repair
v2-ci / unit-tests (push) Has been cancelled
2026-11-10 17:05:06 -05:00
Joseph Doherty 2cf7c1d248 docs: locate close control build receipt
v2-ci / unit-tests (pull_request) Waiting to run
2026-11-10 16:40:28 -05:00
Joseph Doherty 0c0209f9a6 docs: record reviewed close ownership and native attribution 2026-11-10 16:39:34 -05:00
Joseph Doherty ed60970fc2 docs: record final review hold and baseline ownership limits
v2-ci / unit-tests (pull_request) Has been cancelled
2026-11-10 16:09:09 -05:00
Joseph Doherty 331cde68a7 test: record final integrated CI baseline-only failures
v2-ci / unit-tests (pull_request) Has been cancelled
2026-11-10 16:05:13 -05:00
Joseph Doherty 4a04806d22 test: preserve saturation outcome and classify final CI corrections
v2-ci / unit-tests (pull_request) Has been cancelled
2026-11-10 15:46:58 -05:00
Joseph Doherty ab35ddd3c2 test: retain completed two-hour client lifecycle evidence
v2-ci / unit-tests (pull_request) Has been cancelled
2026-11-10 14:54:38 -05:00
Joseph Doherty c6c05a1001 test: record publish smoke with HistorianGateway 1.0 packages 2026-11-10 14:52:08 -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 12dffb9254 docs: retain reviewed memory investigation and prospective gate limits
v2-ci / unit-tests (pull_request) Waiting to run
2026-11-10 14:21:57 -05:00
Joseph Doherty cd60e469a1 docs: remove stale SDK 1 peer and calculator descriptions 2026-11-10 14:02:47 -05:00
Joseph Doherty a7efb39685 docs: record Astra source verdict and preserve failed lifecycle scope
v2-ci / unit-tests (pull_request) Has been cancelled
2026-11-10 13:33:01 -05:00
Joseph Doherty b218321bda docs: retain final CI failures and Linux RSS gate evidence
v2-ci / unit-tests (pull_request) Waiting to run
2026-11-10 13:28:40 -05:00
Joseph Doherty a8ae19b224 docs: reconcile SDK 2 rollout gates with local evidence 2026-11-10 13:28:16 -05:00
Joseph Doherty aa9af0dcff docs: retain backend benchmark and actual publish evidence 2026-11-10 13:17:13 -05:00
Joseph Doherty 780f363f16 docs: document OPC UA 2.0 compatibility and rollout 2026-11-10 11:48:19 -05:00
Joseph Doherty 9efd7667e1 build: update HistorianGateway client packages to 0.4.0 2026-11-10 10:32:13 -05:00
Joseph Doherty 16111a5eee build: update HistorianGateway client packages to 0.4.0
v2-ci / unit-tests (push) Failing after 1h17m0s
2026-11-10 10:31:31 -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 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 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