Metadata-Version: 2.4
Name: airgap-bundle
Version: 0.1.0
Summary: Package a Python ML inference project into a single verifiable air-gapped archive.
Project-URL: Homepage, https://github.com/ziglicis/airgap-bundle
Project-URL: Repository, https://github.com/ziglicis/airgap-bundle
Project-URL: Issues, https://github.com/ziglicis/airgap-bundle/issues
Author: Zigurds Licis
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: air-gap,offline,packaging,sbom,supply-chain
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: System :: Archiving :: Packaging
Classifier: Topic :: System :: Software Distribution
Requires-Python: >=3.11
Requires-Dist: packaging>=24.0
Requires-Dist: pydantic<3.0,>=2.7
Requires-Dist: pyyaml>=6.0
Requires-Dist: rich<15.0,>=13.7
Requires-Dist: tomli-w>=1.0
Requires-Dist: typer<1.0,>=0.12
Requires-Dist: zstandard>=0.22
Provides-Extra: azure
Requires-Dist: azure-storage-blob; extra == 'azure'
Provides-Extra: gcs
Requires-Dist: google-cloud-storage; extra == 'gcs'
Provides-Extra: hf
Requires-Dist: huggingface-hub; extra == 'hf'
Provides-Extra: http
Requires-Dist: httpx>=0.27; extra == 'http'
Provides-Extra: s3
Requires-Dist: boto3; extra == 's3'
Description-Content-Type: text/markdown

# airgap-bundle

Package a Python ML inference project (source, dependencies, container images, and
model weights) into a single verifiable archive that transfers across an air gap and
installs on a disconnected host with one command.

Dependency resolution assumes a network; verification assumes you can enumerate and hash
every artifact before it touches the high side; and installers love to reach out for "just
one thing." `airgap-bundle` closes those gaps: it resolves the full transitive dependency
closure *for the target platform*, vendors images and models alongside it, produces a
hashed manifest a reviewer can audit without executing anything, and fails loudly rather
than silently degrading if the install path attempts the network.

## Install

The tool is not published yet; install it from source:

```sh
git clone https://github.com/ziglicis/airgap-bundle
cd airgap-bundle
uv tool install .
```

Requires Python 3.11+ and [`uv`](https://github.com/astral-sh/uv). Building bundles also
needs `pip` (cross-platform wheel download). Optional features shell out to standard tools,
required only when that feature is used:

| Feature | Tool |
|---|---|
| Container images | `docker`/`podman`, or `skopeo` (preferred) |
| Signing / verifying signatures | `cosign` |
| SBOM generation | `syft` |
| CVE gating | `grype` |

## Build a bundle (low side)

On the connected build host, point it at a project with an `airgap.toml`:

```sh
airgap-bundle build path/to/project --out dist/app.tar.zst
```

This resolves a hash-locked dependency closure for the target platform, downloads the
wheels, exports any declared images and models, scans the source for secrets, and writes a
reproducible `.tar.zst` containing a `manifest.json` inventory and a coreutils-verifiable
`checksums.txt`. Before it emits anything, it verifies the bundle against its own
manifest; a bundle that cannot verify itself is never written.

Useful flags:

```sh
airgap-bundle build proj --platform linux/arm64        # cross-build for another arch
airgap-bundle build proj --sbom both                   # SPDX + CycloneDX SBOM (syft)
airgap-bundle build proj --sign --signing-key ck.key   # detached cosign signature
```

## Verify and inspect (the reviewer's tools)

Read-only, never executes bundle content, never extracts to a persistent location:

```sh
airgap-bundle verify app.tar.zst --deep               # structural + integrity + deep digests
airgap-bundle verify app.tar.zst --policy policy.yaml # + license/size/sdist policy
airgap-bundle verify app.tar.zst --pubkey ck.pub      # + cosign signature (offline)
airgap-bundle inspect app.tar.zst --packages          # contents without extracting
```

`verify` emits a checklist (or `--json`) and exits non-zero on the first failing level:
integrity (6), signature (7), policy (8).

## Install a bundle (high side)

Move `dist/app.tar.zst` across the air gap. On the disconnected host you have two paths.

**With `airgap-bundle` present** — runs under a network guard, loads bundled images, and
writes an audit receipt:

```sh
airgap-bundle install app.tar.zst --prefix /opt/app
```

**With nothing but coreutils + Python** — extract and run the bundled bootstrap. It needs
no `airgap-bundle` and no network:

```sh
tar --zstd -xf app.tar.zst
sh app-*/install.sh /opt/app
```

Either path creates a virtualenv, installs the vendored wheels with `--no-index`, wires up
the project's entrypoints, and (where a runtime is present) loads the container images
(entirely offline).

## What's in a bundle

```
app-1.0.0/
├── manifest.json          # authoritative inventory: every artifact, hash, and size
├── checksums.txt          # sha256sum -c verifiable with coreutils alone
├── install.sh             # POSIX-sh offline bootstrap
├── python/
│   ├── requirements.lock  # fully pinned, hash-locked (the reproducibility anchor)
│   └── wheels/<platform>/  # the transitive closure, target-platform wheels
├── project/               # the project's own wheel, if it is packaged
├── source/                # vendored project source
├── images/                # container images, docker/oci archives, zstd-compressed
├── models/                # fetched model weights, hash-verified
└── sbom/                  # SPDX/CycloneDX SBOM + per-image SBOMs (if --sbom)
```

## Configuration

A project declares what to bundle in `airgap.toml`:

```toml
[bundle]
name = "app"
version = "1.0.0"

[platform]
targets = ["linux/amd64"]
python = "3.11"

[python]
requirements = "requirements.txt"

[source]
include = ["src/**"]

[install]
prefix = "/opt/app"
entrypoints = ["app = app.cli:main"]

[[images]]
ref = "docker.io/library/redis:7.2-alpine"

[[models]]
name = "detector"
uri = "https://models.example.com/detector.onnx"
sha256 = "3f8a…"
dest = "models/detector.onnx"

[policy]
require_cve_scan = true
cve_fail_on = "high"
```

## Supply-chain security

- **Secret scanning** - the build scans vendored source for private keys, cloud access
  keys, and high-entropy tokens in credential-named files, and aborts (exit 10). Overridable
  only by an explicit `--allow-secrets`, which is recorded in the manifest; never by config.
- **SBOM** - `--sbom spdx|cyclonedx|both` runs `syft` over the staging tree and every bundled
  image, with pinned schema versions (SPDX 2.3, CycloneDX 1.5).
- **Signing** - `--sign` produces a detached `cosign` signature over the archive; `verify
  --pubkey` checks it fully offline (key-based, no transparency log needed).
- **CVE gating** - with `policy.require_cve_scan`, `grype` scans the SBOM and fails the build
  on any finding at or above `policy.cve_fail_on`.

## Guarantees

- **Reproducible** - deterministic tar (sorted, pinned mtimes, zeroed ownership) and a
  content-addressed bundle id; a pure-Python bundle built twice from the same inputs is
  byte-identical. (Image tars and SBOMs carry documented non-determinism and are excluded
  from the strict guarantee; the reproducibility test builds the `minimal` and
  `native-deps` fixtures twice and asserts byte-identical output.)
- **Verifiable** - every file is hashed in the manifest and in `checksums.txt`; a reviewer
  can enumerate and check everything, and validate a signature, without executing content.
- **Isolated** - a guarded install runs in a network namespace (or an audited offline
  fallback) and refuses to reach the network. Proven by an acceptance test that installs a
  bundle in a `--network none` container and asserts no connection was attempted.

## Threat model (scope)

`airgap-bundle` defends the *transfer and install* boundary: it ensures the artifact a
reviewer approved is byte-for-byte what installs on the high side, that it carries a
signed, hashed inventory, and that install cannot silently reach the network. It is **not**
a runtime sandbox, does not attest the build environment itself, and (by design for
air-gap) verifies key-based signatures without a transparency log. VEX documents and
in-toto build attestation are recognized next steps, not yet implemented.

## Documentation

- [Architecture & threat model](docs/architecture.md): the build pipeline, verify
  levels, install flow, reproducibility, and what the tool does and does not defend.

## Status

The full command set: `build`, `install`, `verify`, `inspect`, `init`, `diff`; works
today, along with cross-architecture builds, container images, model fetching, the
supply-chain layer (secret scan, SBOM, signing, CVE gating), and byte-identical
reproducible builds. Not yet published to PyPI.

## License

Apache-2.0

