modelferry 0.2.0__tar.gz → 0.2.1__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.
Files changed (46) hide show
  1. {modelferry-0.2.0 → modelferry-0.2.1}/CHANGELOG.md +22 -1
  2. {modelferry-0.2.0 → modelferry-0.2.1}/PKG-INFO +65 -7
  3. {modelferry-0.2.0 → modelferry-0.2.1}/README.md +63 -5
  4. {modelferry-0.2.0 → modelferry-0.2.1}/pyproject.toml +2 -2
  5. {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/__init__.py +1 -1
  6. {modelferry-0.2.0 → modelferry-0.2.1}/uv.lock +1 -1
  7. {modelferry-0.2.0 → modelferry-0.2.1}/.gitattributes +0 -0
  8. {modelferry-0.2.0 → modelferry-0.2.1}/.github/workflows/ci.yml +0 -0
  9. {modelferry-0.2.0 → modelferry-0.2.1}/.github/workflows/release.yml +0 -0
  10. {modelferry-0.2.0 → modelferry-0.2.1}/.gitignore +0 -0
  11. {modelferry-0.2.0 → modelferry-0.2.1}/BUILD_PLAN.md +0 -0
  12. {modelferry-0.2.0 → modelferry-0.2.1}/CLAUDE.md +0 -0
  13. {modelferry-0.2.0 → modelferry-0.2.1}/LICENSE +0 -0
  14. {modelferry-0.2.0 → modelferry-0.2.1}/RESOURCES.md +0 -0
  15. {modelferry-0.2.0 → modelferry-0.2.1}/SPEC.md +0 -0
  16. {modelferry-0.2.0 → modelferry-0.2.1}/docs/Dockerfile.vhs +0 -0
  17. {modelferry-0.2.0 → modelferry-0.2.1}/docs/demo.gif +0 -0
  18. {modelferry-0.2.0 → modelferry-0.2.1}/docs/demo.md +0 -0
  19. {modelferry-0.2.0 → modelferry-0.2.1}/docs/demo.tape +0 -0
  20. {modelferry-0.2.0 → modelferry-0.2.1}/scripts/e2e_airgap.sh +0 -0
  21. {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/cli.py +0 -0
  22. {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/errors.py +0 -0
  23. {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/hf.py +0 -0
  24. {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/manifest.py +0 -0
  25. {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/offline.py +0 -0
  26. {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/pack.py +0 -0
  27. {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/signing.py +0 -0
  28. {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/verify_signature.py +0 -0
  29. {modelferry-0.2.0 → modelferry-0.2.1}/tests/_bundle.py +0 -0
  30. {modelferry-0.2.0 → modelferry-0.2.1}/tests/conftest.py +0 -0
  31. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_cli_skeleton.py +0 -0
  32. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_manifest_v2.py +0 -0
  33. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_corruption.py +0 -0
  34. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_manifest.py +0 -0
  35. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_pathsafety.py +0 -0
  36. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_roundtrip.py +0 -0
  37. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_stdlib_lint.py +0 -0
  38. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_unpack_hardening.py +0 -0
  39. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_verify.py +0 -0
  40. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_pack_dest.py +0 -0
  41. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_pack_hardening.py +0 -0
  42. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_pack_integration.py +0 -0
  43. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_pack_signing.py +0 -0
  44. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_pack_writer.py +0 -0
  45. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_signing.py +0 -0
  46. {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_verify_signature.py +0 -0
@@ -6,6 +6,26 @@ All notable changes to modelferry are recorded here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.1] - 2026-07-22
10
+
11
+ Documentation and packaging. No code changes: `offline.py` and all functionality
12
+ are unchanged from 0.2.0, so a 0.2.1 bundle verifies identically and the bundled
13
+ verifier's canonical sha256 is the same.
14
+
15
+ ### Changed
16
+
17
+ - The README documents `pack --sign` and `verify-signature` in the usage and
18
+ quickstart sections, with a runnable "Signing a bundle" example, instead of only
19
+ in the trust-model discussion. `--sign` is listed among the pack flags, and the
20
+ manifest section notes the optional `signing` block and the `manifest.json.sig`
21
+ sidecar.
22
+
23
+ ### Fixed
24
+
25
+ - The demo GIF in the README uses an absolute raw URL, so it renders on the PyPI
26
+ project page (relative paths do not resolve there).
27
+ - The package author metadata is set instead of the placeholder.
28
+
9
29
  ## [0.2.0] - 2026-07-22
10
30
 
11
31
  Signing. A bundle's manifest can now be signed, and its authenticity checked
@@ -104,6 +124,7 @@ First release.
104
124
  publishes the canonical `offline.py` sha256 so a receiving site can check the
105
125
  bundled verifier out-of-band.
106
126
 
107
- [Unreleased]: https://github.com/HamzaAhmedWajeeh/modelferry/compare/v0.2.0...HEAD
127
+ [Unreleased]: https://github.com/HamzaAhmedWajeeh/modelferry/compare/v0.2.1...HEAD
128
+ [0.2.1]: https://github.com/HamzaAhmedWajeeh/modelferry/releases/tag/v0.2.1
108
129
  [0.2.0]: https://github.com/HamzaAhmedWajeeh/modelferry/releases/tag/v0.2.0
109
130
  [0.1.0]: https://github.com/HamzaAhmedWajeeh/modelferry/releases/tag/v0.1.0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelferry
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Pack Hugging Face models into chunked, hashed, self-verifying bundles for air-gapped transfer.
5
5
  Project-URL: Homepage, https://github.com/HamzaAhmedWajeeh/modelferry
6
6
  Project-URL: Repository, https://github.com/HamzaAhmedWajeeh/modelferry
7
7
  Project-URL: Changelog, https://github.com/HamzaAhmedWajeeh/modelferry/blob/main/CHANGELOG.md
8
8
  Project-URL: Issues, https://github.com/HamzaAhmedWajeeh/modelferry/issues
9
- Author: modelferry contributors
9
+ Author: Hamza Ahmed Wajeeh
10
10
  License-Expression: Apache-2.0
11
11
  License-File: LICENSE
12
12
  Keywords: air-gap,huggingface,model-transfer,offline,sha256
@@ -44,11 +44,11 @@ the security officer is "trust me, it's the same file."
44
44
 
45
45
  modelferry does that in one command and gives you the paperwork at the end.
46
46
 
47
- The manifest is the product. It doubles as the approval document that gets
48
- signed off before the bundle crosses the air gap, and as the checklist the
49
- receiving side runs against on arrival.
47
+ The manifest is the product. It doubles as the approval document a security
48
+ officer reviews and approves before the bundle crosses the air gap, and as the
49
+ checklist the receiving side runs against on arrival.
50
50
 
51
- ![modelferry packing, inspecting, verifying, and unpacking a model](docs/demo.gif)
51
+ ![modelferry packing, inspecting, verifying, and unpacking a model](https://raw.githubusercontent.com/HamzaAhmedWajeeh/modelferry/main/docs/demo.gif)
52
52
 
53
53
  ## What it does
54
54
 
@@ -67,6 +67,11 @@ Python file that ships inside every bundle, so the receiving environment needs
67
67
  nothing installed and no network. Python 3.9 or newer is the only requirement
68
68
  there.
69
69
 
70
+ `pack --sign` optionally signs the manifest on the connected side, and
71
+ `verify-signature` checks that signature against a trusted key. That authenticity
72
+ check runs on the connected or approval side where the key lives, not on the bare
73
+ host. Signing is covered below.
74
+
70
75
  ## Install
71
76
 
72
77
  The connected side is a command-line tool, so install it in its own isolated
@@ -135,6 +140,52 @@ python3 tools/modelferry_offline.py unpack . ../model
135
140
  `../model` now holds the original repo tree, and `../model/UNPACK_RECEIPT.json`
136
141
  records that it was verified on the way in.
137
142
 
143
+ ## Signing a bundle
144
+
145
+ Signing is optional and additive. It adds an authenticity check on top of the
146
+ integrity check above: a signed bundle proves its manifest was signed by a key you
147
+ trust, so a bundle someone else rebuilt from scratch fails verification even when
148
+ its own hashes are internally consistent. Packing without `--sign` is unchanged.
149
+
150
+ You need an ed25519 keypair. Generate one outside any repo and keep the secret key
151
+ off version control:
152
+
153
+ ```
154
+ python3 -c "from modelferry.signing import Ed25519Signer; Ed25519Signer.generate_keypair('signing.key', 'signing.pub')"
155
+ ```
156
+
157
+ Point `MODELFERRY_SIGNING_KEY` at the secret key and pack with `--sign`:
158
+
159
+ ```
160
+ export MODELFERRY_SIGNING_KEY="$PWD/signing.key"
161
+ modelferry pack hf-internal-testing/tiny-random-gpt2 --dest ./bundles \
162
+ --chunk-size 200K --exclude "*.bin" --exclude "*.h5" --sign
163
+ ```
164
+
165
+ The bundle now carries a detached signature at `manifest.json.sig` over the exact
166
+ `manifest.json` bytes, plus a `signing` block in the manifest. Pack self-verifies
167
+ that signature before it finishes, so a bad signature fails the pack rather than
168
+ shipping. Check it yourself against the public key:
169
+
170
+ ```
171
+ modelferry verify-signature ./bundles/tiny-random-gpt2__<sha> --public-key signing.pub
172
+ ```
173
+
174
+ It prints `VALID` when the signature matches the trusted key. Any other result
175
+ exits non-zero: `BAD_SIGNATURE` if the manifest was altered or signed by another
176
+ key, `KEY_MISMATCH` if the signature is valid but for a different key than the one
177
+ given, `MISSING_SIG` if the sidecar is absent, or `UNSIGNED` for a bundle that was
178
+ never signed. None of those should be trusted.
179
+
180
+ The public key is what a verifier trusts, and it has to reach them out-of-band.
181
+ modelferry has no signing key of its own: you sign your bundles with your key, and
182
+ whoever operates the approval authority for the receiving environment distributes
183
+ the trusted public key, the same people who approve the manifest checksum. The
184
+ `verify-signature` check runs where that key lives, on the connected or approval
185
+ side, not on the bare disconnected host. See the [Trust model](#trust-model) for
186
+ the full picture, including why the bare-host integrity check does not verify the
187
+ signature.
188
+
138
189
  ## Packing a real model
139
190
 
140
191
  ```
@@ -155,6 +206,11 @@ Useful options:
155
206
  - `--staging` is where the download lands before packing. It defaults to
156
207
  `~/.cache/modelferry/`. Re-running `pack` after an interruption resumes the
157
208
  download.
209
+ - `--sign` signs the manifest so the bundle can be checked for authenticity with
210
+ `verify-signature`. The signing key path comes from the `MODELFERRY_SIGNING_KEY`
211
+ environment variable, only that, never a flag and never written into the bundle.
212
+ `--sign` with no key set is an error, not a silent unsigned pack. Without
213
+ `--sign` the bundle is unsigned. See [Signing a bundle](#signing-a-bundle).
158
214
 
159
215
  Hugging Face auth is the `HF_TOKEN` environment variable, and only that. It is
160
216
  never a flag and never written into a bundle, manifest, receipt, or log. Custom
@@ -167,7 +223,9 @@ used is recorded in the manifest.
167
223
  so it diffs cleanly and can be compared across sites. It records the source repo
168
224
  and resolved commit, the license and gated flag, every file with its size and
169
225
  whole-file sha256, the parts each chunked file was split into with their own
170
- hashes, and the sha256 of the bundled verifier.
226
+ hashes, and the sha256 of the bundled verifier. When a bundle is signed it also
227
+ carries an optional `signing` block, and the detached signature sits beside it at
228
+ `manifest.json.sig`.
171
229
 
172
230
  `MANIFEST.md` is the same information rendered for a human reviewer. It names two
173
231
  moments: before transfer, approve and keep a copy of it; on arrival, compare the
@@ -16,11 +16,11 @@ the security officer is "trust me, it's the same file."
16
16
 
17
17
  modelferry does that in one command and gives you the paperwork at the end.
18
18
 
19
- The manifest is the product. It doubles as the approval document that gets
20
- signed off before the bundle crosses the air gap, and as the checklist the
21
- receiving side runs against on arrival.
19
+ The manifest is the product. It doubles as the approval document a security
20
+ officer reviews and approves before the bundle crosses the air gap, and as the
21
+ checklist the receiving side runs against on arrival.
22
22
 
23
- ![modelferry packing, inspecting, verifying, and unpacking a model](docs/demo.gif)
23
+ ![modelferry packing, inspecting, verifying, and unpacking a model](https://raw.githubusercontent.com/HamzaAhmedWajeeh/modelferry/main/docs/demo.gif)
24
24
 
25
25
  ## What it does
26
26
 
@@ -39,6 +39,11 @@ Python file that ships inside every bundle, so the receiving environment needs
39
39
  nothing installed and no network. Python 3.9 or newer is the only requirement
40
40
  there.
41
41
 
42
+ `pack --sign` optionally signs the manifest on the connected side, and
43
+ `verify-signature` checks that signature against a trusted key. That authenticity
44
+ check runs on the connected or approval side where the key lives, not on the bare
45
+ host. Signing is covered below.
46
+
42
47
  ## Install
43
48
 
44
49
  The connected side is a command-line tool, so install it in its own isolated
@@ -107,6 +112,52 @@ python3 tools/modelferry_offline.py unpack . ../model
107
112
  `../model` now holds the original repo tree, and `../model/UNPACK_RECEIPT.json`
108
113
  records that it was verified on the way in.
109
114
 
115
+ ## Signing a bundle
116
+
117
+ Signing is optional and additive. It adds an authenticity check on top of the
118
+ integrity check above: a signed bundle proves its manifest was signed by a key you
119
+ trust, so a bundle someone else rebuilt from scratch fails verification even when
120
+ its own hashes are internally consistent. Packing without `--sign` is unchanged.
121
+
122
+ You need an ed25519 keypair. Generate one outside any repo and keep the secret key
123
+ off version control:
124
+
125
+ ```
126
+ python3 -c "from modelferry.signing import Ed25519Signer; Ed25519Signer.generate_keypair('signing.key', 'signing.pub')"
127
+ ```
128
+
129
+ Point `MODELFERRY_SIGNING_KEY` at the secret key and pack with `--sign`:
130
+
131
+ ```
132
+ export MODELFERRY_SIGNING_KEY="$PWD/signing.key"
133
+ modelferry pack hf-internal-testing/tiny-random-gpt2 --dest ./bundles \
134
+ --chunk-size 200K --exclude "*.bin" --exclude "*.h5" --sign
135
+ ```
136
+
137
+ The bundle now carries a detached signature at `manifest.json.sig` over the exact
138
+ `manifest.json` bytes, plus a `signing` block in the manifest. Pack self-verifies
139
+ that signature before it finishes, so a bad signature fails the pack rather than
140
+ shipping. Check it yourself against the public key:
141
+
142
+ ```
143
+ modelferry verify-signature ./bundles/tiny-random-gpt2__<sha> --public-key signing.pub
144
+ ```
145
+
146
+ It prints `VALID` when the signature matches the trusted key. Any other result
147
+ exits non-zero: `BAD_SIGNATURE` if the manifest was altered or signed by another
148
+ key, `KEY_MISMATCH` if the signature is valid but for a different key than the one
149
+ given, `MISSING_SIG` if the sidecar is absent, or `UNSIGNED` for a bundle that was
150
+ never signed. None of those should be trusted.
151
+
152
+ The public key is what a verifier trusts, and it has to reach them out-of-band.
153
+ modelferry has no signing key of its own: you sign your bundles with your key, and
154
+ whoever operates the approval authority for the receiving environment distributes
155
+ the trusted public key, the same people who approve the manifest checksum. The
156
+ `verify-signature` check runs where that key lives, on the connected or approval
157
+ side, not on the bare disconnected host. See the [Trust model](#trust-model) for
158
+ the full picture, including why the bare-host integrity check does not verify the
159
+ signature.
160
+
110
161
  ## Packing a real model
111
162
 
112
163
  ```
@@ -127,6 +178,11 @@ Useful options:
127
178
  - `--staging` is where the download lands before packing. It defaults to
128
179
  `~/.cache/modelferry/`. Re-running `pack` after an interruption resumes the
129
180
  download.
181
+ - `--sign` signs the manifest so the bundle can be checked for authenticity with
182
+ `verify-signature`. The signing key path comes from the `MODELFERRY_SIGNING_KEY`
183
+ environment variable, only that, never a flag and never written into the bundle.
184
+ `--sign` with no key set is an error, not a silent unsigned pack. Without
185
+ `--sign` the bundle is unsigned. See [Signing a bundle](#signing-a-bundle).
130
186
 
131
187
  Hugging Face auth is the `HF_TOKEN` environment variable, and only that. It is
132
188
  never a flag and never written into a bundle, manifest, receipt, or log. Custom
@@ -139,7 +195,9 @@ used is recorded in the manifest.
139
195
  so it diffs cleanly and can be compared across sites. It records the source repo
140
196
  and resolved commit, the license and gated flag, every file with its size and
141
197
  whole-file sha256, the parts each chunked file was split into with their own
142
- hashes, and the sha256 of the bundled verifier.
198
+ hashes, and the sha256 of the bundled verifier. When a bundle is signed it also
199
+ carries an optional `signing` block, and the detached signature sits beside it at
200
+ `manifest.json.sig`.
143
201
 
144
202
  `MANIFEST.md` is the same information rendered for a human reviewer. It names two
145
203
  moments: before transfer, approve and keep a copy of it; on arrival, compare the
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "modelferry"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Pack Hugging Face models into chunked, hashed, self-verifying bundles for air-gapped transfer."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = "Apache-2.0"
12
12
  license-files = ["LICENSE"]
13
- authors = [{ name = "modelferry contributors" }]
13
+ authors = [{ name = "Hamza Ahmed Wajeeh" }]
14
14
  keywords = ["huggingface", "air-gap", "offline", "sha256", "model-transfer"]
15
15
  classifiers = [
16
16
  "Development Status :: 3 - Alpha",
@@ -1,3 +1,3 @@
1
1
  """modelferry: pack Hugging Face models into chunked, self-verifying bundles."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.1"
@@ -325,7 +325,7 @@ wheels = [
325
325
 
326
326
  [[package]]
327
327
  name = "modelferry"
328
- version = "0.2.0"
328
+ version = "0.2.1"
329
329
  source = { editable = "." }
330
330
  dependencies = [
331
331
  { name = "huggingface-hub" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes