Skip to content
BreachPilot

Config Validation

Validates the checked-in operator config (config.yaml) against tools/config_manager.py::CONFIG_SCHEMA and surfaces type/range errors before a run. Validation is lenient by design — most issues are warnings so a partial config degrades rather than crashes.

Entry points

EntryCallBehavior
ConfigValidator.loadconfig_manager.py:720path.exists()==Falseself._config = _build_defaults() (warn). yaml.safe_load(...) or {}. Non-dict root → raise ValueError
ConfigValidator.validateconfig_manager.py:736Populates ConfigValidationResult.errors / warnings / unknown_keys. No I/O.
ConfigValidator.load_and_validateconfig_manager.py:1186load() then validate()(config, result)
ConfigValidator.apply_defaultsconfig_manager.py:1200Deep-merge self._config over copy.deepcopy(CONFIG_SCHEMA); file wins
load_validated_configconfig_manager.py:1281load_and_validate(); raise ValueError on errors; logger.warning on warnings/unknown_keys; return apply_defaults()
tools/kernel/config.load_configtools/kernel/config.py:11Pure: path.exists()==False{}; non-dict → ValueError. Re-exported as tools/config_cli.load_config & tools/mcp_shared.load_config
validate_config_fileconfig_manager.py:1274Quick load_and_validate returning result only
PATCH /api/v1/system/configtools/api/routes/system.py:110Atomic deep-merge + re-validate; rejects on errors; loopback allowed_origins enforced
run_doctortools/doctor.py:388Loads raw yaml via yaml.safe_load, then ConfigValidator.load_and_validate via _check_config; reports errors/warnings/unknown_keys
run_self_testtools/self_test.py:89load_validated_config; any exception → overall_ok=False

Result shape

ConfigValidationResult (config_manager.py:678):

class ConfigValidationResult:
    errors: list[str]        # hard failures — is_valid == False
    warnings: list[str]      # soft — has_warnings == True
    unknown_keys: list[str]  # top-level keys not in KNOWN_TOP_KEYS nor PLUGIN_REGISTRY
    @property is_valid -> bool   # len(errors)==0
    @property has_warnings       # warnings or unknown_keys

What is checked

Top-level unknown keys

KNOWN_TOP_KEYS = set(CONFIG_SCHEMA.keys()) (config_manager.py:671). Any key not in KNOWN_TOP_KEYS ∪ PLUGIN_REGISTRY.config_sectionsunknown_keys warn (not error). This prevents drift where config.yaml adds a block not in schema (tested in tests/test_config_manager.py:542).

Required sections (warn only)

Missing ollama / models / mcp / exploitwarnings.append("Missing section '...' . Defaults will be used.") (config_manager.py:758). Never an error — defaults fill in.

Per-section rules

SectionKeyCheckMode
ollamamust be mapping; host missing → warnwarn
modelsregistry/default_aliasmissing → warnwarn
modelsprovidermust be ollama|chatgpt else warnwarn
modelsroles.*roles must be mapping; each value string; non-empty alias must be in registry else warn; empty string = default_aliaswarn
chatgptportint 1–65535 else warnwarn
chatgptenabled/auto_startbool else warnwarn
chatgptrequest_timeout_seconds/context_window/login_timeout_seconds/start_timeout_seconds/discover_cache_secondsnon-negative number else warnwarn
chatgptruntimeauto|bun|node else warnwarn
chatgptmodelsmust be list else warnwarn
mcpdefault_transportstdio|http|"" else warnwarn
mcphttp_portint 1–65535 else warnwarn
exploitmust be mappingerror
cve_lookupcircuit_failure_thresholdpositive int else warnwarn
cve_lookupcircuit_recovery_timeoutpositive number else warnwarn
cve_lookupsearch_rate_limit_per_minutenon-negative number else warnwarn
researchproviderollama|serpapi|stdlib else warn; fallback_provider ollama|serpapi|stdlib|""warn
researchtimeout_seconds/max_results/max_fetch_depth/max_content_chars/cache_max_entriespositive int else warnwarn
researchcache_ttl_secondsnon-negative number else warnwarn
researchmin_source_qualitylow|medium|high else warnwarn
researchrequire_api_key_for_mcp_toolsbool else warnwarn
researchollama/serpapi/assistantmust be mapping else warn (assistant sub-checks bools/ints + model_alias in registry)warn
memoryexperience_min_samplespositive int else warnwarn
memoryexperience_time_decay_daysnumber (≤0 disables) else warnwarn
memoryattack_memory_enabledbool else warnwarn
memoryattack_memory_max_context_charsint ≥1000 else warnwarn
outcome_judgmentmax_inconclusive_attemptsint ≥2 else warnwarn
outcome_judgmentconfirmation_threshold/refutation_threshold0.5–1.0 else warnwarn
outcome_judgmentmin_evidence_referencespositive int else warnwarn
outcome_judgmentflow_a/peer_reviewbool else warnwarn
reasoningultrathink/llm_reflectionbool else warnwarn
reasoningultrathink_reflection_intervalpositive int else warnwarn
reasoningpeer_consult_on_failure_thresholdint ≥0 else warnwarn
multi_modelenabledbool else warnwarn
multi_modelconsult_aliaseslist of non-empty strings else warnwarn
multi_modelmax_consultations/max_question_chars/max_answer_charspositive int else warnwarn
skillsenabled/maybe_enabled/allow_model_lookup/inject_startup_context/reselect_mid_run/.../semantic_matching/...bool else warnwarn
skillsreselect_max_per_run/.../diversity_penaltynon-negative int else warnwarn
skillssemantic_modelnon-empty string else warnwarn
skillssemantic_min_similarity0–1 else warnwarn
skillsroots/default_enabled/include_tags/exclude_nameslist of non-empty strings else warnwarn
skillsmax_active_skills/max_chars_per_skill/max_total_chars/min_contextual_skills/default_skill_weight/context_skill_weightpositive int else warnwarn
orchestratorsemantic_memorybool else warnwarn
agenttoggles task_graph_enabled/.../reflection_enabledbool else warnwarn
agentmax_retries_per_task/max_actions/generated_code_repair_attemptsnon-negative int (bool rejected) else warnwarn
evalenabledbool else errorerror
evaloutput_dirnon-empty string else errorerror
evalmax_roundsnon-negative int else errorerror
evalwrite_markdown/write_htmlbool else errorerror
apihostloopback 127.0.0.1/localhost/::1 else errorerror
apiportint 1–65535 else errorerror
apitoken_filenon-empty string else errorerror
apiallowed_originslist of strings else errorerror
apievent_buffer_size/shutdown_timeout_secondsnon-negative int else errorerror
apiserve_webuibool else errorerror

Sections stealth, threat_intel, witness, autonomous, recon, opsec, poc_verification, replay_simulator, adaptive_exploits, plugins, webhook_notify, mitre, ticketing, caldera, ics have schema defaults but no dedicated validation branch — unknown nested keys inside them are not flagged; top-level missing is silently defaulted.

Lab extras present in config.yaml:200 not in CONFIG_SCHEMA defaults — autonomous.dedup_targets/skip_non_routable/hard_target_max_rounds, recon.preflight_*, recon.max_retries/retry_delay/timeout_seconds/domain_resolution/fast, api.max_concurrent_runs/multi_operator/graph_route — are carried verbatim; they are not validated and remain opt-in (see docs/configuration/config-reference-generated.md).

Error vs warn vs unknown

  • Errorload() non-mapping ValueError; nested must be a mapping for ollama/models/mcp/exploit/chatgpt/...; api loopback/port/file/origin type failures; eval type failures. is_valid==False; load_validated_config raises; --doctor reports [FAIL] config_valid with errors.
  • Warn — type/range mismatches for everything else (bool/int/range/enum/alias-not-in-registry). Run continues; has_warnings==True; --doctor still [OK] but lists warnings.
  • Unknown key — top-level key not in CONFIG_SCHEMA nor plugin registry. Warn only; never blocks. Use to catch drift after adding a checked-in block without schema entry.

How --doctor and --self-test surface it

_check_config(path) (tools/doctor.py:345) constructs ConfigValidator(path) and calls load_and_validate() — not the old dict misuse. Reports:

[OK/FAIL] config_valid  path
      -> errors: [...]
      -> warnings: [...]
      -> unknown_keys: [...]

Any exception (YAML parse, non-mapping) → {ok: False, error: str(exc)}. Overall failed +=1 on ok==False only; warnings alone do not fail doctor.

self_test.py:89 calls load_validated_config; any exception sets overall_ok=False and stage config_load.

Common failure modes

SymptomCauseFix
config.yaml must contain a YAML mapping, got listRoot is a list/scalar, not a mappingEnsure file starts with ollama: etc., not - item
load_validated_config: Config validation failed: ...api.host non-loopback, api.port out of range, eval typeSet api.host: 127.0.0.1, api.port: 8765, fix eval.* type
Unknown config key: fooTypo or new block not yet in CONFIG_SCHEMAAdd to CONFIG_SCHEMA in tools/config_manager.py or PLUGIN_REGISTRY.config_sections
models.roles.<role> 'xyz' is not in models.registryAlias typoAdd to models.registry or use empty "" (=default_alias)
research.assistant.model_alias 'abc' is not in models.registryAssistant alias typoSame
api.host must be loopback on startup (create_app)api.host set to public IPUse loopback; public bind not supported in v1 (app.py:70, config_manager.py:1161)
Config is empty or not loadedEmpty file or load() not called before validate()Ensure file has content or rely on defaults for missing file
Silent no-op for new keyKey under recon / autonomous etc. not in schemaExpected — those blocks have lab extras carried verbatim; add validation branch if you want warnings

Verifying a change

python main.py --doctor          # env + nmap + Ollama + config + ports
python main.py --self-test       # localhost smoke; boots MCP server
python -m pytest tests/test_config_manager.py -v  # validates schema vs file drift

The phase-5 drift guard tests/test_config_manager.py:542 asserts every top-level key in config.yaml is in CONFIG_SCHEMA (or plugin registry). Add new top-level blocks to CONFIG_SCHEMA before checking in config.yaml.

  • docs/configuration/overview.md — loading and precedence.
  • docs/configuration/config-reference-generated.md — every key with type/default/consumer.
source: repo docs (build sync)Edit this page on GitHub →