Skip to content

bug(config): PATH.CONFIG_DIR names a directory that does not exist — nine consumers read a missing config tree #14892

Description

@mrveiss

Problem

PathConstants.CONFIG_DIR is defined as:

CONFIG_DIR: Path = PROJECT_ROOT / "infrastructure" / "shared" / "config"

There is no infrastructure/ directory in this repository. The directory that
exists is autobot-infrastructure/shared/config/, and it holds the files the
constant is used to reach — error_messages.yaml, and the security/ subtree
(sso_config.yaml, domain_security.yaml, security_policies.yaml,
compliance.yaml, threat_detection.yaml).

So PATH.CONFIG_DIR resolves to a path that does not exist in a checkout, and
PATH.get_config_path(...) inherits the same wrong root.

Consequence: nine consumers read a directory that is not there

Found while fixing #12969, where this was one of two dead candidates in the
error-catalog lookup — the loader searched PATH.CONFIG_DIR and reported "not
found" for a file that was present in autobot-infrastructure/.

Current consumers:

  • autobot-backend/event_manager.py — CONFIG_DIR / "config.yaml"
  • autobot-backend/utils/service_discovery.py — CONFIG_DIR / "services.json"
  • autobot-backend/config/service_config.py — reports CONFIG_DIR as a path
  • five security/enterprise/* managers, each defaulting a config_path
    argument to PATH.get_config_path("security", ...)

Each of these is the same defect class as #12969: a config file that is present
in the repository is not read, and the code carries on with built-in defaults.
Whether each currently notices needs checking per consumer — that is the work
here, not a one-line constant change.

Why this needs its own issue

#12969's fix works around it: the error catalog now derives its own candidates
from the importing module's location and searches both spellings, so it no
longer depends on this constant being right. That is deliberately local. Fixing
the constant is a change to a shared SSOT value with nine consumers whose
current behaviour is unverified, and at least one of them (the enterprise
security managers) governs security policy loading — silently falling back to a
built-in policy is the more serious half of this.

Acceptance criteria

  • Establish what CONFIG_DIR should resolve to in a checkout and on a
    deployed host — they may not be the same spelling, which is the trap
  • Fix the constant, or replace it with a resolver that searches the real
    candidates and reports what it searched on failure
  • For each of the nine consumers, confirm whether a missing config is
    currently distinguishable from a loaded one; where it is not, make it so
  • A test asserts the resolved directory exists in a checkout — the current
    value would have failed that assertion on the day it was written

Related

Activity

  1. mrveiss commented on Aug 25, 2026

    @mrveiss
    OwnerAuthor

    Closure review — merged base 5633e418c (PR #15040): staying open

    Verified against origin/Dev_new_gui (the merged tree), not the PR branch. Three of four acceptance criteria are fully delivered; the third is half delivered, and its unfinished half is an explicit fix obligation, so this issue does not close yet.

    Criterion file:line evidence Verdict
    Establish what CONFIG_DIR should resolve to in a checkout and on a deployed host — they may not be the same spelling One spelling serves both; verified against a real install layout in PR #15040 and asserted by repo_tests/config_dir_resolves_14892_test.py:55 test_config_dir_derives_from_the_canonical_project_root. The "different spelling" trap does not apply here. Met
    Fix the constant, or replace it with a resolver that searches the real candidates and reports what it searched on failure autobot_shared/ssot_constants.py:395 — CONFIG_DIR: Path = PROJECT_ROOT / "autobot-infrastructure" / "shared" / "config", and :394 PROJECT_ROOT now takes the canonical resolver rather than Path(__file__).parent.parent, so the constant is correct from a worktree too. Failure reporting: autobot-backend/security/enterprise/config_loading.py returns a searched_path and WARNs naming it. Met
    For each of the nine consumers, confirm whether a missing config is currently distinguishable from a loaded one; where it is not, make it so Confirmed for all nine. Fixed for eight: autobot-backend/security/enterprise/config_loading.py gives the five enterprise-security managers a source / loaded_from_file / searched_path result and a WARNING naming the path (compliance_manager.py:134, domain_reputation.py:33, security_policy_manager.py:112, sso_integration.py:95, threat_detection/engine.py:62); autobot-backend/utils/error_catalog.py:481 was already distinguishable; autobot-backend/event_manager.py:46 warns on a miss; autobot-backend/config/service_config.py:355 only reports the path and never loads. The ninth is not fixed: autobot-backend/utils/service_discovery.py:125 assigns self.config_file from PATH.CONFIG_DIR / "services.json" and nothing ever reads it — git grep -n 'config_file' autobot-backend/utils/service_discovery.py returns only the parameter at :120 and the assignment at :125. That is the extreme form of exactly what this criterion targets: an absent config indistinguishable from a loaded one, because it is never loaded. It is tracked as #15038, which is open. Not met
    A test asserts the resolved directory exists in a checkout repo_tests/config_dir_resolves_14892_test.py:39 test_config_dir_exists_in_a_checkout, plus :44 pinning the wrong spelling by name and re-checking the premise, plus five parametrised reachability cases (:66) with a separately-asserted population floor (:71). These would have failed on the original value the day it was written. Met

    Why this is not treated the way #13149's audit criterion was

    #13149's outstanding item was an audit obligation ("audited… confirm per consumer") — confirmation was what it asked for and confirmation is what happened, so splitting the new inertness finding to #15044 was correct and did not block it. This criterion is different in kind: it says where it is not, make it so. For the ninth consumer it was not made so; it was filed for later. Filing is the right handling of the work, but it is not delivery of this criterion.

    What remains

    Close this issue once #15038 lands — i.e. ServiceDiscovery either reads services.json (with a miss distinguishable from a load, in the shape config_loading.py established) or stops accepting a config_file it never reads. Nothing else on this issue is outstanding.

    Also fixed here, worth recording: four of the five enterprise-security managers previously mkdir(parents=True) + yaml.dump'd their built-in defaults into the path they had just failed to read. Under the broken CONFIG_DIR that created a decoy config tree, and the next boot loaded the decoy and reported success — a one-time miss became a permanent, self-confirming wrong answer, with the security policy actually in force one nobody had written or reviewed. The miss branch now never writes (repo_tests/config_dir_resolves_14892_test.py:104 asserts nothing lands on disk), and _save_config is gone from all four — git grep '_save_config' -- 'autobot-backend/security/enterprise/' returns 0.

    No sub-issues are linked to this issue.

  2. mrveiss commented on Aug 28, 2026

    @mrveiss
    OwnerAuthor

    Verification against merged Dev_new_gui — NOT closing

    The constant is fixed and guarded. AC3 is not met: one of the nine consumers is still unable to distinguish a missing config from a loaded one, because it never loads one at all.

    Merged commits: 42ac95b53 (#14898), 5633e418c (#15040).

    AC Evidence kind Verdict
    1. Establish what CONFIG_DIR should resolve to in a checkout and on a deployed host static (recorded finding) Met for the checkout, inferred for the host. Checkout answer is recorded and asserted: repo_tests/config_dir_resolves_14892_test.py:40-53. The host answer rests on autobot_shared/paths.py:77-101, which resolves a deployed install root by the autobot- component-directory prefix — so the spelling is the same on both, provided autobot-infrastructure/ is synced to the install (ansible/playbooks/update-node.yml:116 lists it). That last step is inference from the sync manifest, not a host measurement.
    2. Fix the constant, or replace it with a resolver static Met — autobot_shared/ssot_constants.py:395: CONFIG_DIR = PROJECT_ROOT / "autobot-infrastructure" / "shared" / "config", annotated # #14892: was "infrastructure". get_config_path (:406-408) inherits the corrected root.
    3. For each of the nine consumers, confirm whether a missing config is distinguishable from a loaded one; where it is not, make it so static, per consumer NOT met — one consumer outstanding. See table below.
    4. A test asserts the resolved directory exists in a checkout unit Met — repo_tests/config_dir_resolves_14892_test.py:40 asserts PATH.CONFIG_DIR.is_dir(), :47-49 reject the bare infrastructure spelling and assert no such directory exists, and TRACKED_CONFIG_FILES (5 named files, :67) each resolve through get_config_path. These are the assertions the original value would have failed.

    Consumer-by-consumer (AC3)

    Consumer Verdict
    autobot-backend/security/enterprise/{compliance_manager,domain_reputation,security_policy_manager,sso_integration,threat_detection/engine}.py (5) Met — all five route through security/enterprise/config_loading.py, which returns a result carrying source (SOURCE_FILE vs SOURCE_BUILTIN_DEFAULTS), loaded_from_file and searched_path. Covered at repo_tests/config_dir_resolves_14892_test.py:97-128, including that a miss creates nothing on disk.
    autobot-backend/event_manager.py:45 Met — a missing config.yaml logs a WARNING naming the resolved path before returning built-in defaults (:46-48); a load error logs ERROR (:53-55). The file is present in a checkout and is in TRACKED_CONFIG_FILES.
    autobot-backend/config/service_config.py:355 Met (trivially) — reports str(PATH.CONFIG_DIR) as a path, reads nothing.
    autobot-backend/utils/service_discovery.py:124 NOT met — default_config = PATH.CONFIG_DIR / "services.json" is assigned to self.config_file at :125 and never read again: config_file appears exactly twice in the module (:120, :125) and nowhere else in the repository. And services.json does not exist anywhere in the tree — it is not in autobot-infrastructure/shared/config/, and it is deliberately absent from TRACKED_CONFIG_FILES. So ServiceDiscovery resolves a path to a file that has never existed, never opens it, and populates itself from _init_default_services() regardless. A missing config here is not merely indistinguishable from a loaded one — no load is ever attempted. Correcting the constant did not change this consumer's behaviour by one line.

    What remains

    Decide ServiceDiscovery's intent and make it honest — either load config_file and report SOURCE_FILE/SOURCE_BUILTIN_DEFAULTS the way config_loading.py does (which needs services.json to exist, or a stated built-in-only posture), or remove the unread config_file/services.json reference so no reader believes a config governs service discovery. Same defect class as #13852: a config that is absent is indistinguishable from one that was loaded.

    Could not prove

    That CONFIG_DIR resolves to an existing directory on a deployed host — that needs one host check that autobot-infrastructure/shared/config/ is present under the install root. Nothing has been deployed.

  3. mrveiss commented on Aug 28, 2026

    @mrveiss
    OwnerAuthor

    PR #15157 finishes the consumer pass. Opened as Refs, not Closes — AC1's
    deployed-host half cannot be met from a branch, and this comment records what is
    proven and what is not.

    Per-consumer, all nine

    # Consumer Verdict
    1 event_manager.py:45 (config.yaml) wired in — was not distinguishable, contrary to the prior pass
    2 utils/service_discovery.py:124 (services.json) removed — proven debris
    3 config/service_config.py:355 left — reports a path, loads nothing, no miss to distinguish
    4 sso_integration.py:95 left; distinguishable, but the file has never existed → #15154
    5 domain_reputation.py:33 left; distinguishable, file present
    6 security_policy_manager.py:112 left; distinguishable, but the file has never existed → #15154
    7 compliance_manager.py:134 left; distinguishable, file present
    8 threat_detection/engine.py:62 left; distinguishable, file present
    9 utils/error_catalog.py:481 left; already distinguishable, stale docstring corrected

    Two things this issue's own text got wrong

    Three of the five security config files exist, not five.
    security/security_policies.yaml and security/sso_config.yaml have never
    existed in any commit on any branch
    , so SecurityPolicyManager and
    SSOIntegrationFramework have only ever run built-in defaults — the "more serious
    half" this issue named. #14892 has made that observable (config_source, a WARNING
    naming the path, no decoy write); deciding what a reviewed policy should say is a
    security decision, filed as #15154 and pinned as a two-entry ratchet.

    Consumer 1 did not meet AC3 either. event_manager returned the same
    {"agent_behavior": {"debug_mode": False}} on a miss, on a parse error, and on a
    file with no agent_behavior section — and config.yaml has never had that
    section, so debug_publish() could never be enabled and nothing said so. It also
    returned yaml.safe_load's None verbatim for an empty file, so the next
    _is_debug_mode() call would raise AttributeError.

    Consumer 2: why deletion, when debris is normally unfinished work

    The standing rule assumes there is work to finish. Here there is not:

    1. git log --all --oneline -- "**/services.json" returns nothing — the file
      has never existed in any commit on any branch, so there is no landed work to
      point the read at and no unlanded work either.
    2. The values it would have supplied already have a live source:
      _init_default_services() reads every endpoint from
      unified_config_manager.get_distributed_services_config(), the SSOT.

    Wiring it in would not complete anything; it would fork the SSOT. Removal leaves
    exactly one source of truth.

    New guard

    An AST sweep asserts that every config filename named through PATH.CONFIG_DIR is
    actually present, with an empty-sweep floor and a two-entry ratchet for the known
    gaps. Mutation-proved both ways: reintroducing services.json goes red naming
    service_discovery.py:127, and emptying the enumeration goes red rather than
    reporting a clean tree.

    Remaining for closure

  4. mrveiss commented on Aug 28, 2026

    @mrveiss
    OwnerAuthor

    AC1's spelling trap now has repo-side evidence

    This issue's AC1 warns that the repository directory and the deployed directory "may not be the same spelling, which is the trap". PR #15157 (3fc18e79a, merged) reported that half as unprovable without a deployed host, which was correct at the time.

    A sweep done for a different issue has now produced the evidence from the repository alone. 39 tracked references to autobot-user-backend split cleanly into two classes:

    • Repo-relative references — broken. The directory exists in no commit on any branch. Five are live sys.path.insert calls inside database migration scripts.
    • Deployed-path references — correct. /opt/autobot/autobot-user-backend in the systemd unit template, roughly ten slm-backend playbooks, and documented deliberately at autobot-backend/utils/paths_manager.py:29.

    The second class being right while the first is wrong is the proof: the deployed directory genuinely is named differently from the repository one. That is AC1's trap, demonstrated without a host.

    Filed in full as #15193, including the requirement that a sweep must not "fix" the deployed references.

    One site relevant to this issue specifically: scripts/migrate_tests.py:327 reads

    api_tests = PROJECT_ROOT / "infrastructure" / "autobot-user-backend" / "tests"

    That carries the bare infrastructure/ spelling this issue is about, in a location PR #15157's consumer pass did not reach — stacked with the phantom tree. It is recorded on #15193 as needing both defects fixed together.

    This issue stays open: AC3 remains unmet for event_manager.py (config.yaml has never carried the agent_behavior section it reads), and AC1's deployed-host half is still unverified — this evidence establishes that the spellings differ, not what the live host currently resolves.

  5. mrveiss commented on Aug 29, 2026

    @mrveiss
    OwnerAuthor

    Staying open — three of four criteria met. AC1's deployed-host half is the only gap, and it is narrower than it was.

    Audit of open issues whose work landed via a merged commit (Closes never auto-fires on Dev_new_gui). Two merges bear on this: 5633e418cd (PR #15040, which wrote Closes ... #14892) and 3fc18e79a4 (PR #15157, which corrected that to Refs #14892 — AC1's deployed-host half is unmet). The later one is right.

    AC Verdict Evidence in origin/Dev_new_gui
    1. Establish what CONFIG_DIR should resolve to in a checkout and on a deployed host — they may not be the same spelling, which is the trap Half met The checkout half is settled and pinned: repo_tests/config_dir_resolves_14892_test.py:49-50 asserts "autobot-infrastructure" in PATH.CONFIG_DIR.parts and "infrastructure" not in ..., with :53 recording that a failure means the premise changed rather than the test. The deployed half is not established for this constant. See below.
    2. Fix the constant, or replace it with a resolver that searches the real candidates and reports what it searched on failure MET config_dir_resolves_14892_test.py:57 test_config_dir_derives_from_the_canonical_project_root — the constant now derives from the canonical root #13149 established, rather than carrying a hand-written path.
    3. For each of the nine consumers, confirm whether a missing config is distinguishable from a loaded one; where it is not, make it so MET The consumer sweep is derived, not hand-listed: :142 parses every config filename a consumer names literally through the constant, :213 test_no_consumer_names_a_config_file_that_is_absent, and :200 test_the_sweep_finds_consumers_at_all is the non-vacuity control (>= 5 distinct filenames, so the AST matcher breaking cannot read as clean). Distinguishability itself is covered at :85 test_missing_file_reports_builtin_defaults_and_writes_nothing, :99 test_present_file_reports_the_file, and :109 test_an_empty_file_is_not_treated_as_a_loaded_config — that third one is the #13852 shape and is easy to miss.
    4. A test asserts the resolved directory exists in a checkout MET :41 test_config_dir_exists_in_a_checkout, plus :67 test_each_tracked_config_file_is_reachable and :71 test_the_tracked_file_population_is_not_empty.

    Why AC1 is UNCLEAR rather than simply unmet

    The 2026-08-28 comment on this issue argues the trap now has repository-only evidence: 39 tracked references to autobot-user-backend split into a broken repo-relative class and a correct deployed-path class (/opt/autobot/autobot-user-backend in the systemd unit template and roughly ten slm-backend playbooks, documented deliberately at autobot-backend/utils/paths_manager.py:29). The second class being right while the first is wrong does demonstrate that a deployed directory can be spelled differently from its repository counterpart.

    But that is a different directory. It proves the trap exists as a class; it does not establish what CONFIG_DIR specifically resolves to on a deployed host — which is what AC1 asks. The autobot-infrastructure vs bare infrastructure question for this constant is still unanswered on a real install, and #15193 is OPEN.

    So one of two things closes AC1, and it is a scope call I am not making unilaterally:

    1. Read the constant on a deployed host and record it — one command, and AC1 is answered as written.
    2. Re-scope AC1 to the checkout half, on the grounds that bug(paths): 39 references to autobot-user-backend, a directory in no commit — five are live sys.path.insert calls in migration scripts #15193 now owns the deployed-spelling question repo-wide and this issue should not carry a duplicate of it.

    Recommendation: option 2, with a line on #15193 naming CONFIG_DIR as one of its subjects — otherwise this issue waits on a host check that #15193 will have to perform anyway.

    One site still in scope either way, from the same comment: scripts/migrate_tests.py:327 reads PROJECT_ROOT / "infrastructure" / "autobot-user-backend" / "tests" — the bare infrastructure spelling, which is exactly the defect this issue names.

    Not closing.

  6. mrveiss commented on Sep 2, 2026

    @mrveiss
    OwnerAuthor

    AC1's spelling trap now has repo-side evidence (from #15193)

    AC1 warns that the repository directory and the deployed directory "may not be the same spelling, which is the trap", and that was recorded here as unprovable without a deployed host. #15193 supplies the proof from the repository alone.

    Measured on Dev_new_gui @ 9cf9cea80: 196 occurrences of the retired component name across 39 tracked files. They split cleanly on one syntactic property — whether the path token begins at the filesystem root:

    • Repo-relative occurrences resolve against the checkout and address a directory that is in no commit on any branch. Five were live sys.path.insert calls in database migration scripts; two more were silently no-opping since the rename — a post-checkout hook whose repair block was gated on a directory test that could never be true, and an MCP bridge verifier that reported all five bridges absent.
    • Absolute occurrences name the deploy host's directory, and are correct. The systemd unit, roughly ten ansible playbooks and the recovery scripts all depend on that spelling.

    Both classes are simultaneously true, which is exactly AC1's trap: the two directory names genuinely differ, so any sweep-and-replace across all 196 breaks the deployment. The distinguisher is not the name and not the file type — it is the leading character of the path token.

    That property is now enforced. repo_tests/no_repo_relative_phantom_path_test.py fails on a repo-relative reference to a retired component directory and exempts an absolute one, with the contrast proven by mutation in both directions.

    Refs #15193.

  7. mrveiss commented on Sep 3, 2026

    @mrveiss
    OwnerAuthor

    Repo-side evidence for the AC1 spelling trap — satisfying #15193's final acceptance criterion, which asked for this to be recorded here.

    PATH.CONFIG_DIR now reads (autobot_shared/ssot_constants.py:395 on Dev_new_gui):

    CONFIG_DIR: Path = PROJECT_ROOT / "autobot-infrastructure" / "shared" / "config"  # #14892: was "infrastructure"

    The trailing comment is the evidence: the old spelling was bare infrastructure, and the directory is autobot-infrastructure. That one-word difference is why the path resolved to nothing while every consumer read it without raising.

    #15193 found the same trap in a second form — a repo-relative path naming autobot-user-backend, a directory in no commit — and its guard (repo_tests/no_repo_relative_phantom_path_test.py) now fails when a tracked file references a repo-relative path no commit contains. That guard covers this class going forward, so a third instance cannot be added silently.

    Verified in base: autobot-infrastructure/shared/config exists, and the fix commit 5633e418c is an ancestor of Dev_new_gui.

  8. mrveiss commented on Sep 3, 2026

    @mrveiss
    OwnerAuthor

    Closed on code evidence, verified against merged Dev_new_gui — not by this session's work. The fix landed in 5633e418c (PR #15040) and the issue was left open.

    AC Evidence in base
    PATH.CONFIG_DIR resolves to a real directory autobot_shared/ssot_constants.py:395 reads PROJECT_ROOT / "autobot-infrastructure" / "shared" / "config", and that directory exists in the tree. The trailing comment records the trap: the old spelling was bare infrastructure
    The nine consumers behave sanely when a file is absent Five enterprise managers route through autobot_shared/config_file_loading.py's shared helper — WARNING naming the searched path, builtin defaults, a config_source attribute distinguishing the miss, and never a write back. That last is the original defect: the old miss branch wrote defaults to disk, which made the next read succeed and confirmed the wrong answer
    Dead consumers removed rather than repointed ServiceDiscovery.__init__ no longer takes a config_file argument — nothing named services.json was ever committed
    Assert on the resolved directory, not a string repo_tests/config_dir_resolves_14892_test.py — 20 passed

    Two of the five config files (sso_config.yaml, security_policies.yaml) genuinely exist in no commit. That is deliberate and tracked: they sit in _KNOWN_ABSENT_CONFIG_FILES with #15154 to decide their actual content. The consumers degrade to defaults with a warning rather than pretending, which is the correct behaviour for a file whose content is undecided.

    Fix commit confirmed an ancestor of Dev_new_gui via git merge-base --is-ancestor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions