8Centralised policy loader for the ``oct-mcp`` server (Phase 5C, M-C7).
10Loads policy override JSON from a local filesystem path or an HTTPS URL
11at server startup. The loaded overrides are applied on top of the base
12:class:`~oct.mcp.config.McpConfig` before the policy engine is
13instantiated, enabling enterprise deployments to distribute a shared
14security policy from a central location.
18Set ``policy_source`` in ``~/.oct-mcp/config.json``::
21 "policy_source": "https://internal.company.com/oct-policy.json"
24Or provide a local path::
27 "policy_source": "/etc/oct/policy.json"
30Policy JSON schema (all fields optional)::
34 "rate_limit_per_minute": 20,
35 "timeout_default_seconds": 30,
36 "max_output_bytes": 524288,
37 "dry_run_default_for_writes": true
40Only the keys listed in :data:`_ALLOWED_OVERRIDE_KEYS` are honoured.
41Unknown keys are silently dropped. Type mismatches are silently ignored.
45- HTTPS URLs only. HTTP URLs are rejected to prevent cleartext policy
47- File size capped at 64 KB (same as ``config.json``).
48- Schema is an allowlist — no arbitrary key injection possible.
49- Any failure (network error, parse error, schema violation) falls back
50 to built-in defaults silently without crashing the server.
56 L1 — errors: load failures, schema violations
57 L2 — lifecycle: policy loaded, overrides applied
58 L4 — deep trace: resolved field values
62- :func:`load_policy_source` never raises — returns ``{}`` on any error.
63- :func:`apply_policy_overrides` never raises — returns *config* unchanged
65- No ``http://`` URLs are accepted — HTTPS only.
68from __future__
import annotations
72from pathlib
import Path
75 from oc_diagnostics
import _dbg
as _real_dbg
77 def _dbg(*args, **kwargs) -> None:
78 _real_dbg(*args, **kwargs)
80 def _dbg(*args, **kwargs) -> None:
89_POLICY_MAX_BYTES: int = 64 * 1024
93_ALLOWED_OVERRIDE_KEYS: frozenset[str] = frozenset({
95 "rate_limit_per_minute",
96 "timeout_default_seconds",
98 "dry_run_default_for_writes",
108 """Load policy override JSON from *source*.
113 A local filesystem path or an ``https://`` URL. ``http://`` URLs
114 are rejected. Local paths may be absolute or relative.
116 Network timeout in seconds for HTTPS requests (default 5).
120 A ``dict`` containing only the keys in :data:`_ALLOWED_OVERRIDE_KEYS`
121 with validated values. Returns ``{}`` on any failure.
123 func =
"load_policy_source"
129 if source.startswith(
"http://"):
132 "SYSTEM_ERROR: HTTP policy source rejected; use HTTPS",
137 if source.startswith(
"https://"):
145 data = json.loads(raw)
146 if not isinstance(data, dict):
147 _dbg(
"MCP", func,
"SYSTEM_ERROR: policy source is not a JSON object", 1)
151 _dbg(
"MCP", func, f
"loaded {len(overrides)} override(s) from {source}", 2)
154 except Exception
as exc:
155 _dbg(
"MCP", func, f
"SYSTEM_ERROR: failed to load policy source: {exc}", 1)
160 """Return a new :class:`~oct.mcp.config.McpConfig` with *overrides* applied.
165 The base :class:`~oct.mcp.config.McpConfig` instance.
167 Dict of validated overrides from :func:`load_policy_source`.
171 A new ``McpConfig`` with the override values merged in. Returns
172 *config* unchanged if *overrides* is empty or an error occurs.
174 func =
"apply_policy_overrides"
178 result = dataclasses.replace(config, **overrides)
179 _dbg(
"MCP", func, f
"applied overrides: {list(overrides.keys())}", 2)
181 except Exception
as exc:
182 _dbg(
"MCP", func, f
"SYSTEM_ERROR: failed to apply overrides: {exc}", 1)
192 """Fetch *url* via HTTPS and return raw bytes, or ``None`` on error."""
195 import urllib.request
196 req = urllib.request.Request(
198 headers={
"User-Agent":
"oct-mcp/policy-loader"},
200 with urllib.request.urlopen(req, timeout=timeout_s)
as resp:
201 raw = resp.read(_POLICY_MAX_BYTES + 1)
202 if len(raw) > _POLICY_MAX_BYTES:
203 _dbg(
"MCP", func,
"SYSTEM_ERROR: policy source too large (>64 KB)", 1)
206 except Exception
as exc:
207 _dbg(
"MCP", func, f
"SYSTEM_ERROR: failed to fetch {url}: {exc}", 1)
212 """Read *path* from the filesystem and return raw bytes, or ``None``."""
216 size = p.stat().st_size
217 if size > _POLICY_MAX_BYTES:
218 _dbg(
"MCP", func, f
"SYSTEM_ERROR: policy file too large ({size} bytes)", 1)
220 return p.read_bytes()
221 except Exception
as exc:
222 _dbg(
"MCP", func, f
"SYSTEM_ERROR: failed to read {path}: {exc}", 1)
227 """Return a sanitised override dict with only allowed, well-typed values."""
232 if "profile" in data
and data[
"profile"]
in VALID_MCP_PROFILES:
233 result[
"profile"] = data[
"profile"]
235 for int_field
in (
"rate_limit_per_minute",
"timeout_default_seconds",
"max_output_bytes"):
236 if int_field
in data
and isinstance(data[int_field], int)
and data[int_field] > 0:
237 result[int_field] = data[int_field]
239 if "dry_run_default_for_writes" in data
and isinstance(
240 data[
"dry_run_default_for_writes"], bool
242 result[
"dry_run_default_for_writes"] = data[
"dry_run_default_for_writes"]
bytes|None _read_file(str path)
None _dbg(*args, **kwargs)
dict _validate_overrides(dict data)
object apply_policy_overrides(object config, dict overrides)
bytes|None _fetch_url(str url, int timeout_s)
dict load_policy_source(str source, int timeout_s=5)