pdac-knowledge-agent-dev / scripts /render_host_table.py
avoigt1121
Claude Opus 5.5
docs: name no institution in the public repo (the client's Tech Transfer rule)
081f1c7
Raw History Blame Contribute Delete
3.13 kB
"""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 = "<!-- host-table:start (generated by scripts/render_host_table.py; do not edit by hand) -->"
END = "<!-- host-table: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:]))