Repository navigation
bug(config): PATH.CONFIG_DIR names a directory that does not exist — nine consumers read a missing config tree #14892
Description
Activity
Closure review — merged base
5633e418c(PR #15040): staying openVerified 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:lineevidenceVerdict Establish what CONFIG_DIRshould resolve to in a checkout and on a deployed host — they may not be the same spellingOne spelling serves both; verified against a real install layout in PR #15040 and asserted by repo_tests/config_dir_resolves_14892_test.py:55test_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:394PROJECT_ROOTnow takes the canonical resolver rather thanPath(__file__).parent.parent, so the constant is correct from a worktree too. Failure reporting:autobot-backend/security/enterprise/config_loading.pyreturns asearched_pathand 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.pygives the five enterprise-security managers asource/loaded_from_file/searched_pathresult 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:481was already distinguishable;autobot-backend/event_manager.py:46warns on a miss;autobot-backend/config/service_config.py:355only reports the path and never loads. The ninth is not fixed:autobot-backend/utils/service_discovery.py:125assignsself.config_filefromPATH.CONFIG_DIR / "services.json"and nothing ever reads it —git grep -n 'config_file' autobot-backend/utils/service_discovery.pyreturns only the parameter at:120and 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:39test_config_dir_exists_in_a_checkout, plus:44pinning 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.
ServiceDiscoveryeither readsservices.json(with a miss distinguishable from a load, in the shapeconfig_loading.pyestablished) or stops accepting aconfig_fileit 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 brokenCONFIG_DIRthat 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:104asserts nothing lands on disk), and_save_configis gone from all four —git grep '_save_config' -- 'autobot-backend/security/enterprise/'returns 0.No sub-issues are linked to this issue.
Verification against merged
Dev_new_gui— NOT closingThe 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_DIRshould resolve to in a checkout and on a deployed hoststatic (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 onautobot_shared/paths.py:77-101, which resolves a deployed install root by theautobot-component-directory prefix — so the spelling is the same on both, providedautobot-infrastructure/is synced to the install (ansible/playbooks/update-node.yml:116lists 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:40assertsPATH.CONFIG_DIR.is_dir(),:47-49reject the bareinfrastructurespelling and assert no such directory exists, andTRACKED_CONFIG_FILES(5 named files,:67) each resolve throughget_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 carryingsource(SOURCE_FILEvsSOURCE_BUILTIN_DEFAULTS),loaded_from_fileandsearched_path. Covered atrepo_tests/config_dir_resolves_14892_test.py:97-128, including that a miss creates nothing on disk.autobot-backend/event_manager.py:45Met — a missing config.yamllogs 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 inTRACKED_CONFIG_FILES.autobot-backend/config/service_config.py:355Met (trivially) — reports str(PATH.CONFIG_DIR)as a path, reads nothing.autobot-backend/utils/service_discovery.py:124NOT met — default_config = PATH.CONFIG_DIR / "services.json"is assigned toself.config_fileat:125and never read again:config_fileappears exactly twice in the module (:120,:125) and nowhere else in the repository. Andservices.jsondoes not exist anywhere in the tree — it is not inautobot-infrastructure/shared/config/, and it is deliberately absent fromTRACKED_CONFIG_FILES. SoServiceDiscoveryresolves 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 loadconfig_fileand reportSOURCE_FILE/SOURCE_BUILTIN_DEFAULTSthe wayconfig_loading.pydoes (which needsservices.jsonto exist, or a stated built-in-only posture), or remove the unreadconfig_file/services.jsonreference 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_DIRresolves to an existing directory on a deployed host — that needs one host check thatautobot-infrastructure/shared/config/is present under the install root. Nothing has been deployed.PR #15157 finishes the consumer pass. Opened as
Refs, notCloses— 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:355left — reports a path, loads nothing, no miss to distinguish 4 sso_integration.py:95left; distinguishable, but the file has never existed → #15154 5 domain_reputation.py:33left; distinguishable, file present 6 security_policy_manager.py:112left; distinguishable, but the file has never existed → #15154 7 compliance_manager.py:134left; distinguishable, file present 8 threat_detection/engine.py:62left; distinguishable, file present 9 utils/error_catalog.py:481left; 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.yamlandsecurity/sso_config.yamlhave never
existed in any commit on any branch, soSecurityPolicyManagerand
SSOIntegrationFrameworkhave 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_managerreturned the same
{"agent_behavior": {"debug_mode": False}}on a miss, on a parse error, and on a
file with noagent_behaviorsection — andconfig.yamlhas never had that
section, sodebug_publish()could never be enabled and nothing said so. It also
returnedyaml.safe_load'sNoneverbatim for an empty file, so the next
_is_debug_mode()call would raiseAttributeError.Consumer 2: why deletion, when debris is normally unfinished work
The standing rule assumes there is work to finish. Here there is not:
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.- 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_DIRis
actually present, with an empty-sweep floor and a two-entry ratchet for the known
gaps. Mutation-proved both ways: reintroducingservices.jsongoes red naming
service_discovery.py:127, and emptying the enumeration goes red rather than
reporting a clean tree.Remaining for closure
- AC1, deployed-host half — what
CONFIG_DIRresolves to on a host is not
provable from a branch, and nothing here is deployed. Mitigated, not proven:
error_catalog.pykeeps both spellings and reports every path it searched. - security: SecurityPolicyManager and SSOIntegrationFramework read policy files that have never existed #15154 — decide whether the two absent security policy files should exist.
- added a commit that references this issue
on Aug 28, 2026 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-backendsplit cleanly into two classes:- Repo-relative references — broken. The directory exists in no commit on any branch. Five are live
sys.path.insertcalls inside database migration scripts. - Deployed-path references — correct.
/opt/autobot/autobot-user-backendin the systemd unit template, roughly ten slm-backend playbooks, and documented deliberately atautobot-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:327readsapi_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.yamlhas never carried theagent_behaviorsection 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.- Repo-relative references — broken. The directory exists in no commit on any branch. Five are live
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 (
Closesnever auto-fires onDev_new_gui). Two merges bear on this:5633e418cd(PR #15040, which wroteCloses ... #14892) and3fc18e79a4(PR #15157, which corrected that toRefs #14892 — AC1's deployed-host half is unmet). The later one is right.AC Verdict Evidence in origin/Dev_new_gui1. Establish what CONFIG_DIRshould resolve to in a checkout and on a deployed host — they may not be the same spelling, which is the trapHalf met The checkout half is settled and pinned: repo_tests/config_dir_resolves_14892_test.py:49-50asserts"autobot-infrastructure" in PATH.CONFIG_DIR.partsand"infrastructure" not in ..., with:53recording 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:57test_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: :142parses every config filename a consumer names literally through the constant,:213test_no_consumer_names_a_config_file_that_is_absent, and:200test_the_sweep_finds_consumers_at_allis the non-vacuity control (>= 5distinct filenames, so the AST matcher breaking cannot read as clean). Distinguishability itself is covered at:85test_missing_file_reports_builtin_defaults_and_writes_nothing,:99test_present_file_reports_the_file, and:109test_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 :41test_config_dir_exists_in_a_checkout, plus:67test_each_tracked_config_file_is_reachableand:71test_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-backendsplit into a broken repo-relative class and a correct deployed-path class (/opt/autobot/autobot-user-backendin the systemd unit template and roughly ten slm-backend playbooks, documented deliberately atautobot-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_DIRspecifically resolves to on a deployed host — which is what AC1 asks. Theautobot-infrastructurevs bareinfrastructurequestion 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:
- Read the constant on a deployed host and record it — one command, and AC1 is answered as written.
- 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_DIRas 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:327readsPROJECT_ROOT / "infrastructure" / "autobot-user-backend" / "tests"— the bareinfrastructurespelling, which is exactly the defect this issue names.Not closing.
- addedarea: config-ssotWave 0 · cluster R — Config, paths & SSOTWave 0 · cluster R — Config, paths & SSOT
on Sep 1, 2026 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.insertcalls 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.pyfails 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.
- Repo-relative occurrences resolve against the checkout and address a directory that is in no commit on any branch. Five were live
Repo-side evidence for the AC1 spelling trap — satisfying #15193's final acceptance criterion, which asked for this to be recorded here.
PATH.CONFIG_DIRnow reads (autobot_shared/ssot_constants.py:395onDev_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 isautobot-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/configexists, and the fix commit5633e418cis an ancestor ofDev_new_gui.Closed on code evidence, verified against merged
Dev_new_gui— not by this session's work. The fix landed in5633e418c(PR #15040) and the issue was left open.AC Evidence in base PATH.CONFIG_DIRresolves to a real directoryautobot_shared/ssot_constants.py:395readsPROJECT_ROOT / "autobot-infrastructure" / "shared" / "config", and that directory exists in the tree. The trailing comment records the trap: the old spelling was bareinfrastructureThe 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, aconfig_sourceattribute 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 answerDead consumers removed rather than repointed ServiceDiscovery.__init__no longer takes aconfig_fileargument — nothing namedservices.jsonwas ever committedAssert on the resolved directory, not a string repo_tests/config_dir_resolves_14892_test.py— 20 passedTwo 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_FILESwith #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_guiviagit merge-base --is-ancestor.
Problem
PathConstants.CONFIG_DIRis defined as:There is no
infrastructure/directory in this repository. The directory thatexists is
autobot-infrastructure/shared/config/, and it holds the files theconstant is used to reach —
error_messages.yaml, and thesecurity/subtree(
sso_config.yaml,domain_security.yaml,security_policies.yaml,compliance.yaml,threat_detection.yaml).So
PATH.CONFIG_DIRresolves to a path that does not exist in a checkout, andPATH.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_DIRand reported "notfound" 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— reportsCONFIG_DIRas a pathsecurity/enterprise/*managers, each defaulting aconfig_pathargument 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
CONFIG_DIRshould resolve to in a checkout and on adeployed host — they may not be the same spelling, which is the trap
candidates and reports what it searched on failure
currently distinguishable from a loaded one; where it is not, make it so
value would have failed that assertion on the day it was written
Related
absent is indistinguishable from one that was loaded)