> ## Documentation Index
> Fetch the complete documentation index at: https://docs.etalon.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Offline install

> Install from a local wheelhouse with the network namespace disabled.

# Offline install

This page is part of the [operator guide](/docs/introduction.md). A checkout install that
uses the network is in [Getting started](/docs/getting-started.md).

This is the procedure for installing Etalon from a local wheelhouse while the
install process has no network. It is evidence about that install. It does
not determine that a regulation or an internal control has been met.

Package version is 0.0.1.

## What `--no-index` does not prove

`--no-index` and `--find-links` tell pip to read a local wheelhouse instead
of a package index. They do not disable the machine's network. A successful
install with those flags alone is not a claim that the host, or the install
process, was offline.

## How the network is disabled

`examples/offline_install.sh` does two steps.

1. With the caller's network, it downloads `requirements.txt` (hash-checked)
   and builds the project wheel into a wheelhouse. `pip wheel` build
   isolation may download hatchling during this step. That
   traffic is part of the fetch, not the install.
2. It re-executes the install in a new mount and network namespace:

```bash theme={null}
sudo -- unshare --propagation private --mount --net -- examples/offline_install.sh --inner ...
```

Passwordless sudo is preferred because it does not depend on unprivileged
user namespaces. If sudo is not available, the script uses
`unshare --propagation private --mount --net --map-root-user`. If neither
can create a namespace, the script exits. It does not install with the host
network still attached.

`--propagation private` keeps the sysfs mount inside the namespace. The
host's interfaces stay where they are. After the install process exits, the
script connects to `1.1.1.1:443` again from the host.

Before `pip install`, inside the namespace, the script mounts a fresh sysfs
and requires all of the following:

* that sysfs `class/net` lists only `lo`
* a TCP connect to `1.1.1.1:443` fails with `ENETUNREACH` (errno 101)
* resolving `pypi.org` fails within 5 seconds

The same TCP connect must have succeeded on the host before the fetch. The
install environment is a new virtual environment. Its pip is not upgraded.
Install environment variables are `PIP_DISABLE_PIP_VERSION_CHECK=1`,
`PIP_NO_CACHE_DIR=1`, and `PIP_NO_INDEX=1`, so pip does not read a download
cache and does not query an index for a version notice. Proxy variables are
unset for the install commands.

```bash theme={null}
pip install --disable-pip-version-check --no-cache-dir --no-index --find-links "$WHEELHOUSE" --require-hashes -r requirements.txt
pip install --disable-pip-version-check --no-cache-dir --no-index --find-links "$WHEELHOUSE" --no-deps "$WHEEL"
etalon pack validate examples/demo-pack
etalon verify examples/sample-run
```

The project wheel is built locally, so it is not covered by the hashes in
`requirements.txt`. `--no-deps` installs that wheel without resolving
anything else. The script prints the SHA-256 of the wheel it just built.
That archive includes build timestamps, so a later rebuild can have a
different digest. The digest is not a lock. Dependency locks stay in
`requirements.txt`.

A fresh sysfs mount is required for the interface check. Without it,
`/sys/class/net` can still list the host's interfaces after `unshare --net`,
while sockets in the namespace still cannot use them.

On Linux, pip ignores the Windows-only `colorama` line in the lock.

Run it from a checkout:

```bash theme={null}
bash examples/offline_install.sh
```

The host needs Python 3.11+ with the stdlib `venv` module (`python3-venv`
on Debian/Ubuntu), `unshare` from util-linux, and `timeout` from coreutils.
GitHub-hosted Ubuntu runners provide passwordless sudo; the workflow below
uses that path.

## Observed run

Date: 2026-09-24. Host: Linux x86\_64, kernel 6.12.94+, CPython 3.12.3.
Command: `bash examples/offline_install.sh`.

The fetch step used the network. pip printed `Downloading` for the locked
requirements and upgraded the fetch environment to pip 26.2.1. `pip wheel`
reported `Installing build dependencies` while that network was still up
(`pyproject.toml` requires hatchling to build). The wheelhouse
held 23 files. pip printed `Ignoring colorama` because the lock's
`sys_platform == "win32"` marker does not match Linux.

Isolation was:

```text theme={null}
isolation: sudo unshare --propagation private --mount --net
```

Inside the namespace, before `pip install`:

```text theme={null}
sysfs net devices: lo
connect 1.1.1.1:443: errno 101 [Errno 101] Network is unreachable
dns pypi.org: [Errno -3] Temporary failure in name resolution
install pip: pip 24.0 from /tmp/etalon-offline/lib/python3.12/site-packages/pip (python 3.12)
```

The install environment's pip was not upgraded. pip then installed only
from `/tmp/etalon-wheels` (`Looking in links: /tmp/etalon-wheels` and
`Processing /tmp/etalon-wheels/...` for each locked distribution and for
`etalon-0.0.1-py3-none-any.whl`) and printed `Successfully installed etalon-0.0.1`.

```text theme={null}
etalon: etalon 0.0.1
etalon pack validate /workspace/examples/demo-pack
pack: etalon.demo.ticket-routing@0.1.0
content_sha256: sha256:ae38631ded95364b0452ff3fb5f0add969a31d49ed4a04cb42cbfb08d19a3c1b
cases: qualification=72 calibration=12 challenge=16
result: pack matches the schema and pack rules
etalon verify /workspace/examples/sample-run
bundle hashes match
signature: unsigned
offline install checks passed
```

After that process exited, the host sysfs list was `lo docker0 enp0s5` and
`host connect 1.1.1.1:443: ok ('1.1.1.1', 443)`.

The install phase was also started in a mount namespace that did not drop
the host network (`sudo unshare --propagation private --mount`, without
`--net`). It printed `sysfs net devices: docker0 enp0s5 lo`, printed
`expected only lo in the network namespace`, and exited 1. It did not
create the install virtual environment.

## GitHub Actions

`.github/workflows/offline-install.yml` runs the same script on
`ubuntu-latest` with CPython 3.12. The job fails if the namespace cannot be
created, if the egress check does not return `ENETUNREACH`, or if
`etalon pack validate` / `etalon verify` do not succeed. A green run is the
same class of evidence for that runner.
