Files
ScadaBridge/docs/plans/2026-09-28-script-file-access-design.md
T
Joseph Doherty b3021e7c5f feat(scripts): read-only, allow-listed Files global for site scripts; Delmia test receiver parses recipe files
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).
2026-09-28 10:36:42 -04:00

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

  1. Leave parsing in the Galaxy. No change; test machines never parse.
  2. 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.
  3. 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 site ScriptGlobals (+ Instance.Files), each taking an optional CancellationToken. 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) and MaxFileBytes (default 1 MiB, 1…64 MiB). No roots ⇒ every call throws Disabled, 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 name System.IO exceptions. Commons.Types.Scripts joins 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 throws Disabled at run time. ScriptCompileSurface.CompileFiles mirrors it for the deploy gate; parity is pinned by CompileSurfaceParityTests.
  • 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/.