The Script Sandbox
Python scripts run in a restricted environment on the Python worker. This page covers what is available, how to extend it, and the gates that apply.
The sandbox stops mistakes, not a determined author. It is a guardrail: it keeps an ordinary
script from reaching the filesystem or the network by accident, and it makes the intended route —
system.files, system.db, system.tag — the easy one.
It is not a security boundary, and sizing a decision on it being one is the mistake this note exists to prevent. The real boundary is who may author a script: treat Manage the Designer as equivalent to server access, and grant it accordingly. New in 1.0.4
What is available by default
| Available | Notes |
|---|---|
system | The full scripting API |
math | Standard library |
json | Standard library |
datetime | Standard library |
| Installed packages | Anything an administrator has added, see below |
Direct filesystem, network and process access are not part of the surface. Files go through
system.files; databases through system.db; devices through system.tag. Each of those is
audited, pooled and permission-gated in a way an arbitrary open() or socket would not be.
Installing additional packages
Administrators can install Python packages from the interface:
Settings → Script Sandbox → Add & install. Everything on that screen — reading the configuration,
listing or installing packages, and the connectivity check — requires Manage the script sandbox.
Packages install into a managed directory inside the data directory (python-packages/), which
is placed on the worker's module path. Keeping them there rather than inside the bundled runtime is
what makes them survive an upgrade — the installer replaces the runtime, not your data.
The connectivity check exists because an air-gapped plant network is the normal case, not the exception — it tells you whether the installer can reach an index before you try.
A package runs with the worker's authority, and a data library brings its own reach: numpy
re-exports ctypes through numpy.ctypeslib, torch.load and joblib.load deserialise with
pickle, and pandas.read_csv reading any path on disk is that API doing its job.
An import allow-list cannot take any of that back once the package is present, because the check is on the root package name and these libraries hand back the very modules the sandbox refused. The install screen says so at the button. New in 1.0.4
Install deliberately, hold Manage the script sandbox to administrators only, and review an addition the way you would review a dependency in any other production system.
Capability scopes
Every system call is gated by a capability scope, enforced at the worker.
| Namespace / method | Scope |
|---|---|
tag.read | tag:read |
tag.write | tag:write |
db.runNamedQuery | db:read |
db.runQuery, db.callProc, all transaction calls | db:write |
hist.getTrends | hist:read |
vars.get, vars.keys, vars.list | vars:read |
vars.set, vars.delete, vars.incr | vars:write |
files.read, files.size, files.readChunk | file:read |
files.save, files.delete | file:write |
message.send, script.runOnGateway | script:execute |
Two design points worth knowing:
runQueryneedsdb:write,runNamedQueryneeds onlydb:read. Arbitrary SQL may write; a named query cannot. Preferring named queries lets scripts run at a lower privilege.- The gate is fail-safe. A method with no explicit mapping falls back to its namespace's strong scope, so a newly-added API is gated by default rather than accidentally open.
The full per-method mapping is on the System API reference.
Scopes also apply to API keys calling REST endpoints, which is what lets one key be granted read access without write.
A script may not do what its caller could not New in 1.0.4
Two gaps are closed, both of the same shape: a script doing on somebody's behalf what that person had been refused directly.
Tag write levels now reach system.tag.write. A capability scope is all-or-nothing per
resource, so tag:write alone would let a script write any tag. Each tag's own
write level now travels with the grant and is checked against
the target before the write reaches equipment — the same rule, in the same place, as a write asked
for directly from a screen.
An unknown tag resolves to the baseline level, so a namespace mid-import does not brick every write. A failed level lookup refuses the batch instead: a database blip must not quietly downgrade a tag, and a write that did not happen can be retried while one that should not have happened cannot be taken back.
A button in LiveView no longer runs an unclamped script. When a browser calls
script.runOnGateway, the Gateway script now carries a grant derived from the caller's own verified
realm and write level:
| Caller | Grant |
|---|---|
| Platform (Designer) | Unrestricted — a designer may already invoke every method directly |
| Operator below Operate | Reads only |
| Operator at Operate or above | Reads, plus tag, database, history, variable and file writes |
| Operator at Supervise or above | The above, plus running another Gateway script |
| Anonymous, or a realm we do not recognise | Reads only |
The grant is set server-side from session state and never from request parameters, so a client
cannot widen it. QUBIQ_BROWSER_SCRIPT_SCOPES=off restores the previous behaviour for a site that
genuinely depends on an operator button running an automation broader than the operator themselves.
"An operator presses a button and something privileged happens" is a real design, and it now needs the operator to hold the level the automation uses — or the escape hatch above. Prefer raising the role, so the audit trail keeps saying who did what.
Resource limits
| Limit | Default | Where set |
|---|---|---|
| Python node timeout | 60 s | Node configuration |
| Gateway entry-point timeout | Per entry point | @gateway(timeout_ms=...) |
| Database transaction TTL | Per call | timeoutMs in transaction options |
| Worker replicas | Configured | PYTHON_WORKER_REPLICAS |
A script that exceeds its timeout is terminated and the failure logged. Raising PYTHON_WORKER_REPLICAS
increases parallel script capacity; it does not make an individual script faster.
The interpreter
The worker runs on a private Python runtime bundled by the installer, inside QUBIQ's own install
folder with PYTHON_PATH already pointing at it. It is version-pinned by us, is not on your system
PATH, cannot collide with any Python you already have, and is removed on uninstall.
You do not install Python, create a virtual environment, or run pip install to use scripting.
It ships with the worker's transport library (required) and ruff (the in-editor linter).
Without ruff the worker still runs — linting falls back to a syntax-only check — so a missing
linter degrades the editing experience rather than breaking execution.
If the bundled runtime is missing, QUBIQ boots degraded: the interface works and connections
poll, but Python script nodes and Gateway scripts fail. Reinstall rather than repointing
PYTHON_PATH at a system Python — the bundled one is pinned against the worker's expectations.
Good practice
- Keep scripts small. A transform that does one thing is testable and readable; a 300-line node is neither.
- Do not embed credentials. Use a connection; its secrets are encrypted and never exposed.
- Prefer named queries. Lower privilege, no injection surface, one place to change the SQL.
- Check tag quality before acting. Repeated because it is the most common real-world failure.
- Handle errors deliberately. Decide whether a failure should drop the message, route to a notification branch, or fail the node.
Next
→ Alarms