Script file access (design docs/plans/2026-09-28-script-file-access-design.md): Files.ReadTextAsync / ReadLinesAsync / ExistsAsync on site ScriptGlobals (+ Instance.Files), backed by the site-singleton ScriptFileReader. The one host-mediated exception to the System.IO ban - scripts still never name a System.IO type. OFF by default: ScadaBridge:SiteRuntime:ScriptFileAccess:AllowedRoots (empty = every call throws Disabled) + MaxFileBytes (1 MiB, validated 1..64 MiB, roots must be absolute). Guard: fully qualified, no device paths / ADS, canonical path strictly inside a root (directory-boundary match), no reparse point on the file or any directory below the root, size cap before and during the read. Errors are one Commons type, ScriptFileAccessException(+ScriptFileAccessFailure); Commons.Types.Scripts joins the default script imports on every surface so it binds by short name. Mirrored on ScriptCompileSurface (CompileFiles, parity-pinned) and the Test Run sandbox (binds, refuses Disabled). Inbound (central) scripts do not get it. Rig: docker/recipes (sample RecipeValueName,RecipeValue files incl. an invalid one) bind-mounted read-only at /app/recipes on both site-a nodes with AllowedRoots [/app/recipes]. The TestMesMachine SimDelmiaRecipe simulator now reads + parses the downloaded file, converts each value to its recipe tag's type and loads it all-or-nothing (14 new TestDelmiaReceiver attributes); verify-mes-test-machine.sh group A covers accept / re-download / invalid lines / NotFound / OutsideAllowedRoots, VERIFY_GROUPS selects groups (rig: AD).
30 KiB
Component: Script Analysis
Purpose
The Script Analysis component is the single authoritative source of truth for the ScadaBridge script trust model. It provides a unified forbidden-API deny-list, a fused semantic and syntactic trust validator, a Roslyn compile wrapper, and compile-only globals stubs used by the design-time deploy gate. All five call sites that enforce the script trust boundary — Template Engine, Site Runtime, Inbound API, Central UI, and Transport bundle import — delegate to this component rather than maintaining their own divergent implementations.
Location
src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/
Referenced by: Template Engine, Site Runtime, Inbound API, Central UI, Transport.
Responsibilities
- Define the canonical forbidden-API deny-list (
ScriptTrustPolicy) as the single source of truth for all trust enforcement decisions across the system. - Provide an authoritative forbidden-API verdict (
ScriptTrustValidator.FindViolations) that fuses semantic symbol resolution with syntactic reflection-gateway hardening. - Wrap Roslyn
CSharpScriptcompilation (RoslynScriptCompiler) so callers share one implementation of compile + diagnostics extraction. - Provide compile-only globals stubs (
ScriptCompileSurface,TriggerCompileSurface) that mirror the real execution-time globals member-for-member, allowing the design-time deploy gate to do a real type-checking compile without depending on the execution-time projects. - Own the shared process-wide metadata reference resolver (
CachingScriptMetadataResolver) that every Roslyn script-compile surface in the system attaches to itsScriptOptions.
Requirements
REQ-SA-1: Trust Policy (ScriptTrustPolicy)
ScriptTrustPolicy is a static class (or record) that publishes the complete, authoritative forbidden-API policy used at every call site.
Forbidden scopes
The following namespace/type prefixes are forbidden in all scripts:
| Scope | Rationale |
|---|---|
System.IO |
File system access — forbidden entirely. The only file access a script has is the host-mediated, allow-listed, read-only Files global on site scripts (2026-09-28, Component-SiteRuntime.md → File Access) — the script never touches a System.IO type, so this scope is unchanged |
System.Diagnostics.Process |
Process spawning — forbidden; Stopwatch, Debug, Activity, and other System.Diagnostics types are allowed |
System.Threading |
Raw thread manipulation — forbidden, with the exceptions below |
System.Reflection |
Reflection — forbidden entirely |
System.Net |
Raw network access — forbidden entirely (scripts must use ExternalSystem.Call) |
System.Runtime.InteropServices |
Native interop — forbidden entirely |
System.Runtime.CompilerServices.Unsafe, System.Runtime.CompilerServices.RuntimeHelpers |
Type-punning and constructor/runtime bypass — forbidden types; compiler attributes and async support remain allowed |
System.Runtime.Loader |
Assembly loading and load-context control — forbidden entirely |
Microsoft.Win32 |
Win32 API access — forbidden entirely |
System.Environment |
Whole type — forbidden entirely; exposes process control (Exit/FailFast), the host environment, and secrets via environment variables (e.g. SCADABRIDGE_API_KEY). Scripts that need a line break use "\n" instead of Environment.NewLine. |
System.GC |
Whole type — forbidden entirely; GC control (Collect, KeepAlive, memory-pressure knobs) has no legitimate script use. |
Microsoft.Data |
Concrete ADO.NET provider namespace (Microsoft.Data.SqlClient, …) — forbidden; closes the arbitrary-host new SqlConnection("Server=attacker") channel |
System.Data.SqlClient |
Concrete ADO.NET SQL Server provider — forbidden (see provider-namespace posture below) |
System.Data.Odbc |
Concrete ADO.NET ODBC provider — forbidden |
System.Data.OleDb |
Concrete ADO.NET OLE DB provider — forbidden |
System.Data provider-namespace posture
Only the concrete ADO.NET provider namespaces are forbidden — Microsoft.Data (which covers Microsoft.Data.SqlClient), System.Data.SqlClient, System.Data.Odbc, and System.Data.OleDb. The abstract System.Data.Common types (DbConnection, DbCommand, …) and System.Data broadly are deliberately NOT forbidden: scripts reach a database only through the sanctioned Database helper, whose surface exposes those abstract System.Data.Common types. Forbidding the concrete providers blocks a script from constructing its own connection to an arbitrary host (the new SqlConnection(...) channel) while leaving the sanctioned Database path intact.
Allowed exceptions within forbidden scopes
The following types are explicitly allowed despite falling within a forbidden namespace:
System.Threading.Tasks(and all subtypes) — async/await supportSystem.Threading.CancellationToken— cooperative cancellationSystem.Threading.CancellationTokenSource— cooperative cancellation
The scoping rationale: System.Diagnostics.Process is the dangerous type (spawns processes); Stopwatch, Debug, and Activity are harmless diagnostic utilities. Forbidding the whole System.Diagnostics namespace, as some earlier call sites did, was overly broad. In contrast, System.Environment and System.GC are forbidden as whole types because — unlike System.Diagnostics — they have no harmless members a script legitimately needs.
Reflection-gateway members
The following member names are blocked regardless of the receiver type, to prevent reflection-based bypasses such as typeof(x).Assembly.GetType("System.IO.File"):
GetType, GetTypeInfo, Assembly, Module, CreateInstance, InvokeMember, GetMethod, GetMethods, GetConstructor, GetConstructors, GetField, GetFields, GetProperty, GetProperties, GetMember, GetMembers, GetRuntimeMethod, GetRuntimeMethods, MethodHandle, TypeHandle, GetTypes, EntryPoint, DeclaredMethods, DeclaredMembers, DeclaredConstructors, DynamicInvoke.
Invoke is deliberately excluded from this list: the syntactic pass rejects a gateway member regardless of receiver, and delegate.Invoke() (Func<>/Action<> invocation) is a legitimate, common script pattern. The reflection late-bind MethodInfo.Invoke is already caught semantically via the System.Reflection scope, so excluding Invoke here avoids a false-positive blast radius without weakening the reflection deny-list.
Forbidden identifiers
The identifiers dynamic and Activator are forbidden at any scope, as they provide type-system escape hatches equivalent to reflection.
Default references and imports
ScriptTrustPolicy also publishes DefaultReferences (the canonical set of trusted-platform MetadataReference entries used when constructing the Roslyn script compilation context) and DefaultImports (the default using directives injected into every script). These are consumed by RoslynScriptCompiler and by the compile-only surfaces below.
REQ-SA-2: Trust Validator (ScriptTrustValidator)
ScriptTrustValidator.FindViolations(string code, IEnumerable<MetadataReference>? extraReferences = null) is the authoritative forbidden-API gate. It returns a list of violation messages; an empty list means the script is clean.
Two-pass design
Pass 1 — semantic symbol resolution (adapted from Site Runtime)
- Builds a Roslyn compilation using
ScriptTrustPolicy.AnalysisReferences(plus anyextraReferences). - For each identifier in the syntax tree, resolves the underlying symbol to its fully qualified containing namespace and type name.
- Flags any symbol whose containing namespace or type matches a forbidden scope in
ScriptTrustPolicy.ForbiddenScopes, takingAllowedExceptionsinto account. - Correctly handles aliases (
using X = System.IO.File),using static, andglobal::prefixes — the resolved symbol is checked, not the spelling. - The validation compilation applies the same default namespace imports (
ScriptTrustPolicy.DefaultImports—using System;,System.Collections.Generic,System.Linq,System.Threading.Tasks, and since 2026-09-28ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts, so script-facing Commons types such asScriptFileAccessExceptionbind by short name) thatRoslynScriptCompilercompiles the real script with. Without them the semantic pass under-resolves bare identifiers the running script sees fully bound — e.g.Environment.Exit(0)orGC.Collect(), whoseEnvironment/GCresolve to the forbiddenSystem.Environment/System.GCtypes only whenusing System;is in scope. Matching the runtime import set closes that blind spot; it can only make more identifiers resolve to their true (forbidden) namespace, never produce a false allow. - Because the full reference set is loaded, this pass also catches a forbidden type accessed inside an otherwise-allowed namespace (e.g., bare
Processafterusing System.Diagnostics;).
AnalysisReferences vs DefaultReferences
The two reference sets are deliberately distinct and must not be conflated:
DefaultReferences— the minimal, runtime-fidelity set (built fromDefaultAssemblies: CoreLib, LINQ, Math, the C# runtime binder, and the Commons API-surface assembly). It is consumed byRoslynScriptCompiler(the compile gate) and must mirror exactly what the site runtime compiles/executes against. It deliberately does not reference the forbidden-API anchor assemblies (System.Diagnostics.Process.dll,System.Net.Sockets.dll, …) so that a forbidden type remains an undefined symbol at compile time — the compile gate then independently rejects it, providing a second layer of defence. This set must stay minimal.AnalysisReferences— the full-framework set used only byScriptTrustValidator's Pass 1. It is built fromAppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")(the TPA list of the host) so that every type a script names resolves to its true namespace and is judged authoritatively. Enriching the analysis set can only improve detection (the verdict is by namespace/type, never a false allow), which is why the Central UI run gate may safely forward its full compilation reference surface asextraReferences.- TPA-fallback behaviour — on a host that does not publish the TPA list (single-file, AOT, or trimmed deployment),
AnalysisReferencesfalls back toDefaultReferencesenriched withForbiddenAnchorAssemblies(the assemblies that host the forbidden-API types). This keeps the documented forbidden anchors — notably bareProcessinside the allowedSystem.Diagnosticsnamespace — resolvable, so the semantic pass stays authoritative even in the degraded mode. The fallback is not silent:ScriptTrustPolicy.AnalysisReferencesDegradedis set totrueand a warning is emitted viaSystem.Diagnostics.Traceso operators and tests can detect the weakened mode.
Pass 2 — syntactic reflection-gateway and identifier hardening (adapted from Inbound API)
- Walks the syntax tree for member-access expressions and simple name references.
- Flags any member name found in
ScriptTrustPolicy.ReflectionGatewayMembers, regardless of receiver type. - Flags any identifier token found in
ScriptTrustPolicy.ForbiddenIdentifiers(dynamic,Activator).
Violations from both passes are merged and deduplicated before being returned.
Design notes
FindViolationsneeds no globals type; it operates solely on the script text and the trusted-platform reference set.- The function is stateless and thread-safe — callers share a single instance or call it as a static method.
- A violation does not abort compilation; callers may choose to report violations and continue, or treat any violation as a hard reject.
REQ-SA-3: Roslyn Compile Wrapper (RoslynScriptCompiler)
RoslynScriptCompiler wraps CSharpScript to give callers a single implementation of compile + diagnostics extraction.
Compile(string code, Type? globalsType = null, IEnumerable<MetadataReference>? extraReferences = null, IEnumerable<string>? extraImports = null)
- Creates a
CSharpScriptwith the given code,globalsType, references (defaults fromScriptTrustPolicy.DefaultReferencesplusextraReferences), and imports (defaults fromScriptTrustPolicy.DefaultImportsplusextraImports). - Calls
.Compile()and returns the resultingDiagnostic[]filtered to error-severity diagnostics only. This is a compile gate: a warning must not block a deploy, so only errors (undefined symbols, type mismatches) are surfaced to callers as gate failures. - Each caller passes its own
globalsType—ScriptCompileSurfacefor the design-time deploy gate, the realScriptGlobalsfor Site Runtime execution,nullfor pure syntax checks.
ParseDiagnostics(string code)
- Parses the script text using Roslyn's
CSharpSyntaxTree.ParseTextand returns error-severity syntax-level diagnostics only (consistent withCompile— warnings do not fail the gate). - No compilation is performed — useful for fast syntax checks where no globals type is available.
REQ-SA-4: Compile-Only Globals Stubs
The deploy gate in Template Engine must do a real type-checking compile (to catch undefined-symbol and type errors) but cannot depend on the execution-time projects (Site Runtime, Inbound API) that own the real globals. Two compile-only stubs solve this:
ScriptCompileSurface
Mirrors ScriptGlobals member-for-member (same public property names, same return types, same method signatures) but with no implementation bodies. All properties return default and all methods return default or Task.CompletedTask. Depends only on Commons.Types — no Akka.NET, no external system clients.
Used by the Template Engine deploy gate:
var errors = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface));
This allows the compile to bind Attributes["name"], Notify.To("x").Send(...), ExternalSystem.Call(...), Alarms.CurrentAsync(), Alarms.MesReportableAsync() (2026-09-11), and similar API calls against real types, catching undefined-symbol and type-mismatch errors before deployment.
The surface also mirrors the Log global (step logs design docs/plans/2026-09-09-flow-debugger-step-logs-design.md D4) as the compile-only ScriptCompileSurface.CompileLog — Debug / Info / Warn / Error(string message, Exception? exception = null), including the optional exception parameter, so Log.Warn("…", ex) binds at the design-time gate exactly as it does at the site. Log needs no ScriptTrustPolicy change for the same reason Alarms did not: the trust boundary is a deny-list over API roots, not an allow-list of context members.
Where an accessor returns a domain type rather than a primitive, the stub returns the same type the runtime returns (not a compile-only stand-in) — Alarms.CurrentAsync() yields IReadOnlyList<ScriptAlarm> on both surfaces, so a script reading a.Severity / a.AckTime binds identically at the gate and at the site. This works because those types live in Commons, which is already in DefaultAssemblies.
Trust-model note: the script trust boundary is a deny-list over API roots, not an allow-list of context members, so adding a globals member such as Alarms requires no ScriptTrustPolicy change. What must hold is that the member's return type resolves in a permitted namespace — pinned by a test asserting no entry in ForbiddenScopes prefixes the Commons script-surface namespace.
TriggerCompileSurface
Mirrors TriggerExpressionGlobals in the same way. Used by ValidationService.CheckExpressionSyntax in the Template Engine for conditional and expression trigger validation.
CheckExpressionSyntax memoises its verdict in the Template Engine's process-wide ScriptCompileVerdictCache, whose key is the pair (globals surface, SHA-256 of the code) — not the code alone. The surface discriminator is load-bearing: a trigger expression vetted against TriggerCompileSurface is not interchangeable with a ScriptCompileSurface script-body verdict (different globals resolve different identifiers), so a code-only key could return a stale "clean" for code never compiled against the caller's surface. Keying by surface makes that cross-surface verdict reuse structurally impossible.
Eviction is segmented, never wholesale (WP2.5). The cache is bounded at two generations of 2048 entries. New verdicts land in the hot generation; a hit in cold is promoted back into hot; on overflow hot becomes the new cold and only the old cold — the half nothing has touched for a full generation — is dropped.
The bound must never be enforced by clearing the cache outright. That reads as
harmless ("a verdict is cheap to recompute") but is not: a recompute is a fresh
Roslyn compile, and every script compile loads an assembly through a
non-collectible InteractiveAssemblyLoader. Bounding that leak is the entire
reason this cache exists, so dropping every hot entry at the 4096th distinct script
would re-open it for the whole working set at once. Segmenting retains anything in
active use across an eviction while keeping the same ceiling.
Parity guard
A reflection-based parity test in SiteRuntime.Tests compares the public member names on ScriptCompileSurface against ScriptGlobals (and TriggerCompileSurface against TriggerExpressionGlobals). Any drift between the stub and the real globals causes this test to fail, ensuring the stubs cannot silently fall out of sync.
There is a third hand-maintained mirror of the runtime globals: the Central UI Test-Run host SandboxScriptHost (see REQ-SA-5 / Interactions). Because Central UI deliberately does not reference Site Runtime, it cannot share the reflection-based parity test above; instead it is guarded by representative-script "diagnose clean" tests in CentralUI.Tests (one per non-trivial surface — e.g. the Attributes.WriteBatchAndWaitAsync (including the reset-aware nine-argument overload) / WaitAsync / WaitForAsync batch-write-and-wait helpers, the inbound Database/WaitForAttribute helpers, and the Notify outbox shape). A member that drifts out of SandboxScriptHost does not fail the deploy gate (which compiles against ScriptCompileSurface) — it surfaces only as an in-editor CS1061 false error against otherwise-valid scripts, so these compile-clean tests are the safety net for that surface.
REQ-SA-5: Consumer Delegation
All five call sites that previously maintained their own script trust enforcement (or, for Transport, had none) now delegate to this component. The key behavioral changes per consumer:
| Consumer | Before | After |
|---|---|---|
Template Engine ScriptCompiler.TryCompile |
Substring scan + brace-balance (advisory, bypassable) | FindViolations + real Compile against ScriptCompileSurface — authoritative gate |
Template Engine ValidationService.CheckExpressionSyntax |
Regex / brace scan | FindViolations + Compile against TriggerCompileSurface |
Site Runtime ScriptCompilationService.ValidateTrustModel |
Semantic resolver, no reflection-gateway hardening | Delegates to FindViolations; retains CSharpScript.Compile against real ScriptGlobals for execution |
Inbound API ForbiddenApiChecker.FindViolations |
Syntactic walker, forbade all System.Diagnostics |
Thin shim delegating to ScriptTrustValidator.FindViolations; System.Diagnostics loosened to .Process-only |
Central UI ScriptAnalysisService |
Semantic + full compile, lenient threading | Delegates forbidden-API verdict and sources editor-marker deny-list from ScriptTrustPolicy; retains Test-Run execution host |
Transport BundleImporter.RunSemanticValidationAsync / DetectBlockersAsync |
No trust gate — a bundle's forbidden-API scripts imported clean, deferred to runtime (fifth write path) | Runs ScriptTrustValidator.FindViolations over every non-Skip template / shared / ApiMethod body, template script + alarm Expression-trigger bodies, and instance alarm-override trigger expressions at import review — hard error (apply) / Blocker row (preview) for all kinds |
The static enforcement is defence-in-depth, not a true runtime sandbox. The denied API list blocks disallowed APIs at compile time but does not establish an operating-system security boundary. Site-runtime consumers execute in-process. Central UI Test Run uses a bounded, killable worker process; that adds resource/lifetime containment, while the worker retains the same operating-system identity and permitted gateway access.
REQ-SA-6: Shared Metadata Reference Resolver (CachingScriptMetadataResolver)
CachingScriptMetadataResolver is a process-wide memoizing decorator over Roslyn's default script metadata resolver (ScriptOptions.Default.MetadataResolver), owned by this component because it is a property of script compilation, not of any one consumer. CachingScriptMetadataResolver.Instance is the single shared instance — the cache lives on the instance, so a second instance is a second (empty) cache.
The invariant
Every Roslyn script-compile surface in the system MUST attach the shared instance via ScriptOptions.WithMetadataResolver(CachingScriptMetadataResolver.Instance). There are four such surfaces today: Site Runtime ScriptCompilationService, Inbound API InboundScriptExecutor, Central UI ScriptAnalysisService, and this component's own RoslynScriptCompiler. A surface that omits it — including a new fifth surface — reintroduces a per-compile native-memory leak, and does so silently.
Why
The ScriptOptions on each surface carry only the direct API-surface references; every script.Compile() binds their transitive closure, and each transitively-referenced assembly is resolved through the options' MetadataReferenceResolver. Roslyn's default resolver has no cross-compilation cache: each resolution calls MetadataReference.CreateFromFile, which eagerly copies the whole assembly into native memory (AssemblyMetadata → PEReader → NativeHeapMemoryBlock). So a 5-assembly explicit set re-resolves its ~74-assembly closure afresh on every compile, and those references are pinned for the process lifetime by the compile caches that hold the compiled scripts. Confirmed live (2026-08-12 gcdumps): 21 site scripts held 6,640 metadata objects, 6 central inbound methods 2,699. With the decorator, each distinct assembly is materialized once per process regardless of compile count.
This is the second and dominant half of the leak whose first half — per-compile ScriptOptions construction — was fixed by making the options static (see Site Runtime).
Trust-model neutrality
The decorator changes no resolution result, only object identity: it returns exactly what the inner (undecorated) resolver returned for the same arguments, so it can never resolve anything the undecorated options would not have resolved. DefaultReferences stays minimal and the compile gate keeps seeing exactly what it saw before — this fix deliberately does not widen any explicit reference set, precisely to avoid semantic drift in a security gate.
Cache-correctness assumptions
ResolveMissingAssemblykeys onAssemblyIdentity.GetDisplayName()(case-insensitive, matching .NET simple-name binding) and deliberately ignores the requestingdefinition, whose directory is a search path in the inner resolver. Safe here: every node runs from a single publish directory plus the shared framework, so identity → path is stable process-wide. Anullresult is cached too — "not found" is a legitimate, sticky answer.ResolveReferencekeys on the full(reference, baseFilePath, properties)tuple, case-sensitively, becausebaseFilePathis a filesystem path on the Linux containers these nodes run in.- A
GetOrAddfactory race can mint one duplicate — bounded and benign. Entries are never disposed; the cache is bounded by the distinct assemblies on disk, the same order as the staticAnalysisReferences.
REQ-SA-7: Flow Shape Predictor (FlowShapePredictor)
FlowShapePredictor (Flow Debugger design docs/plans/2026-09-05-flow-debugger-design.md §8) predicts, from script syntax alone, the swimlane shape a run would produce before anything executes: PredictInboundMethod(methodName, methodScript, resolveRouteScript, maxDepth = 4) and PredictSiteScript(scriptName, scriptBody, maxDepth = 4) return a PredictedFlow of PredictedNode(Id, Kind, Lane, Title, InputDependent, Note) + PredictedEdge(From, To, CrossesBoundary, Async = false).
- Syntax walk, not semantic. A
CSharpSyntaxWalkeroverCSharpSyntaxTree.ParseText(code, SourceCodeKind.Script)matches the script-API member-access chains by whitespace-stripped text, exactly asScriptTrustValidator's hardening walker does:Route.To(x).Call(y)/.SetAttributes/.WaitForAttribute,Scripts.Call(y),…Attributes.WriteBatchAndWaitAsync(the reset-aware overload additionally yields a resetAttributeWrite+AttributeWaitpair ahead of the batch) /WaitAsync/WaitForAsync/SetAsync,…Attributes[x] = v,ExternalSystem.Call/CachedCall,Database.Execute/Query/QuerySingle/CachedWrite,Notify.Send(list)/Notify.To(list).Send(...)(Async-suffixed spellings map identically). Reads (GetAttributes, indexer reads) produce no node because the runtime records no trace row for them; nor doesDatabase.Connection(...)— the number of commands on a raw handle is not statically knowable. - Outbound calls land on the External lane (2026-09-06, plan
docs/plans/2026-09-06-flow-debugger-outbound-calls.mdD8).ExternalSystem.*→ApiCalltitled{system}.{method};Database.*→DbCalltitled{connection} · {member};Notify→NotifytitledNotify {list}. ACachedCall,CachedWriteorNotifynode carriesNote = "cached"where applicable and its incoming edge isAsync; an async edge does not advance the chain — the next node links from the previous non-async node — mirroringFlowGraphBuilder.IsAsyncso a predicted async branch renders like a real one. - Literals resolve; everything else is
InputDependentand titled with the expression's source text (Route.To(code)). Branching is not modelled: every recognised call is emitted once in source order, and anif (…) return/throwguard becomes aNote = "early return: <condition>"on the node that follows it (the condition is visited first, so a guard whose condition is the traced call — Delmia'sif (!await …WriteBatchAndWaitAsync(…)) return …— attributes correctly). - DB-free by contract. Routed and nested script bodies come from the caller-supplied
resolveRouteScript(instanceCode, scriptName)(Central UI resolves instance → template parent chain → script; called with the literal code when constant, else with the expression text so a resolver may still answer); recursion stops atmaxDepthwith adepth limitnote. - Total. Parse errors or any walker fault degrade to the request/response pair (or the root
ScriptRun) carryingNote = "parse error"; the predictor never throws into the page. - Predicted kinds are exactly the traced kinds (
FlowStepKind, includingApiCall/DbCall/Notify), soFlowGraphViewcan overlay the real graph on the prediction by kind and grey the nodes a run never produced. There is deliberately no separate central "method script" node — the real graph records none, so theInboundRequestnode stands for it.
Dependencies
- Commons: Shared types referenced by
ScriptCompileSurfaceandTriggerCompileSurface(e.g.,DataType, attribute access types). - Microsoft.CodeAnalysis.CSharp.Scripting: Roslyn scripting APIs used by
RoslynScriptCompilerandScriptTrustValidator. - Microsoft.CodeAnalysis.CSharp.Workspaces: Roslyn workspace/syntax APIs used by
ScriptTrustValidator.
No dependency on Akka.NET, ASP.NET Core, Entity Framework, or any other ScadaBridge component above Commons.
Interactions
- Template Engine (#1): Consumes
ScriptTrustValidator.FindViolations,RoslynScriptCompiler.Compile,ScriptCompileSurface, andTriggerCompileSurfacein the design-time deploy gate (ScriptCompiler.TryCompile) and expression syntax validator (ValidationService.CheckExpressionSyntax). - Site Runtime (#3):
ScriptCompilationService.ValidateTrustModeldelegates the trust verdict toScriptTrustValidator.FindViolations; retains its ownCSharpScript.Compileagainst the realScriptGlobalsfor execution-time compilation. - Inbound API (#14):
ForbiddenApiChecker.FindViolationsis a thin shim overScriptTrustValidator.FindViolations. - Central UI (#9):
ScriptAnalysisServicedelegates the run-gate forbidden-API verdict and sources the editor-marker deny-list fromScriptTrustPolicy; retains the Test-Run execution host (SandboxScriptHost). - All script-compile surfaces: Site Runtime
ScriptCompilationService, Inbound APIInboundScriptExecutor, Central UIScriptAnalysisService, and this component's ownRoslynScriptCompilereach attachCachingScriptMetadataResolver.Instanceto theirScriptOptions(REQ-SA-6). - Transport (#24):
BundleImporterrunsScriptTrustValidator.FindViolationsover every non-Skip template, shared, and ApiMethod script body, template script + alarm Expression-trigger bodies, and instance alarm-override trigger expressions (InstanceAlarmOverrideDto.TriggerConfigurationOverride) during import validation (RunSemanticValidationAsyncPass 0) and preview (DetectBlockersAsync), rejecting forbidden-API scripts at import review rather than at runtime. Trust violations are hard errors for all script kinds (unlike the false-positive-prone name-resolution heuristic, whose template-script findings are advisory).