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
2 changes: 1 addition & 1 deletion docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5637,7 +5637,7 @@ Anchoring on `$PSScriptRoot` binds the script to the checkout it lives in, which

## 1062. `check` validates the env value file under `--project-root` then reads the values from the current directory

> 🔢 **Filed 2026-08-06 — not started.** Value **7/10** · Difficulty **2/10** · _quick win_. `messagefoundry check --project-root R` anchors `--config` under `R` and **hard-fails** if `R/<env_dir>/<env>.toml` is absent — then drops `R`. `run_checks` takes no project root, so the build check re-derives the value anchor from `Path.cwd()`. The gate therefore **verifies the file under the root you supplied and reads the values from wherever your shell happens to be.** `serve` does not have this defect, in the same file, by one line.
> ✅ **SHIPPED 2026-08-06 — the root is threaded through and applied the way `serve` applies it.** Value **7/10** · Difficulty **2/10** · _quick win_. `run_checks` gained a `project_root` parameter, threaded to the build check and set as a `[environments].base_dir` **CLI override** — the same mechanism `serve` uses, so `load_settings`' CLI > env > file precedence puts it above a file-set `base_dir`. Left unset the resolution is unchanged and still falls back to the process directory, so `check --config config` is untouched. Two tests, asserted by the DIVERGENCE (the process directory holds its own value file with a different host); the pre-fix behaviour was reproduced directly rather than inferred — values were read from the process directory while the root was the one validated. Original filing follows. `messagefoundry check --project-root R` anchors `--config` under `R` and **hard-fails** if `R/<env_dir>/<env>.toml` is absent — then drops `R`. `run_checks` takes no project root, so the build check re-derives the value anchor from `Path.cwd()`. The gate therefore **verifies the file under the root you supplied and reads the values from wherever your shell happens to be.** `serve` does not have this defect, in the same file, by one line.

**Cluster:** Configuration anchoring / gate integrity. **Priority:** P2. **Verdict:** build (small). **Severity:** would mis-decide a **required, blocking** check on a deploying site. Nothing is deployed (§0), so this is what a deploying site would hit on first use, not something happening today. It is also the only finding in this cluster on **product code** rather than developer tooling.

Expand Down
4 changes: 4 additions & 0 deletions messagefoundry/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4267,6 +4267,10 @@ def _check(args: argparse.Namespace) -> int:
handler_security_allow=frozenset(args.handler_security_allow or ()),
service_config=service_config,
suppress_service_toml_search=args.project_root is not None,
# The root was already used to anchor --config/--service-config and to REQUIRE that
# <root>/<env_dir>/<env>.toml exists; pass it on so the build check READS the values from there
# too, rather than from wherever the shell happens to be (BACKLOG #1062).
project_root=args.project_root,
)
if args.json:
_print_json(report.to_json(), compact=True)
Expand Down
25 changes: 24 additions & 1 deletion messagefoundry/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def run_checks(
handler_security_allow: frozenset[str] = frozenset(),
service_config: str | Path | None = None,
suppress_service_toml_search: bool = False,
project_root: str | Path | None = None,
) -> CheckReport:
"""Run the gate against ``config_dir``; ``messages_dir`` enables the dry-run check when it has
fixtures. Set ``run_lint=False`` to skip the advisory ruff/mypy pass. ``strict_handler_security``
Expand All @@ -133,6 +134,14 @@ def run_checks(
given) suppresses the legacy upward-walk so ``check`` matches ``serve``'s resolution. With both
defaulted (today's ``messagefoundry check --config config``), the upward-walk is preserved — no
regression.

``project_root`` (``--project-root``) is the anchor for ``environments/<env>.toml``, and it exists
because the gate previously VALIDATED that file under the supplied root and then READ the values
from the process working directory (BACKLOG #1062). It is threaded to the build check and applied
the way ``serve`` applies it — as a ``[environments].base_dir`` CLI override, which by
``load_settings``' CLI > env > file precedence overrides a file-set ``base_dir`` exactly as
``__main__.py``'s ``serve`` does. Left ``None`` the resolution is unchanged and still falls back to
the process directory, so the documented ``check --config config`` invocation is untouched.
"""
results = [
_check_validate(config_dir),
Expand All @@ -151,6 +160,7 @@ def run_checks(
config_dir,
service_config=service_config,
suppress_search=suppress_service_toml_search,
project_root=project_root,
),
_check_reference_backend(
config_dir,
Expand Down Expand Up @@ -1234,6 +1244,7 @@ def _check_build(
*,
service_config: str | Path | None = None,
suppress_search: bool = False,
project_root: str | Path | None = None,
) -> CheckResult:
"""Run the **posture-stamped** ``build_check_registry`` that ``serve``/``reload`` run, so a config
``serve`` would REFUSE — most importantly a production-PHI cleartext / weakened-TLS transport hop
Expand Down Expand Up @@ -1275,8 +1286,20 @@ def _check_build(
return CheckResult(
"build-check", ok=True, required=True, skipped=True, detail="no messagefoundry.toml"
)
# --project-root anchors the env VALUES, applied exactly as `serve` applies it (__main__.py, the
# `cli["environments"]["base_dir"] = args.project_root` line): as a [environments].base_dir CLI
# override, which load_settings' CLI > env > file precedence puts above a file-set base_dir.
#
# Without it the caller's root was used to VALIDATE that <root>/<env_dir>/<env>.toml EXISTS and then
# discarded, while the values below were read from the PROCESS directory -- so the ADR 0092
# posture-keyed insecure-hop refusal could be decided against a different environment's hosts and
# schemes than the one the operator named (BACKLOG #1062). Left None, resolution is unchanged and
# still falls back to the process directory, so `check --config config` is untouched.
cli: dict[str, dict[str, object]] | None = (
{"environments": {"base_dir": str(project_root)}} if project_root is not None else None
)
try:
settings = load_settings(config_path=toml)
settings = load_settings(config_path=toml, cli=cli)
except (FileNotFoundError, ValueError, ValidationError, OSError) as exc:
return CheckResult(
"build-check",
Expand Down
89 changes: 89 additions & 0 deletions tests/test_config_anchoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,3 +489,92 @@ def test_malformed_env_file_fails_cleanly_not_traceback(
err = capsys.readouterr().err
assert "could not read environment values" in err
assert "dev.toml" in err


# --- AC-6 / BACKLOG #1062: the build check must READ values from the root it VALIDATED -------------


def _root_with_values(root: Path, peer_host: str, *, env: str = "prod") -> Path:
"""A project root carrying a config, a service toml, and one environment value file."""
_config_dir(root, _ENV_GRAPH)
(root / "messagefoundry.toml").write_text(f'[ai]\nenvironment = "{env}"\n', encoding="utf-8")
envdir = root / "environments"
envdir.mkdir(exist_ok=True)
(envdir / f"{env}.toml").write_text(f'peer_host = "{peer_host}"\n', encoding="utf-8")
return root


def test_build_check_reads_values_from_the_root_not_the_cwd(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""BACKLOG #1062. ``check --project-root R`` hard-fails when ``R/<env_dir>/<env>.toml`` is absent,
then used to DROP R and read the values from the process directory -- so the ADR 0092 posture-keyed
insecure-hop refusal could be decided against a different environment's hosts and schemes than the
one the operator named. ``serve`` never had this: it writes the root into ``[environments].base_dir``
before ``load_settings``.

Asserted by the DIVERGENCE, which is the only shape that can fail: the process directory holds its
OWN value file with a different host. A test run from inside the root passes with the bug present,
because both anchors agree there -- the same masking that hid the rule 3d defect on Windows.
"""
root = _root_with_values(tmp_path / "R", "10.0.0.9")
elsewhere = _root_with_values(tmp_path / "W", "192.168.1.1")
monkeypatch.chdir(elsewhere) # the process directory is NOT the root

seen: list[str] = []
import messagefoundry.checks as checks_mod
import messagefoundry.config.environments as env_mod

# `_check_build` imports this inside the function, so the patch must land on the SOURCE module.
real = env_mod.load_environment_values

def _spy(*, base_dir: Path, **kw: object) -> object:
seen.append(str(base_dir))
return real(base_dir=base_dir, **kw) # type: ignore[arg-type]

monkeypatch.setattr(env_mod, "load_environment_values", _spy)

checks_mod.run_checks(
root / "config",
run_lint=False,
service_config=str(root / "messagefoundry.toml"),
suppress_service_toml_search=True,
project_root=str(root),
)

assert seen, "the build check never resolved environment values -- the test proves nothing"
assert seen[0] == str(root), f"values were read from {seen[0]}, not the supplied root {root}"
assert str(elsewhere) not in seen


def test_without_a_root_the_build_check_still_falls_back_to_the_cwd(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The other half, and the one that makes the fix a correction rather than a widening: with no
``--project-root`` the resolution is unchanged and still anchors on the process directory, so the
documented ``messagefoundry check --config config`` invocation is untouched."""
here = _root_with_values(tmp_path / "here", "10.0.0.9")
monkeypatch.chdir(here)

seen: list[str] = []
import messagefoundry.checks as checks_mod
import messagefoundry.config.environments as env_mod

# `_check_build` imports this inside the function, so the patch must land on the SOURCE module.
real = env_mod.load_environment_values

def _spy(*, base_dir: Path, **kw: object) -> object:
seen.append(str(base_dir))
return real(base_dir=base_dir, **kw) # type: ignore[arg-type]

monkeypatch.setattr(env_mod, "load_environment_values", _spy)

checks_mod.run_checks(
here / "config",
run_lint=False,
service_config=str(here / "messagefoundry.toml"),
suppress_service_toml_search=True,
)

assert seen, "the build check never resolved environment values -- the test proves nothing"
assert seen[0] == str(here)
Loading