oct.mcp.safety module#

Purpose#

Safety Gate (Layer 2.5) for the oct-mcp six-layer pipeline. Enforces the Human-in-the-Loop (HITL) consent protocol for all write/destructive tool calls, and applies dry-run defaults.

Responsibilities#

  • Define SafetyDecision — the structured result of a safety check (proceed, requires_confirmation, dry_run, reason).

  • Define SafetyGate with a single SafetyGate.check() entry point consumed by oct.mcp.tools._run_tool_write().

  • Define _format_confirmation_request() which produces the structured [oct-mcp requires_confirmation] response string returned to the AI host when confirmation is needed.

HITL Protocol (D-5B-1 — stateless)#

No server-side pending state.

  • First call (confirm=False, default): safety gate returns proceed=False. The caller returns a [oct-mcp requires_confirmation] string to the AI host; no execution occurs.

  • Second call (confirm=True): safety gate returns proceed=True; full execution proceeds.

The AI host re-submits the identical call with confirm=True after the user approves in the host UI. There is no replay window to exploit (T-08) because each call is evaluated independently.

Dry-run rules (D-5B-2, D-5B-5)#

  1. Non-destructive tools → always proceed=True, dry_run=False.

  2. Destructive, confirm=Falseproceed=False, requires_confirmation=True.

  3. oct_format, confirm=True, apply=Falseproceed=True, dry_run=True.

  4. oct_format, confirm=True, apply=Trueproceed=True, dry_run=False.

  5. Other destructive, confirm=Trueproceed=True, dry_run=False. (Tools with their own dry_run field honour that field directly — the executor reads it from the validated args dict.)

Diagnostics#

Domain: MCP Levels:

L2 — lifecycle: safety decision L4 — deep trace: field values

Contracts#

  • This module does not import click, the MCP SDK, or any oct.mcp.executor / oct.mcp.tools module (no cycles).

  • SafetyGate.check() never raises for valid input.

  • The confirmation request string always starts with the sentinel [oct-mcp requires_confirmation] so callers can detect it via simple prefix matching.

class oct.mcp.safety.SafetyDecision(proceed: bool, requires_confirmation: bool, dry_run: bool, reason: str)[source]#

Bases: object

Result of a SafetyGate.check() call.

proceed#

True → execution may continue; False → return the confirmation request to the AI host.

Type:

bool

requires_confirmation#

True when the AI host must re-submit the call with confirm=True before execution proceeds.

Type:

bool

dry_run#

True → executor should pass --dry-run (or equivalent) to the CLI subprocess. Applies only to oct_format (where apply=False triggers dry-run even with confirm=True). Other tools that have their own dry_run field manage it independently.

Type:

bool

reason#

Human-readable explanation forwarded to audit logs.

Type:

str

dry_run: bool#
proceed: bool#
reason: str#
requires_confirmation: bool#
class oct.mcp.safety.SafetyGate(config: McpConfig)[source]#

Bases: object

HITL + dry-run enforcement gate for write tools.

One instance is created per MCP server session (in server.py) and stored in ServerState.

The gate is stateless — each check() call is independent. There is no in-memory pending-confirmation queue. This eliminates the T-08 replay-window attack surface.

Parameters:

config – Active McpConfig. The gate reads config.dry_run_default_for_writes to decide the fallback dry-run behaviour.

check(tool_name: str, spec: ToolSpec, args: dict) SafetyDecision[source]#

Evaluate the HITL gate and dry-run rules for tool_name.

Parameters:
  • tool_name – MCP tool name (e.g. "oct_format").

  • specToolSpec for tool_name.

  • args – Validated args dict (model.model_dump()).

Return type:

SafetyDecision — never raises.