90 lines
3.3 KiB
Python
90 lines
3.3 KiB
Python
"""Sanctioned MCP daemon guards for imports and credential access (#558).
|
|
|
|
Direct ``import gitea_mcp_server`` / ``import gitea_auth`` from a shell, plus
|
|
raw keychain dumps, bypass preflight purity and role gates. Mutation helpers
|
|
and keychain fallbacks therefore require an explicit sanctioned runtime.
|
|
|
|
Sanctioned contexts (any one):
|
|
- ``GITEA_MCP_SANCTIONED_DAEMON=1`` (set by the official MCP entrypoint)
|
|
- pytest (``PYTEST_CURRENT_TEST`` present)
|
|
- explicit operator opt-in ``GITEA_ALLOW_DIRECT_MCP_IMPORT=1`` (tests/tools only)
|
|
|
|
Credential keychain fill additionally allows:
|
|
- ``GITEA_ALLOW_KEYCHAIN_CLI=1`` for operator-only non-MCP scripts that must
|
|
use git-credential (never the default for LLM shells).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
SANCTIONED_DAEMON_ENV = "GITEA_MCP_SANCTIONED_DAEMON"
|
|
ALLOW_DIRECT_IMPORT_ENV = "GITEA_ALLOW_DIRECT_MCP_IMPORT"
|
|
ALLOW_KEYCHAIN_CLI_ENV = "GITEA_ALLOW_KEYCHAIN_CLI"
|
|
|
|
|
|
class UnsanctionedRuntimeError(RuntimeError):
|
|
"""Raised when mutation/credential code runs outside a sanctioned MCP daemon."""
|
|
|
|
|
|
def is_pytest_runtime() -> bool:
|
|
if os.environ.get("GITEA_TEST_FORCE_UNSANCTIONED") == "1":
|
|
return False
|
|
import sys
|
|
if "pytest" in sys.modules:
|
|
return True
|
|
return bool((os.environ.get("PYTEST_CURRENT_TEST") or "").strip())
|
|
|
|
|
|
def is_sanctioned_mcp_daemon() -> bool:
|
|
if (os.environ.get(SANCTIONED_DAEMON_ENV) or "").strip() in {"1", "true", "yes"}:
|
|
return True
|
|
if (os.environ.get(ALLOW_DIRECT_IMPORT_ENV) or "").strip() in {"1", "true", "yes"}:
|
|
return True
|
|
if is_pytest_runtime():
|
|
return True
|
|
return False
|
|
|
|
|
|
def mark_sanctioned_daemon() -> None:
|
|
"""Call from the official MCP server entrypoint before serving tools."""
|
|
os.environ[SANCTIONED_DAEMON_ENV] = "1"
|
|
|
|
|
|
def assert_sanctioned_mutation_runtime(context: str = "mutation") -> None:
|
|
"""Fail closed when server mutation code is used outside the MCP daemon."""
|
|
if is_sanctioned_mcp_daemon():
|
|
return
|
|
raise UnsanctionedRuntimeError(
|
|
f"Unsanctioned runtime blocked {context} (#558). "
|
|
"Do not import gitea_mcp_server / call mutation helpers from a raw "
|
|
"shell or ad-hoc script. Use the official MCP daemon entrypoint "
|
|
f"(sets {SANCTIONED_DAEMON_ENV}=1), or run under pytest. "
|
|
f"Operator-only override: {ALLOW_DIRECT_IMPORT_ENV}=1 (not for LLM sessions)."
|
|
)
|
|
|
|
|
|
def assert_keychain_access_allowed() -> None:
|
|
"""Fail closed for git-credential keychain fill outside sanctioned contexts."""
|
|
if is_sanctioned_mcp_daemon():
|
|
return
|
|
if (os.environ.get(ALLOW_KEYCHAIN_CLI_ENV) or "").strip() in {"1", "true", "yes"}:
|
|
return
|
|
raise UnsanctionedRuntimeError(
|
|
"Unsanctioned keychain/credential fill blocked (#558). "
|
|
"Token extraction via git-credential is only allowed inside the "
|
|
f"official MCP daemon ({SANCTIONED_DAEMON_ENV}=1), pytest, or with "
|
|
f"explicit operator opt-in {ALLOW_KEYCHAIN_CLI_ENV}=1."
|
|
)
|
|
|
|
|
|
def runtime_status() -> dict[str, Any]:
|
|
return {
|
|
"sanctioned_daemon": is_sanctioned_mcp_daemon(),
|
|
"pytest": is_pytest_runtime(),
|
|
"sanctioned_env": SANCTIONED_DAEMON_ENV,
|
|
"allow_direct_import_env": ALLOW_DIRECT_IMPORT_ENV,
|
|
"allow_keychain_cli_env": ALLOW_KEYCHAIN_CLI_ENV,
|
|
}
|