fix: add diagnostic warnings for config loading failures (#91)

When config files have errors, users had no way to know why their
settings weren't being applied. Added logging.warning() calls to
diagnose:
- Empty config files
- Missing 'version' field
- Invalid structure (not a dict)
- Invalid command entries
- Exceeding 100 command limit
- YAML parse errors
- File read errors

Also added .resolve() to project path to handle symlinks correctly.

Fixes: leonvanzyl/autocoder#91

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
cabana8471
2026-01-25 12:08:53 +01:00
parent 836bc8ae16
commit 1d67fff9e0

View File

@@ -499,36 +499,45 @@ 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
@@ -550,7 +559,11 @@ def load_org_config() -> Optional[dict]:
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 +577,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,31 +587,39 @@ 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
@@ -620,7 +641,11 @@ def load_project_commands(project_dir: Path) -> Optional[dict]:
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