fix(site-runtime): reject invalid runtime actor names at the site gate and never let ActorOf escalate to the Deployment Manager (review 2026-10-21 01/SR-C1)

An instance unique name Akka refuses as a path element (space, '#', '/',
non-ASCII) made Context.ActorOf throw on the singleton's own thread; the
default Restart stopped every Instance Actor on the site. DeployCompileValidator
now checks the instance + script/alarm/native-source actor names with
ActorPath.IsValidPathElement (the Commons ActorNameRules mirror is stricter
than Akka for " ( ), pinned by a parity sweep), CreateInstanceActor catches
InvalidActorNameException and every caller reports it, and the Instance Actor
skips a refused child name like a compile failure.
This commit is contained in:
Joseph Doherty
2026-10-22 06:24:28 -04:00
parent b5cebf9fa6
commit f5090fcd9e
8 changed files with 606 additions and 23 deletions
@@ -1,4 +1,5 @@
using System.Text.Json;
using Akka.Actor;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
@@ -30,6 +31,18 @@ public sealed class DeployCompileValidator
public DeployCompileValidator(ScriptCompilationService compilationService)
=> _compilationService = compilationService;
/// <summary>
/// Prefix carried by every runtime-name error, so a caller can tell a name rejection
/// (<see cref="IsActorNameError"/>) from a compile failure when wording its reply.
/// </summary>
private const string ActorNameErrorPrefix = "Invalid runtime name: ";
/// <summary>True when <paramref name="error"/> came from the runtime-name check.</summary>
/// <param name="error">One error returned by <see cref="Validate"/>.</param>
/// <returns><see langword="true"/> for a runtime-name error.</returns>
public static bool IsActorNameError(string error) =>
error.StartsWith(ActorNameErrorPrefix, StringComparison.Ordinal);
/// <summary>
/// Validates every compilable artifact in the flattened configuration.
/// Returns an empty list when the whole configuration compiles; otherwise
@@ -37,8 +50,13 @@ public sealed class DeployCompileValidator
/// single error (this also fronts the poison-config case for startup load).
/// </summary>
/// <param name="configJson">The flattened-configuration JSON to validate.</param>
/// <param name="instanceName">
/// The instance unique name the Deployment Manager will create the Instance Actor under.
/// When supplied it is checked as an actor name along with every script/alarm/native-source
/// child name (review 2026-10-21 01/SR-C1); <see langword="null"/> checks the child names only.
/// </param>
/// <returns>A list of compile-error messages; empty if the configuration is clean.</returns>
public IReadOnlyList<string> Validate(string configJson)
public IReadOnlyList<string> Validate(string configJson, string? instanceName = null)
{
FlattenedConfiguration? config;
try
@@ -55,6 +73,10 @@ public sealed class DeployCompileValidator
var errors = new List<string>();
// Runtime names first: they are cheap, and a name Akka refuses would otherwise surface
// as an InvalidActorNameException at Context.ActorOf instead of a readable rejection.
AddActorNameErrors(errors, config, instanceName);
// Scripts: full execution-path compile against ScriptGlobals.
foreach (var script in config.Scripts)
{
@@ -97,6 +119,48 @@ public sealed class DeployCompileValidator
return errors;
}
/// <summary>
/// Review 2026-10-21 01/SR-C1: every name that becomes an Akka actor name on the site — the
/// Instance Actor itself (<c>DeploymentManagerActor.CreateInstanceActor</c>) and its
/// <c>script-</c>/<c>alarm-</c>/<c>native-alarm-</c> children
/// (<c>InstanceActor.CreateChildActors</c>) — must be a legal path element. An instance named
/// <c>"Furnace #3"</c> used to make <c>Context.ActorOf</c> throw on the Deployment Manager
/// singleton's own thread, restarting it and every Instance Actor on the site.
///
/// <para>
/// Decided with Akka's own <see cref="ActorPath.IsValidPathElement"/>, not the Commons mirror
/// <c>ActorNameRules</c>: the mirror is currently STRICTER (it refuses <c>"</c>, <c>(</c> and
/// <c>)</c>, which Akka accepts), so gating on it would reject deployments that work today.
/// </para>
/// </summary>
private static void AddActorNameErrors(
List<string> errors, FlattenedConfiguration config, string? instanceName)
{
if (instanceName != null)
AddActorNameError(errors, "Instance", instanceName, instanceName);
foreach (var script in config.Scripts)
AddActorNameError(errors, "Script", script.CanonicalName, $"script-{script.CanonicalName}");
foreach (var alarm in config.Alarms)
AddActorNameError(errors, "Alarm", alarm.CanonicalName, $"alarm-{alarm.CanonicalName}");
foreach (var source in config.NativeAlarmSources)
AddActorNameError(errors, "Native alarm source", source.CanonicalName,
$"native-alarm-{source.CanonicalName}");
}
private static void AddActorNameError(
List<string> errors, string kind, string? name, string actorName)
{
if (ActorPath.IsValidPathElement(actorName))
return;
errors.Add($"{ActorNameErrorPrefix}{kind} name '{name}' cannot be used as a runtime actor " +
"name — use ASCII letters, digits and -_.*$+:@&=,!~';() only (no spaces, '#', " +
"'/', '[', ']', '%' or non-ASCII letters), and do not start it with '$'.");
}
/// <summary>
/// Compiles the boolean trigger expression for an Expression-triggered script
/// or alarm and records any failure. No-op for non-Expression triggers or a