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.
- {modelferry-0.2.0 → modelferry-0.2.1}/CHANGELOG.md +22 -1
- {modelferry-0.2.0 → modelferry-0.2.1}/PKG-INFO +65 -7
- {modelferry-0.2.0 → modelferry-0.2.1}/README.md +63 -5
- {modelferry-0.2.0 → modelferry-0.2.1}/pyproject.toml +2 -2
- {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/__init__.py +1 -1
- {modelferry-0.2.0 → modelferry-0.2.1}/uv.lock +1 -1
- {modelferry-0.2.0 → modelferry-0.2.1}/.gitattributes +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/.github/workflows/ci.yml +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/.github/workflows/release.yml +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/.gitignore +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/BUILD_PLAN.md +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/CLAUDE.md +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/LICENSE +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/RESOURCES.md +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/SPEC.md +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/docs/Dockerfile.vhs +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/docs/demo.gif +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/docs/demo.md +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/docs/demo.tape +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/scripts/e2e_airgap.sh +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/cli.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/errors.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/hf.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/manifest.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/offline.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/pack.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/signing.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/src/modelferry/verify_signature.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/_bundle.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/conftest.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_cli_skeleton.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_manifest_v2.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_corruption.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_manifest.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_pathsafety.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_roundtrip.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_stdlib_lint.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_unpack_hardening.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_offline_verify.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_pack_dest.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_pack_hardening.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_pack_integration.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_pack_signing.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_pack_writer.py +0 -0
- {modelferry-0.2.0 → modelferry-0.2.1}/tests/test_signing.py +0 -0
- {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.
|
|
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.
|
|
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:
|
|
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
|
|
48
|
-
|
|
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
|
-

|
|
51
|
+

|
|
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
|
|
20
|
-
|
|
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
|
-

|
|
23
|
+

|
|
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.
|
|
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 = "
|
|
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",
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|