oct.mcp.sandbox module#

Purpose#

Execution Sandbox (Layer 4, outer wrapper) for the oct-mcp six-layer pipeline. Provides environment sanitisation, working-directory pinning, timeout enforcement, and output-size limiting for subprocess execution.

Phase 5C adds pluggable OS-level sandbox backends:

  • NativeBackend — existing behavior (subprocess + env sanitization). Always available; default on Windows.

  • BubblewrapBackend — Linux only. Wraps every command in bwrap --unshare-net --die-with-parent. Requires bwrap in PATH.

  • SandboxExecBackend — macOS only. Prepends sandbox-exec -p "(deny network*)(allow default)". Requires sandbox-exec in PATH.

Use make_sandbox_backend() to select the appropriate backend based on McpConfig.

Responsibilities#

  • Strip sensitive environment variables before forking (PYTHONPATH, common secret-named vars).

  • Pin the subprocess working directory to the validated project root.

  • Enforce per-tool timeouts and kill the process on expiry.

  • Cap combined stdout+stderr output to max_output_bytes.

  • Never use shell=True.

  • Return a SandboxResult — never raise for subprocess errors.

Diagnostics#

Domain: MCP Levels:

L1 — errors: process launch failures, unexpected errors L2 — lifecycle: process start, process end, backend selection L3 — command details (argv) L4 — deep trace: per-line output, env keys

Contracts#

  • shell=False is enforced unconditionally in all backends — no caller path can override it.

  • SandboxExecutor.run() never raises. Any OSError or subprocess.SubprocessError is caught, logged at level 1, and returned as a SandboxResult with a non-zero exit code.

  • The sanitised environment always preserves PATH, HOME, LANG, TERM, USER, USERNAME, LOGNAME, VIRTUAL_ENV, CONDA_DEFAULT_ENV, and OCT_MCP_SESSION_ID / OCT_MCP_REQUEST_ID correlation IDs.

  • make_sandbox_backend() never raises — it always returns a usable backend (falls back to NativeBackend).

class oct.mcp.sandbox.BubblewrapBackend[source]#

Bases: object

Linux bubblewrap backend — network-isolated, die-with-parent.

Wraps every command in bwrap --unshare-net --die-with-parent. Requires bwrap to be available in PATH.

Network isolation is provided by --unshare-net. The project root is bind-mounted read-write; system paths are read-only.

run(cmd: list[str], env: dict[str, str], cwd: Path, timeout: int, memory_limit_bytes: int | None) tuple[int, str, str][source]#
class oct.mcp.sandbox.NativeBackend[source]#

Bases: object

Subprocess-only backend — existing Phase 5A/5B behavior.

On POSIX, when memory_limit_bytes is non-None and non-zero, applies resource.setrlimit(RLIMIT_AS, ...) via preexec_fn to limit virtual address space. On Windows the limit is silently ignored (use Docker for Windows memory enforcement).

run(cmd: list[str], env: dict[str, str], cwd: Path, timeout: int, memory_limit_bytes: int | None) tuple[int, str, str][source]#
class oct.mcp.sandbox.SandboxBackend(*args, **kwargs)[source]#

Bases: Protocol

Protocol for OS-level sandbox backends.

Each backend wraps the subprocess call with platform-specific isolation (network namespace, resource limits, etc.). The backend receives a sanitised environment and returns raw (exit_code, stdout, stderr) — output capping and error handling live in SandboxExecutor.

run(cmd: list[str], env: dict[str, str], cwd: Path, timeout: int, memory_limit_bytes: int | None) tuple[int, str, str][source]#

Run cmd and return (exit_code, stdout, stderr).

Parameters:
  • cmd – Command + arguments list. Must not use shell=True.

  • env – Sanitised environment dict.

  • cwd – Working directory (validated project root).

  • timeout – Maximum wall-clock seconds.

  • memory_limit_bytes – Virtual memory ceiling in bytes, or None to skip. Implementations on POSIX apply this via setrlimit; on Windows it is silently ignored.

class oct.mcp.sandbox.SandboxExecBackend[source]#

Bases: object

macOS sandbox-exec backend — network-denied policy.

Prepends sandbox-exec -p "<profile>" to every command. Requires sandbox-exec to be available in PATH.

The policy denies all network access while allowing everything else (file I/O, process creation, etc.) needed to run oct commands.

run(cmd: list[str], env: dict[str, str], cwd: Path, timeout: int, memory_limit_bytes: int | None) tuple[int, str, str][source]#
class oct.mcp.sandbox.SandboxExecutor(backend: SandboxBackend | None = None, memory_limit_mb: int = 0)[source]#

Bases: object

Sandboxed subprocess runner for the MCP execution layer.

Wraps a SandboxBackend with environment sanitisation, output capping, and error handling. A single SandboxExecutor instance may be reused across multiple tool calls within the same server session.

Parameters:
  • backend – OS-level isolation backend. Defaults to NativeBackend when not supplied (Phase 5A/5B backward compatibility).

  • memory_limit_mb – Per-subprocess virtual memory limit in megabytes. 0 means no limit. Forwarded to the backend as bytes. POSIX only — silently ignored on Windows.

run(cmd: list[str], cwd: Path, timeout: int, max_output_bytes: int = 1048576) SandboxResult[source]#

Execute cmd in a sanitised environment under cwd.

Parameters:
  • cmd – The command and arguments to run. Must not contain shell metacharacters — this is enforced by passing shell=False in every backend.

  • cwd – Working directory, pinned to the validated project root.

  • timeout – Maximum wall-clock seconds before the process is killed.

  • max_output_bytes – Combined stdout+stderr cap. When exceeded, output is truncated and SandboxResult.output_truncated is set.

Return type:

SandboxResult — never raises.

class oct.mcp.sandbox.SandboxResult(exit_code: int, stdout: str, stderr: str, timed_out: bool = False, output_truncated: bool = False, error_message: str = '')[source]#

Bases: object

Result of a sandboxed subprocess execution.

exit_code#

The subprocess return code, or 1 on launch failure.

Type:

int

stdout#

Captured standard output (possibly truncated).

Type:

str

stderr#

Captured standard error (possibly truncated).

Type:

str

timed_out#

True if the process was killed due to timeout.

Type:

bool

output_truncated#

True if combined stdout+stderr exceeded max_output_bytes.

Type:

bool

error_message#

Non-empty if the process could not be launched (e.g. not found on PATH). Distinct from stderr — set on OSError.

Type:

str

error_message: str = ''#
exit_code: int#
output_truncated: bool = False#
stderr: str#
stdout: str#
timed_out: bool = False#
oct.mcp.sandbox.make_sandbox_backend(config: object) SandboxBackend[source]#

Select and return the best available SandboxBackend.

Parameters:

config – A McpConfig instance. Reads the sandbox_backend field ("auto" / "native" / "bubblewrap" / "sandbox_exec") and memory_limit_mb.

Returns:

  • A concrete backend instance. On "auto", picks

  • * BubblewrapBackend on Linux when bwrap is in PATH.

  • * SandboxExecBackend on macOS when sandbox-exec is in PATH.

  • * NativeBackend otherwise (always safe).

  • When a specific backend is forced (e.g. "bubblewrap") but its

  • binary is not found, raises RuntimeError.

  • Never raises on "auto" or "native" — always returns a

  • usable backend.