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).
This commit is contained in:
Joseph Doherty
2026-11-10 23:10:36 -05:00
parent d50ee809e3
commit 2b45694f91
7 changed files with 185 additions and 9 deletions
@@ -134,16 +134,8 @@ public sealed class HeidenhainDriver : IDriver, IReadable, ISubscribable, ITagDi
{
var devices = _options.Devices ?? [];
_skipped.BeginRebuild(_options.RawTags.Count);
var deviceErrors = new List<string>();
var perDevice = new Dictionary<HeidenhainDeviceOptions, List<HeidenhainTagDefinition>>();
foreach (var d in devices)
{
var errs = HeidenhainOptionsParser.ValidateDevice(d);
if (errs.Count > 0) deviceErrors.Add($"Device '{d.DeviceName ?? d.Host}': {string.Join(" ", errs)}");
else perDevice[d] = [];
}
if (deviceErrors.Count > 0) throw new InvalidOperationException(string.Join("; ", deviceErrors));
foreach (var d in devices) perDevice[d] = [];
foreach (var raw in _options.RawTags)
{
@@ -171,6 +163,15 @@ public sealed class HeidenhainDriver : IDriver, IReadable, ISubscribable, ITagDi
list.Add(def);
}
// A control is validated only when it carries tags: a freshly created /raw Device has an empty
// DeviceConfig, and must not fault the whole driver (or the deploy) before it is configured.
var deviceErrors = perDevice.Where(kv => kv.Value.Count > 0)
.Select(kv => (kv.Key, Errors: HeidenhainOptionsParser.ValidateDevice(kv.Key)))
.Where(x => x.Errors.Count > 0)
.Select(x => $"Device '{x.Key.DeviceName ?? x.Key.Host}': {string.Join(" ", x.Errors)}")
.ToList();
if (deviceErrors.Count > 0) throw new InvalidOperationException(string.Join("; ", deviceErrors));
_skipped.Commit();
var runtimes = new List<HeidenhainDeviceRuntime>();
var byPath = new Dictionary<string, HeidenhainDeviceRuntime>(StringComparer.Ordinal);