05f06ed6a9
1. bin/bullpen-up ROSTER collapsed silently. `{cfg.ROOM_HOST: …, cfg.COORD_HOST: …,
cfg.GRIND_HOST: …}` is a dict literal with CONFIGURABLE keys — a small fleet putting
two roles on one machine (the plausible target of #100) lost the earlier entry
without a word. That is the same silent-no-op the commit claimed to fix, one level
down. Now a list of roles merged per host AND per scope, so a combined
room+coordinator box brings up both its system and its user units. Verified: split
roles unchanged (8 system + 12 user), collapsed onto one host keeps all 20.
Also renamed the local `cfg` in main(), which shadowed the config module.
2. bin/bullpen-selfimprove posted as `from=$BULLPEN_COORD_HOST` — a room NICK confused
with a HOSTNAME. "noether" is a privileged nick that merely happens to equal the
coordinator's hostname here; on any other fleet the run would post as an unknown
nick and be rejected silently, which the R4 comment directly above explains. New
cfg.POST_NICK.
Test gaps @reviewer named, both closed:
* shell entrypoints were entirely outside the AST check — and bullpen-selfimprove is
Bash and was one of the offenders that commit had to fix. Added a grep-based check.
* `fritz.box` was missing from the host list, though it is the strongest fleet marker
there is. Adding it immediately surfaced three more real offenders: a hardcoded
mneme URL in bullpen_participant, another in the lurker, and the legitimate
last-resort fallback in bullpen_mem (exempted, with the reason).
Documented rather than fixed: %h resolves to the service manager's home regardless of
User=, and a stray /root/.local/bin copy would shadow every system unit — both now in
the units-test docstring. cfg.FLEET_HOSTS lost the `pve\d`/`dcw\d` families the regex
had, so a future pve5 needs adding by hand; noted at the definition. `orca` added.
57/57 across both portability suites. bullpen-up --dry-run on noether unchanged;
selfimprove composes a brief with zero origin-fleet mentions under overridden hosts.
152 lines
6.4 KiB
Python
152 lines
6.4 KiB
Python
"""No fleet hostname may reach a RUNTIME string in the Python entrypoints (#100).
|
|
|
|
Prose is fine — docstrings and comments explain why things are the way they are, and
|
|
"runs on boltzmann" is useful history. What must not survive is a hostname in a string
|
|
the program actually *uses*: an sic target, a URL, or a line of advice posted back into
|
|
the room. The last kind is the sneaky one — @dispatcher used to answer every routing
|
|
question with `sic hertz room-ask …`, so a foreign fleet was told to talk to a machine
|
|
it does not have.
|
|
|
|
The check is AST-based rather than grep-based for exactly that reason: grep cannot tell
|
|
`# grinds on boltzmann` (fine) from `sic("boltzmann", …)` (not fine), and a grep-based
|
|
rule would either fail on honest comments or have to be so narrow it misses the strings
|
|
that matter. Docstrings are located by position (first statement of module/class/def)
|
|
and excluded; every other string constant, including the literal parts of f-strings, is
|
|
checked.
|
|
|
|
lib/bullpen_config.py and lib/bullpen_config.sh are exempt: they are where the origin
|
|
fleet's values legitimately live, as the last fallback behind env and config file.
|
|
|
|
Known limits, all deliberate (the target is an oversight, not an adversary):
|
|
* A docstring that is *printed* at runtime (`print(__doc__)`, as bin/room_tail does)
|
|
counts as prose here — a usage text naming a fleet host would pass.
|
|
* Concatenation (`"her" + "tz"`) and bytes literals are not folded, so they slip past.
|
|
* Shell entrypoints get the grep-based check below rather than an AST walk.
|
|
"""
|
|
import ast
|
|
import os
|
|
|
|
import pytest
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
# Deliberately NOT the full cfg.FLEET_HOSTS list: "nc" and "data" are ordinary words that
|
|
# would false-positive on every substring match. `fritz.box` is in — it is the strongest
|
|
# fleet marker there is, and a URL like "http://ampere.fritz.box:9090/" would otherwise
|
|
# sail through (@reviewer, 2026-08-02).
|
|
HOSTS = ("hertz", "noether", "boltzmann", "orca", "dcw2", "hossenfelder", "fritz.box")
|
|
EXEMPT = {
|
|
"lib/bullpen_config.py",
|
|
# holds the same last-resort default in its `except ImportError` branch, for the
|
|
# case where the config module is not on the path at all — there is nothing to
|
|
# resolve it from there, so the literal is the fallback of last resort.
|
|
"lib/bullpen_mem.py",
|
|
}
|
|
|
|
|
|
def _py_files():
|
|
out = []
|
|
for sub in ("bin", "lib", "lurker"):
|
|
d = os.path.join(REPO, sub)
|
|
if not os.path.isdir(d):
|
|
continue
|
|
for name in sorted(os.listdir(d)):
|
|
path = os.path.join(d, name)
|
|
if not os.path.isfile(path) or name.endswith((".sh", ".service", ".timer", ".md")):
|
|
continue
|
|
rel = f"{sub}/{name}"
|
|
if rel in EXEMPT:
|
|
continue
|
|
try:
|
|
src = open(path, encoding="utf-8").read()
|
|
except (OSError, UnicodeDecodeError):
|
|
continue
|
|
if src.startswith("#!") and "python" not in src.splitlines()[0]:
|
|
continue
|
|
out.append((rel, path))
|
|
return out
|
|
|
|
|
|
FILES = _py_files()
|
|
|
|
|
|
def _docstring_nodes(tree):
|
|
"""Constant nodes that are docstrings — prose, not runtime values."""
|
|
found = set()
|
|
for node in ast.walk(tree):
|
|
if not isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
continue
|
|
body = getattr(node, "body", None)
|
|
if (body and isinstance(body[0], ast.Expr)
|
|
and isinstance(body[0].value, ast.Constant)
|
|
and isinstance(body[0].value.value, str)):
|
|
found.add(id(body[0].value))
|
|
return found
|
|
|
|
|
|
@pytest.mark.parametrize("rel,path", FILES, ids=[r for r, _ in FILES])
|
|
def test_no_fleet_host_in_runtime_string(rel, path):
|
|
tree = ast.parse(open(path, encoding="utf-8").read(), filename=path)
|
|
skip = _docstring_nodes(tree)
|
|
offenders = []
|
|
for node in ast.walk(tree):
|
|
if not isinstance(node, ast.Constant) or not isinstance(node.value, str):
|
|
continue
|
|
if id(node) in skip:
|
|
continue
|
|
for host in HOSTS:
|
|
if host in node.value:
|
|
offenders.append(f"line {node.lineno}: {node.value[:70]!r}")
|
|
break
|
|
assert not offenders, (
|
|
f"{rel} puts a fleet hostname in a runtime string — use bullpen_config "
|
|
f"(cfg.ROOM_HOST / cfg.GRIND_HOST / cfg.COORD_HOST / cfg.PROXY / cfg.OC_URL):\n "
|
|
+ "\n ".join(offenders))
|
|
|
|
|
|
def _sh_files():
|
|
"""Shell entrypoints: bin/* with a sh/bash shebang, plus lib/*.sh."""
|
|
out = []
|
|
for sub in ("bin", "lib", "deploy"):
|
|
d = os.path.join(REPO, sub)
|
|
if not os.path.isdir(d):
|
|
continue
|
|
for name in sorted(os.listdir(d)):
|
|
path = os.path.join(d, name)
|
|
rel = f"{sub}/{name}"
|
|
if not os.path.isfile(path) or rel in {"lib/bullpen_config.sh"}:
|
|
continue
|
|
try:
|
|
first = open(path, encoding="utf-8").readline()
|
|
except (OSError, UnicodeDecodeError):
|
|
continue
|
|
if name.endswith(".sh") or ("sh" in first and first.startswith("#!")
|
|
and "python" not in first):
|
|
out.append((rel, path))
|
|
return out
|
|
|
|
|
|
SH_FILES = _sh_files()
|
|
|
|
|
|
@pytest.mark.parametrize("rel,path", SH_FILES, ids=[r for r, _ in SH_FILES] or ["none"])
|
|
def test_shell_entrypoint_has_no_fleet_host(rel, path):
|
|
"""The AST walk above cannot see shell. bin/bullpen-selfimprove is Bash and was one of
|
|
the offenders the config migration had to fix, so without this the regression is
|
|
unguarded there (@reviewer, 2026-08-02).
|
|
|
|
Comments are stripped crudely (a leading `#`), which is enough: the point is to catch a
|
|
hostname in a command or a message, and those do not live in comment lines."""
|
|
offenders = []
|
|
for n, ln in enumerate(open(path, encoding="utf-8").read().splitlines(), 1):
|
|
stripped = ln.strip()
|
|
if not stripped or stripped.startswith("#"):
|
|
continue
|
|
for host in HOSTS:
|
|
if host in ln:
|
|
offenders.append(f"line {n}: {ln.strip()[:70]}")
|
|
break
|
|
assert not offenders, (
|
|
f"{rel} names a fleet host outside a comment — source lib/bullpen_config.sh and use "
|
|
f"$BULLPEN_ROOM_HOST / $BULLPEN_GRIND_HOST / $BULLPEN_COORD_HOST:\n "
|
|
+ "\n ".join(offenders))
|