
otgate — OT policy gateway
io.github.alikup-ai由 GitHub 帳號 alikup-ai 發布v0.1.0更新於 Nov 7, 2026
Gates an LLM agent's writes to an OPC UA server on ranges, rate of change and live interlocks.
概覽
位於 AI 代理與 OPC UA 伺服器之間的 MCP 閘道,依數值範圍、變化速率、聯鎖與審批策略放行或拒絕標籤讀寫。
- 可以做什麼
- 透過經策略檢查的代理讀取、寫入與瀏覽 OPC UA 標籤
- 依標籤套用規則:數值範圍、累計漂移、變化速率、聯鎖、呼叫次數上限
- 暫存需要人工審批的寫入,並列出、核准或拒絕這些請求
- 讀取記錄每次呼叫及其決策的附加式稽核日誌
- 以影子模式執行,被允許的寫入只會被記錄而不會真正執行
- 適用情境
- 當希望代理能操作工業製程標籤、但只能在人工設定的限制內操作,且需要稽核每一次呼叫時使用。它先面向本機試驗(內建記憶體模擬後端),策略調校好後再連接真實 OPC UA 伺服器。
- 執行需求
- 本機 Python 3.11+ 程序,從 PyPI 安裝 otgate,透過 stdio 執行。環境變數用於指定後端(OTGATE_BACKEND)、策略檔(OTGATE_POLICY)、稽核路徑(OTGATE_AUDIT)、影子模式(OTGATE_SHADOW)、速率歷史(OTGATE_RATE_HISTORY);連接真實伺服器還需 OTGATE_OPCUA_ENDPOINT 及 asyncua 額外相依套件。可選的 HTTP 模式需要 OTGATE_AGENT_TOKEN 與 OTGATE_OPERATOR_TOKEN。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 otgate — OT policy gateway,將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。
其他 MCP 客戶端
參照 儲存庫 中的啟動說明。
README
otgate
An OT-aware authorization gateway that sits between an LLM agent and an industrial OPC UA server, applying a process-aware access policy — value ranges, rate limits, interlocks — to every call the agent makes.
The same decisions as terminal output (python examples/demo.py)
[otgate demo: an LLM agent's writes to a reactor gated by policy — ALLOW, ASK, DENY with reasons]
otgate is itself an MCP server. The agent connects to it as if it were an ordinary OPC UA MCP; internally otgate holds a client to the real OPC UA server. Every call is transparent to the agent, and otgate intercepts each one to apply policy and record an audit line.
Why
- Existing OPC UA MCP servers write to tags with no authorization layer — the agent has full, unbounded access.
- Existing LLM guardrails (LlamaFirewall, NeMo Guardrails) work on text/prompts and do not understand process semantics — they know nothing about ranges, rates of change, or interlocks.
- otgate closes exactly that gap: OT semantics + admission policy + audit.
It is not "you may write to this tag" but "you may write within range X, no faster than Y over interval Z, and only while the emergency tag W is not active".
Where this fits: OWASP ASI02
In the OWASP Top 10 for Agentic Applications, the risk otgate targets is ASI02 — Tool Misuse & Exploitation: an agent that stays inside its granted privileges but uses a legitimate tool unsafely — parameter pollution, excessive execution, or a call that is only dangerous given the current system state.
Identity and access layers (ASI03) answer may this agent call this tool. They do not look at what it writes, how fast, or under which conditions. otgate is the layer that does:
Per-step limits judge one write; cumulative limits judge a series. Without the
latter an agent walks around max_rate with many small legal steps:
A full mapping against all ten OWASP agentic risks — including what otgate explicitly does not cover (tool-chain manipulation, goal hijacking, memory poisoning, rogue-agent detection) — is in docs/threat-model.md.
Install
Python 3.11+.
Policy example
A policy is a YAML list of per-tag rules. Any tag not listed is denied by default.
Access levels: read, write, write_with_approval, deny.
Write check order (first failing check wins): tag-in-policy → access → value range → interlocks → rate of change → approval → allow.
Demo
Run as an MCP server
Published to the official MCP Registry as
io.github.alikup-ai/otgate-opcua, and to PyPI asotgate. The registry name carriesopcuabecause its search matches on the server name only and does not index descriptions, so a bareotgateis findable only by someone who already knows the word.
otgate exposes: read_tag, write_tag, browse, get_audit_log, and the
operator approval tools list_pending, approve, deny.
Configuration is via environment variables (so the backend switches without code changes):
Example MCP client config (FakeBackend):
Related work: the layer underneath
otgate governs what the process allows right now — ranges, rate against the live value, interlocks, drift, approval. It does not try to be the access layer underneath that, and there is a good one: IndustriAgents/OPCUA-MCP answers may this caller touch this node at all — read-only by default, control profiles, allowlisted targets, pinned certificates, audited control calls.
The two questions are genuinely different. A write of 40 °C to an allowlisted target satisfies every access rule and is still wrong if the tag reads 22 °C and the ramp violates a thermal limit. Access control without process limits permits that write; process limits without access control have nothing to stand on.
A visible difference in the audit trail shows the split: their acceptance criteria require records to carry profile, tool, decision and target IDs without values, which is right for an access log. otgate records the value, because "200.0 refused, outside [40, 80]" is not a sentence you can write without it.
Connecting a real OPC UA server
No code changes — install the extra and point the config at your server:
The backend lives behind an abstract interface (otgate/backends/base.py) with two implementations — FakeBackend (in-memory simulator, no dependencies) and AsyncuaBackend (real OPC UA). asyncua is imported lazily, so the fake backend, the tests and the demo run without it.
Shadow mode
With OTGATE_SHADOW=1, a write that the engine allows is not executed on the backend — instead the audit records a "WOULD execute" line with executed: false. This lets you run an agent and see what it would do to the process without any real effect.
Human approval workflow
A write_with_approval tag returns ASK. Rather than silently drop the write,
otgate parks it as a pending request with a TTL and hands the agent an
approval id. A human then resolves it through the operator tools:
list_pending— show parked write requests.approve <id>— approve and execute, after re-checking policy against the current process state. If an interlock tripped or the value drifted out of range while the request waited, the approval is recorded but the write is blocked — an approval is intent, never a blind execute.deny <id>— reject; the write never runs.
Requests that nobody answers within the TTL expire (fail-safe: silence means no). Every step — the ASK, the approval/denial, the eventual execution — is audited.
The stdio server exposes all tools in one process (fine for local use). For a real deployment, run the two authenticated HTTP channels below so the agent physically cannot reach
approve/deny.
Deployment & security
For anything beyond local experimentation, run otgate over HTTP as two separate channels, each behind its own bearer token:
- agent channel —
read_tag,write_tag,browse - operator channel —
get_audit_log,list_pending,approve,deny
The agent holds only the agent token, so it cannot even see the approval tools — isolation is by channel, not by convention.
Every request must carry Authorization: Bearer <token>; anything else gets
401. Tokens are compared in constant time. otgate refuses to start if either
token is unset or if the two are equal.
Network isolation is not optional. otgate is a soft policy layer: if the OPC UA server is reachable directly, an agent can bypass the gateway entirely and the policy means nothing. A safe deployment therefore requires:
- The OPC UA server accepts connections only from the otgate host/process (bind to loopback or a private segment; firewall everything else).
- The agent can reach only the agent HTTP channel — never the OPC UA server and never the operator channel.
- The operator channel is reachable only by the human/console that approves.
Put another way: otgate is effective exactly when it is the only path to the server. It complements — never replaces — hardware interlocks / SIS.
Multiple agents with different rights
To run several agents — each with its own token, its own policy, and its own
identity in the audit trail — point OTGATE_AGENTS at an agents.yaml:
Each agent is served on its own port (from OTGATE_AGENT_PORT upward) behind its
own token, so the diagnostics agent physically cannot use the optimizer's rights
and vice versa. All agents share one backend, one audit log, one rate-history and
one approval store — so rate limits are global per tag and an operator sees every
agent's pending approvals. Every audit line carries the agent that made the
call; when an operator approves a parked write, it is attributed to the agent
that requested it, not the operator.
Startup is fail-closed: otgate refuses to launch on a duplicate id/token, a missing policy file, an operator token that matches an agent token, or an operator port that collides with the agent port range.
Resilience & health
otgate fails closed when the backend is unreachable — it never turns an outage into a silent success:
-
A write whose rate or drift baseline cannot be read is denied: an unverifiable baseline is treated exactly like an unverifiable interlock.
-
A read or write during an outage returns an ERROR decision (not an exception, not a fake success) and is written to the audit log with
executed: false. -
A write whose interlock cannot be read is denied — otgate will not run a write it could not safety-check.
-
A parked write approved during an outage is not executed; it stays pending (or is denied on the fail-closed re-check) so the operator can retry.
The real OPC UA backend auto-reconnects: a dropped connection triggers a
bounded retry-with-backoff and one operation retry before giving up with a
BackendError. A missing tag stays a plain error and never triggers reconnects.
Each HTTP channel exposes an unauthenticated GET /health for liveness
probes — status only, never tag data:
Opening /health does not open anything else — /mcp still requires the token.
If the otgate process itself dies, the agent simply loses its channel (no
process, no access) while the physical process keeps running on the PLC — which
is the safe direction. Run otgate under a supervisor (systemd, a container
restart policy) and point your monitor at /health.
Performance & fidelity
otgate is a policy layer, not a signal transformer — it decides whether a call may
pass, it does not reshape the value. Measured against the bundled asyncua reactor
simulator over a real opc.tcp:// connection:
- No signal distortion. Values written through otgate read back byte-for-byte
identical from the server — worst absolute error
0across values from0.1to123456.789andπto full double precision. otgate never rounds, rescales or coerces; the value goes toasyncuaas-is and the type is the server's. - No data loss on allowed traffic; intentional loss on denied traffic —
a
DENY/ASKwrite never reaches the server (that is the whole point), and every call, allowed or not, produces exactly one append-only audit line. - Latency overhead ≈ +0.16 ms/call (mean) over the raw backend read — about
+20 % relative on a sub-millisecond local read. The policy check itself is
microseconds;
audit.record()is ~0.03 ms (the file handle is kept open and flushed, not reopened per call). A rate-limited write costs one extra backend read to fetch the live baseline, and an interlocked write one read per guard tag — deliberate safety round-trips.
This is comfortably fast for the intended use — an LLM/agent issuing setpoints on
the scale of seconds. otgate is not designed to sit inside a real-time control
loop (PLC scan-cycle, millisecond determinism); deterministic control stays
below it in the PLC/DCS. Audit durability is OS-level (flushed on every record,
survives a process crash) but not fsync'd per line — a stricter mode is a TODO.
Tests
The unit suite runs entirely on the FakeBackend with no external dependencies. Each policy rule has a test proving it works: in-range → ALLOW/ASK, out-of-range → DENY, ESD interlock → DENY, over-rate → DENY, approval tag → ASK, read-only → DENY, tag-not-in-policy → DENY — plus policy validation, audit fields, shadow mode, rate-history persistence, the approval workflow, channel auth/isolation, per-agent policies, and fail-closed/health behaviour.
The integration suite (tests/test_integration_asyncua.py) starts the asyncua reactor simulator on a fresh port and drives the same scenarios — including an approval that really lands on the server and a mid-session server stop that must fail closed — over a live opc.tcp:// connection. It is skipped automatically when asyncua is not installed.
Both suites run in CI (GitHub Actions) on Python 3.11 and 3.12.
What this does NOT do yet (v0.1)
Honest limitations — these are deliberate cuts, not oversights:
- No agent roles. Every caller is treated the same; there is no per-identity policy.
- No time windows. You cannot express "only during the day shift".
- Scalars only. Values are numbers or booleans; arrays/structs are not supported.
- Single client. No multi-user access, queueing, or locking.
- Agent config is static. Multiple agents, each with their own token, policy and audit identity, are supported via
agents.yaml. But the set of agents is read at startup — there is no live add/revoke; changing it means a restart. Tokens are long-lived (no rotation/expiry). max_rateis wall-clock, and its time base is per-process. The baseline is the tag's live value, read from the backend at check time, so the limit holds even when something other than otgate moves the tag. The time base still comes from otgate's own record of the last write it let through: with no record there is no measurable interval, so the write is bounded to one fullrate_intervalallowance rather than being rate-checked against elapsed time. SetOTGATE_RATE_HISTORYto a file path to keep that time base across restarts. otgate still does not read the historian trend, so it cannot see how the tag got to its current value.- No GUI.
Roadmap
- Live agent management (add/revoke without restart, token rotation/expiry)
- Time-window conditions (shifts, maintenance windows)
- Historian-trend rate limits (the live-value baseline landed in v0.2; reading the actual trend is the remaining step)
- Cross-tag budgets: a group of tags sharing one cumulative allowance, so an agent cannot spread its effect across several individually-compliant tags
- More interlock actions beyond
deny - High availability (otgate is single-instance today; a crash drops agent access until restart)
License
Apache-2.0. See LICENSE.
來源:README.md,提交 4f54555
工具
0版本歷史
1- v0.1.0最新Nov 7, 2026
評分
已滿足適用檢查項的 50%
該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。
- 近 30 天內可連通25 分
請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。
- 已宣告認證方式10 分
請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。
- 已列出工具15 分
請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。
- 描述必需15 分
- 圖示10 分
- 文件15 分
- 授權條款10 分