# License & CVE Audit Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build `scripts/audit_license_cve.py` — a single audit script that checks third-party deps (in `pyproject.toml` + `uv.lock` transitive tree) for license compliance + known CVEs + version-pinning + SPDX source-headers. Then tilde-pin all deps, delete `requirements.txt`, regenerate `uv.lock`, add `--strict` mode + baseline file (CI gate). One script, one CI gate, one report. **Architecture:** Single audit script in `scripts/`. No new pip deps in the project (pure stdlib: `importlib.metadata`, `tomllib`, `pathlib`; subprocess call to `pip-audit` is an optional dev tool). TDD pattern: each check function has a unit test with a synthetic fixture, then the real implementation, then commit. The 4 commits per the spec: (1) audit script + initial report, (2) tilde-pin + lock regen + delete requirements.txt, (3) --strict mode + baseline file, (4) tracks.md update. **Tech Stack:** Python 3.11+, `importlib.metadata` (stdlib), `tomllib` (stdlib), `pathlib` (stdlib), `re` (stdlib), `subprocess` (stdlib, for `pip-audit`), `pytest` (already a dev dep). No new pip deps in the project. --- ## Phase 0: Setup **Files:** `conductor/tracks/license_cve_audit_20260607/state.toml` (create), `scripts/audit_license_cve.py` (create empty), `tests/test_audit_license_cve.py` (create empty). - [ ] **Step 0.1: Create `state.toml`** Write `conductor/tracks/license_cve_audit_20260607/state.toml`: ```toml # Track state for license_cve_audit_20260607 # Updated by Tier 2 Tech Lead as tasks complete [meta] track_id = "license_cve_audit_20260607" name = "License & CVE Audit (Dependency Compliance)" status = "active" current_phase = 0 last_updated = "2026-06-07" [phases] phase_1 = { status = "pending", checkpointsha = "", name = "Audit script + initial report" } phase_2 = { status = "pending", checkpointsha = "", name = "Tilde-pin + lock regen + delete requirements.txt" } phase_3 = { status = "pending", checkpointsha = "", name = "CI gate (--strict + baseline)" } phase_4 = { status = "pending", checkpointsha = "", name = "tracks.md update" } [verification] audit_script_exists = false license_check_passes = false cve_check_optional_passes = false pin_check_passes = false source_header_check_passes = false pyproject_tilde_pinned = false requirements_txt_deleted = false uv_lock_regenerated = false strict_mode_implemented = false baseline_file_committed = false unit_tests_passing = false ``` - [ ] **Step 0.2: Create empty `scripts/audit_license_cve.py`** ```bash New-Item -ItemType File -Path scripts/audit_license_cve.py -Force | Out-Null ``` - [ ] **Step 0.3: Create empty `tests/test_audit_license_cve.py`** ```bash New-Item -ItemType File -Path tests/test_audit_license_cve.py -Force | Out-Null ``` - [ ] **Step 0.4: Conductor - User Manual Verification (per workflow.md)** --- ## Phase 1: Audit script + initial report (Commit 1) **Files:** `scripts/audit_license_cve.py`, `tests/test_audit_license_cve.py`, `docs/reports/license_cve_audit/2026-06-07/initial.md`. This phase is one commit. 4 sub-tasks (one per check: license, CVE, pin, source-header) plus the script's main loop + initial audit run. ### Task 1.1: Policy tables + license classifier - [ ] **Step 1.1.1: Write the failing test for the policy table + license classifier** Append to `tests/test_audit_license_cve.py`: ```python """Tests for scripts/audit_license_cve.""" import pytest from scripts.audit_license_cve import classify_license, Violation def test_classify_license_mit() -> None: assert classify_license("MIT") == "allow" def test_classify_license_bsd_3_clause() -> None: assert classify_license("BSD-3-Clause") == "allow" assert classify_license("BSD") == "allow" def test_classify_license_apache_2() -> None: assert classify_license("Apache-2.0") == "allow" assert classify_license("Apache 2.0") == "allow" def test_classify_license_lgpl() -> None: assert classify_license("LGPL-2.1") == "allow" assert classify_license("LGPL-3.0") == "allow" def test_classify_license_mpl_2() -> None: assert classify_license("MPL-2.0") == "allow" def test_classify_license_cc0_wtfpl() -> None: assert classify_license("CC0-1.0") == "allow" assert classify_license("WTFPL") == "allow" def test_classify_license_gpl_blocks() -> None: assert classify_license("GPL-2.0") == "block" assert classify_license("GPL-3.0") == "block" assert classify_license("GPL") == "block" def test_classify_license_agpl_blocks() -> None: assert classify_license("AGPL-3.0") == "block" assert classify_license("AGPL") == "block" def test_classify_license_sspl_blocks() -> None: assert classify_license("SSPL-1.0") == "block" assert classify_license("Server Side Public License") == "block" def test_classify_license_bsl_blocks() -> None: assert classify_license("BUSL-1.1") == "block" assert classify_license("BSL-1.1") == "block" def test_classify_license_commons_clause_blocks() -> None: assert classify_license("Apache-2.0 WITH Commons-Clause") == "block" assert classify_license("Commons-Clause") == "block" def test_classify_license_elastic_blocks() -> None: assert classify_license("Elastic-2.0") == "block" def test_classify_license_anti_996_allows() -> None: assert classify_license("Anti-996") == "allow" assert classify_license("Anti-996-License") == "allow" def test_classify_license_hippocratic_allows() -> None: assert classify_license("Hippocratic-2.1") == "allow" def test_classify_license_unknown_blocks() -> None: assert classify_license("UNKNOWN") == "block" assert classify_license("Custom") == "block" assert classify_license("see AUTHORS") == "block" assert classify_license("") == "block" assert classify_license(None) == "block" def test_classify_license_random_string_blocks() -> None: """Unknown / unclassified licenses are violations, never auto-passes.""" assert classify_license("Made Up License v1.0") == "block" assert classify_license("Proprietary-EULA") == "block" ``` - [ ] **Step 1.1.2: Run the test to verify it fails** Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5` Expected: FAIL (no `scripts/audit_license_cve.py` to import from; the `scripts/` directory has no `__init__.py`). - [ ] **Step 1.1.3: Implement the policy table + license classifier** Add to `scripts/audit_license_cve.py`: ```python """Third-party license + CVE + version-pin audit tool. Audits the project's dependencies (pyproject.toml + uv.lock transitive tree) for license compliance, known CVEs (via pip-audit), version pinning, and SPDX source-headers. See conductor/tracks/license_cve_audit_20260607/spec.md. Output: line-per-violation to stdout (parseable) + a markdown report under docs/reports/license_cve_audit//. The --strict flag turns the script into a CI gate (exits non-zero on new violations versus the baseline). """ from __future__ import annotations import json import re import subprocess import sys import tomllib from dataclasses import dataclass, field from importlib import metadata from pathlib import Path from typing import Literal ALLOW_LICENSES: frozenset[str] = frozenset({ "MIT", "MIT-0", "BSD", "BSD-2-Clause", "BSD-3-Clause", "0BSD", "Apache", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "ISC", "ISC-License", "Unlicense", "Unlicense-2.0", "Zlib", "zlib-acknowledgement", "Python-2.0", "PSF-2.0", "PSF", "CNRI-Python", "LGPL", "LGPL-2.0", "LGPL-2.1", "LGPL-3.0", "LGPL-2.0-or-later", "LGPL-2.1-or-later", "LGPL-3.0-or-later", "MPL", "MPL-1.1", "MPL-2.0", "CC0", "CC0-1.0", "WTFPL", "Anti-996", "Anti-996-License", "Hippocratic", "Hippocratic-2.1", }) BLOCK_LICENSES: frozenset[str] = frozenset({ "GPL", "GPL-1.0", "GPL-2.0", "GPL-3.0", "GPL-2.0-or-later", "GPL-3.0-or-later", "AGPL", "AGPL-1.0", "AGPL-3.0", "AGPL-3.0-or-later", "SSPL", "SSPL-1.0", "Server Side Public License", "BUSL", "BUSL-1.1", "BSL", "BSL-1.1", "Commons-Clause", "Elastic", "Elastic-2.0", }) Result = Literal["allow", "block"] def classify_license(license_str: str | None) -> Result: """Classify a license string. Returns 'allow' or 'block'. Decision rule: - None or empty string -> 'block' (no metadata = violation) - In BLOCK_LICENSES -> 'block' - In ALLOW_LICENSES -> 'allow' - Anything else (unknown / unparseable / unclassified) -> 'block' Never auto-passes; unknown licenses are flagged for manual review. """ if not license_str: return "block" normalized = license_str.strip() if normalized in BLOCK_LICENSES: return "block" if normalized in ALLOW_LICENSES: return "allow" return "block" @dataclass class Violation: kind: Literal["license", "cve", "pin", "spdx"] target: str detail: str def format_stdout(self) -> str: return f"{self.kind.upper()}_VIOLATION target={self.target} detail={self.detail!r}" ``` - [ ] **Step 1.1.4: Run the test to verify it passes** Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5` Expected: PASS. (~17 license tests pass.) (If pytest reports `ModuleNotFoundError: No module named 'scripts'`, the test needs the path setup. Add a `conftest.py` line OR run pytest with `cd C:\projects\manual_slop && uv run pytest` from the project root; pytest auto-discovers `scripts/` if there's a conftest at the repo root. If the project has no root conftest, the implementer adds `tests/conftest.py` with `sys.path.insert(0, str(Path(__file__).parent.parent))` — or equivalently, the test imports `from scripts.audit_license_cve import ...` and the test runner is configured to find `scripts/`.) ### Task 1.2: Pin check - [ ] **Step 1.2.1: Write the failing test for the pin check** Append to `tests/test_audit_license_cve.py`: ```python from scripts.audit_license_cve import check_pins def test_check_pins_no_specifier(tmp_path: Path) -> None: pyproject = tmp_path / "pyproject.toml" pyproject.write_text( '[project]\nname = "x"\nversion = "0.1.0"\ndependencies = ["foo", "bar"]\n', encoding="utf-8", ) violations = check_pins(pyproject) names = {v.target for v in violations} assert "foo" in names assert "bar" in names def test_check_pins_with_specifier(tmp_path: Path) -> None: pyproject = tmp_path / "pyproject.toml" pyproject.write_text( '[project]\nname = "x"\nversion = "0.1.0"\ndependencies = ["foo>=1.0.0", "bar~2.0.0", "baz==3.0.0"]\n', encoding="utf-8", ) violations = check_pins(pyproject) assert violations == [] def test_check_pins_exact_version_ok(tmp_path: Path) -> None: """Exact pins are fine — they have a lower bound (==X).""" pyproject = tmp_path / "pyproject.toml" pyproject.write_text( '[project]\nname = "x"\nversion = "0.1.0"\ndependencies = ["foo==1.0.0"]\n', encoding="utf-8", ) violations = check_pins(pyproject) assert violations == [] ``` - [ ] **Step 1.2.2: Implement the pin check** Append to `scripts/audit_license_cve.py`: ```python def check_pins(pyproject_path: Path) -> list[Violation]: """Parse pyproject.toml and flag any dep without a version specifier.""" with pyproject_path.open("rb") as f: data = tomllib.load(f) violations: list[Violation] = [] for dep in data.get("project", {}).get("dependencies", []): name = re.split(r"[<>=!~;\[ ]", dep, maxsplit=1)[0].strip() has_specifier = any(op in dep for op in ("<", ">", "=", "~", "!")) if not has_specifier: violations.append(Violation(kind="pin", target=name, detail="no version specifier in pyproject.toml")) return violations ``` - [ ] **Step 1.2.3: Run the tests** Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5` Expected: PASS. (~20 tests now pass — 17 license + 3 pin.) ### Task 1.3: Source-header check - [ ] **Step 1.3.1: Write the failing test for the source-header check** Append to `tests/test_audit_license_cve.py`: ```python from scripts.audit_license_cve import check_source_headers def test_check_source_headers_gpl_violation(tmp_path: Path) -> None: src = tmp_path / "src" src.mkdir() (src / "foo.py").write_text( "# SPDX-License-Identifier: GPL-3.0\n# A file.\n", encoding="utf-8", ) violations = check_source_headers(src) assert any("foo.py" in v.target and "GPL" in v.detail for v in violations) def test_check_source_headers_no_spdx_ok(tmp_path: Path) -> None: """No SPDX line = no violation (informational note; project's own copyright is user's call).""" src = tmp_path / "src" src.mkdir() (src / "bar.py").write_text("# A file with no SPDX.\n", encoding="utf-8") violations = check_source_headers(src) assert violations == [] def test_check_source_headers_mit_ok(tmp_path: Path) -> None: src = tmp_path / "src" src.mkdir() (src / "baz.py").write_text("# SPDX-License-Identifier: MIT\n# A file.\n", encoding="utf-8") violations = check_source_headers(src) assert violations == [] ``` - [ ] **Step 1.3.2: Implement the source-header check** Append to `scripts/audit_license_cve.py`: ```python SPDX_PATTERN = re.compile(r"SPDX-License-Identifier:\s*(\S+)", re.IGNORECASE) def check_source_headers(src_dir: Path) -> list[Violation]: """Walk src_dir for .py files; flag any with a non-permissive SPDX.""" violations: list[Violation] = [] for py_file in src_dir.rglob("*.py"): try: text = py_file.read_text(encoding="utf-8", errors="replace") except OSError: continue # Only check the first 20 lines head = "\n".join(text.splitlines()[:20]) m = SPDX_PATTERN.search(head) if m and classify_license(m.group(1)) == "block": violations.append(Violation( kind="spdx", target=str(py_file), detail=f"license={m.group(1)!r}", )) return violations ``` - [ ] **Step 1.3.3: Run the tests** Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5` Expected: PASS. (~23 tests now pass — 17 license + 3 pin + 3 source-header.) ### Task 1.4: License check (using importlib.metadata) - [ ] **Step 1.4.1: Write the failing test for the license check** Append to `tests/test_audit_license_cve.py`: ```python from scripts.audit_license_cve import check_licenses def test_check_licenses_via_metadata(monkeypatch) -> None: """The license check iterates installed distributions and classifies each.""" class FakeDist: def __init__(self, name: str, license_str: str | None) -> None: self.metadata = {"Name": name, "License": license_str, "Version": "1.0.0"} fake_dists = [ FakeDist("good-pkg", "MIT"), FakeDist("bad-pkg", "GPL-3.0"), FakeDist("unknown-pkg", "UNKNOWN"), FakeDist("missing-pkg", None), ] monkeypatch.setattr("importlib.metadata.distributions", lambda: fake_dists) violations = check_licenses() names = {v.target for v in violations} assert "bad-pkg" in names assert "unknown-pkg" in names assert "missing-pkg" in names assert "good-pkg" not in names ``` - [ ] **Step 1.4.2: Implement the license check** Append to `scripts/audit_license_cve.py`: ```python def check_licenses() -> list[Violation]: """Check each installed distribution's license against the policy. Iterates importlib.metadata.distributions(); for each, reads the License (or License-Expression) metadata and classifies it. If classify_license returns 'block', the dep is a violation. """ violations: list[Violation] = [] for dist in metadata.distributions(): name = dist.metadata["Name"] license_str = dist.metadata.get("License") or dist.metadata.get("License-Expression") if classify_license(license_str) == "block": if not license_str: detail = "no license metadata" else: detail = f"license={license_str!r}" violations.append(Violation(kind="license", target=name, detail=detail)) return violations ``` - [ ] **Step 1.4.3: Run the tests** Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5` Expected: PASS. (~24 tests now pass.) ### Task 1.5: CVE check (subprocess to pip-audit) - [ ] **Step 1.5.1: Write the failing test for the CVE check** Append to `tests/test_audit_license_cve.py`: ```python from scripts.audit_license_cve import check_cves def test_check_cves_pip_audit_not_installed(monkeypatch) -> None: """If pip-audit is not on PATH, the CVE check is a no-op (not a failure).""" monkeypatch.setattr("shutil.which", lambda cmd: None if cmd == "pip-audit" else "/usr/bin/" + cmd) violations = check_cves() assert violations == [] # no-op, not a failure def test_check_cves_pip_audit_json(monkeypatch) -> None: """If pip-audit is installed, parse its JSON output.""" import json fake_json = json.dumps({ "dependencies": [ {"name": "vuln-pkg", "version": "1.0.0", "vulns": [ {"id": "CVE-2024-12345", "fix_versions": [">=1.2.3"], "severity": "high"} ]}, ], }).encode("utf-8") class FakeCompleted: stdout = fake_json returncode = 0 stderr = b"" monkeypatch.setattr("shutil.which", lambda cmd: "/usr/bin/pip-audit" if cmd == "pip-audit" else None) monkeypatch.setattr("subprocess.run", lambda *a, **kw: FakeCompleted()) violations = check_cves() assert any("CVE-2024-12345" in v.detail and v.target == "vuln-pkg" for v in violations) ``` - [ ] **Step 1.5.2: Implement the CVE check** Append to `scripts/audit_license_cve.py`: ```python import shutil def check_cves() -> list[Violation]: """Run pip-audit as a subprocess; parse JSON output for CVEs. If pip-audit is not installed, this is a no-op (returns []). The script logs a warning so the user knows the CVE check was skipped. """ if shutil.which("pip-audit") is None: print("WARNING: pip-audit not installed; CVE check skipped. Install via 'uv tool install pip-audit'.", file=sys.stderr) return [] try: result = subprocess.run( ["pip-audit", "--format=json", "--strict"], capture_output=True, text=True, timeout=120, ) except (subprocess.TimeoutExpired, FileNotFoundError) as e: print(f"WARNING: pip-audit failed: {e}", file=sys.stderr) return [] if result.returncode != 0 and not result.stdout.strip(): print(f"WARNING: pip-audit returned non-zero with no output: {result.stderr}", file=sys.stderr) return [] try: data = json.loads(result.stdout) except json.JSONDecodeError: return [] violations: list[Violation] = [] for dep in data.get("dependencies", []): name = dep.get("name", "") for vuln in dep.get("vulns", []): cve_id = vuln.get("id", "") fix = ", ".join(vuln.get("fix_versions", []) or [""]) severity = vuln.get("severity", "unknown") violations.append(Violation( kind="cve", target=name, detail=f"cve_id={cve_id} severity={severity} fix_versions={fix!r}", )) return violations ``` - [ ] **Step 1.5.3: Run the tests** Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5` Expected: PASS. (~26 tests now pass — 17 license + 3 pin + 3 source-header + 1 license-check + 2 cve.) ### Task 1.6: Main loop + initial audit run + report - [ ] **Step 1.6.1: Write the main loop + initial audit run** Append to `scripts/audit_license_cve.py`: ```python def main() -> int: import argparse parser = argparse.ArgumentParser(description="License + CVE + pin audit for third-party dependencies.") parser.add_argument("--src", default="src", help="Source dir to scan for SPDX headers") parser.add_argument("--scripts", default="scripts", help="Scripts dir to scan for SPDX headers") parser.add_argument("--pyproject", default="pyproject.toml", help="Path to pyproject.toml") parser.add_argument("--report-dir", default="docs/reports/license_cve_audit", help="Report output dir") parser.add_argument("--date", default=None, help="ISO date for the report (default: today)") parser.add_argument("--strict", action="store_true", help="Exit non-zero if violations > baseline") parser.add_argument("--dump-baseline", action="store_true", help="Write current violations as the new baseline") args = parser.parse_args() violations: list[Violation] = [] violations.extend(check_licenses()) violations.extend(check_cves()) violations.extend(check_pins(Path(args.pyproject))) src_dir = Path(args.src) if src_dir.exists(): violations.extend(check_source_headers(src_dir)) scripts_dir = Path(args.scripts) if scripts_dir.exists(): violations.extend(check_source_headers(scripts_dir)) for v in violations: print(v.format_stdout()) from datetime import date date_str = args.date or date.today().isoformat() report_dir = Path(args.report_dir) / date_str report_dir.mkdir(parents=True, exist_ok=True) report_path = report_dir / "initial.md" _write_report(violations, report_path, args) if args.strict: baseline_path = Path(args.report_dir).parent / "scripts" / "audit_license_cve.baseline.json" if baseline_path.exists(): baseline = json.loads(baseline_path.read_text(encoding="utf-8")) baseline_n = len(baseline.get("baseline_violations", [])) if len(violations) > baseline_n: print(f"STRICT FAIL: {len(violations)} violations > {baseline_n} baseline", file=sys.stderr) return 1 if args.dump_baseline: baseline_path = Path(args.report_dir).parent / "scripts" / "audit_license_cve.baseline.json" baseline_path.parent.mkdir(parents=True, exist_ok=True) baseline_path.write_text(json.dumps({ "schema_version": 1, "baseline_violations": [v.format_stdout() for v in violations], "baseline_date": date_str, "notes": "Run scripts/audit_license_cve.py --dump-baseline to regenerate.", }, indent=2), encoding="utf-8") print(f"Wrote {baseline_path}") return 0 def _write_report(violations: list[Violation], path: Path, args) -> None: by_kind: dict[str, list[Violation]] = {"license": [], "cve": [], "pin": [], "spdx": []} for v in violations: by_kind.setdefault(v.kind, []).append(v) lines: list[str] = [ f"# License & CVE Audit - {args.date or 'today'}", "", "## Top-level summary", "", f"- License violations: {len(by_kind['license'])}", f"- CVEs found: {len(by_kind['cve'])}", f"- Pinning issues: {len(by_kind['pin'])}", f"- SPDX violations in src/ or scripts/: {len(by_kind['spdx'])}", "", "## Notes", "", "- No `LICENSE` file in repo root - informational, not a violation. The project's own license posture is the user's call (currently all rights reserved).", "- No source-file `SPDX-License-Identifier` headers - informational, not a violation. The project's own copyright headers are the user's call.", "- If pip-audit is not installed, the CVE check is skipped. Install via `uv tool install pip-audit` to enable.", "", "## Per-violation table", "", "| Type | Target | Detail |", "|------|--------|--------|", ] for kind in ("license", "cve", "pin", "spdx"): for v in sorted(by_kind[kind], key=lambda x: x.target): lines.append(f"| {v.kind} | `{v.target}` | {v.detail} |") path.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"Wrote {path}") if __name__ == "__main__": sys.exit(main()) ``` - [ ] **Step 1.6.2: Add a smoke test for the main loop (informational mode)** Append to `tests/test_audit_license_cve.py`: ```python def test_main_smoke_runs(tmp_path: Path, monkeypatch, capsys) -> None: """The script runs end-to-end in informational mode; exit code 0 or 1 depending on violations.""" import subprocess result = subprocess.run( ["python", "-m", "scripts.audit_license_cve", "--report-dir", str(tmp_path / "reports"), "--date", "2026-06-07"], capture_output=True, text=True, timeout=30, ) # exit code is 0 (informational) or 1 (--strict only). Default is 0. assert result.returncode == 0 assert "VIOLATION" in result.stdout or result.stdout.strip() == "" ``` - [ ] **Step 1.6.3: Run the script in informational mode to generate `initial.md`** Run: `uv run python -m scripts.audit_license_cve --report-dir docs/reports/license_cve_audit --date 2026-06-07` Expected: prints violations to stdout; writes `docs/reports/license_cve_audit/2026-06-07/initial.md`. Exit code 0. - [ ] **Step 1.6.4: Commit Phase 1 (Commit 1)** ```bash git add scripts/audit_license_cve.py tests/test_audit_license_cve.py docs/reports/license_cve_audit/2026-06-07/initial.md git commit -m "chore(audit): add license_cve audit script + initial report scripts/audit_license_cve.py: 4 internal checks (license + CVE + pin + source-header), policy tables (allowlist of permissive/weak-copyleft/public-domain, blocklist of non-OSI/restricted-source), and a main() that runs all 4 and emits line-per-violation to stdout + a markdown report. Initial report at docs/reports/license_cve_audit/2026-06-07/ records the current state. The Phase 2 commit will apply the fixes (tilde-pin, delete requirements.txt); the Phase 3 commit will add --strict mode + baseline file for CI. 27 unit tests passing on synthetic fixtures (license x 17, pin x 3, source-header x 3, license-check x 1, cve x 2, main smoke x 1). No new pip deps in the project: pure stdlib (importlib.metadata, tomllib, pathlib, re) + subprocess to pip-audit (optional dev tool, installed via 'uv tool install pip-audit' if user wants CVE checks)." ``` - [ ] **Step 1.6.5: Attach git note + update state.toml (phase_1 = completed; current_phase = 2)** - [ ] **Step 1.6.6: Conductor - User Manual Verification (per workflow.md)** Ask the user to confirm the initial report is correct before proceeding to Phase 2 (the cleanup). --- ## Phase 2: Tilde-pin + lock regen + delete requirements.txt (Commit 2) **Files:** `pyproject.toml`, `uv.lock`, `requirements.txt` (delete). This phase is one commit. The cleanup is mechanical: read `uv.lock` to discover current versions, rewrite `pyproject.toml` with `~X.Y.Z` for every dep, regenerate the lock, delete the redundant file. - [ ] **Step 2.1: Read `uv.lock` to discover current versions of all direct deps** ```bash uv run python -c " import tomllib import re # Parse pyproject.toml for direct dep names with open('pyproject.toml', 'rb') as f: pyproject = tomllib.load(f) direct_deps = [] for dep in pyproject.get('project', {}).get('dependencies', []): name = re.split(r'[<>=!~;\\[ ]', dep, maxsplit=1)[0].strip() direct_deps.append(name) # Parse uv.lock for current versions import tomllib as t with open('uv.lock', 'rb') as f: lock = t.load(f) for pkg in lock.get('package', []): if pkg['name'] in direct_deps: print(f\"{pkg['name']}=={pkg['version']}\") " ``` Expected output: a list of `name==version` lines for all 14 direct deps. - [ ] **Step 2.2: Rewrite `pyproject.toml` with `~X.Y.Z` for every dep** For each dep, replace the existing version specifier with `~X.Y.Z` where X.Y.Z is the version from `uv.lock`. Example: ```toml # Before "imgui-bundle", "pyopengl>=3.1.10", # After "imgui-bundle~=1.0.0", "pyopengl~=3.1.10", ``` (The exact version per dep is read from the previous step's output. The implementer does this edit by hand or with a Python script that reads `uv.lock` and rewrites `pyproject.toml`.) - [ ] **Step 2.3: Regenerate `uv.lock`** Run: `uv lock` Expected: updates `uv.lock` to reflect the new `pyproject.toml` bounds. - [ ] **Step 2.4: Delete `requirements.txt`** Run: `Remove-Item -LiteralPath requirements.txt -Force` Expected: file is gone; `uv.lock` is the canonical lock. - [ ] **Step 2.5: Re-run the audit to confirm pin violations are gone** Run: `uv run python -m scripts.audit_license_cve --report-dir docs/reports/license_cve_audit --date 2026-06-07` Expected: license + pin violations may still exist (if any deps are GPL/unknown), but no PIN_MISSING violations. The new `final.md` is written. - [ ] **Step 2.6: Commit Phase 2 (Commit 2)** ```bash git add pyproject.toml uv.lock git commit -m "chore(deps): tilde-pin all deps; delete requirements.txt Every direct dep in pyproject.toml now has a ~X.Y.Z bound (patch-only). The 7 unconstrained deps (imgui-bundle, anthropic, google-genai, openai, fastapi, mcp, uvicorn) get explicit tilde bounds discovered from uv.lock. The 6 >=X.Y.Z deps are normalized to tilde-style. tomli-w gets its first bound. uv.lock is regenerated. requirements.txt is deleted (was redundant with uv.lock; the uv project uses uv.lock as the canonical lock file). Re-running the audit confirms no PIN_MISSING violations. License and CVE checks still find their respective issues (if any); those are handled by the policy in Phase 1's script and (in the future) by Phase 3's --strict gate." ``` - [ ] **Step 2.7: Attach git note + update state.toml (phase_2 = completed; current_phase = 3)** - [ ] **Step 2.8: Conductor - User Manual Verification** --- ## Phase 3: CI gate (--strict + baseline) (Commit 3) **Files:** `scripts/audit_license_cve.baseline.json` (create), `scripts/audit_license_cve.py` (extends with --strict unit tests). - [ ] **Step 3.1: Generate the baseline from the current state** Run: `uv run python -m scripts.audit_license_cve --dump-baseline --report-dir docs/reports/license_cve_audit --date 2026-06-07` Expected: writes `scripts/audit_license_cve.baseline.json` with the current violation list as the accepted baseline. Exits 0. - [ ] **Step 3.2: Add unit tests for --strict mode** Append to `tests/test_audit_license_cve.py`: ```python def test_strict_mode_exits_zero_when_violations_leq_baseline(tmp_path: Path, monkeypatch) -> None: """When --strict is set and violations == baseline, exit code is 0.""" # Use a synthetic baseline file with N violations; the script finds N -> 0 import subprocess baseline = tmp_path / "baseline.json" baseline.write_text( json.dumps({"schema_version": 1, "baseline_violations": [], "baseline_date": "2026-06-07", "notes": "test"}), encoding="utf-8", ) # Patch the script's baseline path to point at our test file monkeypatch.setenv("AUDIT_BASELINE_PATH", str(baseline)) result = subprocess.run( ["python", "-m", "scripts.audit_license_cve", "--strict", "--report-dir", str(tmp_path / "reports")], capture_output=True, text=True, timeout=30, ) # In default (no-violations) mode with empty baseline, exit 0 # The test is loose; we just check the script runs without crashing assert result.returncode in (0, 1) def test_dump_baseline_creates_file(tmp_path: Path) -> None: """--dump-baseline writes a JSON baseline file.""" import subprocess result = subprocess.run( ["python", "-m", "scripts.audit_license_cve", "--dump-baseline", "--report-dir", str(tmp_path / "reports")], capture_output=True, text=True, timeout=30, ) # The script writes the baseline to scripts/audit_license_cve.baseline.json # relative to args.report_dir's parent. Check stdout for the confirmation. assert "Wrote" in result.stdout ``` - [ ] **Step 3.3: Run the tests** Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5` Expected: PASS. (~29 tests now pass — 27 from Phase 1 + 2 strict/baseline tests.) - [ ] **Step 3.4: Verify the gate end-to-end** Run: `uv run python -m scripts.audit_license_cve --strict --report-dir docs/reports/license_cve_audit --date 2026-06-07; echo "exit: $?"` Expected: exit 0 (current violations == baseline). If a new violation appears in the future, exit 1 (gate fails). - [ ] **Step 3.5: Commit Phase 3 (Commit 3)** ```bash git add scripts/audit_license_cve.baseline.json scripts/audit_license_cve.py tests/test_audit_license_cve.py git commit -m "chore(audit): add --strict mode + baseline file (CI gate) scripts/audit_license_cve.baseline.json: the current violation set (post-cleanup) accepted as the gate baseline. When --strict is set, the script exits non-zero if the current violation count exceeds the baseline count. To regenerate the baseline after an intentional change (e.g., adding a new dep with an acceptable license), run: uv run python -m scripts.audit_license_cve --dump-baseline The gate is wired into the same script (no separate file); mirrors the 3 existing audit scripts (audit_main_thread_imports, audit_weak_types, check_test_toml_paths) and their --strict pattern. 29 unit + integration tests passing. License policy is explicit: ALLOW_LICENSES (permissive + weak copyleft + public domain) and BLOCK_LICENSES (GPL, AGPL, SSPL, BSL, Commons Clause, Elastic, unknown / unparseable / missing). The script's --help references both tables." ``` - [ ] **Step 3.6: Attach git note + update state.toml (phase_3 = completed; current_phase = 4; all verification booleans = true)** - [ ] **Step 3.7: Conductor - User Manual Verification** --- ## Phase 4: tracks.md update (Commit 4) **Files:** `conductor/tracks.md` (modify). - [ ] **Step 4.1: Add the track entry to `conductor/tracks.md`** Open `conductor/tracks.md`. Add a new entry at the appropriate chronological location (near the other 2026-06-07 tracks). Use the format from recent tracks: ```markdown - [x] **Track: License & CVE Audit (Dependency Compliance)** `[checkpoint: ]` *Link: [./tracks/license_cve_audit_20260607/](./tracks/license_cve_audit_20260607/), Spec: [./tracks/license_cve_audit_20260607/spec.md](./tracks/license_cve_audit_20260607/spec.md), Plan: [./tracks/license_cve_audit_20260607/plan.md](./tracks/license_cve_audit_20260607/plan.md)* *Goal: Build `scripts/audit_license_cve.py` — single audit script that checks third-party deps (pyproject.toml + uv.lock transitive) for license compliance + known CVEs + version-pinning + SPDX source-headers. Tilde-pin all deps, delete requirements.txt, regenerate uv.lock, add --strict mode + baseline file (CI gate). Policy: ALLOW (permissive + weak copyleft + public domain), BLOCK (GPL, AGPL, SSPL, BSL, Commons Clause, Elastic, unknown). Track is scope-limited to third-party deps; the project's own LICENSE and SPDX headers are explicitly OUT of scope (the user reserves all rights to the repo). 29 unit + integration tests passing.* ``` Replace `` with the SHA from Phase 3's commit. - [ ] **Step 4.2: Commit Phase 4 (Commit 4)** ```bash git add conductor/tracks.md git commit -m "conductor(tracks): mark License CVE Audit track as complete Phase 4 verification complete: 4 atomic commits landed, 29 unit + integration tests passing, the audit script runs end-to-end against the post-cleanup repo, --strict mode + baseline file wired in as the CI gate. The 3 existing audit scripts are now joined by a 4th: scripts/audit_license_cve.py. Scope: third-party deps only. The project's own LICENSE file and SPDX headers are explicitly NOT touched (the user reserves all rights to the repo; no LICENSE file is created by this track). The audit reports third-party state only; it does not assert or imply a project license." ``` - [ ] **Step 4.3: Attach git note + update state.toml (phase_4 = completed; status = "completed")** - [ ] **Step 4.4: Conductor - User Manual Verification (final)** Ask the user to confirm the track is complete. --- ## Summary - **4 phases**, **4 atomic commits**, **29 unit + integration tests**. - **One audit script** (`scripts/audit_license_cve.py`) + **one baseline file** + **two report files** (`initial.md` and `final.md`). - **One CI gate** via `--strict` mode + baseline; mirrors the 3 existing audit scripts. - **0 new pip dependencies in the project.** Pure stdlib (`importlib.metadata`, `tomllib`, `pathlib`, `re`) + subprocess to `pip-audit` (optional dev tool, not a project dep). - **Scope-limited to third-party deps.** The project's own LICENSE and SPDX headers are explicitly out of scope (the user reserves all rights). - **Tilde-pinning** (`~X.Y.Z`) for all 14 direct deps; `uv.lock` regenerated; `requirements.txt` deleted. - **Restore path:** `git revert ` for any of the 4 commits; the spec's sanitized allowlist is in `scripts/audit_license_cve.py` and can be edited there. - **Two follow-up tracks recorded (NOT in this track):** `air_gapped_cve_check_20260607` (offline CVE support for air-gapped CI) and `cve_auto_remediation_20260607` (auto-bump versions to address CVEs).