File size: 4,277 Bytes
2e658e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env python3
"""Generate a deterministic dependency/build-input inventory.

The inventory is intentionally offline and reproducible. It records only inputs
that are committed in this repository: exact Python pins, container image tags,
the immutable upstream commit, and runtime version declarations. External
vulnerability scanners consume this inventory in CI, but generating it never
requires network access.
"""
from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any

ROOT = Path(__file__).resolve().parents[1]


def _noncomment_lines(path: Path) -> list[str]:
    return [
        line.strip()
        for line in path.read_text(encoding="utf-8").splitlines()
        if line.strip() and not line.lstrip().startswith("#")
    ]


def build_inventory(root: Path = ROOT) -> dict[str, Any]:
    dockerfile = (root / "Dockerfile").read_text(encoding="utf-8")
    requirements = _noncomment_lines(root / "requirements.lock")
    ci_requirements = _noncomment_lines(root / "requirements-ci.lock")
    python_version = (root / ".python-version").read_text(encoding="utf-8").strip()
    package_json = json.loads((root / "package.json").read_text(encoding="utf-8"))
    package_lock = json.loads((root / "package-lock.json").read_text(encoding="utf-8"))

    images = []
    for raw in re.findall(r"^FROM\s+([^\s]+)", dockerfile, re.MULTILINE):
        images.append(raw)

    upstream = re.search(
        r"^ARG HERMES_UPSTREAM_REF=([0-9a-f]{40})$", dockerfile, re.MULTILINE
    )
    if not upstream:
        raise ValueError("Dockerfile does not contain an immutable upstream commit")

    for requirement in [*requirements, *ci_requirements]:
        if "==" not in requirement or any(op in requirement for op in (">=", "<=", "~=", "!=", "*")):
            raise ValueError(f"non-exact dependency pin: {requirement}")

    return {
        "schemaVersion": 1,
        "project": "Hermes Futures Desk V5",
        "runtime": {
            "python": python_version,
            "node": package_json["engines"]["node"],
            "npm": package_json["engines"]["npm"],
        },
        "upstream": {
            "repository": "https://github.com/NousResearch/hermes-agent.git",
            "commit": upstream.group(1),
        },
        "pythonDependencies": sorted(requirements),
        "ciPythonDependencies": sorted(ci_requirements),
        "containerImages": images,
        "nodeDependencyPolicy": {
            "upstreamLockfile": "package-lock.json",
            "installCommand": "npm ci --prefer-offline --no-audit",
            "localLockfile": "package-lock.json",
            "localLockfileVersion": package_lock.get("lockfileVersion"),
            "localNodeDependencies": sorted((package_json.get("dependencies") or {}).items()),
            "localNodeDevDependencies": sorted((package_json.get("devDependencies") or {}).items()),
        },
    }


def render(inventory: dict[str, Any]) -> str:
    return json.dumps(inventory, indent=2, sort_keys=True) + "\n"


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--root", default=str(ROOT))
    parser.add_argument("--output")
    parser.add_argument("--check", action="store_true")
    args = parser.parse_args(argv)

    root = Path(args.root).resolve()
    output = Path(args.output).resolve() if args.output else root / "docs" / "dependency_inventory.json"
    try:
        rendered = render(build_inventory(root))
    except (OSError, ValueError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1

    if args.check:
        try:
            current = output.read_text(encoding="utf-8")
        except OSError as exc:
            print(f"ERROR: dependency inventory missing: {exc}", file=sys.stderr)
            return 1
        if current != rendered:
            print("ERROR: dependency inventory is stale", file=sys.stderr)
            return 1
        print(f"dependency_inventory=PASS path={output}")
        return 0

    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(rendered, encoding="utf-8")
    print(f"wrote {output}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())