"""Render the security addendum's host table from config, so the addendum cannot drift from the code. The table between the ``host-table`` markers in ``docs/security/addendum-knowledge-specialist.md`` is generated from ``config/sources.yaml`` (hosts, licences) and ``config/tools_allowlist.yaml`` (which endpoints call which source). ``tests/test_sources_hosts.py`` already proves every host a loaded tool can reach is in ``sources.yaml``; this script carries that same list into the document the client's security reviewers read. python scripts/render_host_table.py # rewrite the block in place python scripts/render_host_table.py --check # exit 1 if the committed block is stale """ from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) from src import config ADDENDUM = ROOT / "docs" / "security" / "addendum-knowledge-specialist.md" START = "" END = "" def endpoints_by_source() -> dict[str, list[str]]: used: dict[str, set[str]] = {} for tool in config.allowlist()["tools"].values(): used.setdefault(tool["source"], set()).update(tool["endpoints"]) return {sid: [e for e in config.ENDPOINTS if e in eps] for sid, eps in used.items()} def render() -> str: by_source = endpoints_by_source() rows = [ "| Source | Hosts called | Licence carried on every row | Called in Phase 1 by | Key needed |", "|---|---|---|---|---|", ] for sid, src in config.sources().items(): eps = by_source.get(sid) or [] called = ", ".join(f"`{e}`" for e in eps) if eps else "not called (declared ahead of use)" hosts = ", ".join(f"`{h}`" for h in src["hosts"]) rows.append(f"| {src['name']} | {hosts} | {src['licence']} | {called} | none |") n_hosts = sum(len(s["hosts"]) for s in config.sources().values()) n_called = sum(len(s["hosts"]) for sid, s in config.sources().items() if by_source.get(sid)) rows.append("") rows.append( f"{n_hosts} hosts in all; {n_called} are called by a Phase 1 endpoint. Source of truth: " "`config/sources.yaml`, checked against the installed ToolUniverse by " "`tests/test_sources_hosts.py`." ) return "\n".join([START, *rows, END]) def main(argv: list[str]) -> int: text = ADDENDUM.read_text(encoding="utf-8") if START not in text or END not in text: print(f"{ADDENDUM.name}: host-table markers missing", file=sys.stderr) return 2 before, rest = text.split(START, 1) _, after = rest.split(END, 1) updated = before + render() + after if "--check" in argv: if updated != text: print( f"{ADDENDUM.name}: host table is stale; run scripts/render_host_table.py", file=sys.stderr, ) return 1 return 0 ADDENDUM.write_text(updated, encoding="utf-8") return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))