Skip to content

Feature Spec: Config Resolver

Feature ID: FE-07 Status: Ready for Implementation Priority: P0 Parent: Tech Design v1.0 Section 8.8 SRS Requirements: FR-DISP-005


1. Description

The Config Resolver implements the 4-tier configuration precedence hierarchy used throughout apcore-cli. For any configurable value, the resolver checks sources in order: CLI flag (highest priority), environment variable, config file (apcore.yaml), and built-in default (lowest priority). It handles malformed config files gracefully and supports flattened dot-notation key access.


2. Requirements Traceability

Req ID SRS Ref Description
FR-07-01 FR-DISP-005 4-tier configuration precedence: CLI > Env > File > Default.
FR-07-02 FR-DISP-005 AF-1 Config file not found: skip silently.
FR-07-03 FR-DISP-005 AF-2 Config file malformed: log WARNING, use defaults.

3. Module Path

apcore_cli/config.py


4. Implementation Details

4.1 Class: ConfigResolver

Constructor: __init__(self, cli_flags: dict[str, Any] = None, config_path: str = "apcore.yaml")

Logic steps: 1. Store self._cli_flags = cli_flags or {}. 2. Store self._config_path = config_path. 3. Call self._config_file = self._load_config_file().

4.2 Method: resolve

Signature: resolve(key: str, cli_flag: str = None, env_var: str = None) -> Any

Logic steps: 1. Tier 1 — CLI flag: If cli_flag is not None and cli_flag exists in self._cli_flags: a. Get value = self._cli_flags[cli_flag]. b. If value is not None: return value. 2. Tier 2 — Environment variable: If env_var is not None: a. Get env_value = os.environ.get(env_var). b. If env_value is not None and env_value != "": return env_value. 3. Tier 3 — Config file: If self._config_file is not None and key in self._config_file: a. Return self._config_file[key]. 4. Tier 4 — Default: Return self.DEFAULTS.get(key).

4.3 Default Values

DEFAULTS = {
    "extensions.root": "./extensions",
    "logging.level": "INFO",
    "sandbox.enabled": False,
    "cli.stdin_buffer_limit": 10_485_760,  # 10 MB
}

4.4 Method: _load_config_file

Signature: _load_config_file() -> dict | None

Logic steps: 1. Try to open self._config_path. 2. On FileNotFoundError: return None (silent, per FR-DISP-005 AF-1). 3. Parse with yaml.safe_load(). 4. On yaml.YAMLError: log WARNING "Configuration file '{path}' is malformed, using defaults." Return None. 5. If parsed result is not a dict: log WARNING, return None. 6. Call self._flatten_dict(config) to convert nested YAML to dot-notation. 7. Return flattened dict.

4.5 Method: _flatten_dict

Signature: _flatten_dict(d: dict, prefix: str = "") -> dict

Logic steps: 1. Initialize result = {}. 2. For each (key, value) in d.items(): a. full_key = f"{prefix}.{key}" if prefix else key. b. If isinstance(value, dict): recurse result.update(self._flatten_dict(value, full_key)). c. Else: result[full_key] = value. 3. Return result.

Example:

# apcore.yaml
extensions:
  root: /custom/path
auth:
  api_key: "keyring:auth.api_key"
logging:
  level: DEBUG

Flattened to:

{
    "extensions.root": "/custom/path",
    "auth.api_key": "keyring:auth.api_key",
    "logging.level": "DEBUG",
}


5. Configuration Keys

Key CLI Flag Environment Variable Default Type SRS Reference
extensions.root --extensions-dir APCORE_EXTENSIONS_ROOT ./extensions str (path) FR-DISP-003, FR-DISP-005
auth.api_key --api-key APCORE_AUTH_API_KEY None str (secret) FR-SEC-001
logging.level --log-level APCORE_LOGGING_LEVEL INFO str (enum) NFR-MNT-002
sandbox.enabled --sandbox APCORE_CLI_SANDBOX False bool FR-SEC-004
cli.auto_approve --yes APCORE_CLI_AUTO_APPROVE False bool FR-APPR-004

6. Parameter Validation

Parameter Type Valid Values Invalid Handling
config_path str Valid file path or non-existent path. Non-existent: skip silently. Invalid YAML: WARNING + use defaults.
cli_flags dict Keys are flag names, values are parsed types. None: treated as empty dict.
key str Dot-notation config key. Unknown key: returns None from DEFAULTS.
env_var str Valid env var name. Unset or empty string: skip to next tier.

7. Precedence Test Matrix

Test CLI Flag Env Var Config File Default Expected Result
All sources set /cli-path /env-path /config-path ./extensions /cli-path
No CLI flag /env-path /config-path ./extensions /env-path
No CLI flag, no env /config-path ./extensions /config-path
No CLI flag, no env, no config ./extensions ./extensions
CLI flag is None None /env-path ./extensions /env-path
Env var is empty string "" /config-path ./extensions /config-path
Config file not found (missing) ./extensions ./extensions
Config file malformed (invalid YAML) ./extensions ./extensions + WARNING

8. Error Handling

Condition Behavior SRS Reference
Config file not found Skip silently, fall through to default. FR-DISP-005 AF-1
Config file malformed YAML Log WARNING, fall through to default. FR-DISP-005 AF-2
Config file not a dict at root Log WARNING, fall through to default. FR-DISP-005 AF-2
Unknown config key Return None (from DEFAULTS.get).

9. Verification

Test ID Description Expected Result
T-CFG-01 CLI flag --extensions-dir /cli with env var set Uses /cli (Tier 1 wins).
T-CFG-02 No CLI flag, APCORE_EXTENSIONS_ROOT=/env set Uses /env (Tier 2 wins).
T-CFG-03 No CLI flag, no env, apcore.yaml has extensions.root: /config Uses /config (Tier 3 wins).
T-CFG-04 No CLI flag, no env, no config file Uses ./extensions (Tier 4 default).
T-CFG-05 Config file does not exist No error. Default used.
T-CFG-06 Config file is invalid YAML WARNING logged. Default used.
T-CFG-07 Config file has nested keys Flattened correctly: extensions.root accessible.
T-CFG-08 Env var set to empty string Skipped. Falls through to config file or default.
T-CFG-09 CLI flag explicitly set to None Skipped. Falls through to env var or lower.