factorly 0.18.1__tar.gz → 0.20.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: factorly
3
- Version: 0.18.1
3
+ Version: 0.20.0
4
4
  Summary: Security and governance layer for AI agent tool access
5
5
  License: GPL-3.0
6
6
  Project-URL: Homepage, https://github.com/factorly-dev/factorly
@@ -3,4 +3,4 @@
3
3
 
4
4
  """Factorly — security and governance layer for AI agent tool access."""
5
5
 
6
- __version__ = "0.18.1"
6
+ __version__ = "0.20.0"
@@ -0,0 +1,182 @@
1
+ # Copyright 2026 Jordan Sherer <hi@jordansherer.com>
2
+ # SPDX-License-Identifier: gpl
3
+
4
+ """Factorly CLI wrapper — downloads and runs the Go binary."""
5
+
6
+ import hashlib
7
+ import os
8
+ import platform
9
+ import stat
10
+ import subprocess
11
+ import sys
12
+ import urllib.request
13
+
14
+ from factorly import __version__
15
+
16
+ REPO = "factorly-dev/factorly"
17
+
18
+ PLATFORM_MAP = {
19
+ "Darwin": "darwin",
20
+ "Linux": "linux",
21
+ "Windows": "windows",
22
+ }
23
+
24
+ ARCH_MAP = {
25
+ "x86_64": "amd64",
26
+ "AMD64": "amd64",
27
+ "aarch64": "arm64",
28
+ "arm64": "arm64",
29
+ }
30
+
31
+
32
+ def get_bin_dir():
33
+ """Return the directory where the binary is stored."""
34
+ return os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin")
35
+
36
+
37
+ def get_binary_path():
38
+ """Return the full path to the factorly binary."""
39
+ ext = ".exe" if platform.system() == "Windows" else ""
40
+ return os.path.join(get_bin_dir(), f"factorly{ext}")
41
+
42
+
43
+ def get_binary_name():
44
+ """Return the platform-specific binary name for download."""
45
+ plat = PLATFORM_MAP.get(platform.system())
46
+ arch = ARCH_MAP.get(platform.machine())
47
+
48
+ if not plat:
49
+ print(f"Unsupported platform: {platform.system()}", file=sys.stderr)
50
+ sys.exit(1)
51
+ if not arch:
52
+ print(f"Unsupported architecture: {platform.machine()}", file=sys.stderr)
53
+ sys.exit(1)
54
+
55
+ ext = ".exe" if platform.system() == "Windows" else ""
56
+ return f"factorly-{__version__}-{plat}-{arch}{ext}"
57
+
58
+
59
+ def is_installed():
60
+ """Check if the correct version is already installed."""
61
+ binary = get_binary_path()
62
+ if not os.path.exists(binary):
63
+ return False
64
+ try:
65
+ # The probe runs on every launch; keep the binary's own daily
66
+ # update check out of it so the probe can't block on the network.
67
+ env = dict(os.environ, FACTORLY_NO_UPDATE_CHECK="1")
68
+ result = subprocess.run(
69
+ [binary, "version"],
70
+ capture_output=True,
71
+ text=True,
72
+ timeout=5,
73
+ env=env,
74
+ )
75
+ return __version__ in result.stdout
76
+ except Exception:
77
+ return False
78
+
79
+
80
+ def expected_checksum(checksums_text, binary_name):
81
+ """Return the sha256 listed for binary_name in a checksums.txt body
82
+ (``<hex> <name>`` per line, as sha256sum writes it), or None."""
83
+ for line in checksums_text.splitlines():
84
+ parts = line.split()
85
+ if len(parts) >= 2 and parts[-1] == binary_name:
86
+ return parts[0].lower()
87
+ return None
88
+
89
+
90
+ def verify_checksum(data, checksums_text, binary_name):
91
+ """Raise ValueError if data's sha256 does not match the published one.
92
+ A checksums file that does not list the binary is also an error."""
93
+ want = expected_checksum(checksums_text, binary_name)
94
+ if want is None:
95
+ raise ValueError(f"{binary_name} is not listed in checksums.txt")
96
+ got = hashlib.sha256(data).hexdigest()
97
+ if got != want:
98
+ raise ValueError(f"sha256 mismatch for {binary_name}: got {got}, expected {want}")
99
+
100
+
101
+ def _fetch(url):
102
+ req = urllib.request.Request(url, headers={"User-Agent": "factorly-pip-installer"})
103
+ with urllib.request.urlopen(req) as response:
104
+ return response.read()
105
+
106
+
107
+ def download_binary():
108
+ """Download the factorly binary from GitHub Releases and verify it
109
+ against the release's checksums.txt."""
110
+ binary_name = get_binary_name()
111
+ base = f"https://github.com/{REPO}/releases/download/v{__version__}"
112
+ url = f"{base}/{binary_name}"
113
+
114
+ # Everything the wrapper says goes to stderr: on a first launch the
115
+ # command may be `factorly serve`, whose stdout is an MCP client's
116
+ # JSON-RPC stream.
117
+ log = lambda msg: print(msg, file=sys.stderr)
118
+ log(f"Downloading factorly {__version__} for {platform.system()}/{platform.machine()}...")
119
+ log(f" {url}")
120
+
121
+ bin_dir = get_bin_dir()
122
+ os.makedirs(bin_dir, exist_ok=True)
123
+ binary_path = get_binary_path()
124
+
125
+ try:
126
+ data = _fetch(url)
127
+ except urllib.error.HTTPError as e:
128
+ log(f"Download failed: HTTP {e.code}")
129
+ log("")
130
+ log("Install manually from:")
131
+ log(f" https://github.com/{REPO}/releases/tag/v{__version__}")
132
+ sys.exit(1)
133
+ except Exception as e:
134
+ log(f"Download failed: {e}")
135
+ sys.exit(1)
136
+
137
+ # Verify against the release's checksums.txt. A release without one
138
+ # cannot be verified and says so; a mismatch is a hard stop.
139
+ try:
140
+ checksums = _fetch(f"{base}/checksums.txt").decode("utf-8", "replace")
141
+ except urllib.error.HTTPError as e:
142
+ log(f"warning: checksums.txt not available for v{__version__} (HTTP {e.code}); binary not verified")
143
+ checksums = None
144
+ except Exception as e:
145
+ log(f"warning: could not fetch checksums.txt ({e}); binary not verified")
146
+ checksums = None
147
+ if checksums is not None:
148
+ try:
149
+ verify_checksum(data, checksums, binary_name)
150
+ except ValueError as e:
151
+ log(f"Download rejected: {e}")
152
+ sys.exit(1)
153
+ log(" sha256 verified")
154
+
155
+ with open(binary_path, "wb") as f:
156
+ f.write(data)
157
+
158
+ # Make executable
159
+ if platform.system() != "Windows":
160
+ st = os.stat(binary_path)
161
+ os.chmod(binary_path, st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
162
+
163
+ log(f"Installed factorly to {binary_path}")
164
+
165
+
166
+ def main():
167
+ """Entry point: ensure binary exists, then run it."""
168
+ if not is_installed():
169
+ download_binary()
170
+
171
+ binary = get_binary_path()
172
+ if not os.path.exists(binary):
173
+ print("factorly binary not found after download attempt.", file=sys.stderr)
174
+ print("Run: pip install --force-reinstall factorly", file=sys.stderr)
175
+ sys.exit(127)
176
+
177
+ result = subprocess.run([binary] + sys.argv[1:])
178
+ sys.exit(result.returncode)
179
+
180
+
181
+ if __name__ == "__main__":
182
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: factorly
3
- Version: 0.18.1
3
+ Version: 0.20.0
4
4
  Summary: Security and governance layer for AI agent tool access
5
5
  License: GPL-3.0
6
6
  Project-URL: Homepage, https://github.com/factorly-dev/factorly
@@ -7,4 +7,5 @@ factorly.egg-info/PKG-INFO
7
7
  factorly.egg-info/SOURCES.txt
8
8
  factorly.egg-info/dependency_links.txt
9
9
  factorly.egg-info/entry_points.txt
10
- factorly.egg-info/top_level.txt
10
+ factorly.egg-info/top_level.txt
11
+ tests/test_cli.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "factorly"
7
- version = "0.18.1"
7
+ version = "0.20.0"
8
8
  description = "Security and governance layer for AI agent tool access"
9
9
  readme = "README.md"
10
10
  license = {text = "GPL-3.0"}
@@ -0,0 +1,36 @@
1
+ # Copyright 2026 Jordan Sherer <hi@jordansherer.com>
2
+ # SPDX-License-Identifier: gpl
3
+
4
+ import hashlib
5
+ import unittest
6
+
7
+ from factorly.cli import expected_checksum, verify_checksum
8
+
9
+
10
+ class ChecksumTests(unittest.TestCase):
11
+ def setUp(self):
12
+ self.data = b"binary bytes"
13
+ self.digest = hashlib.sha256(self.data).hexdigest()
14
+ self.text = (
15
+ f"{self.digest} factorly-0.1.0-linux-amd64\n"
16
+ "deadbeef factorly-0.1.0-darwin-arm64\n"
17
+ )
18
+
19
+ def test_expected_checksum_finds_binary(self):
20
+ self.assertEqual(expected_checksum(self.text, "factorly-0.1.0-linux-amd64"), self.digest)
21
+ self.assertIsNone(expected_checksum(self.text, "factorly-0.1.0-windows-amd64.exe"))
22
+
23
+ def test_verify_accepts_matching_digest(self):
24
+ verify_checksum(self.data, self.text, "factorly-0.1.0-linux-amd64")
25
+
26
+ def test_verify_rejects_mismatch(self):
27
+ with self.assertRaises(ValueError):
28
+ verify_checksum(b"tampered", self.text, "factorly-0.1.0-linux-amd64")
29
+
30
+ def test_verify_rejects_unlisted_binary(self):
31
+ with self.assertRaises(ValueError):
32
+ verify_checksum(self.data, self.text, "factorly-0.1.0-windows-amd64.exe")
33
+
34
+
35
+ if __name__ == "__main__":
36
+ unittest.main()
@@ -1,128 +0,0 @@
1
- # Copyright 2026 Jordan Sherer <hi@jordansherer.com>
2
- # SPDX-License-Identifier: gpl
3
-
4
- """Factorly CLI wrapper — downloads and runs the Go binary."""
5
-
6
- import os
7
- import platform
8
- import stat
9
- import subprocess
10
- import sys
11
- import urllib.request
12
-
13
- from factorly import __version__
14
-
15
- REPO = "factorly-dev/factorly"
16
-
17
- PLATFORM_MAP = {
18
- "Darwin": "darwin",
19
- "Linux": "linux",
20
- "Windows": "windows",
21
- }
22
-
23
- ARCH_MAP = {
24
- "x86_64": "amd64",
25
- "AMD64": "amd64",
26
- "aarch64": "arm64",
27
- "arm64": "arm64",
28
- }
29
-
30
-
31
- def get_bin_dir():
32
- """Return the directory where the binary is stored."""
33
- return os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin")
34
-
35
-
36
- def get_binary_path():
37
- """Return the full path to the factorly binary."""
38
- ext = ".exe" if platform.system() == "Windows" else ""
39
- return os.path.join(get_bin_dir(), f"factorly{ext}")
40
-
41
-
42
- def get_binary_name():
43
- """Return the platform-specific binary name for download."""
44
- plat = PLATFORM_MAP.get(platform.system())
45
- arch = ARCH_MAP.get(platform.machine())
46
-
47
- if not plat:
48
- print(f"Unsupported platform: {platform.system()}", file=sys.stderr)
49
- sys.exit(1)
50
- if not arch:
51
- print(f"Unsupported architecture: {platform.machine()}", file=sys.stderr)
52
- sys.exit(1)
53
-
54
- ext = ".exe" if platform.system() == "Windows" else ""
55
- return f"factorly-{__version__}-{plat}-{arch}{ext}"
56
-
57
-
58
- def is_installed():
59
- """Check if the correct version is already installed."""
60
- binary = get_binary_path()
61
- if not os.path.exists(binary):
62
- return False
63
- try:
64
- result = subprocess.run(
65
- [binary, "version"],
66
- capture_output=True,
67
- text=True,
68
- timeout=5,
69
- )
70
- return __version__ in result.stdout
71
- except Exception:
72
- return False
73
-
74
-
75
- def download_binary():
76
- """Download the factorly binary from GitHub Releases."""
77
- binary_name = get_binary_name()
78
- url = f"https://github.com/{REPO}/releases/download/v{__version__}/{binary_name}"
79
-
80
- print(f"Downloading factorly {__version__} for {platform.system()}/{platform.machine()}...")
81
- print(f" {url}")
82
-
83
- bin_dir = get_bin_dir()
84
- os.makedirs(bin_dir, exist_ok=True)
85
- binary_path = get_binary_path()
86
-
87
- try:
88
- req = urllib.request.Request(url, headers={"User-Agent": "factorly-pip-installer"})
89
- with urllib.request.urlopen(req) as response:
90
- data = response.read()
91
- except urllib.error.HTTPError as e:
92
- print(f"Download failed: HTTP {e.code}", file=sys.stderr)
93
- print(f"", file=sys.stderr)
94
- print(f"Install manually from:", file=sys.stderr)
95
- print(f" https://github.com/{REPO}/releases/tag/v{__version__}", file=sys.stderr)
96
- sys.exit(1)
97
- except Exception as e:
98
- print(f"Download failed: {e}", file=sys.stderr)
99
- sys.exit(1)
100
-
101
- with open(binary_path, "wb") as f:
102
- f.write(data)
103
-
104
- # Make executable
105
- if platform.system() != "Windows":
106
- st = os.stat(binary_path)
107
- os.chmod(binary_path, st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
108
-
109
- print(f"Installed factorly to {binary_path}")
110
-
111
-
112
- def main():
113
- """Entry point: ensure binary exists, then run it."""
114
- if not is_installed():
115
- download_binary()
116
-
117
- binary = get_binary_path()
118
- if not os.path.exists(binary):
119
- print("factorly binary not found after download attempt.", file=sys.stderr)
120
- print("Run: pip install --force-reinstall factorly", file=sys.stderr)
121
- sys.exit(127)
122
-
123
- result = subprocess.run([binary] + sys.argv[1:])
124
- sys.exit(result.returncode)
125
-
126
-
127
- if __name__ == "__main__":
128
- main()
File without changes
File without changes