Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fad44669d9 |
@@ -0,0 +1,70 @@
|
|||||||
|
# MCP Config Drift Diagnostic & Sanctioned Repair Runbook (#672)
|
||||||
|
|
||||||
|
This document describes the diagnostic framework for detecting configuration drift between the active IDE MCP configuration (`~/.gemini/antigravity-ide/mcp_config.json`) and the offline/global canonical configuration (`~/.gemini/config/mcp_config.json`), and establishes the **sanctioned repair runbook**.
|
||||||
|
|
||||||
|
## Background & Problem Statement
|
||||||
|
|
||||||
|
Offline tools like `test_mcp_conn.py` test the global configuration (`~/.gemini/config/mcp_config.json`) via `subprocess.Popen`. However, the active IDE/client namespace uses `~/.gemini/antigravity-ide/mcp_config.json`. When required Gitea role servers (`gitea-author`, `gitea-reviewer`, `gitea-merger`, `gitea-reconciler`, `gitea-controller`, `gitea-tools`) are missing or carry mismatched profile environments in the active IDE config:
|
||||||
|
|
||||||
|
1. Offline tests pass (`test_mcp_conn.py` green).
|
||||||
|
2. The IDE client returns `EOF` / `transport closed` when attempting role-scoped mutations.
|
||||||
|
3. Operators misdiagnose missing server definitions as stale runtimes, leading to forbidden `pkill` attempts (#630) or `mtime` hacks (#655).
|
||||||
|
|
||||||
|
## Diagnostic Tool: `mcp_config_drift.py`
|
||||||
|
|
||||||
|
Run the diagnostic tool directly to compare configurations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 mcp_config_drift.py --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Or specify custom config locations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 mcp_config_drift.py \
|
||||||
|
--active-config ~/.gemini/antigravity-ide/mcp_config.json \
|
||||||
|
--global-config ~/.gemini/config/mcp_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Diagnostic Outputs
|
||||||
|
|
||||||
|
- `in_sync`: Boolean indicating if all required Gitea role servers exist in the active IDE config with matching profile declarations.
|
||||||
|
- `missing_role_servers`: List of role servers present in global config but missing from active IDE config.
|
||||||
|
- `profile_mismatches`: List of profile environment mismatches per server.
|
||||||
|
- `reasons`: Explicit, human-readable list of drift causes.
|
||||||
|
|
||||||
|
All returned payloads automatically redact secret tokens, DSNs, Authorization headers, and private keys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sanctioned Repair Path (Step-by-Step)
|
||||||
|
|
||||||
|
When `mcp_config_drift.py` reports drift (`in_sync: false`), execute the following **sanctioned repair steps**:
|
||||||
|
|
||||||
|
1. **Backup Active IDE Config:**
|
||||||
|
```bash
|
||||||
|
cp ~/.gemini/antigravity-ide/mcp_config.json ~/.gemini/antigravity-ide/mcp_config.json.bak
|
||||||
|
```
|
||||||
|
2. **Patch Active IDE Config:**
|
||||||
|
Copy the missing Gitea role server JSON blocks (`gitea-author`, `gitea-reviewer`, etc.) from `~/.gemini/config/mcp_config.json` into `~/.gemini/antigravity-ide/mcp_config.json`.
|
||||||
|
3. **Reconnect via IDE/Client:**
|
||||||
|
Use the IDE / client UI reconnection control (or restart the IDE client app).
|
||||||
|
4. **Verify Active Namespace Health:**
|
||||||
|
Invoke `gitea_whoami` (and optional `gitea_resolve_task_capability`) through the active IDE client on each required role namespace.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FORBIDDEN Repair Actions (#630 / #655)
|
||||||
|
|
||||||
|
The following actions are **strictly forbidden** for config drift repair:
|
||||||
|
|
||||||
|
- ❌ **`pkill` or manual daemon process kill commands:** Process kills cause contamination and break active session leases.
|
||||||
|
- ❌ **`mtime` touch edits:** Artificial mtime modifications mask stale runtimes without updating configuration.
|
||||||
|
- ❌ **Source code edits:** Mutating python tool logic to bypass missing server entries.
|
||||||
|
- ❌ **Session-state edits:** Direct database or lock-file state mutation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Report Guidelines
|
||||||
|
|
||||||
|
A workflow final report **must not** rely on offline `test_mcp_conn.py` output alone. Final reports must include active-config evidence from live `gitea_whoami` calls on the active IDE namespaces.
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
"""Antigravity IDE vs Global MCP Config Drift Diagnostic (#672).
|
||||||
|
|
||||||
|
Diagnoses config drift between the active IDE MCP configuration
|
||||||
|
(e.g. ``~/.gemini/antigravity-ide/mcp_config.json``) and the offline/global
|
||||||
|
canonical configuration (e.g. ``~/.gemini/config/mcp_config.json``).
|
||||||
|
|
||||||
|
Hard rules (#672 / #630 / #655):
|
||||||
|
* Distinguish offline/global success from active IDE namespace availability.
|
||||||
|
* Never print tokens, DSNs, Authorization headers, or secret-bearing env vars.
|
||||||
|
* Sanctioned repair path is: backup active config -> patch active config from canonical
|
||||||
|
-> reconnect through IDE/client -> verify with live ``gitea_whoami``.
|
||||||
|
* FORBIDDEN: ``pkill``, mtime edits, source edits, or session-state edits for repair.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from webui import console_redaction
|
||||||
|
|
||||||
|
DEFAULT_ACTIVE_IDE_CONFIG = "~/.gemini/antigravity-ide/mcp_config.json"
|
||||||
|
DEFAULT_GLOBAL_CONFIG = "~/.gemini/config/mcp_config.json"
|
||||||
|
|
||||||
|
REQUIRED_GITEA_ROLE_SERVERS = (
|
||||||
|
"gitea-author",
|
||||||
|
"gitea-reviewer",
|
||||||
|
"gitea-merger",
|
||||||
|
"gitea-reconciler",
|
||||||
|
"gitea-controller",
|
||||||
|
"gitea-tools",
|
||||||
|
)
|
||||||
|
|
||||||
|
SANCTIONED_REPAIR_RUNBOOK: tuple[str, ...] = (
|
||||||
|
"1. Backup active IDE config: cp ~/.gemini/antigravity-ide/mcp_config.json ~/.gemini/antigravity-ide/mcp_config.json.bak",
|
||||||
|
"2. Patch active IDE config: copy required missing Gitea role server entries from global config (~/.gemini/config/mcp_config.json) into active IDE config.",
|
||||||
|
"3. Reconnect via IDE/client UI or client restart (do NOT use host process kill).",
|
||||||
|
"4. Verify active namespace health using live gitea_whoami and gitea_resolve_task_capability on each role namespace.",
|
||||||
|
"FORBIDDEN REPAIR PATHS: pkill / host process kill, mtime touch edits, source code edits, or session-state edits.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_config_path(path_str: str) -> Path:
|
||||||
|
"""Expand user and resolve absolute path."""
|
||||||
|
return Path(os.path.expanduser(path_str)).resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def load_mcp_config(config_path: str | Path) -> tuple[dict[str, Any] | None, str | None]:
|
||||||
|
"""Load and parse JSON MCP configuration from file.
|
||||||
|
|
||||||
|
Returns (config_dict, error_message).
|
||||||
|
"""
|
||||||
|
resolved = resolve_config_path(str(config_path))
|
||||||
|
if not resolved.exists():
|
||||||
|
return None, f"file_not_found: {resolved}"
|
||||||
|
try:
|
||||||
|
with open(resolved, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return None, f"invalid_schema: root is not a JSON object in {resolved}"
|
||||||
|
return data, None
|
||||||
|
except Exception as exc:
|
||||||
|
return None, f"unreadable_json: {exc} in {resolved}"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_mcp_servers(config: dict[str, Any] | None) -> dict[str, dict[str, Any]]:
|
||||||
|
"""Extract the mcpServers or mcp_servers mapping safely."""
|
||||||
|
if not config:
|
||||||
|
return {}
|
||||||
|
servers = config.get("mcpServers") or config.get("mcp_servers") or {}
|
||||||
|
if isinstance(servers, dict):
|
||||||
|
return {str(k): v for k, v in servers.items() if isinstance(v, dict)}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_redact_server_config(srv_cfg: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Redact secrets from environment variables and command line args."""
|
||||||
|
safe = {}
|
||||||
|
if "command" in srv_cfg:
|
||||||
|
safe["command"] = str(srv_cfg["command"])
|
||||||
|
if "args" in srv_cfg and isinstance(srv_cfg["args"], list):
|
||||||
|
safe["args"] = [console_redaction.redact_text(str(a)) for a in srv_cfg["args"]]
|
||||||
|
if "env" in srv_cfg and isinstance(srv_cfg["env"], dict):
|
||||||
|
safe_env = {}
|
||||||
|
for k, v in srv_cfg["env"].items():
|
||||||
|
if any(secret_kw in k.lower() for secret_kw in ("token", "secret", "pass", "key", "auth")):
|
||||||
|
safe_env[k] = "[REDACTED]"
|
||||||
|
else:
|
||||||
|
safe_env[k] = console_redaction.redact_text(str(v))
|
||||||
|
safe["env"] = safe_env
|
||||||
|
return safe
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_config_drift(
|
||||||
|
active_config_path: str = DEFAULT_ACTIVE_IDE_CONFIG,
|
||||||
|
global_config_path: str = DEFAULT_GLOBAL_CONFIG,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Analyze MCP configuration drift between active IDE config and global config.
|
||||||
|
|
||||||
|
Returns structured diagnostic output.
|
||||||
|
"""
|
||||||
|
active_resolved = resolve_config_path(active_config_path)
|
||||||
|
global_resolved = resolve_config_path(global_config_path)
|
||||||
|
|
||||||
|
active_cfg, active_err = load_mcp_config(active_resolved)
|
||||||
|
global_cfg, global_err = load_mcp_config(global_resolved)
|
||||||
|
|
||||||
|
active_servers = extract_mcp_servers(active_cfg)
|
||||||
|
global_servers = extract_mcp_servers(global_cfg)
|
||||||
|
|
||||||
|
missing_role_servers: list[str] = []
|
||||||
|
present_role_servers: list[str] = []
|
||||||
|
profile_mismatches: list[dict[str, Any]] = []
|
||||||
|
reasons: list[str] = []
|
||||||
|
|
||||||
|
if active_err:
|
||||||
|
reasons.append(f"Active IDE config error: {active_err}")
|
||||||
|
if global_err:
|
||||||
|
reasons.append(f"Global canonical config error: {global_err}")
|
||||||
|
|
||||||
|
# Check Gitea role servers
|
||||||
|
for srv_name in REQUIRED_GITEA_ROLE_SERVERS:
|
||||||
|
in_active = srv_name in active_servers
|
||||||
|
in_global = srv_name in global_servers
|
||||||
|
|
||||||
|
if in_active:
|
||||||
|
present_role_servers.append(srv_name)
|
||||||
|
elif in_global:
|
||||||
|
missing_role_servers.append(srv_name)
|
||||||
|
reasons.append(
|
||||||
|
f"Missing Gitea role server '{srv_name}' in active IDE config ({active_resolved})"
|
||||||
|
)
|
||||||
|
|
||||||
|
if in_active and in_global:
|
||||||
|
# Compare profiles & environments
|
||||||
|
act_env = active_servers[srv_name].get("env", {}) if isinstance(active_servers[srv_name], dict) else {}
|
||||||
|
glo_env = global_servers[srv_name].get("env", {}) if isinstance(global_servers[srv_name], dict) else {}
|
||||||
|
|
||||||
|
act_prof = act_env.get("GITEA_MCP_PROFILE") or act_env.get("GITEA_PROFILE_NAME")
|
||||||
|
glo_prof = glo_env.get("GITEA_MCP_PROFILE") or glo_env.get("GITEA_PROFILE_NAME")
|
||||||
|
|
||||||
|
if act_prof != glo_prof:
|
||||||
|
mismatch_item = {
|
||||||
|
"server": srv_name,
|
||||||
|
"active_profile": act_prof,
|
||||||
|
"global_profile": glo_prof,
|
||||||
|
}
|
||||||
|
profile_mismatches.append(mismatch_item)
|
||||||
|
reasons.append(
|
||||||
|
f"Profile mismatch for '{srv_name}': active='{act_prof}' != global='{glo_prof}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
in_sync = bool(
|
||||||
|
not active_err
|
||||||
|
and not global_err
|
||||||
|
and not missing_role_servers
|
||||||
|
and not profile_mismatches
|
||||||
|
)
|
||||||
|
|
||||||
|
report = {
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"in_sync": in_sync,
|
||||||
|
"active_config_path": str(active_resolved),
|
||||||
|
"active_config_exists": active_cfg is not None,
|
||||||
|
"global_config_path": str(global_resolved),
|
||||||
|
"global_config_exists": global_cfg is not None,
|
||||||
|
"required_role_servers": list(REQUIRED_GITEA_ROLE_SERVERS),
|
||||||
|
"present_role_servers": present_role_servers,
|
||||||
|
"missing_role_servers": missing_role_servers,
|
||||||
|
"profile_mismatches": profile_mismatches,
|
||||||
|
"reasons": reasons,
|
||||||
|
"sanctioned_repair_runbook": list(SANCTIONED_REPAIR_RUNBOOK),
|
||||||
|
"forbidden_repair_methods": [
|
||||||
|
"pkill / host process kill",
|
||||||
|
"mtime touch edits",
|
||||||
|
"source code edits",
|
||||||
|
"session-state edits",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return console_redaction.redact_payload(report)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Diagnose Gitea MCP role server config drift between active IDE and global config."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--active-config",
|
||||||
|
default=DEFAULT_ACTIVE_IDE_CONFIG,
|
||||||
|
help="Path to active IDE MCP config JSON",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--global-config",
|
||||||
|
default=DEFAULT_GLOBAL_CONFIG,
|
||||||
|
help="Path to global/canonical MCP config JSON",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--json", action="store_true", help="Print raw JSON report"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
report = analyze_config_drift(args.active_config, args.global_config)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(report, indent=2))
|
||||||
|
else:
|
||||||
|
print("=== MCP Config Drift Diagnostic Report ===")
|
||||||
|
print(f"Timestamp: {report['timestamp']}")
|
||||||
|
print(f"In Sync: {report['in_sync']}")
|
||||||
|
print(f"Active IDE Config: {report['active_config_path']} (exists={report['active_config_exists']})")
|
||||||
|
print(f"Global Config: {report['global_config_path']} (exists={report['global_config_exists']})")
|
||||||
|
print(f"Present Role Servers: {', '.join(report['present_role_servers']) if report['present_role_servers'] else 'None'}")
|
||||||
|
print(f"Missing Role Servers: {', '.join(report['missing_role_servers']) if report['missing_role_servers'] else 'None'}")
|
||||||
|
if report['profile_mismatches']:
|
||||||
|
print("Profile Mismatches:")
|
||||||
|
for m in report['profile_mismatches']:
|
||||||
|
print(f" - {m['server']}: active={m['active_profile']} vs global={m['global_profile']}")
|
||||||
|
if report['reasons']:
|
||||||
|
print("Drift Reasons:")
|
||||||
|
for r in report['reasons']:
|
||||||
|
print(f" - {r}")
|
||||||
|
print("\nSanctioned Repair Runbook:")
|
||||||
|
for step in report['sanctioned_repair_runbook']:
|
||||||
|
print(f" {step}")
|
||||||
|
|
||||||
|
sys.exit(0 if report["in_sync"] else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
"""Unit tests for mcp_config_drift.py (#672)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from mcp_config_drift import (
|
||||||
|
REQUIRED_GITEA_ROLE_SERVERS,
|
||||||
|
analyze_config_drift,
|
||||||
|
load_mcp_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_global_config() -> dict:
|
||||||
|
return {
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea-author": {
|
||||||
|
"command": "python3",
|
||||||
|
"args": ["gitea_mcp_server.py"],
|
||||||
|
"env": {"GITEA_MCP_PROFILE": "prgs-author", "SENTRY_AUTH_TOKEN": "secret-token-999"},
|
||||||
|
},
|
||||||
|
"gitea-reviewer": {
|
||||||
|
"command": "python3",
|
||||||
|
"args": ["gitea_mcp_server.py"],
|
||||||
|
"env": {"GITEA_MCP_PROFILE": "prgs-reviewer"},
|
||||||
|
},
|
||||||
|
"gitea-merger": {
|
||||||
|
"command": "python3",
|
||||||
|
"args": ["gitea_mcp_server.py"],
|
||||||
|
"env": {"GITEA_MCP_PROFILE": "prgs-merger"},
|
||||||
|
},
|
||||||
|
"gitea-reconciler": {
|
||||||
|
"command": "python3",
|
||||||
|
"args": ["gitea_mcp_server.py"],
|
||||||
|
"env": {"GITEA_MCP_PROFILE": "prgs-reconciler"},
|
||||||
|
},
|
||||||
|
"gitea-controller": {
|
||||||
|
"command": "python3",
|
||||||
|
"args": ["gitea_mcp_server.py"],
|
||||||
|
"env": {"GITEA_MCP_PROFILE": "prgs-controller"},
|
||||||
|
},
|
||||||
|
"gitea-tools": {
|
||||||
|
"command": "python3",
|
||||||
|
"args": ["gitea_mcp_server.py"],
|
||||||
|
"env": {"GITEA_MCP_PROFILE": "prgs-author"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(path: Path, data: dict) -> str:
|
||||||
|
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
||||||
|
return str(path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_drift_detection_in_sync(tmp_path, sample_global_config):
|
||||||
|
glob_file = tmp_path / "global_mcp.json"
|
||||||
|
act_file = tmp_path / "active_mcp.json"
|
||||||
|
|
||||||
|
write_json(glob_file, sample_global_config)
|
||||||
|
write_json(act_file, sample_global_config)
|
||||||
|
|
||||||
|
report = analyze_config_drift(active_config_path=str(act_file), global_config_path=str(glob_file))
|
||||||
|
|
||||||
|
assert report["in_sync"] is True
|
||||||
|
assert report["missing_role_servers"] == []
|
||||||
|
assert report["profile_mismatches"] == []
|
||||||
|
assert set(report["present_role_servers"]) == set(REQUIRED_GITEA_ROLE_SERVERS)
|
||||||
|
|
||||||
|
|
||||||
|
def test_drift_detection_missing_author(tmp_path, sample_global_config):
|
||||||
|
glob_file = tmp_path / "global_mcp.json"
|
||||||
|
act_file = tmp_path / "active_mcp.json"
|
||||||
|
|
||||||
|
active_config = json.loads(json.dumps(sample_global_config))
|
||||||
|
del active_config["mcpServers"]["gitea-author"]
|
||||||
|
|
||||||
|
write_json(glob_file, sample_global_config)
|
||||||
|
write_json(act_file, active_config)
|
||||||
|
|
||||||
|
report = analyze_config_drift(active_config_path=str(act_file), global_config_path=str(glob_file))
|
||||||
|
|
||||||
|
assert report["in_sync"] is False
|
||||||
|
assert "gitea-author" in report["missing_role_servers"]
|
||||||
|
assert "gitea-author" not in report["present_role_servers"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_drift_detection_missing_reviewer(tmp_path, sample_global_config):
|
||||||
|
glob_file = tmp_path / "global_mcp.json"
|
||||||
|
act_file = tmp_path / "active_mcp.json"
|
||||||
|
|
||||||
|
active_config = json.loads(json.dumps(sample_global_config))
|
||||||
|
del active_config["mcpServers"]["gitea-reviewer"]
|
||||||
|
|
||||||
|
write_json(glob_file, sample_global_config)
|
||||||
|
write_json(act_file, active_config)
|
||||||
|
|
||||||
|
report = analyze_config_drift(active_config_path=str(act_file), global_config_path=str(glob_file))
|
||||||
|
|
||||||
|
assert report["in_sync"] is False
|
||||||
|
assert "gitea-reviewer" in report["missing_role_servers"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_drift_detection_profile_mismatch(tmp_path, sample_global_config):
|
||||||
|
glob_file = tmp_path / "global_mcp.json"
|
||||||
|
act_file = tmp_path / "active_mcp.json"
|
||||||
|
|
||||||
|
active_config = json.loads(json.dumps(sample_global_config))
|
||||||
|
active_config["mcpServers"]["gitea-author"]["env"]["GITEA_MCP_PROFILE"] = "dadeschools-author"
|
||||||
|
|
||||||
|
write_json(glob_file, sample_global_config)
|
||||||
|
write_json(act_file, active_config)
|
||||||
|
|
||||||
|
report = analyze_config_drift(active_config_path=str(act_file), global_config_path=str(glob_file))
|
||||||
|
|
||||||
|
assert report["in_sync"] is False
|
||||||
|
assert len(report["profile_mismatches"]) == 1
|
||||||
|
mismatch = report["profile_mismatches"][0]
|
||||||
|
assert mismatch["server"] == "gitea-author"
|
||||||
|
assert mismatch["active_profile"] == "dadeschools-author"
|
||||||
|
assert mismatch["global_profile"] == "prgs-author"
|
||||||
|
|
||||||
|
|
||||||
|
def test_secret_redaction_in_drift_report(tmp_path, sample_global_config):
|
||||||
|
glob_file = tmp_path / "global_mcp.json"
|
||||||
|
act_file = tmp_path / "active_mcp.json"
|
||||||
|
|
||||||
|
write_json(glob_file, sample_global_config)
|
||||||
|
write_json(act_file, sample_global_config)
|
||||||
|
|
||||||
|
report = analyze_config_drift(active_config_path=str(act_file), global_config_path=str(glob_file))
|
||||||
|
serialized = str(report)
|
||||||
|
|
||||||
|
assert "secret-token-999" not in serialized
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanctioned_runbook_forbids_pkill():
|
||||||
|
report = analyze_config_drift(active_config_path="/nonexistent/path/active.json", global_config_path="/nonexistent/path/global.json")
|
||||||
|
|
||||||
|
runbook_text = " ".join(report["sanctioned_repair_runbook"]).lower()
|
||||||
|
forbidden_text = " ".join(report["forbidden_repair_methods"]).lower()
|
||||||
|
|
||||||
|
assert "pkill" in forbidden_text
|
||||||
|
assert "mtime" in forbidden_text
|
||||||
|
assert "source" in forbidden_text
|
||||||
|
assert "session-state" in forbidden_text
|
||||||
Reference in New Issue
Block a user