Merge pull request #100 from cabana8471-arch/fix/config-loading-diagnostics

fix: add diagnostic warnings for config loading failures
This commit is contained in:
Leon van Zyl
2026-01-29 11:03:55 +02:00
committed by GitHub
2 changed files with 56 additions and 10 deletions

View File

@@ -499,58 +499,74 @@ def load_org_config() -> Optional[dict]:
config = yaml.safe_load(f) config = yaml.safe_load(f)
if not config: if not config:
logger.warning(f"Org config at {config_path} is empty")
return None return None
# Validate structure # Validate structure
if not isinstance(config, dict): if not isinstance(config, dict):
logger.warning(f"Org config at {config_path} must be a YAML dictionary")
return None return None
if "version" not in config: if "version" not in config:
logger.warning(f"Org config at {config_path} missing required 'version' field")
return None return None
# Validate allowed_commands if present # Validate allowed_commands if present
if "allowed_commands" in config: if "allowed_commands" in config:
allowed = config["allowed_commands"] allowed = config["allowed_commands"]
if not isinstance(allowed, list): if not isinstance(allowed, list):
logger.warning(f"Org config at {config_path}: 'allowed_commands' must be a list")
return None return None
for cmd in allowed: for i, cmd in enumerate(allowed):
if not isinstance(cmd, dict): if not isinstance(cmd, dict):
logger.warning(f"Org config at {config_path}: allowed_commands[{i}] must be a dict")
return None return None
if "name" not in cmd: if "name" not in cmd:
logger.warning(f"Org config at {config_path}: allowed_commands[{i}] missing 'name'")
return None return None
# Validate that name is a non-empty string # Validate that name is a non-empty string
if not isinstance(cmd["name"], str) or cmd["name"].strip() == "": if not isinstance(cmd["name"], str) or cmd["name"].strip() == "":
logger.warning(f"Org config at {config_path}: allowed_commands[{i}] has invalid 'name'")
return None return None
# Validate blocked_commands if present # Validate blocked_commands if present
if "blocked_commands" in config: if "blocked_commands" in config:
blocked = config["blocked_commands"] blocked = config["blocked_commands"]
if not isinstance(blocked, list): if not isinstance(blocked, list):
logger.warning(f"Org config at {config_path}: 'blocked_commands' must be a list")
return None return None
for cmd in blocked: for i, cmd in enumerate(blocked):
if not isinstance(cmd, str): if not isinstance(cmd, str):
logger.warning(f"Org config at {config_path}: blocked_commands[{i}] must be a string")
return None return None
# Validate pkill_processes if present # Validate pkill_processes if present
if "pkill_processes" in config: if "pkill_processes" in config:
processes = config["pkill_processes"] processes = config["pkill_processes"]
if not isinstance(processes, list): if not isinstance(processes, list):
logger.warning(f"Org config at {config_path}: 'pkill_processes' must be a list")
return None return None
# Normalize and validate each process name against safe pattern # Normalize and validate each process name against safe pattern
normalized = [] normalized = []
for proc in processes: for i, proc in enumerate(processes):
if not isinstance(proc, str): if not isinstance(proc, str):
logger.warning(f"Org config at {config_path}: pkill_processes[{i}] must be a string")
return None return None
proc = proc.strip() proc = proc.strip()
# Block empty strings and regex metacharacters # Block empty strings and regex metacharacters
if not proc or not VALID_PROCESS_NAME_PATTERN.fullmatch(proc): if not proc or not VALID_PROCESS_NAME_PATTERN.fullmatch(proc):
logger.warning(f"Org config at {config_path}: pkill_processes[{i}] has invalid value '{proc}'")
return None return None
normalized.append(proc) normalized.append(proc)
config["pkill_processes"] = normalized config["pkill_processes"] = normalized
return config return config
except (yaml.YAMLError, IOError, OSError): except yaml.YAMLError as e:
logger.warning(f"Failed to parse org config at {config_path}: {e}")
return None
except (IOError, OSError) as e:
logger.warning(f"Failed to read org config at {config_path}: {e}")
return None return None
@@ -564,7 +580,7 @@ def load_project_commands(project_dir: Path) -> Optional[dict]:
Returns: Returns:
Dict with parsed YAML config, or None if file doesn't exist or is invalid Dict with parsed YAML config, or None if file doesn't exist or is invalid
""" """
config_path = project_dir / ".autocoder" / "allowed_commands.yaml" config_path = project_dir.resolve() / ".autocoder" / "allowed_commands.yaml"
if not config_path.exists(): if not config_path.exists():
return None return None
@@ -574,53 +590,68 @@ def load_project_commands(project_dir: Path) -> Optional[dict]:
config = yaml.safe_load(f) config = yaml.safe_load(f)
if not config: if not config:
logger.warning(f"Project config at {config_path} is empty")
return None return None
# Validate structure # Validate structure
if not isinstance(config, dict): if not isinstance(config, dict):
logger.warning(f"Project config at {config_path} must be a YAML dictionary")
return None return None
if "version" not in config: if "version" not in config:
logger.warning(f"Project config at {config_path} missing required 'version' field")
return None return None
commands = config.get("commands", []) commands = config.get("commands", [])
if not isinstance(commands, list): if not isinstance(commands, list):
logger.warning(f"Project config at {config_path}: 'commands' must be a list")
return None return None
# Enforce 100 command limit # Enforce 100 command limit
if len(commands) > 100: if len(commands) > 100:
logger.warning(f"Project config at {config_path} exceeds 100 command limit ({len(commands)} commands)")
return None return None
# Validate each command entry # Validate each command entry
for cmd in commands: for i, cmd in enumerate(commands):
if not isinstance(cmd, dict): if not isinstance(cmd, dict):
logger.warning(f"Project config at {config_path}: commands[{i}] must be a dict")
return None return None
if "name" not in cmd: if "name" not in cmd:
logger.warning(f"Project config at {config_path}: commands[{i}] missing 'name'")
return None return None
# Validate name is a string # Validate name is a non-empty string
if not isinstance(cmd["name"], str): if not isinstance(cmd["name"], str) or cmd["name"].strip() == "":
logger.warning(f"Project config at {config_path}: commands[{i}] has invalid 'name'")
return None return None
# Validate pkill_processes if present # Validate pkill_processes if present
if "pkill_processes" in config: if "pkill_processes" in config:
processes = config["pkill_processes"] processes = config["pkill_processes"]
if not isinstance(processes, list): if not isinstance(processes, list):
logger.warning(f"Project config at {config_path}: 'pkill_processes' must be a list")
return None return None
# Normalize and validate each process name against safe pattern # Normalize and validate each process name against safe pattern
normalized = [] normalized = []
for proc in processes: for i, proc in enumerate(processes):
if not isinstance(proc, str): if not isinstance(proc, str):
logger.warning(f"Project config at {config_path}: pkill_processes[{i}] must be a string")
return None return None
proc = proc.strip() proc = proc.strip()
# Block empty strings and regex metacharacters # Block empty strings and regex metacharacters
if not proc or not VALID_PROCESS_NAME_PATTERN.fullmatch(proc): if not proc or not VALID_PROCESS_NAME_PATTERN.fullmatch(proc):
logger.warning(f"Project config at {config_path}: pkill_processes[{i}] has invalid value '{proc}'")
return None return None
normalized.append(proc) normalized.append(proc)
config["pkill_processes"] = normalized config["pkill_processes"] = normalized
return config return config
except (yaml.YAMLError, IOError, OSError): except yaml.YAMLError as e:
logger.warning(f"Failed to parse project config at {config_path}: {e}")
return None
except (IOError, OSError) as e:
logger.warning(f"Failed to read project config at {config_path}: {e}")
return None return None

View File

@@ -455,6 +455,21 @@ commands:
print(" FAIL: Non-allowed command 'rustc' should be blocked") print(" FAIL: Non-allowed command 'rustc' should be blocked")
failed += 1 failed += 1
# Test 4: Empty command name is rejected
config_path.write_text("""version: 1
commands:
- name: ""
description: Empty name should be rejected
""")
result = load_project_commands(project_dir)
if result is None:
print(" PASS: Empty command name rejected in project config")
passed += 1
else:
print(" FAIL: Empty command name should be rejected in project config")
print(f" Got: {result}")
failed += 1
return passed, failed return passed, failed