Repository navigation
fix(config): one canonical project root, and no path that resolves to the wrong thing silently (#13149, #13357, #14113, #14892, #14070) - #15040
Merged
Conversation
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No new hardcoded values of either class — Known backlog in |
Applied Black, isort, and autoflake to match code-quality checks. Triggered by workflow auto-fix-formatting.yml.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…e ratchet after formatting (#14070)
This was referenced Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thinking Path
Five issues, one defect class: a path or config key resolves to the wrong thing instead of failing.
Nobody sees an error; they see behaviour that is subtly off.
I started at #13357, because it explains the others' blast radius. What I found there changed the
shape of the work: the canonical resolver already exists and is already correct.
autobot_shared/paths.py::project_root()was landed by #14517/#14544/#14624, walks up in a singlepass with a checkout root as a hard boundary, treats
.gitwithexists()so a worktree's.gitfile matches, and raises
ProjectRootUndeterminablerather than guessing. So the ask was not"write a resolver" — it was find who still isn't using it, and prove the worktree case with a test
that cannot be faked.
That reframing drove four findings the issues did not predict:
The existing worktree test is fixture-shaped.
TestWorktreeBoundarybuilds a directory thatlooks like a worktree and calls
resolve_project_root(path_it_chose). It never callsproject_root(), and.gitis a file only because the fixture wrote one — the test supplies theanswer it checks. I added a test that creates a real
git worktreeand runs the resolver in asubprocess whose
__file__is inside it.The placeholder guard has a hole, and a live offender was sitting in it. The guard walks
ast.Constantonly. An f-string never holds the placeholder in any single Constant —f"${AUTOBOT_PROJECT_ROOT:-/opt/...}"parses asConstant('$')+FormattedValue(Name(...))— sotest_distributed_system_integration.py:703carried one while the guard reported the file clean.The guard's docstring claims pyflakes F821 covers the f-string form; F821 only fires where pyflakes
runs, and
autobot-infrastructure/shared/tests/is outside its scope.Fixing
get_data_pathwould have destroyed the secrets it was fixed to protect.secrets_store_migration._legacy_absolute_pathdeliberately delegates to the buggy resolver tofind the old location, with a comment saying this keeps it correct "even if that resolver's
behavior changes later". It does the opposite: once
get_data_pathreturns the canonicaldirectory, source == destination and
migrate_legacy_secrets_storeno-ops (:207). Every existingdeployment's
secrets.key/.json/.dbwould be orphaned and a fresh key minted over the top —exactly the "Docs improvements #1 risk" bug(config): PathsManager reads a config.yaml key that does not exist and silently falls back to a CWD-relative 'data' path #14113's migration note names. The formula is now pinned as a literal.
A bot commit is a change, not noise. An
Auto-fix: code formattingcommit landed onthis branch mid-review. Reviewed line by line it was genuinely formatting-only — but it
reflowed one
Field(...)across three lines, pushing a grandfathered file over the veryceiling this PR had just recorded, and its isort pass trimmed a line from four others,
leaving those stale in the opposite direction. Formatting commits are not behaviour-neutral
with respect to guards that measure the file.
Four of bug(config): PATH.CONFIG_DIR names a directory that does not exist — nine consumers read a missing config tree #14892's consumers didn't just fail to read — they wrote a decoy. On a miss they
mkdir(parents=True)andyaml.dumptheir built-in defaults into the path they had just failed toread. The next boot loads the decoy and reports success. A one-time miss became a permanent,
self-confirming wrong answer, and the security policy in force was one nobody wrote or reviewed.
Correcting
CONFIG_DIRalone would have relocated that into the real tree for the two files thatare absent at either spelling.
Measured populations
Derived here, not taken from the issues.
scripts/lib/project_root.sh; the single surviving:-/optliteral is that helper's own deployed-install fallback, which is where it belongsPathConstants.PROJECT_ROOT = Path(__file__).parent.parentCONFIG_DIR/get_config_pathconsumerserror_catalog.pywas distinguishablepaths_manageraccessorsexcept-path ultimate fallbacks, also CWD-relativeWhat Changed
One canonical resolver, everything routed through it.
autobot_shared.paths.project_root()is the single implementation; nothing new was added beside it.
PathConstants.PROJECT_ROOTnow takesit (via
ssot_config.PROJECT_ROOT) instead ofPath(__file__).parent.parent, which was blind toAUTOBOT_PROJECT_ROOT/AUTOBOT_BASE_DIRand resolved to the main checkout from a worktree.Per-case decisions for missing keys and directories — each one stated, not defaulted:
PathsManagerreads apaths:keyconfig.yamlthat the backend'sConfigManagerloads has ever had a top-levelpaths:key, so every call took the fallback — the fallback was the real behaviourPathsManagerrelative fallbackssot_configPATH.CONFIG_DIRnames a missing dirautobot-infrastructure/shared/configsourceattributeOther changes: the placeholder guard now reassembles f-strings via
ast.unparse; the audit-logdefault has one definition (
ssot_config.default_audit_log_file), late-bound so derivation isprovable; the
Field-default guard is now AST-based and lives intools/lint/check_field_defaults.py.The guard's own literal is assembled from fragments.
LIVE_INSTALL_PREFIX = "/opt/" + "autobot".Inside
ssot_config_test.pythe surroundingField(context happened to satisfy a skip in therepository's hardcoded-value rule; standing alone as its own module it did not, so extracting the
guard made its own source trip detector 3. The sibling guard in this same PR already assembles its
banned text this way — this one now matches, plus a self-guard that replicates detector 3's
complete skip set and fails if either the guard or its tests re-acquires the literal. The self-guard
has its own can-it-fail test, because a self-guard that cannot fire is decoration.
Removals (the most-missed criteria — grepped explicitly):
security_layer._AUDIT_LOG_FILE_DEFAULT— gone.git grep '_AUDIT_LOG_FILE_DEFAULT' -- '*.py'returns only prose.default(?:_factory)?\s*=\s*"[^"]*/opt/autobot[^"]*"— gone._save_configin four security managers — its only caller was the decoy write.git grep '_save_config' -- 'autobot-backend/security/enterprise/'returns 0.File-size ratchet, diffed against the base branch: 6 entries lowered, 0 raised, 1 removed,
0 added.
security_layer_test.pycame in under the 600-line limit entirely, so its grandfatherentry was deleted rather than lowered. Four of the six were lowered twice — once by this PR and
again after the formatter's isort pass trimmed a line from each. The formatter's reflow was
reverted at source rather than absorbed by raising a number — see Verification.
Verification
The worktree test, and its mutation evidence. Real
git init+git worktree add, realpaths.pycopied in, resolver run in a subprocess. Preconditions asserted (.gitis a file, theworktree has no
.env, the main tree does), plus a control asserting the main checkout stillresolves to itself — without it, a resolver returning
Path.cwd()would pass for the wrong reason.paths.py.env, then markers) — the pre-#13357 form.../main, expected.../main/.worktrees/issue-1. Control still passed.gittested withis_dir()instead ofexists()git diff autobot_shared/paths.pyempty; 28 passedThe
Field-default guard, old vs new (grep -c MUTATION= 0 in both files afterwards):Injecting a real
default_factory=lambda: "/opt/autobot/logs/audit.log"intossot_config.py— theexact form the regex missed — failed the live sweep, then restored.
Population floors are asserted before offender counts, and floored separately per sweep
(
OFFENDING_SPELLINGSandBENIGN_SPELLINGSeach get their own floor, so one populated tuple cannotsatisfy the other's).
field_call_count("") == 0 < FIELD_CALL_FLOORproves the floor can fail;the real file reaches 582 against a floor of 200.
The bot commit's subject needs no squash — the gate exempts it by design.
lint-conventions.shskips any commit whose author name or email contains
[bot], with a comment explaining exactly why(this repository's own auto-fix workflows push commits with no issue number, and without the
exemption every PR touching a generated file would fail a required check on a commit no human wrote).
Replaying the gate's own logic over the PR range: 6 commits PASS, 1 BOT-EXEMPT, 0 fail.
Squashing it would have required a force-push for no gain.
Post-review re-verification, against the post-format tree.
The formatter's reflow was undone at source, not absorbed:
# noqa: PLW0108,E501was removed(nothing in this repository runs ruff — Black, isort, autoflake and flake8 only, so the code was
suppressing a rule no linter enforces), which brings the
Field(...)call to 113 characters againsta 120 limit.
black --checknow reports the file unchanged, so it is stable rather than waitingto be re-split, and
ssot_config.pyis back at its recorded 3297 with no ceiling raised.ssot_config.pycompliance_manager.pysecurity_policy_manager.pysso_integration.pythreat_detection/engine.pyThe re-measurement script asserts every change is a lowering before writing, so a raise cannot
be recorded by accident.
python_file_size_ratchet_test.py: 31 passed.Hardcoded-value rule. The failing finding was real and is fixed. Re-running detector 3's logic
over all changed files — with its own
*_test.pyexclusion and its complete skip set applied —returns 0 findings across 15 non-test files. A second finding the CI run had not reached, in a
comment this PR itself rewrote in
ssot_config.py, was cleaned in the same pass.Formatters run proactively (
black --target-version py310,isort,autoflake) over all 23changed files: no further changes, so there is nothing left for a bot pass to land.
Mutation evidence re-confirmed after all of the above — the two-pass walk still fails
TestRealGitWorktree::test_resolves_to_the_worktree_not_the_main_checkoutwhile the control passes;restored to an empty diff,
grep -c MUTATION= 0.Test runs (only changed files — this machine is the CI runner):
ssot_config_test,paths_test,paths_install_root_test,security_layer_test,security_layer_audit_default_test,paths_manager_test,config_dir_resolves_14892_test,check_field_defaults_test,check_no_shell_placeholder_paths_test,python_file_size_ratchet_testerror_catalog_resolution,config/,editable_install_layout,deployment_declares_project_root_14544,compliance_audit_path,source_paths)flake8clean on every changed file; placeholder audit clean over 5,136 files(miss →
builtin-defaults+ nothing written to disk; hit →file; empty YAML →builtin-defaults)Pre-existing failure, not from this branch:
test_ci_import_smoke_paths_14252.py::test_every_extracted_program_parsesfails identically on unmodified
Dev_new_guiat14b7e6be5. Filed as #15039.Per-issue criteria
#13149 — no canonical project-root resolution → Closes
git grep 'AUTOBOT_PROJECT_ROOT:-' -- '*.py'returns 0check_no_shell_placeholder_paths.py, now covering the f-string form it was blind toThe YAML audit the criterion asked for. It anticipated two outcomes — "expanded, fine" or
"unexpanded, broken". The answer is neither: nothing is broken, and most of it is never read.
logging.yml:15filename:--setup, read by nothing. NodictConfig/fileConfigloads itpromtail-config.yml:40__path__promtail-config.yml:50__path__compliance.yaml:15threat_detection.yaml:28model_storage_pathml_models.*keys; persistence uses a path computed in codeThree supporting facts, each verified rather than assumed:
git grep expandvars -- '*.py'returns twoprose comments and no implementation — so any
${...}reaching a Python consumer would be aliteral. That is what makes "which consumer?" the whole question.
-config.expand-env=true. So the orphaned copy is not merelyunused — if anything were ever pointed at it, both lines would break.
logging.yml's generator emits absolute paths. The checked-in placeholder cannot have comefrom the writer; it is a hand-edit to a generated artifact that the next
--setupclobbers.Why this closes #13149 rather than deferring it: the criterion asked to confirm per consumer
rather than bulk-editing, and that is done, per occurrence, with consumers named. The inertness is a
genuinely new finding — a config that reads as if it configures something and configures nothing —
and it is tracked as #15044 with the acting-on-it criteria, together with nine further
occurrences in Ansible playbooks that #13149's criterion did not name and which deserve the same
treatment. If you read "audited" as requiring those to be resolved here rather than tracked, this is
one label change from
Refsand I will not argue it.#13357 —
PROJECT_ROOTescapes a worktree → Closes__file__, not a marker walkPath(__file__).resolve(), checkout root a hard boundary.gitas file or dir; fail loudlyCHECKOUT_MARKERSusesexists();ProjectRootUndeterminablereplaces the guessPROJECT_ROOTresolves inside the tree containing the moduleTestRealGitWorktree— real worktree, real subprocess, mutation-proven. This is the criterion the pre-existing fixture test did not satisfyPATH.*also escaped the worktreePathConstants.PROJECT_ROOTnow canonical#14113 —
PathsManagerreads a key that does not exist → Closesget_data_path()andssot_config.path.data_pathagree, or one is removedtest_both_resolvers_agreeAUTOBOT_DATA_DIRdemonstrably moves the secrets storetest_data_dir_override_moves_the_resolved_pathassertsget_data_path("secrets.db")lands under the override. Both secrets classes already resolve viaconfig.path.data_path(#14081)test_no_accessor_returns_a_cwd_relative_pathsweeps all 6 accessors undermonkeypatch.chdir, floor asserted first. All 6except-path fallbacks converted tooget_data_path()caller audited and listedAUTOBOT_DATA_DIRset, the secrets store path resolves under it#14892 —
PATH.CONFIG_DIRnames a directory that does not exist → Closestest_config_dir_exists_in_a_checkoutwould have failed on the original value the day it was writtenconfig_source/config_searched_pathand WARN naming the path;error_catalogalready was. The 9th (service_discovery) needs aservices.jsonschema that does not exist — filed as #15038#14070 — audit-log default hand-copied; guard is text-based → Closes
_AUDIT_LOG_FILE_DEFAULTremoved; both sites calldefault_audit_log_file()TestAuditLogFileDerivesFromOneDefinitionredirects the canonical formula and asserts both the SSOT field andsecurity_layer._AUDIT_LOG_FILEmove. Two hand-copied formulas cannot pass. Required making the factory late-bound — bound directly, the field would not have moved and "derivation" would have stayed unprovabledefault_factory=lambda:bodyssot_config.py_resolve_audit_log_file'sif not valuebranch is unreachableAUTOBOT_AUDIT_LOG_FILE=""makes the field falsy. Kept and covered bytest_the_empty_env_fallback_also_derives_from_it, rather than removed as the issue suggestedCloses #13149, #13357, #14113, #14892, #14070
Refs #15038, #15039, #15044
Model Used
Claude Opus 5 (1M context)