"""Build the GTI Fiscal Agent Windows package (zip) and verify it contains no secrets. python scripts/build_agent_package.py -> dist/GTIFiscalAgent-.zip (+ .sha256) The zip root is the install folder: gti-agent.ps1 / gti-agent.cmd / requirements-agent.txt / agent.env.template / pilot-construcao.env.example / profiles/ agent/{src,scripts,service}, scripts/firebird_write_executor.py, central/app/{engine modules}, VERSION, MANIFEST.json """ import hashlib import json import re import sys import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parents[1] VERSION = (ROOT / "release" / "VERSION").read_text(encoding="utf-8").strip() NAME = f"GTIFiscalAgent-{VERSION}" INCLUDE = [ # (source, destination inside the package) *[(p, str(p.relative_to(ROOT)).replace("\\", "/")) for p in sorted((ROOT / "agent" / "src" / "gti_fiscal_agent").glob("*.py"))], (ROOT / "agent" / "scripts" / "run_agent_service.py", "agent/scripts/run_agent_service.py"), (ROOT / "agent" / "scripts" / "preflight.py", "agent/scripts/preflight.py"), (ROOT / "agent" / "service" / "gti_agent_windows_service.py", "agent/service/gti_agent_windows_service.py"), (ROOT / "scripts" / "firebird_write_executor.py", "scripts/firebird_write_executor.py"), (ROOT / "central" / "__init__.py", "central/__init__.py"), *[(ROOT / "central" / "app" / f, f"central/app/{f}") for f in ("__init__.py", "fiscal_decision.py", "dry_run_planner.py", "product_defaults.py", "icms_context.py")], *[(p, p.name) for p in sorted((ROOT / "deploy" / "agent-windows").glob("*")) if p.is_file()], *[(p, f"profiles/{p.name}") for p in sorted((ROOT / "deploy" / "agent-windows" / "profiles").glob("*")) if p.is_file()], (ROOT / "release" / "VERSION", "VERSION"), ] SECRET_PATTERNS = [ (re.compile(r"^[ ]*(AGENT_TOKEN|FB_PASSWORD|POSTGRES_PASSWORD)[ ]*=[ ]*\S+", re.M), "non-empty secret assignment"), (re.compile(r"masterkey", re.I), "default Firebird password"), (re.compile(r"[A-Za-z0-9_\-]{40,}"), "long token-like string"), ] ALLOWED_LONG = re.compile(r"^[A-Za-z0-9_\-]{40,}$") def scan(name, data): if name.endswith((".ps1", ".cmd", ".py", ".txt", ".example", ".template", "VERSION")): text = data.decode("utf-8", "replace") for rx, why in SECRET_PATTERNS: for m in rx.finditer(text): token = m.group(0) if why == "long token-like string" and (token.startswith(("_", "-")) or "_" in token and token.count("_") > 3 or token.isalpha()): continue # identifiers / prose, not a secret if why == "long token-like string" and not re.search(r"\d", token): continue if why == "long token-like string" and re.fullmatch(r"[0-9a-f]{64}", token): continue # schema fingerprint (public SHA-256), not a secret raise SystemExit(f"secret scan failed in {name}: {why}: {token[:6]}...") def main(): dist = ROOT / "dist" dist.mkdir(exist_ok=True) out = dist / f"{NAME}.zip" manifest = {"name": NAME, "version": VERSION, "files": {}} with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z: for src, dst in INCLUDE: data = Path(src).read_bytes() assert not dst.endswith((".env", ".sqlite", ".log", ".FDB")), dst scan(dst, data) manifest["files"][dst] = hashlib.sha256(data).hexdigest() z.writestr(f"{NAME}/{dst}", data) z.writestr(f"{NAME}/MANIFEST.json", json.dumps(manifest, indent=1)) digest = hashlib.sha256(out.read_bytes()).hexdigest() (dist / f"{NAME}.zip.sha256").write_text(f"{digest} {out.name}\n", encoding="utf-8") print(json.dumps({"package": str(out.relative_to(ROOT)), "files": len(manifest["files"]), "sha256": digest})) if __name__ == "__main__": main()