Repository navigation
bug(infra): 32 unexpanded ${AUTOBOT_PROJECT_ROOT} placeholders in plain Python string constants, invisible to every flake8 code #14517
Description
Activity
Swept and guarded in #14545 (branch
issue-14517).Measured count reconciles exactly with the issue: an AST sweep found 36
ast.Constantstring hits, minus the deliberatecompliance_manager.pyliteral (#13658) and the 3 fixture sites = 32 defects across 25 files — 26 undershared/scripts/(21 files), 6 undershared/tests/. 6 further hits are docstrings, excluded structurally rather than by allowlist.Guard:
tools/lint/check_no_shell_placeholder_paths.py, wired intocode-qualitywith--audit, no baseline. It fails against the pre-fix tree (32 findings / 25 files), passes on the fixed tree, and fails on an empty enumeration rather than reading clean. The three deliberate sites are re-proved every run by path, anchor symbol and exact count, so a rename cannot strand an exemption.Two defects the repaired paths made visible, filed rather than absorbed:
- bug(infra): run_code_analysis.py shells out to five scripts under tools/code-analysis-suite/, a directory that exists nowhere in the repo #14543 —
run_code_analysis.pyshells out to five scripts under a directory that exists nowhere in the repo - tech-debt(infrastructure): 18 sys.path bootstraps open-code the project root and default to the live install instead of the checkout #14544 — 18
sys.pathbootstraps open-code the root and default to the live install instead of the checkout
The
knowledge_baseimport failures in several of these scripts are already covered by #14518 and were not re-filed.- bug(infra): run_code_analysis.py shells out to five scripts under tools/code-analysis-suite/, a directory that exists nowhere in the repo #14543 —
- added a commit that references this issue
on Aug 18, 2026 Closed by PR #14545, squash-merged as
d4b64475eb95and verified inorigin/Dev_new_gui. The guard is wired intocode-quality.32 defects across 25 files, all resolved through
autobot_shared.paths.project_root(). The count was re-derived by an independent AST sweep sharing no code with the shipped guard: 36 total non-docstring hits across 28 files, minus 4 exemptions. Both halves of the split reconcile — 26 sites / 21 files undershared/scripts/, 6 / 4 undershared/tests/. The 6 docstring hits were read individually and are prose, includingautobot_shared/paths.py's own docstring about this defect class.What the fixes made reachable — the part worth reading
These were never "the path was cosmetically wrong". Each named a directory that cannot exist, so the code failed early and reported success:
init_memory_graph_redis.py— theFileHandlerran at import, so the entire module was dead.- Three
shared/tests/suites —mkdir(exist_ok=True)would now raiseFileNotFoundError, since the resolved parent genuinely does not exist where the junk-tree parent did. All takeparents=True. ci_cd_integration.py— a badcwd=makessubprocess.runraise before the stage starts, which theexceptreported as an ordinary stage failure.- 9 KB/doc ingest scripts — every glob matched nothing and reported "Found 0 files" as success.
Two structural catches beyond the sweep
11 sites assigned to a local named
project_root, which would have converted a silent empty result into anUnboundLocalError— renamed toroot. Proved rather than assumed.run_code_analysis.pyalready bound a module-levelproject_roottoautobot-infrastructure/shared— a different directory. It read as the canonical resolver and was not. Renamed to_SHARED_DIRwith its value byte-identical and all 6 readers updated. A name that looks like the thing everyone else means is worse than an obviously broken one.The guard, and why its exemption mechanism matters
No baseline — every defect fixed rather than grandfathered. Discrimination verified eleven ways, each executed: pre-fix FAIL (32/25), post-fix PASS, empty tree FAIL on a floor of 4000, an unparseable file fails rather than being skipped, and all four exemption-decay modes fail — exempt file moved, its anchor renamed, the entry gone dormant, and an exempt file growing a second literal that would otherwise inherit the exemption.
That last set is what makes an exemption list a mechanism rather than a liability. This repo has been bitten by the opposite: a permission table holding a key for a renamed tool left the real tool undeclared at read level (#14494).
The module self-guards by assembling its needle from fragments, so it needs no entry on its own list. And it is unaffected both when run from inside a path named
.worktreesand when one is nested in the scanned tree — worth noting, because the author's own first sweep returned 0 hits for exactly that reason, and reported it rather than quietly fixing it. An empty enumeration reading as a clean tree is the failure this guard exists to refuse (#14484).Filed rather than absorbed
- bug(infra): run_code_analysis.py shells out to five scripts under tools/code-analysis-suite/, a directory that exists nowhere in the repo #14543 — five scripts shell out to
tools/code-analysis-suite/, a directory existing nowhere; all five return "Script not found" andmain()prints that as a completed run. - tech-debt(infrastructure): 18 sys.path bootstraps open-code the project root and default to the live install instead of the checkout #14544 — 18
sys.pathbootstraps default to/opt/autobot/code_sourcewhen the env var is unset, so a checkout silently imports the live install. Re-labelled from tech-debt to bug: a change under test can appear to work when it was never loaded. Not a substitution — those lines run beforeautobot_sharedis importable — so it needs its own design. - safety(infra): optimize_agents.py now rewrites .claude/agents/*.md in place with no --dry-run, after the path fix made it reachable #14546 —
optimize_agents.pynow genuinely rewrites.claude/agents/*.mdin place with no--dry-run. A no-op against today's content, but the transition from "always errors" to "edits tracked files" happened with no signal at the call site.
- added a commit that references this issue
on Sep 3, 2026
Problem
Found while fixing #14507, which named one site of this defect class. A sweep of the tree found 32 more.
A shell placeholder pasted into a plain Python string constant is never expanded — Python has no shell interpolation in string literals — so the value resolves to a literal directory name starting with
${AUTOBOT_PROJECT_ROOT:-. Reads silently miss, andmkdir(parents=True)creates a junk tree actually named after the expression. This is the defect classautobot_shared.paths.project_root()exists for (#13149).#14405 / PR #14504 could only see the f-string form of this, because that produces an F821 for the undefined
AUTOBOT_PROJECT_ROOTname. A plain literal is invisible to every flake8 code, so none of the sites below are reported by any lint that runs today, including the F821 audit added for this tree.Scope
AST sweep for
ast.Constantstring values containing${AUTOBOT_PROJECT_ROOT:autobot-infrastructure/shared/scripts/autobot-infrastructure/shared/tests/Deliberately excluded, not defects:
autobot-backend/security/enterprise/compliance_manager.py:87— the one deliberate literal, already guarded by bug(security): compliance audit logs and .audit_key are written to a literal ${AUTOBOT_PROJECT_ROOT:-...} directory relative to cwd #13658repo_tests/compliance_audit_path_test.pyandscripts/check_ansible_file_references_test.py— fixtures that quote the pattern on purposeA representative sample (full list reproducible with the AST sweep):
The log-file and results-directory sites are the harmful half: they are write targets, so they create the junk tree rather than merely failing to read.
Fix
Replace each with
autobot_shared.paths.project_root(), exactly as #14504 did at the reachable sites and #14507 did at the one plain-literal site it named. Do not open-codeos.environ.get.Do not add the guard first. A repo-wide lint banning the literal in Python string constants is the right end state, but it would need a baseline of 32 entries on day one, and this repo's experience with dormant exemption lists says a guard should land after the sites, not before it. Land the sweep, then the guard with no allowlist.
Related