Repository navigation
tech-debt(infrastructure): no canonical project-root resolution — 60 unexpanded Python literals + 34 shell scripts defaulting to the live install #13149
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Jul 31, 2026 Related: #13151. Same shape — a documented rule with no gate decaying after a correctly-scoped fix. #3391 (encoding), #4945 (
sys.path) and #13092 (run_agent.sh) each fixed their stated scope and each lacked a guard, so the pattern reappeared elsewhere. Both issues propose an AST-based pre-commit hook following the existingno-utcnow-isoformat/no-literal-ttl-secondsidiom.Concrete data point from #13551 (PR forthcoming), for Task 1 of the decomposition above.
There are two hand-rolled Python project-root accessors, neither of which is one of the 60
${AUTOBOT_PROJECT_ROOT:-…}string literals this issue enumerates — sogit grep 'AUTOBOT_PROJECT_ROOT:-' -- '*.py'returning 0 would not have caught either:accessor fallback when the marker walk finds nothing autobot_shared/ssot_config.py::_find_project_rootPath(os.environ.get("AUTOBOT_BASE_DIR", "/opt/autobot"))autobot-backend/pki/config.py::_find_project_rootPath(config.base_dir)— same default, reached via configBoth walk upward for a
.env, which is git-ignored. So in any checkout without one — a fresh clone, a container build, every CI job — the walk finds nothing and the root resolves to the live install. That is Class B of this issue ("defaults to the live install"), expressed in Python rather than shell, in the module every other module imports its configuration from:ssot_configthen feeds that path to ~20env_file=str(PROJECT_ROOT / ".env")declarations.Measured on a CI runner:
PROJECT_ROOTresolved toPosixPath('/opt/autobot'), andssot_config_test.py::TestProjectRoot::test_project_root_existsfailed simply because the directory is not there.#13551 fixes the
ssot_configaccessor only — the.envwalk is preserved unchanged (deployments place.envabove the code directory, so bounding it is a separate call), and a checkout-root fallback is inserted ahead of the live-install default.pki/config.pystill carries the duplicate and is untouched, because changing it moves PKI certificate path resolution — that belongs to Task 1's consolidation here, not to a test-failure batch.Also relevant: #13357 covers the other half of the same accessor — the walk is unbounded, so from inside a git worktree it escapes into the neighbouring main checkout's
.env. #13551 does not change that behaviour.The fallback resolves to the live install, confirmed on a real machine
Owner confirmed AutoBot is installed at
/opt/autobot. That makes thessot_configfallback concrete rather than theoretical:# autobot_shared/ssot_config.py::_find_project_root for parent in [current] + list(current.parents): if (parent / ".env").exists(): return parent # Fallback to runtime location (env var or /opt/autobot) return Path(os.environ.get("AUTOBOT_BASE_DIR", "/opt/autobot"))
Verified present on a developer box:
/opt/autobot, ownerautobot, 31 entries.So on any machine where AutoBot is installed and no
.envis reachable,PROJECT_ROOTsilently becomes the live install directory.CLAUDE.mdstates "Codebase is source of truth: never edit/opt/autobot/" — but the resolver can makePROJECT_ROOTbe/opt/autobot, and 181 sites read or derive from it. Nothing in the resolution announces which branch it took..envis git-ignored, which is what makes the fallback reachable at all — a fresh clone or CI job never has one.Three different outcomes for the same code, which is why this was confusing
context branch taken result main checkout .envwalk finds the repo's own.envcorrect, legitimately a git worktree walk escapes upward to the main checkout's .env(#13357)accidentally correct CI runner no .envanywhere → fallback/opt/autobot, which does not exist →test_project_root_existsfailsI want to correct something I relayed earlier: I described the local pass as being caused by the worktree escape. That is true in a worktree; in the main checkout the walk legitimately finds the repo's own
.env. Both paths mask the CI failure, but they are different mechanisms and only the worktree one is #13357.Seven distinct project-root definitions, not one
autobot_shared/ssot_config.py _find_project_root() .env walk, /opt/autobot fallback autobot-backend/pki/config.py _find_project_root() duplicate of the above autobot-backend/pki/cli.py parents[2] autobot-backend/utils/template_loader parent.parent.parent autobot-backend/utils/hot_reload_mgr _SRC_ROOT.parent autobot-backend/api/codebase_analytics/storage.py parent x4 autobot-backend/conversation_file_manager.py:46 Path(__file__).parentThat last one is worth singling out:
Path(__file__).parentisautobot-backend/, not a project root at all — andconversation_file_manager.py:127-128derivesdata/conversation_filesandconversation_files.dbfrom it. So that data lands somewhere different from every other definition's idea of the root.This is the concrete shape of what this issue describes. Related: #13572 (19 OpenAPI defaults publish absolute paths derived from
PROJECT_ROOT, which blocks correcting the resolver), #13357 (the unbounded.envwalk), and #13551 (whosetest_project_root_existsbox stays open on exactly this).- added a commit that references this issue
on Aug 5, 2026 Ran a full-repo hunt for hardcoded absolute paths. This issue's two classes are accurate but cover
roughly a tenth of the/opt/autobotliterals in the repo. The rest carry no placeholder at all, so
no search forAUTOBOT_PROJECT_ROOTcan find them.Full audit:
docs/audit/hardcoded_absolute_paths_2026_08_05.mdCounts
2278 raw absolute-path literals, whole working tree 1635 tracked, non-generated files 416 defect candidates after triage 163 /opt/autobot literals in non-test source 14 carry ${AUTOBOT_PROJECT_ROOT:-...} <- this issue's Class A + B 149 plain literal, no placeholder <- uncovered 17 comments / docstring prose 132 active code 82 shipped runtime (excl. scripts/, debug/, *.stories.*, install.sh)Triage that produced 416 from 1635 — the excluded categories are correct as literals, not defects:
docs 461, test fixtures 302, ansible/template/unit deploy targets 258, system binaries 159, system
files (/etc/os-release,/proc) 39.Also ruled out:
storybook-static/contains 580/home/martins/...literals, but it is gitignored
andgit ls-filesreports 0 tracked files there. Local build output, not a repo problem — recorded so
a later scan does not re-raise it.The uncovered class
Bare hardcoded paths, by kind (132 active-code):
python 57 shell 42 ts 19 vue 7 yaml 4 json 3By component:
autobot-slm-backend55,autobot-infrastructure39,autobot-frontend19,
autobot-backend11,autobot_shared11,autobot-slm-frontend10.Runtime defaults — these decide where real data is written
Module-level constants and default arguments, in effect on import in the shipped services:
autobot-backend/security_layer.py:32 _AUDIT_LOG_FILE_DEFAULT = "/opt/autobot/logs/audit.log" autobot-backend/api/codebase_analytics/source_paths.py:7 CODE_SOURCES_BASE: Path = Path("/opt/autobot/data/code-sources") autobot-backend/services/execution/snapshot_index.py:25 _DEFAULT_SNAPSHOT_PATH = "/opt/autobot/snapshots" autobot-backend/services/autoresearch/config.py:26 default_factory=lambda: Path(config.misc.autoresearch_dir or "/opt/autobot/autoresearch") autobot-backend/services/autoresearch/config.py:77 default_factory=lambda: Path(config.misc.autoresearch_data_dir or "/opt/autobot/autoresearch/data") autobot-backend/cli/doctor.py:113 def check_env_file(env_path: str = "/opt/autobot/autobot-backend/.env") -> CheckResult:Two things stand out. The
or "/opt/..."fallbacks inautoresearch/config.pyfire whenever the
config key is unset or blank — the same blank-value-defeats-the-default shape as #12782 and
#13220. Andcli/doctor.pyis worse than a bad default: the diagnostic reads the live install's
.envregardless of which checkout invoked it, so it reports on a system the developer is not
running.The frontend is not mentioned in this issue and has the same gap
autobot-frontend/src/composables/analytics/useSourceRegistry.ts:108 const rootPath = ref(savedPath || '/opt/autobot') autobot-frontend/src/composables/analytics/useCodeIntelScores.ts:212 rootPath.value === '/opt/autobot' || autobot-frontend/src/views/EvolutionView.vue:288 placeholder="/opt/autobot" autobot-frontend/src/views/EvolutionView.vue:387 repo_path: '/opt/autobot',useCodeIntelScores.ts:212branches on the literal, so behaviour depends on a path string the
deployment may not use.A resolver already exists
autobot_shared/ssot_config.py:67—PROJECT_ROOT = _find_project_root(). All 132 bypass it. Note
thatssot_config.pyitself holds 7 absolute literals, so the module defining the canonical root is
among the offenders.Highest-density files
[24] autobot-infrastructure/shared/scripts/bulletproof-frontend/zero-downtime-update.sh [14] autobot-slm-backend/api/code_sync.py [ 9] autobot-infrastructure/shared/scripts/utilities/sync-all-vms.sh [ 8] autobot-infrastructure/shared/scripts/utilities/complete-vm-sync-templates.sh [ 8] install.sh [ 7] autobot_shared/ssot_config.pyDeliberately not asserted as defects
Infrastructure templates that name the install they render into
(browser-start.sh:11,browser-status.sh:46,frontend-status.sh:55,ollama-start.sh:30) may be
correct by design. The deciding question is whether the install prefix is ever configurable. Listed
for a decision rather than counted as broken.Other prefixes, recorded so they are not lost
/tmp— 78 in non-test code: fixed-name temp paths, a collision risk and a symlink-attack
surface for predictable names./tmpsemantics here are already non-obvious — CRITICAL regression(slm): self-update fails in ~1s every run after #12775 — caller's finally unlinks the temp inventory/extra-vars out from under the now-detached playbook; API still returns success:true and status reports has_update:false #12803 turned out to
bePrivateTmp=yesnamespace isolation./home— 68, mostly/home/autobot(service account, arguably correct), plus/home/kali
and/home/alicein docs and a few.tsfixtures./etc57 //var43 outside ansible — application code reaching into system-owned locations.
Suggested scope change
- Widen this issue to the plain-literal class, or split it out — either way it needs to be findable,
because greppingAUTOBOT_PROJECT_ROOTmisses 149 of 163. - Fix the six runtime defaults first: small, and they govern audit logs and snapshots.
- Add a guard test enumerating
/opt/autobotliterals in tracked non-test source against an
explicit allowlist — same shape asrepo_tests/audio_extension_allowlist_test.pyfrom tech-debt(transcriber): allowed-audio-extension set duplicated across three modules, one of them a security boundary #13512.
bug(infrastructure): 99 scripts use sys.path.append("${AUTOBOT_PROJECT_ROOT...}") — Python never expands shell variable syntax #4945 was correctly scoped tosys.pathand still left siblings nothing detected; without a guard
this regrows.
Task 1 in PR #13652. Two corrections to this issue's plan, both from auditing before building.
1. The proposed accessor would have been a duplicate.
ssot_config.PROJECT_ROOTalready resolves the root correctly — #13646 landed the checkout step. A newautobot_shared.paths.project_root()beside it would have been a second implementation of the very thing this issue is about. Instead the logic moved into a stdlib-onlyautobot_shared/paths.pyandssot_configdelegates to it.2. There were four implementations, not one. Beyond
ssot_config:Site Defect autobot-backend/pki/config.pyown _find_project_root(), no checkout step — from a source tree with no.envit pointed PKI's certificate paths at the deployed installautobot_shared/env_drift_detector.pyhand-copied walk, commented "Mirror ssot_config._find_project_root() logic", drifted the same way autobot-infrastructure/shared/scripts/run_code_analysis.pya third form, Path(__file__).parent.parent(a localsys.pathbootstrap — left to the migration PR)The first two are consolidated in #13652.
A live bug this exposed
Implementing the resolver exactly as proposed here — env var, then a
.envwalk, then a checkout-marker walk — reproduces a defect present on the base branch right now:project_root() from .worktrees/issue-13149 -> <main tree> # wrongWorktrees are git-ignored and carry no
.env, so the.envpass climbs past the worktree root and matches the main tree's.env. Every worktree resolved its project root to a different tree — and this repository's whole workflow runs from worktrees. Fixed by making it one walk with a checkout root as a hard boundary. CI is unaffected (it checks out directly and has no.enveither way); local worktree runs were wrong and now are not.Count corrections
- Class A: 57 occurrences in 41 files, not 60 in 40. Two of the 57 are prose (a comment in
mcp_security_test.py, a docstring inwebhook_authentication_security_test.py) — not defects, and the Task 1 gate has to tell them apart from real path literals. - Class B: 36 shell files, not 34.
Remaining
Tasks 2-5 are unstarted. The gate is deliberately held back to ship with the 57 migrations, since it cannot pass until they land. This issue stays open until then.
- Class A: 57 occurrences in 41 files, not 60 in 40. Two of the 57 are prose (a comment in
- added a commit that references this issue
on Aug 5, 2026 Task 2 is blocked on a decision, not on effort
The 28 Class-A sites under
autobot-infrastructure/cannot takefrom autobot_shared.paths import project_rootas they stand.autobot_sharedis not importable from them:$ python3 autobot-infrastructure/shared/scripts/<script>.py NOT importable; sys.path[0] = <repo>/autobot-infrastructure/shared/scriptsautobot_sharedis not pip-installed, and forpython3 path/to/script.pyPython puts the script's own directory onsys.path, never the working directory. Adding the import to these 28 files would turn a wrong-path bug into an immediateModuleNotFoundError— strictly worse than what is there now.The circularity: the usual fix is a
sys.pathbootstrap ahead of the import, but the bootstrap has to locate the repo root to do it — which is the exact computation this issue exists to centralise. A bootstrap ofparents[3]is one more hardcoded root derivation, just spelled differently.Three ways out, and picking between them is an owner call:
Option Cost Notes A. Make the repo importable — pip install -e .in the venvs, or a.pthone-time, touches env setup scripts then just import; no per-file bootstrap. Cleanest, but changes how environments are provisioned B. A 3-line marker-search bootstrap in each of the 28 28 files, mechanical duplicates a minimal walk 28 times; the duplication is 3 lines instead of a path literal, but it is still duplication C. Leave these 28 and scope the gate to importable trees none now honest, but git grepnever reaches 0 and this issue's acceptance is not metI have not guessed. Nothing under
autobot-infrastructure/has been touched.What did land
autobot-backend(7 files, 9 occurrences) migrated in branchissue-13149-migration, stacked on #13652:occurrences: 57 -> 50 python3 -m pytest -q autobot_shared/paths_test.py autobot_shared/ssot_config_test.py \ autobot-backend/pki/ autobot-backend/monitoring/monitoring_and_alerts_test.py 60 passedA second correction: the sites are not one kind
Treating all 57 uniformly is wrong. They fall into three groups:
- Genuine defects —
Path("${…}/tests/results").mkdir(), which creates or misses a directory literally named${AUTOBOT_PROJECT_ROOT:-. This is what the issue describes, and what I fixed. - Inert fixture strings —
autobot-slm-backend/api/code_source_test.pypasses the literal as a mock return value and then asserts it appears in an error message;tests/services/git_tracker_test.pyuses it as a fakerepo_path. The value is arbitrary there. Substituting the real project root is churn and mildly weakens the test, whose point is "a path that does not exist". These want an obviously-fake constant instead. - Prose — a comment in
autobot-backend/mcp/mcp_security_test.pyand a docstring inapi/webhook_authentication_security_test.pythat describe the pattern. Not defects.
Group 3 is why the Task 1 gate must be AST-based, not a
git grep. A grep gate would force the prose to be reworded to satisfy a checker, and would also fire on this issue's own acceptance criterion.One more discovery
autobot-backend/advanced_rag_optimizer_rerank_test.pydefines_WORKTREEpointing at.worktrees/issue-2034/autobot-backendand never references it. Residue of finished work. I migrated it rather than removing it; whether it should simply go is an owner call.Also outside the stated classes
The placeholder additionally appears in
autobot-backend/code_analysis/auto-tools/results/*.json(generated analysis output) and in severalautobot-slm-backend/ansible/playbooks/*.yml. The JSON files are stale generated artefacts — regenerating them is the fix, not editing them. Both belong with Task 5.- Genuine defects —
- added a commit that references this issue
on Aug 5, 2026 23 remaining items
Tasks 1, 3, 4 and 5 have landed
PR #13652 (
a4e3bd465) — Task 1, the canonical accessor.
PR #13659 (81950bdf2) — Tasks 3, 4 and 5.Verified on the merged base:
scripts/lib/project_root.sh present yes .sh files sourcing the helper 37 .sh files with the live-install default 1 (project_root.sh's own doc comment) .py files with the placeholder 33Class B is complete: every shell script that defaulted to
/opt/autobot/code_sourcenow resolves from its own location. The destructive ones went first —rotate_logs.shwas gzipping and-deleteing the live install's logs when run from any checkout, which is the #13092 failure class repeating.Task 1 also fixed a live bug found while writing it: the two-pass walk (
.envacross all ancestors, then markers across all ancestors) escaped every worktree and resolved to the main tree's.env. Since this repository's whole workflow runs from worktrees, each one's config, log and results paths pointed at another tree. A single walk with a checkout root as a hard boundary fixes it, pinned in both directions.What remains
Task 2 — the 28
autobot-infrastructure/scripts — is still blocked on a decision, not on effort. They cannot takefrom autobot_shared.paths import project_root:$ python3 autobot-infrastructure/shared/scripts/<script>.py NOT importable; sys.path[0] = <repo>/autobot-infrastructure/shared/scriptsautobot_sharedis not pip-installed, and Python puts the script's directory onsys.path, never the cwd. Adding the import to all 28 converts a wrong-path bug into an immediateModuleNotFoundError. The obvioussys.pathbootstrap is circular — it must locate the repo root before it can import the thing that locates the repo root.Three options were costed earlier in this thread (make the repo importable; a 3-line marker-search bootstrap per script; scope the gate to importable trees). None chosen, nothing touched.
The AST gate ships with Task 2, since it cannot pass while those 28 remain. Two of the 33 remaining Python occurrences are prose — a comment and a docstring — which is why the gate must parse rather than grep.
Discovered along the way
Six distinct ad-hoc root derivations, beyond the two classes this issue describes:
ssot_config,pki/config.py,env_drift_detector.py,run_code_analysis.py,log_aggregator.py:127(wrong for its own location), andapi/logs.py:55. The first three are consolidated by #13652.Task 5's per-consumer audit is at
docs/audit/project_root_yaml_json_audit.md. Its one confirmed bug — compliance audit logs written to a literal${AUTOBOT_PROJECT_ROOT:-directory — is #13658, fixed and closed.The numbers in this issue are stale — most of it has already shipped
Verified against
Dev_new_guitip5dfe77607(tracked files only, viagit grep). Recording this because anyone picking the issue up from its current text would redo finished work.claim reality 34 shell scripts default to the live install 0 remain — 37/37 now source scripts/lib/project_root.sh. The only string match left is that helper's own doc comment.60 unexpanded Python literals 34 files still match the pattern, but only 28 files / ~40 occurrences are real defects, all under autobot-infrastructure/shared/The other 6 Python matches are intentional and should not be "fixed":
autobot_shared/paths.py(the resolver itself),compliance_manager.py, two test-prose files, a regression fixture and a guard-test fixture.The canonical mechanism exists and is sound
autobot_shared/paths.py::project_root()(stdlib-only) andscripts/lib/project_root.sh, cross-verified consistent. The duplicate resolvers inpki/config.pyandenv_drift_detector.pywere consolidated in PR #13652. So this is no longer "build a canonical source" — it is "finish migrating onto the one that exists".What is actually dangerous
Only 6 files / 7 sites default to
/opt/autobot, i.e. can read or write live-install state from a dev run:security_layer.py,source_paths.py,snapshot_index.py,autoresearch/config.py(×2),cli/doctor.pysync-user-backend.yml(Ansible)
The 28-file infra remnant is untidy, not dangerous — those create local junk paths rather than touching
/opt/autobot.Decomposition into dispatchable slices
Non-overlapping in files, ordered by risk:
slice scope notes A the 6 dangerous Python runtime defaults highest risk, verifiable without a host — do first B sync-user-backend.ymlstructural fix is host-free; behavioural proof needs a real deploy C1 fix the broken editable install (see below) small, packaging-only, unblocks C2 C2 the 28 infra scripts blocked on the import-path decision since 2026-08-05 D 4 frontend literals independent, parallelisable with everything E cleanup capstone + AST pre-commit gate must land last — the gate cannot pass CI while C2's files remain New finding, filed separately
The
autobot_sharededitable install already exists in the venv but is silently broken:import autobot_sharedfails from every directory including the repo root, becauseautobot_shared/pyproject.toml's package discovery registers only the submodules, never the top-level package.That matters here because it is exactly the mechanism the blocked "Option A" import-path decision was weighing. Fixing it is cheap and reframes that decision rather than answering it in the abstract.
Slices A and C1 are being dispatched now.
github-actions commented
on Aug 11, 2026 on Aug 11, 2026 – with GitHub ActionsContributorMore actions- added a commit that references this issue
on Aug 18, 2026 - added a commit that references this issue
on Aug 25, 2026 github-actions commented
on Aug 25, 2026 on Aug 25, 2026 – with GitHub ActionsContributorMore actionsPR #15040 (merged to
Dev_new_gui) references this issue with a close keyword.fix(config): one canonical project root, and no path that resolves to the wrong thing silently (#13149, #13357, #14113, #14892, #14070)
If this issue is fully resolved, close it manually. If work remains, no action is needed.
Closure verification — merged base
5633e418c(PR #15040)Verified against
origin/Dev_new_gui(the merged tree), not the PR branch.Criterion file:lineevidenceVerdict git grep 'AUTOBOT_PROJECT_ROOT:-' -- '*.py'returns 0Grep returns 29 hits / 17 files. Every one is prose (comment/docstring documenting this defect) or a derived, re-proved exemption: autobot-backend/security/enterprise/compliance_manager.py:88(_LEGACY_AUDIT_ROOT— must retain the literal to find the junk tree the bug created, or up to 2555 days of encrypted audit records are orphaned, #13658),repo_tests/compliance_audit_path_test.py:101,scripts/check_ansible_file_references_test.py:79. All three are registered with an anchor symbol and a site count intools/lint/check_no_shell_placeholder_paths.py:125-149, re-checked on every run so the file cannot be rewritten around the exemption and a fresh defect cannot inherit its pass. Zero live path-resolution sites remain.Met on substance — a literal zero would require deleting the bug's own documentation and the migration that depends on the literal Shell scripts resolve their root from a shared helper; none embeds the live-install path as a default scripts/lib/project_root.shis sourced by 38 scripts. Spot-checked every risky script the issue named — the two ansible deploy scripts,cleanup-disk-space.sh,rotate_logs.sh,bulletproof-frontend/zero-downtime-update.sh,scripts/service-auth/emergency-rollback.sh,sync-frontend.sh— all source it. The only surviving:-live-install literal in any*.shisscripts/lib/project_root.sh:11, inside the helper's own docstring showing the anti-pattern.run_agent.sh:25defaults toBASH_SOURCE, not the install.Met A gate rejects reintroduction tools/lint/check_no_shell_placeholder_paths.py, run in CI at.github/workflows/code-quality.yml:496(--audit). It now reassembles f-strings viaast.unparse(:184-214), closing the hole that hid a live offender from the previousast.Constant-only walk. Companion gatetools/lint/check_no_live_install_sys_path_default.pyis wired atcode-quality.yml:222.Met Config/YAML occurrences audited per consumer, not bulk-edited Four files / five occurrences traced to their consumer in PR #15040 ( logging.yml:15,promtail-config.yml:40,50,compliance.yaml:15,threat_detection.yaml:28). Result: none is broken; four are inert (no reader) and one is inside a comment block. Supported bygit grep expandvars -- '*.py'returning no implementation, so no YAML loader expands${...}anywhere.Met — the criterion's verb is audit and confirm per consumer, and that is what was delivered Root cause closed:
autobot_shared/paths.py::project_root()is the single resolver;autobot_shared/ssot_config.py:63takes it, andautobot_shared/ssot_constants.py:394now readsPROJECT_ROOT: Path = CANONICAL_PROJECT_ROOTinstead of the second open-codedPath(__file__).parent.parent. Two competing Python root resolvers became one.Not blocking: the inertness finding is genuinely new — a config that reads as if it configures something and configures nothing — and is tracked with acting-on-it criteria as #15044, together with nine further Ansible-playbook occurrences the criterion did not name. That is the correct handling of a new finding, not an unmet criterion of this issue.
No sub-issues are linked to this issue (
sub_issuesAPI returns empty); the "Suggested decomposition" section was never filed as children, so the Acceptance list above is the binding set.Closing.
Discovered while fixing #13092 (
run_agent.shtargeting the live install). That fix repaired one script; the same pattern is spread across the repo in two distinct failure modes, and there is no canonical way to resolve the project root in either language.Class A — Python: the placeholder is inside a string literal and never expands (60 occurrences, 40 files)
Python does not expand shell syntax inside string literals:
The path is a literal directory name. Reads miss, and
mkdireither fails on the missing parent or creates a junk tree named${AUTOBOT_PROJECT_ROOT:-. Nothing in the repo callsos.path.expandvars— verified:git grep expandvars -- '*.py'returns nothing.This is the same defect as #4945, in call sites that issue did not cover. #4945 was scoped to
sys.path.append/insertand is genuinely complete —git grep 'sys\.path\.\(append\|insert\).*AUTOBOT_PROJECT_ROOT:-'returns 0. The remaining 60 arePath(...), log paths, config paths, and.envlookups that were never in scope. Not a premature closure — an uncovered sibling.22 of the 40 files are non-test tooling, e.g.:
autobot-infrastructure/shared/scripts/populate_knowledge_base.pyautobot-infrastructure/shared/scripts/run_code_analysis.pyautobot-infrastructure/shared/scripts/analyze_duplicates.pyautobot-infrastructure/shared/scripts/ai-ml/optimize_llm_models.pyautobot-infrastructure/shared/scripts/utilities/migrate_consolidated_config.pyClass B — Shell: expands correctly, but defaults to the live install (35 scripts)
BASE_DIR="${AUTOBOT_PROJECT_ROOT:-/opt/autobot/code_source}"Here the expansion works, so the failure is subtler: with no env set, a script run from a checkout or a git worktree operates on
/opt/autobot/instead of its own tree. That is what #13092 was —run_agent.shrm -rf'd the live install's frontendnode_modulesfrom any checkout.run_agent.shis fixed; 34 other scripts still carry it, including:autobot-slm-backend/ansible/deploy-native.sh,deploy-hybrid.shautobot-infrastructure/shared/scripts/cleanup-disk-space.shautobot-infrastructure/shared/scripts/rotate_logs.shautobot-infrastructure/shared/scripts/bulletproof-frontend/zero-downtime-update.shscripts/service-auth/emergency-rollback.shsync-frontend.shScripts that delete or rotate files are the risk here, for the same reason #13092 was.
Root cause: no canonical project-root resolution
Both classes exist because each call site re-invents root resolution by pasting a literal. There is no shared helper in either language, so a fix applied in one place (as in #13092) does not propagate.
Proposed consolidation
scripts/lib/project_root.shexportingPROJECT_ROOT, resolved fromBASH_SOURCEwithAUTOBOT_PROJECT_ROOTtaking precedence — the pattern merged in fix(scripts): resolve run_agent.sh paths from the script, not the live install (#13092) #13093. Scriptssourceit instead of pasting the default.autobot_shared(e.g.autobot_shared.paths.project_root()) readingAUTOBOT_PROJECT_ROOTand falling back to a repo-root walk from__file__. Replace all 60 literals with it.${AUTOBOT_PROJECT_ROOT:-inside a Python string literal — the mechanism that let bug(infrastructure): 99 scripts use sys.path.append("${AUTOBOT_PROJECT_ROOT...}") — Python never expands shell variable syntax #4945 regress into 60 new sites.Step 3 matters most: without it this recurs, which is exactly what happened between #4945 and now.
Acceptance
git grep 'AUTOBOT_PROJECT_ROOT:-' -- '*.py'returns 0.logging.yml,promtail-config.yml,compliance.yaml,threat_detection.yaml) audited separately — those may legitimately rely on the consuming process expanding env vars; confirm per consumer rather than bulk-editing.Suggested decomposition
autobot_shared+ CI gate