🧰 What a script can do: what is available and what is forbidden
Updated
A business script is compiled by the platform inside a sandbox. The boundary is set by three different mechanisms, and confusing them is expensive.
Three mechanisms, not one#
The RuntimeCompiler reference set |
The ScriptSecurityService policy |
The ScriptServices deny-list |
|
|---|---|---|---|
| What it is | which assemblies the compiler can see | semantic analysis for dangerous BCL APIs | a filter on RESOLVING a service out of DI |
| When | compilation | checking/saving the script | GetService<T>() at runtime |
| The refusal | CS0103 / CS0246 / CS0012 |
a finding in the check | InvalidOperationException |
| Its nature | an omission, if something is missing | a decision | a decision |
Hence the rule: before you design your way around a "ban", find out which of the three lists it sits in. Absence from the reference set is something to fix; presence in the other two is something to respect.
The third mechanism is the one most often missed, and it guards the most
sensitive boundary. IOutboundTransport and ICredentialResolver sit NOT in the
policy — ScriptSecurityService parses syntax and knows BCL types only — but in
the ScriptServices.Denied dictionary, by full type name. The practical
consequence: ScriptSecurity.Mode = Off does not lift these two bans.
And about the policy's mode: by default it is Warn, not Enforce — the
finding is reported, the save goes through. On top of that, IsExemptAsync
exempts the system models always, and everything listed in
ScriptSecurity.TrustedModels.
Cryptography is not on the deny-list; it is a reference-set assembly. Without
System.Security.Cryptography, CS0103: SHA256 does not exist looks like a
sandbox rule, and a country's tax hash, CSR and QR signature would have to
live in the platform kernel that is installed everywhere. JSON is the same
class: a script that reads an external response needs System.Text.Json in
the reference set, or the gap shows up as CS0012 rather than a policy hit.
What is available#
System, the collections, LINQ,System.Text, tasks.- Cryptography —
System.Security.CryptographyandX509Certificates: hashes, ECDSA,CertificateRequest, reading PEM. - JSON —
System.Text.Json(plusSystem.Memory: the public surface ofJsonDocumentmentionsReadOnlySequence<byte>, and without it the script fails withCS0012, naming neither JSON nor the missing reference). System.Data(SlimTable), DataAnnotations,System.Linq.Expressions.- Platform services and model contracts through
GetService<T>():IDocumentManager,IDictionaryManager,IDataEntityManager(Data Entity projection — data-entities.md),ILinkTableManager, …
What is forbidden — and what replaces it#
The deny-list returns an exception, not null, including for TryGet: a
silent null would look like "not registered" and would send the author hunting for
a posting error that does not exist.
| Forbidden | Why | What instead |
|---|---|---|
IOutboundTransport |
sends bytes outside the outbox, the unique index and the attempt log | IOutboundGateway.EnqueueAsync / IOutboundCall.SendAsync by channel name |
ICredentialResolver |
returns the secret in clear text | name the credential; the host substitutes it itself |
HttpClient, sockets, WebRequest, SMTP |
the network | the same gateway; mail — IEmailService; print — IPrintQueueService |
Process, the file system |
leaving the bounds of the application | no replacement, and there should not be one |
Reflection.Emit, Assembly.Load |
loading code past the compiler | — |
The first two rows of the table come from ScriptServices.Denied; the rest come
from the policy.
The boundary runs along the secret, not along cryptography. A script hashes and signs by itself; what it cannot do is read the configured key. If it is the host's key you need to sign with, there is a wrapper that encapsulates the secret instead of handing it over:
var stamp = ScriptServices.Get<ICredentialSigner>().SignHash("fatoora-csid", hashBase64);
// the signature, the public key and the certificate's signature — all public;
// null if the certificate is not configured
A script can log#
A widespread misconception says it cannot; because of it, empty catch blocks
breed in the tree. It can:
ScriptServices.Get<ZuloOne.Log.IZuloOneLogger<MyService>>().Info("…", arg);
The generic form only, and the full type name. The output goes to
docker logs.
Implementing a platform seam#
A model's service may implement a PLATFORM interface, and the platform will find it:
public partial class FatooraSigner : IOutboundSigner { … }
ServiceRegistry.ResolveImplementingAsync— "who overrides the behavior"; two implementations in one layer is an error, and it throws.ServiceRegistry.ResolveAllImplementingAsync— a keyed set, where coexistence is normal: signing profiles, one per country.
If something is missing#
Do not work around it — check whether it is an omission. Adding an assembly to
RuntimeCompiler.GetDefaultReferences() is cheap and does not widen the
deny-list; ScriptCryptoSandboxTests holds both halves: cryptography and JSON
compile and produce no findings, the network and processes still do.
Where next#
- Outbound integrations — the queue, the channels, the signature.
- Script hosts and scopes — transactions and lifetimes.
- Chain of Command — how the links stack up.