Air-gapped and restricted-network operation¶
korvid's own runtime dependencies each point at an internal endpoint, so no internet access is required for core operation. The decisions are which endpoint to internalize, and who owns the trust for it — korvid configures TLS only for the connections it makes itself, and there is no way to disable TLS verification through korvid configuration, by design. Optional features that authenticate against an external identity provider (for example the GitHub Copilot device login, or Microsoft Entra ID) still need their own external connectivity; they are called out below.
The artifact and trust path¶
flowchart LR
BUILD["Connected build host<br/>checksums + provenance verified"]
BUNDLE["Offline bundle<br/>wheels, SHA256SUMS, SBOM"]
HOST["korvid host<br/>inside the boundary"]
INDEX["Internal package index<br/>or the bundle's wheels/"]
CHARTS["Internal chart repository<br/>helm --ca-file"]
LLM["Internal model endpoint<br/>network.ca_bundle"]
CLUSTER[("Kubernetes API<br/>kubeconfig CA")]
REGISTRY["Internal image registry<br/>node/runtime trust"]
BUILD --> BUNDLE --> HOST
HOST --> INDEX
HOST --> CHARTS
HOST --> LLM
HOST --> CLUSTER
CLUSTER --> REGISTRY
Every connection korvid or a feature it depends on can make, who owns the trust decision for it, and how to configure that trust:
| Connection | Owner | Configure with |
|---|---|---|
| Agent LLM endpoint (OpenAI-compatible, native Ollama) | korvid | network.ca_bundle |
:ai wizard connection test |
korvid | network.ca_bundle (same builder — the test and the live agent cannot disagree) |
:ai model discovery (listing a profile endpoint's models) |
korvid | network.ca_bundle (same builder again — an endpoint the agent can reach is one the setup screen can list) |
| Prometheus / Loki observability connectors | korvid | network.ca_bundle (same builder again — see docs/observability.md) |
| models.dev metadata refresh (optional, explicit) | korvid | network.ca_bundle (same builder again); disable entirely with agent.model_search.models_dev: false |
| Internal Helm chart repository | helm (korvid passes it through) | CA-file field in the repo dialog → helm repo add --ca-file |
| Kubernetes API server | kubeconfig | certificate-authority[-data] in kubeconfig |
| OLM catalogs, bundle/operand images | cluster nodes / container runtime | registry mirror + node trust configuration |
| Workload, debug, and node-shell images | container runtime | registry mirror + node trust configuration |
| Telepresence and other external CLIs | the CLI itself | its own configuration |
| GitHub Copilot device login | the provider SDK | requires its usual external connectivity |
Microsoft Entra ID (azure profiles) |
azure-identity |
requires its usual external connectivity; network.ca_bundle does not reach the token endpoint |
Corporate CA for the agent (network.ca_bundle)¶
Internal LLM gateways (Ollama, vLLM, an OpenAI-compatible proxy) are usually served over HTTPS signed by a corporate CA. Point korvid at the bundle once:
# ~/.config/korvid/config.yaml
network:
ca_bundle: /etc/korvid/company-ca.pem
agent:
active: corp
profiles:
corp:
model: openai/qwen3:32b
endpoint: https://llm.corp.example/v1
auth:
method: none
- The bundle is validated at startup: a missing, unreadable, or malformed file fails with an error naming the configured path — never a silent fallback to default trust.
- The same bundle covers OpenAI-compatible completions, native Ollama
completions, the
:aisetup wizard's connection test, the model listing that wizard offers for an endpoint, and the optional models.dev metadata refresh. - When
network.ca_bundleis unset, standard environment behavior applies (SSL_CERT_FILE,HTTP_PROXY,HTTPS_PROXY,NO_PROXY). - The system trust store is never modified.
Offline model catalog¶
korvid's model catalog has two layers, and the primary one works with no network at all.
Primary layer (always available): LiteLLM ships
model_prices_and_context_window.json inside its wheel. korvid reads that
table at startup — no GET request, no internet required. The :ai wizard's
model search screen and tier routing both use it. Over 2,000 models from dozens
of providers are discoverable and routable offline, as long as you can reach
the model endpoint itself.
Optional enrichment layer (models.dev): korvid may fetch a single JSON
document from https://models.dev/api.json to add context lengths,
quantization info, and env-variable hints. This fetch is never made at
startup, on mount, on a keystroke, or during routing. It happens only
when you press Ctrl+R on the :ai wizard's model search
screen; opening that screen, typing a query and picking a model make no
request, and no HTTP client is built until the key is pressed.
The cache is $XDG_CACHE_HOME/korvid/models-dev.json whenever that variable is
set — on every platform, Windows and macOS included. With it unset the path
follows the platform convention: ~/Library/Caches/korvid/models-dev.json on
macOS, %LOCALAPPDATA%\korvid\models-dev.json on Windows (falling back to
~/AppData/Local when that variable is unset too), and
~/.cache/korvid/models-dev.json everywhere else. The file is written with
mode 0600. korvid never revalidates it on its own initiative more than once a
day; Ctrl+R is an explicit request and revalidates
regardless of that window, as a conditional If-None-Match request that
transfers no document when nothing has changed. That request is built by the
same client builder as every other korvid HTTPS call, so network.ca_bundle
covers it.
In a fully air-gapped deployment, disable the models.dev fetch permanently:
The key defaults to true. With it set to false korvid builds no models.dev
source and no HTTP client for one, so there is no code path left that could
open the socket: model search uses only the LiteLLM bundled table, and
Ctrl+R answers Model metadata refresh is disabled — no
source is configured, nothing was contacted. without touching the network.
The key is parsed strictly. Only the booleans true and false are accepted;
any other value (including the string "false") is reported as a config
warning and treated as false — it fails closed, so a typo in an air-gapped
deployment cannot silently re-enable the fetch. A model_search block that is
not a mapping names no key at all: that warns and leaves the default in
place.
Offline installation bundles¶
Every tagged release attaches versioned offline archives built from the
committed lockfile with all extras (korvid[all,entra]):
| Archive | Platform | Python |
|---|---|---|
korvid-X.Y.Z-offline-linux-manylinux_2_28-x86_64-py3.11.tar.gz … py3.13 |
Linux x86-64, glibc 2.28+ | 3.11–3.13 |
korvid-X.Y.Z-offline-windows-x86_64-py3.11.zip … py3.13 |
Windows x86-64 | 3.11–3.13 |
Pick the archive matching the target's OS and Python minor version. Each
contains wheels/ (korvid plus every locked dependency, built on a native
runner of that platform), install.sh/install.ps1, SHA256SUMS,
sbom.cdx.json, and README.txt. Linux wheel selection and offline
verification run inside the pinned manylinux_2_28_x86_64 image, making
glibc 2.28 the explicit compatibility floor rather than inheriting
whichever newer glibc happens to be installed on ubuntu-latest.
On the connected side, verify before carrying it across the boundary:
sha256sum -c SHA256SUMS --ignore-missing # release-level checksums
gh attestation verify korvid-X.Y.Z-offline-linux-manylinux_2_28-x86_64-py3.12.tar.gz \
--repo hellices/korvid # build provenance
On the air-gapped side:
tar xzf korvid-X.Y.Z-offline-linux-manylinux_2_28-x86_64-py3.12.tar.gz
cd korvid-X.Y.Z-offline-linux-manylinux_2_28-x86_64-py3.12
sha256sum -c SHA256SUMS # bundle-internal integrity
./install.sh # or: python -m pip install --no-index \
# --find-links ./wheels "korvid[all,entra]==X.Y.Z"
Upgrading is the same procedure with the newer archive — pip replaces the
installed version in place (--no-index still applies). Every archive is
verified in CI in a clean environment with package indexes unreachable,
including a negative check proving the install fails when a dependency wheel
is missing (i.e. nothing is silently fetched).
Boundary: the Python bundle contains korvid and its locked Python dependencies only. Helm, Telepresence, debug images, model artifacts, and Kubernetes credentials are separate operator-supplied dependencies.
Internalize the remaining dependencies¶
- LLM endpoint: run Ollama/vLLM inside the network and point the active
profile's
endpointat it, withnetwork.ca_bundlefor its CA. - Helm charts: mirror charts into an internal repository (e.g.
ChartMuseum, Harbor). The repository dialog (
Ctrl-Rfrom the chart picker) has an optional CA file field; when set, korvid validates the path and runshelm repo add <name> <url> --ca-file <path>through its fixed-argv wrapper (never a shell). Helm remains the owner of persisted repository configuration and credentials — korvid stores nothing. - OLM catalogs: mirror the catalog and its bundle/operand images with
oc-mirrororopm, and configure the cluster'sCatalogSourceand registry mirrors (ImageContentSourcePolicy/ containerd mirror config). - Debug images (pods):
son a shell-less pod offers an ephemeral debug container built fromdebug.default_image, or from one of the nameddebug.imagesoffered in the picker — point both at your internal registry (see ops.md). - Node shell image (nodes):
son a node is a separate config key. It creates a privilegednode-debugger-…pod fromnode_shell.image, which falls back to korvid's built-in public default when unset, so it must be pointed at the internal registry in its own right.kubectl debug node/…pins that pod to the node withnodeNameand a toleration for every taint, so a cordon or scheduling pressure elsewhere is not what bounds it — the node's kubelet still has to be healthy enough to start a pod and pull the (mirrored) image. It is node-level troubleshooting on a node whose kubelet, network, and container runtime are still working, not a rescue path for one where they aren't: an unreachable kubelet can't start the pod at all, and an unmirrored image fails on the pull. - Workload images: standard registry mirroring; korvid never pulls images itself.
Registry trust for OLM catalogs, debug/node-shell images, and workload images belongs to the nodes and the container runtime, not to korvid.
Readiness checklist (detect, don't assume)¶
Run these from the korvid host to detect leftover external dependencies instead of assuming the firewall catches them:
# 1. Agent endpoint reachable and trusted with the configured bundle:
curl --cacert /etc/korvid/company-ca.pem https://llm.corp.example/v1/models
# 2. Helm repositories all point inside (no public URLs left):
helm repo list
# 3. Cluster image sources: every image referenced by running pods —
# including init and ephemeral (kubectl debug) containers — should
# resolve to the internal mirror. Branch on kubectl success so a
# connectivity/auth error is never mistaken for "all internal", and
# anchor the registry prefix so look-alike hostnames don't count:
if images=$(kubectl get pods -A -o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{range .spec.initContainers[*]}{.image}{"\n"}{end}{range .spec.ephemeralContainers[*]}{.image}{"\n"}{end}{end}'); then
printf '%s\n' "$images" | sort -u | grep -v '^registry\.corp\.example/' \
|| echo "all images internal"
else
echo "kubectl failed — cannot verify images"
fi
# 4. Both image keys configured to the internal registry — the pod debug
# images and the node shell image are separate settings:
grep -A3 -e '^debug:' -e '^node_shell:' ~/.config/korvid/config.yaml
# 5. OLM catalog sources (if OLM is installed) point at the mirror:
kubectl get catalogsources -A -o jsonpath='{range .items[*]}{.spec.image}{"\n"}{end}'
A korvid start with network.ca_bundle set verifies that the bundle loads;
the :ai wizard's test call verifies the agent endpoint end to end with the
exact trust the runtime will use.