SafetyGate Audit — rigol-mcp vs Rei stack

STEP 1512 audit only novelty: zero (pattern extraction) 2026-08-28 · docs/safety-gate-audit-rigol-comparison.md

Purpose: erebusnz/rigol-mcp (★25, MIT, Python) の send_raw safety pattern を audit し、 Rei stack SafetyGate (benchtop-mcp v0.5.0-alpha, STEP 1345) と 比較。 Rei-scout Finding B (2026-08-27 report) の pattern-learning 実行。 Rei-AIOS 側 は Rigol hardware 未所有 = direct wire-up rejection、 pattern 抽出 のみが 実 audit 価値。

Threat model comparison

SystemDesign goalThreat prevented
rigol-mcp制御 target hardware (scope) with escape hatchInstrument state pollution / arbitrary SCPI discoverability
Rei stack SafetyGateImport 外部 provenance record + 保存前 validatePhysical damage (short circuit / DUT destruction) via unit-confusion (Kikusui PLZ-5W CR mode Siemens vs Ω)

rigol-mcp — 3 layer safety design

Layer 1: Whitelist by default

20+ dedicated tools (screenshot / idn / measure / get_waveform / run / stop / autoscale etc.) each with bounded semantics。 Arbitrary SCPI (send_raw) is NOT among defaults

Layer 2: env var opt-in gate

RIGOL_ENABLE_SEND_RAW を truthy にした場合のみ send_raw を expose。 2 checkpoint defense-in-depth:

_SEND_RAW_ENV = "RIGOL_ENABLE_SEND_RAW"

def _send_raw_enabled() -> bool:
    return os.environ.get(_SEND_RAW_ENV, "").strip().lower() \
           not in ("", "0", "false", "no", "off")

# server.py:398 — list_tools() filter
if not _send_raw_enabled():
    tools = [t for t in tools if t.name != "send_raw"]

# server.py:540 — call_tool() re-check
if name == "send_raw":
    if not _send_raw_enabled():
        raise ValueError(
            f"send_raw is disabled. Set {_SEND_RAW_ENV}=1 to enable ..."
        )

Client が cached tool schema や 直接 construction で bypass しても call path で reject。 error message に fix hint (env var 名) 埋込。

Layer 3: Post-write SCPI error queue drain

# scope.py:347
def send_raw(scope, command: str) -> str:
    if command.strip().endswith("?"):
        return scope.query(command).strip()
    scope.write(command)
    if err := check_scpi_error(scope):
        raise RuntimeError(f"SCPI error after '{command}': {err}")

check_scpi_error は queue を drain (最初の 1 件でなく 全件)、 stale error が 次 tool call に leak するのを防止。

Rei stack SafetyGate (STEP 1345)

Rule-based dispatch

class SafetyGate:
    def __init__(self, rules=None):
        self.rules = rules or [_rule_kikusui_plz5w_conductance_hazard]

    def check_scpi(self, vendor, model, command, args) -> SafetyVerdict:
        for rule in self.rules:
            verdict = rule(vendor, model, command, args)
            if verdict is not None:
                return verdict  # fail-fast
        return SafetyVerdict(ok=True, severity="info",
            rule_id="no-rule-matched", ...)

Built-in rule (Kikusui PLZ-5W CR mode)

Hazard: CR mode は CONDuctance (Siemens) を 使う、 RESistance (Ω) ではない。 caller が CONDuctance 100 を 「100Ω」 のつもりで 送ると 実際は 100S = 0.01 Ω = 短絡 (DUT 破壊)、 syntax error にならず そのまま 実行。

Rule threshold: > 1 S ⇒ < 1 Ωseverity: dangerous で reject、 suggested_fixCONDuctance {1/value} 埋込。 Kikusui マニュアル (「ジーメンス」 明記) と 突合済。

SafetyVerdict schema (rigol-mcp ValueError より rich)

class SafetyVerdict:
    ok: bool
    severity: Literal["info", "warn", "dangerous"]
    rule_id: str
    reason: str
    suggested_fix: str | None
    detail: dict[str, Any] | None

Side-by-side

Dimensionrigol-mcpRei stack SafetyGate
Default postureWhitelist 20+ tool; arbitrary SCPI hiddenRule-match のみ; arbitrary-SCPI wrapper なし
Gate mechanismenv var (process 全体 toggle)per-command rule dispatch (regex + threshold)
Defense-in-depth2 checkpoint (discover + call)1 checkpoint per call
Fix hintValueError string 内 env var 名structured suggested_fix field
Post-write verifyerror queue auto-drain + raiseN/A (import-only、 direct write なし)
Overrideenv var toggle (session level)未実装 (spike scope-out)
Rule extensibilityN/A (gate 1 個)Composable SafetyGate(rules=[...])
Hazard granularityCoarse (any SCPI → gate)Fine (vendor/model/command/value → rule)
Blast radiusInstrument state pollutionPhysical damage (short circuit)

Interpretation — not competing, complementary

Different threat model at different pipeline stage:

Production write-capable MCP server は 両方持つべき: (1) rigol-mcp-style gate at write path (whitelist + env opt-in for escape hatch) + (2) Rei-stack-style SafetyGate on write pre-check + on import path。

Refinement candidates for Rei stack (defer)

全 defer、 subsequent STEP directive 待ち:

Rejection reason for direct benchtop-mcp integration

Rei-scout Finding B は MEDIUM priority で 「Rigol hardware 導入 予定 なし → Rejection candidate」 と 記録。 本 audit で direct wire-up rejection is correct 確認:

Wire-up は hardware ownership 依存で block 継続。 学べた design pattern が 実 deliverable。

Honest scope

Attribution