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).
3.5 KiB
Script file access — the Files global (2026-09-28)
Problem
A Delmia recipe download hands the machine a DownloadPath; in production the Galaxy DelmiaReceiver
object's own script opens that text file, parses it and loads the values into its tags. ScadaBridge scripts
cannot do the same thing: the trust policy forbids System.IO outright, and the deploy gate refuses any
script that names a System.IO type. So an in-memory test machine (and any future ScadaBridge-side
receiver) could exercise the handshake but never the file parse.
Options considered
- Leave parsing in the Galaxy. No change; test machines never parse.
- A small file-reader HTTP service reached through
ExternalSystem.Call. No trust-model change, but a new service to deploy and secure on every site. - A host-mediated, allow-listed platform API. Chosen (user decision). A deliberate, narrow exception
to the trust boundary: the script never touches
System.IO; the host reads on its behalf, only beneath directories an administrator lists.
Rejected outright: reading through SQL Server (OPENROWSET(BULK …) via Database.Connection) — needs
elevated SQL rights, reads the SQL host's filesystem, and sidesteps the trust policy.
Decisions
- Surface.
Files.ReadTextAsync(path),Files.ReadLinesAsync(path),Files.ExistsAsync(path)on the siteScriptGlobals(+Instance.Files), each taking an optionalCancellationToken. Read-only — no write, list, delete or directory enumeration; add one only with its own use case. - Off by default.
ScadaBridge:SiteRuntime:ScriptFileAccess:AllowedRoots(absolute local/UNC dirs, default empty) andMaxFileBytes(default 1 MiB, 1…64 MiB). No roots ⇒ every call throwsDisabled, so upgrading a site changes nothing until an operator opts in. Validated at boot. - Guard (host side,
ScriptFileReader). Fully-qualified path; no device paths or alternate data streams; canonicalize, then a directory-boundary prefix match against each root; no reparse point on the file or any directory between root and file; size cap enforced before and during the read. - One exception type in Commons —
ScriptFileAccessException+ScriptFileAccessFailure— because scripts cannot nameSystem.IOexceptions.Commons.Types.Scriptsjoins the default script imports on every surface so it binds by short name. - Site scripts only. Inbound API scripts run on central and have no
Files. The Test Run sandbox mirrors the member (so scripts bind) and throwsDisabledat run time.ScriptCompileSurface.CompileFilesmirrors it for the deploy gate; parity is pinned byCompileSurfaceParityTests. - Not audited. File reads are not one of the audit channels; refusals log a node Warning.
Accepted residuals
- Time-of-check/time-of-use between the link check and the open: a writer inside an allowed root could swap a file for a link in between. Bounded by the allow-list (only administrator-chosen directories) and by the API being read-only.
- The node's service account permissions still apply; on a site pair both nodes need the same roots and the same access (a script can run on either node after failover).
First consumer
docker/seed-mes-test-machine.sh: the SimDelmiaRecipe simulator on TestMesMachine reads the downloaded
recipe file (RecipeValueName,RecipeValue per line), types each value (int → double → bool → string) and
loads it into the matching TestDelmiaReceiver recipe attributes, rejecting unknown names and malformed
lines. Sample files: docker/recipes/.