Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion autobot-infrastructure/shared/config/logging.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# GENERATED ARTIFACT (#15044) -- do not hand-edit `filename` below.
#
# `autobot-infrastructure/shared/scripts/log_aggregator.py --setup`
# (`setup_centralized_logging`) opens this exact path in "w" mode and
# `yaml.dump`s a config it just built, in which `filename` is already an
# absolute, resolved `Path` (`self.logs_dir / "system.log"`) -- never a
# `${VAR}` token, because no YAML loader anywhere in this codebase expands
# one (`git grep expandvars -- '*.py'` finds no implementation). Nothing
# calls `logging.config.dictConfig` on this file either, so it configures no
# running service; the sole reader is
# `repo_tests/logging_formatter_path_test.py`, which checks that the
# `formatters.json.class` below still agrees with the class
# `log_aggregator.py` declares.
#
# A previous hand-edit put a literal
# "${AUTOBOT_PROJECT_ROOT:-/opt/autobot/code_source}/logs/system.log" in
# `filename`, which read as configurable and was not: no consumer expands
# it, and the next `--setup` run silently overwrites it with a resolved
# absolute path anyway. `filename` below is a relative placeholder instead,
# so it cannot be mistaken for a live, environment-driven value.
disable_existing_loggers: false
formatters:
json:
Expand All @@ -12,7 +32,7 @@ handlers:
file:
backupCount: 5
class: logging.handlers.RotatingFileHandler
filename: ${AUTOBOT_PROJECT_ROOT:-/opt/autobot/code_source}/logs/system.log
filename: logs/system.log
formatter: json
maxBytes: 10485760
root:
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ ml_models:
min_training_samples: 1000 # Minimum samples required for training
contamination_rate: 0.1 # Expected anomaly rate for Isolation Forest
enable_auto_tuning: true # Automatically tune model parameters

# Model persistence
model_storage_path: "${AUTOBOT_PROJECT_ROOT:-/opt/autobot/code_source}/data/security/models"
save_trained_models: true
load_existing_models: true
# #15044: `model_storage_path`, `save_trained_models` and
# `load_existing_models` used to live here, advertising trained-model
# persistence the engine does not implement -- `ThreatDetectionEngine`
# reads only `model_retrain_hours` and `min_training_samples` above.
# Profile persistence is a different, real feature, at
# `PATH.get_data_path("security", "user_profiles.json")` in
# `threat_detection/engine.py`, unrelated to this key.

# Detection Thresholds
thresholds:
Expand Down
14 changes: 11 additions & 3 deletions docs/audit/project_root_yaml_json_audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,18 @@ updater, not ad hoc. Marked for confirmation rather than assumed.

1. `compliance.yaml` — the only confirmed bug. Drop the placeholder so the
consumer's own `PATH.get_log_path("audit")` default applies, or resolve it
through `autobot_shared.paths.project_root()`. Tracked as #13658.
`threat_detection.yaml` and `logging.yml` need no fix: a dead key and a
regenerated artefact respectively.
through `autobot_shared.paths.project_root()`. Tracked as #13658, done.
2. `synchronize` playbooks — confirm on a live run before editing.
3. `sync-user-backend.yml` — keep the expansion, replace the live-install default.
4. `vue_*.json` — regenerate; editing generated output is pointless.
5. **Superseded by #15044.** `threat_detection.yaml`'s `model_storage_path`
(with siblings `save_trained_models`, `load_existing_models`) was removed
rather than left: the file advertised trained-model persistence the engine
never implemented, which is the same defect as a dead key one layer up.
`logging.yml` needed a fix after all — "no fix" undersold it. It is
regenerated by a live `--setup` path, so the checked-in copy is now
documented as a generated artefact with no `${…}` placeholder, rather than
deleted; see `repo_tests/no_inert_config_placeholder_15044_test.py`. The
orphaned `logging/promtail/promtail-config.yml` copy (never deployed —
Ansible renders its own template) is removed outright.
5. Promtail — leave unless `-config.expand-env=true` is absent from the unit.
1 change: 0 additions & 1 deletion pipeline-scripts/hardcoded_values_baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,6 @@
1|other|autobot-infrastructure/shared/config/deployment/distributed.yml|http://monitoring.autobot.prod:9090
1|other|autobot-infrastructure/shared/config/distributed.yaml|30
1|other|autobot-infrastructure/shared/config/logging/loki/grafana-datasources.yml|http://loki:3100
1|other|autobot-infrastructure/shared/config/logging/promtail/promtail-config.yml|/home/autobot
1|other|autobot-infrastructure/shared/config/npu_workers.example.yaml|300
1|other|autobot-infrastructure/shared/config/npu_workers.example.yaml|60
10|ssot|autobot-infrastructure/shared/config/prometheus/alertmanager.yml|:8001
Expand Down
127 changes: 127 additions & 0 deletions repo_tests/no_inert_config_placeholder_15044_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Copyright 2025-2026 mrveiss
# SPDX-License-Identifier: Apache-2.0
# AutoBot - AI-Powered Automation Platform
# Author: mrveiss
"""Three YAML configs stop carrying placeholders nothing ever reads (#15044).

#13149's YAML audit (PR #15040) found three configs where a
``${AUTOBOT_PROJECT_ROOT:-...}`` token read as a live, environment-driven
value and was not one:

* ``shared/config/logging.yml`` -- generated by
``log_aggregator.py --setup``, read by no running service, and the
checked-in copy's placeholder would be silently clobbered by the next
``--setup`` run anyway.
* ``shared/config/logging/promtail/promtail-config.yml`` -- an orphaned
copy; the deployed unit uses a distinct file rendered by an Ansible
template (``roles/centralized_logging/templates/promtail-config.yml.j2``),
never this one, and passes no ``-config.expand-env=true``.
* ``shared/config/security/threat_detection.yaml``'s ``model_storage_path``
(plus its siblings ``save_trained_models`` / ``load_existing_models``) --
``ThreatDetectionEngine`` never reads any of the three; trained-model
persistence is not implemented.

None of these were ever expanded by anything -- ``git grep expandvars --
'*.py'`` returns no implementation anywhere in this codebase. These
assertions are on the *resolved* YAML values and the resolved filesystem
state, not on grepping for the string "AUTOBOT_PROJECT_ROOT" -- a rename of
the placeholder would still leave the underlying defect (an unread,
unexpanded path) undetected by a name-grep.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

import pytest
import yaml

from autobot_shared.paths import project_root

_CONFIG_ROOT = project_root() / "autobot-infrastructure" / "shared" / "config"
_LOGGING_YML = _CONFIG_ROOT / "logging.yml"
_PROMTAIL_ORPHAN = _CONFIG_ROOT / "logging" / "promtail" / "promtail-config.yml"
_THREAT_DETECTION_YAML = _CONFIG_ROOT / "security" / "threat_detection.yaml"

#: No YAML loader in this codebase expands a shell placeholder, so such a token
#: in a value is always inert -- the fact the whole guard exists to enforce.
#:
#: Assembled from parts rather than written literally: this file is a test ABOUT
#: inert placeholders, and `tools/lint/check_no_shell_placeholder_paths.py`
#: forbids the literal in a Python string constant with no baseline, on purpose
#: (#14517). A file that names the thing it guards against would otherwise be
#: the one file that cannot obey the guard.
_ROOT_VAR = "AUTOBOT_PROJECT_ROOT"
_UNEXPANDED_MARKER = "${" + _ROOT_VAR


def _load_yaml(path: Path) -> dict[str, Any]:
return yaml.safe_load(path.read_text(encoding="utf-8"))


def contains_unexpanded_project_root(value: Any) -> bool:
"""True when *value* still carries the literal, never-expanded token."""
return isinstance(value, str) and _UNEXPANDED_MARKER in value


class TestLoggingYmlCarriesNoLivePlaceholder:
def test_filename_is_not_an_unexpanded_placeholder(self) -> None:
data = _load_yaml(_LOGGING_YML)
filename = data["handlers"]["file"]["filename"]
assert not contains_unexpanded_project_root(filename), (
f"logging.yml handlers.file.filename is {filename!r}: a {_UNEXPANDED_MARKER}}} "
"token here is never expanded by anything and is clobbered by the next "
"`log_aggregator.py --setup` run regardless (#15044)."
)

def test_the_file_documents_that_it_is_generated(self) -> None:
header = _LOGGING_YML.read_text(encoding="utf-8").splitlines()[0]
assert header.startswith("# GENERATED ARTIFACT"), (
"logging.yml lost its generated-artifact header -- restore it so a future "
"hand-edit does not look like a reviewed source of truth (#15044)."
)


def test_the_orphaned_promtail_copy_stays_removed() -> None:
"""Ansible renders its own promtail config; this copy was never deployed."""
assert not _PROMTAIL_ORPHAN.exists(), (
f"{_PROMTAIL_ORPHAN} exists again. It is not the file Ansible deploys -- "
"roles/centralized_logging/templates/promtail-config.yml.j2 is -- and this "
"copy was never wired up with `-config.expand-env=true` (#15044)."
)


class TestThreatDetectionYamlCarriesNoDeadModelPersistenceKeys:
#: Keys removed because no consumer in the codebase reads them (#15044).
DEAD_KEYS = ("model_storage_path", "save_trained_models", "load_existing_models")

def test_none_of_the_dead_keys_are_present(self) -> None:
ml_models = _load_yaml(_THREAT_DETECTION_YAML)["ml_models"]
present = [key for key in self.DEAD_KEYS if key in ml_models]
assert not present, (
f"threat_detection.yaml ml_models carries {present}, which ThreatDetectionEngine "
"does not read. Wire them into the engine or keep them out (#15044)."
)

def test_the_keys_the_engine_actually_reads_are_still_present(self) -> None:
"""The floor: a sweep that also deleted the live keys would pass vacuously."""
ml_models = _load_yaml(_THREAT_DETECTION_YAML)["ml_models"]
assert "model_retrain_hours" in ml_models
assert "min_training_samples" in ml_models


@pytest.mark.parametrize(
"value,should_flag",
[
(_UNEXPANDED_MARKER + ":-/opt/autobot/code_source}/logs/system.log", True),
(_UNEXPANDED_MARKER + "}/data/security/models", True),
("logs/system.log", False),
("/opt/autobot/logs/system.log", False),
(None, False),
(42, False),
],
)
def test_the_placeholder_detector_contrast(value: Any, should_flag: bool) -> None:
"""The mutation this whole module exists to catch, isolated from disk state."""
assert contains_unexpanded_project_root(value) is should_flag
Loading