Skip to main content

Pack Hugging Face models into chunked, hashed, self-verifying bundles for air-gapped transfer.

Project description

modelferry

Pack a Hugging Face model into a chunked, hashed, self-verifying bundle on the connected side. Verify and reassemble it on the disconnected side with no network and no third-party dependencies.

I thought air-gapped just meant no internet. Same stack, cut the outbound route. How hard could it be.

Then I had to get a model in. You don't pull 40GB from Hugging Face and go. It moves through whatever channel the client approves, checksums get verified by hand, and the file usually gets chopped up because the transfer media won't take a single object that size. I've watched senior engineers lose an entire afternoon to this. At the end of the afternoon, the only thing you can hand the security officer is "trust me, it's the same file."

modelferry does that in one command and gives you the paperwork at the end.

The manifest is the product. It doubles as the approval document that gets signed off before the bundle crosses the air gap, and as the checklist the receiving side runs against on arrival.

modelferry packing, inspecting, verifying, and unpacking a model

What it does

modelferry has two sides.

On the connected side, pack downloads a model repo pinned to a commit, splits files that are too big for the transfer media into parts, hashes everything with sha256, and writes a bundle: the payload, a machine-readable manifest.json, a human-readable MANIFEST.md for review, and a copy of the offline verifier itself.

On the disconnected side, verify recomputes every hash and checks it against the manifest, and unpack reassembles the original file tree so vLLM or transformers can load it directly. Both run from a single standard-library Python file that ships inside every bundle, so the receiving environment needs nothing installed and no network. Python 3.9 or newer is the only requirement there.

Install

The connected side is a command-line tool, so install it in its own isolated environment. Nothing imports modelferry, and an isolated install keeps its dependency versions from colliding with whatever your projects have pinned.

uv tool install modelferry

or

pipx install modelferry

Plain pip install modelferry works too if you would rather put it in an existing environment.

The disconnected side installs nothing. The verifier travels inside the bundle at tools/modelferry_offline.py and runs against the system Python.

Five-minute quickstart

This uses a tiny public test repo so you can see the whole round trip in a couple of minutes without downloading real weights.

That repo ships one model in three serialization formats: model.safetensors, pytorch_model.bin (PyTorch), and tf_model.h5 (TensorFlow). You only need one, so exclude the other two. The safetensors file that remains is only a few hundred KiB, so pass a small --chunk-size to force it to split into parts and see chunking work:

modelferry pack hf-internal-testing/tiny-random-gpt2 --dest ./bundles \
    --chunk-size 200K --exclude "*.bin" --exclude "*.h5"

That writes ./bundles/tiny-random-gpt2__<sha>/. pack prints the exact bundle path on its last line, so use whatever it printed in place of <sha> below.

Now pretend you have carried the bundle across the air gap. Everything from here runs from inside the bundle with the bundled tool, exactly as the receiving side would, with no modelferry install. This is the same three-command sequence MANIFEST.md prints and scripts/e2e_airgap.sh proves under --network none.

cd ./bundles/tiny-random-gpt2__<sha>

Look at what a reviewer would see, then check every hash against the manifest:

python3 tools/modelferry_offline.py inspect .
python3 tools/modelferry_offline.py verify .

inspect prints the recomputed manifest_sha256 to compare against the copy you approved before transfer, and verify prints "verify OK" only when every object matches. Then reassemble the model tree:

python3 tools/modelferry_offline.py unpack . ../model

../model now holds the original repo tree, and ../model/UNPACK_RECEIPT.json records that it was verified on the way in.

Packing a real model

modelferry pack Qwen/Qwen2.5-7B-Instruct --dest /media/usb --revision main

Useful options:

  • --revision pins the download. It defaults to main and is resolved to a commit SHA before anything is fetched. The manifest records both the requested revision and the resolved SHA.
  • --chunk-size sets the maximum part size. It defaults to 3900M, which fits under FAT32's 4 GiB file limit with margin. Pass 16G for larger media, or none to store every file whole.
  • --include and --exclude are fnmatch patterns against repo-relative paths. Exclude wins. The common case is --exclude "*.bin" when safetensors already cover the weights.
  • --staging is where the download lands before packing. It defaults to ~/.cache/modelferry/. Re-running pack after an interruption resumes the download.

Hugging Face auth is the HF_TOKEN environment variable, and only that. It is never a flag and never written into a bundle, manifest, receipt, or log. Custom hub endpoints work through the standard HF_ENDPOINT variable, and the endpoint used is recorded in the manifest.

The manifest

manifest.json is deterministic: the same inputs produce a byte-identical file, so it diffs cleanly and can be compared across sites. It records the source repo and resolved commit, the license and gated flag, every file with its size and whole-file sha256, the parts each chunked file was split into with their own hashes, and the sha256 of the bundled verifier.

MANIFEST.md is the same information rendered for a human reviewer. It names two moments: before transfer, approve and keep a copy of it; on arrival, compare the manifest.json checksum to the copy you kept, then run verify. If the license could not be determined from repo metadata it is recorded as UNKNOWN and flagged at the top of the document.

Trust model

Stated honestly, because this is the part a security review turns on.

v1 protects against accidental corruption, incomplete transfers, media errors, and casual tampering with payload files. Any of those shows up as a hash mismatch, a missing part, or an extra file, and verify exits non-zero and names what failed.

v1 does not protect against an adversary who can modify the payload, the manifest, and the bundled verifier together. Defending against that requires signature verification with a key held out-of-band, which is planned for v1.1 (minisign).

The mitigation available today is out-of-band verification of the verifier. manifest.json records the sha256 of the bundled offline.py, and every release publishes the canonical offline.py hash in its release notes. A receiving site can check the bundled verifier against that published hash before trusting it, or ignore the bundled copy and bring its own known-good verifier.

Exit codes

0  success
1  integrity failure (verify mismatch, missing or extra file, unpack hash
   failure, path-safety violation)
2  usage error (bad arguments, unknown manifest version, malformed manifest)
3  source error (network failure, HF auth, 404, gated without token)
4  local filesystem error (permissions, disk full, destination exists
   without --force)

Development

uv sync
uv run pytest -m "not network"     # default, offline
uv run pytest                      # includes the network integration test
uv run ruff check . && uv run ruff format .
bash scripts/e2e_airgap.sh         # docker, runs verify/unpack with --network none

# Resolve every dependency to its declared floor and run the suite, so the
# lower bounds in pyproject are tested claims. The network test is included so
# the huggingface_hub floor is exercised for real.
uv sync --resolution lowest-direct && uv run pytest -m "not network" && uv run pytest -m network

# Build the wheel and round-trip pack/verify/unpack from a clean venv with the
# repo off the path, so the installed package (not the checkout) is exercised.
uv build && uv venv /tmp/wenv && uv pip install --python /tmp/wenv/bin/python dist/*.whl

SPEC.md is the contract. src/modelferry/offline.py is the trust surface: it is standard library only, runs on CPython 3.9, imports nothing from the modelferry package, and is copied verbatim into every bundle. A test enforces all of that.

License

Apache-2.0. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

modelferry-0.1.0.tar.gz (1.7 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

modelferry-0.1.0-py3-none-any.whl (28.6 kB view details)

Uploaded Python 3

File details

Details for the file modelferry-0.1.0.tar.gz.

File metadata

  • Download URL: modelferry-0.1.0.tar.gz
  • Upload date:
  • Size: 1.7 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for modelferry-0.1.0.tar.gz
Algorithm Hash digest
SHA256 7bd81c72d8a5e9d486d97526e6bd7f21b2438a3c399283fb5cb70a73a1d03b41
MD5 b8f9f35857e12cf5247c0156bcf98101
BLAKE2b-256 7c95014f1c1002fde8ba9d2d223a9684b44dd90a454278d98de01e44872f9d3f

See more details on using hashes here.

Provenance

The following attestation bundles were made for modelferry-0.1.0.tar.gz:

Publisher: release.yml on HamzaAhmedWajeeh/modelferry

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file modelferry-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: modelferry-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 28.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for modelferry-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4ce17aa94ad15e2e236a7e1dc60929601bd1afd239830903dc5a70369ea0f98a
MD5 3e2974ce08bb4ed05e0faacfc5703e33
BLAKE2b-256 2f9be20dcc22cda7b79f294ed10a6c186ea8751b9bb508d62ed0b17a79b4038c

See more details on using hashes here.

Provenance

The following attestation bundles were made for modelferry-0.1.0-py3-none-any.whl:

Publisher: release.yml on HamzaAhmedWajeeh/modelferry

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page