feat: prepare ARR for controlled public deployment
This commit is contained in:
108
arr_mcp/launch.py
Normal file
108
arr_mcp/launch.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""Local secure launcher that loads routes and Keychain-backed MCP secrets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Sequence
|
||||
|
||||
from arr_mcp.run import main as run_mcp
|
||||
|
||||
|
||||
KEYCHAIN_ACCOUNT = "arr-web"
|
||||
KEYCHAIN_SERVICES = {
|
||||
"OSS_ACCESS_KEY_ID": "com.chillishark.arr.oss-access-key-id",
|
||||
"OSS_ACCESS_KEY_SECRET": "com.chillishark.arr.oss-access-key-secret",
|
||||
"ARR_MCP_BEARER_TOKEN": "com.chillishark.arr.mcp-bearer",
|
||||
}
|
||||
ROUTE_KEYS = frozenset(
|
||||
{
|
||||
"ARR_OSS_REGION",
|
||||
"ARR_OSS_ENDPOINT",
|
||||
"ARR_OSS_BUCKET",
|
||||
"ARR_OBJECT_PREFIX",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _private_route_values(path: Path) -> Dict[str, str]:
|
||||
candidate = path.expanduser()
|
||||
if candidate.is_symlink():
|
||||
raise ValueError("ARR MCP route configuration is invalid")
|
||||
resolved = candidate.resolve(strict=True)
|
||||
metadata = resolved.stat()
|
||||
if (
|
||||
not stat.S_ISREG(metadata.st_mode)
|
||||
or metadata.st_uid != os.getuid()
|
||||
or stat.S_IMODE(metadata.st_mode) != 0o600
|
||||
):
|
||||
raise ValueError("ARR MCP route configuration is invalid")
|
||||
values: Dict[str, str] = {}
|
||||
for raw in resolved.read_text(encoding="utf-8").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
if key in ROUTE_KEYS:
|
||||
if key in values:
|
||||
raise ValueError("ARR MCP route configuration is invalid")
|
||||
values[key] = value.strip().strip('"').strip("'")
|
||||
if any(not values.get(key) for key in ("ARR_OSS_REGION", "ARR_OSS_BUCKET")):
|
||||
raise ValueError("ARR MCP route configuration is incomplete")
|
||||
return values
|
||||
|
||||
|
||||
def _keychain_secret(service: str) -> str:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
"security",
|
||||
"find-generic-password",
|
||||
"-s",
|
||||
service,
|
||||
"-a",
|
||||
KEYCHAIN_ACCOUNT,
|
||||
"-w",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
raise ValueError("required ARR MCP Keychain item is unavailable") from None
|
||||
value = completed.stdout.strip()
|
||||
if not value:
|
||||
raise ValueError("required ARR MCP Keychain item is unavailable")
|
||||
return value
|
||||
|
||||
|
||||
def load_runtime_environment(route_config: Path) -> None:
|
||||
for key, value in _private_route_values(route_config).items():
|
||||
os.environ[key] = value
|
||||
for environment_name, service in KEYCHAIN_SERVICES.items():
|
||||
os.environ[environment_name] = _keychain_secret(service)
|
||||
|
||||
|
||||
def _parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument(
|
||||
"--route-config",
|
||||
type=Path,
|
||||
default=Path("~/.config/arr/agent-writeback.env"),
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
args, remaining = _parser().parse_known_args(argv)
|
||||
load_runtime_environment(args.route_config)
|
||||
return run_mcp(remaining)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user