Airgapped, secure-by-default deployment and inference harness for agentic AI using open-source models (GGUF via llama.cpp, or any loopback OpenAI-compatible server).
No cloud APIs, no telemetry, no outbound network at runtime.
- Secure startup:
docs/SECURE_STARTUP.md - Threat model:
docs/THREAT_MODEL.md - Security policy:
SECURITY.md - Contributing:
CONTRIBUTING.md - Code of conduct:
CODE_OF_CONDUCT.md - Changelog:
CHANGELOG.md
- Tool gate:
TOOL_CALLmust be the first token; JSON parsed withraw_decode; schema-validated arguments; allowlist enforced server-side. - Injection containment: Per-run delimiter tags; tool/file output sanitized (NFKC, zero-width, bidi controls, injection patterns); user tasks and session history re-sanitized each turn.
- Session safety: Stored chat turns sanitized; API session creation requires capability token when enabled.
- Replay protection: Nonce cache persisted to
api.replay_cache_path(strict default). - Strict Python: Production strict mode requires
security.python_sandbox.mode: dockerforrun_python(override withAIRGAP_ALLOW_PROCESS_PYTHON=1). - System prompt pin: Optional
agent.system_prompt_sha256to detect tampering of custom prompts. - Python sandbox: AST allowlist (no attributes), subprocess isolation (
python -I -S), timeout. - Python sandbox (optional): Docker-isolated execution (
--network none, read-only FS, dropped caps) whensecurity.python_sandbox.mode: dockerand the image is preloaded. - Filesystem: Symlink rejection, path traversal blocked, bounded reads/list/search.
- API: Loopback
serverequires bundle/policy bootstrap parity withrun; Bearer token viaAIRGAP_API_TOKENby default. - Audit: Hash chain restored across encrypted lines when key is present; chain verified on restore.
- Dev mode: Blocked under
/etc/airgap-agentand/var/lib/airgap-agentunlessAIRGAP_ALLOW_DEV=1. - Budgets (per-run): total tool calls, total bytes read, and total python execs are capped (defense-in-depth against DoS and over-broad scanning).
- Canaries: run
airgap-agent canaryagainst your configured backend to catch parser/prompt regressions. - Harness guide: see
docs/HARNESS.mdfor chat sessions, eval, metrics, structured JSON output, and opt-in writes.
| Control | Default |
|---|---|
| Network egress | Denied (policy + no HTTP tools) |
| Inference endpoint | Loopback only (127.0.0.1) |
| Tools | Allowlist: read_file, list_directory, search_text, run_python |
| Filesystem | Workspace jail; path traversal blocked |
| Python tool | AST allowlist + subprocess isolation (python -I -S) + timeout; optional Docker isolation |
| Models | SHA-256 manifest + Ed25519 signature (public key only on deploy) |
| Policy | Signed YAML; unsigned policy rejected in strict mode |
| Audit | Hash-chained JSONL; optional ChaCha20-Poly1305 encryption at rest |
| Agent loop | Hard cap on iterations |
Third parties can verify artifacts without trusting the runtime host — only your offline public keys.
| Artifact | Integrity | Authenticity | Verify command |
|---|---|---|---|
| Model bundle | SHA-256 per file | Ed25519 over manifest digest | airgap-agent verify-bundle --trust-dir ./trust |
| Policy | SHA-256 | Ed25519 detached sig | airgap-agent verify-policy policies/default.yaml |
| Audit log | Hash chain per entry | (no signer — tamper-evident) | airgap-agent verify-audit /var/log/.../audit.jsonl |
| Audit (sensitive) | Same chain inside ciphertext | AEAD key (env only) | verify-audit --decrypt with AIRGAP_AUDIT_ENCRYPTION_KEY |
airgap-agent keys --out ./release-keys --key-id prod-2025
# Deploy trust/prod-2025.pub.pem to /etc/airgap-agent/trust/ on airgapped hosts.
# Keep signing/prod-2025.pem offline; never copy to airgap.
./scripts/sign-bundle.sh ./models ./release-keys/signing/prod-2025.pem prod-2025
airgap-agent sign-file policies/default.yaml -k ./release-keys/signing/prod-2025.pem --key-id prod-2025sudo cp trust/*.pub.pem /etc/airgap-agent/trust/
airgap-agent verify-bundle --models-dir /var/lib/airgap-agent/models --trust-dir /etc/airgap-agent/trust
airgap-agent verify-policy policies/default.yaml --trust-dir /etc/airgap-agent/trust
airgap-agent run "Your task"
airgap-agent verify-audit /var/log/airgap-agent/audit.jsonlOptional encrypted audit (32-byte key as 64-char hex):
export AIRGAP_AUDIT_ENCRYPTION_KEY=$(openssl rand -hex 32)
export AIRGAP_AUDIT__ENCRYPT_AT_REST=truepython -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
mkdir -p workspace models .airgap
echo '{"note": "hello"}' > workspace/note.txt
airgap-agent run "List files in the workspace and summarize note.txt" --dev
airgap-agent health --dev
airgap-agent init
airgap-agent chat --dev
airgap-agent eval eval/fixtures --devexport AIRGAP_API_TOKEN=$(openssl rand -hex 32)
export AIRGAP_API_HMAC_KEY=$(openssl rand -hex 32)
airgap-agent serve --dev
# Create session → run with history
curl -s -H "Authorization: Bearer $AIRGAP_API_TOKEN" -H "X-Airgap-Capability-Token: $(airgap-agent mint-token)" \
-X POST http://127.0.0.1:8741/v1/sessions
curl -s -H "Authorization: Bearer $AIRGAP_API_TOKEN" -H "X-Airgap-Capability-Token: $(airgap-agent mint-token)" \
-H "Content-Type: application/json" \
-d '{"task":"List workspace files","session_id":"<id>"}' \
http://127.0.0.1:8741/v1/agent/run
curl -s -H "Authorization: Bearer $AIRGAP_API_TOKEN" http://127.0.0.1:8741/metricsBy default, run_python executes in a restricted subprocess. For stronger isolation, run it inside Docker.
- Preload the image on the host (connected staging, then transfer if needed):
docker pull python:3.12-slim- Enable Docker mode in
config/default.yaml(or env overrides):
security:
python_sandbox:
mode: docker
docker_image: python:3.12-slimThis runs the python tool with:
--network none--read-only--cap-drop ALL--security-opt no-new-privileges- workspace mounted read-only at
/ws
Copy GGUF weights into models/ (e.g. Mistral, Llama, Qwen — your choice), then:
./scripts/bundle-models.sh ./modelsInstall the optional Hub dependency:
pip install -e ".[hf]"Download a GGUF model repo (examples use patterns to avoid pulling unnecessary files):
airgap-agent hf-download "TheBloke/Mistral-7B-Instruct-v0.2-GGUF" \
--models-dir ./models \
--pattern "*.gguf" \
--pattern "*Q4_K_M.gguf"This writes models/HF_SOURCE.json with the upstream repo/revision/commit used, and the bundle manifest/signature then makes the resulting transfer verifiable.
Sign the bundle, then transfer models/, MANIFEST.sha256, MANIFEST.sig.json, and trust/*.pub.pem to the isolated host (USB, sneakernet, etc.):
./scripts/sign-bundle.sh ./models ./release-keys/signing/prod-2025.pem prod-2025pip install -e ".[llama]"
export AIRGAP_INFERENCE__BACKEND=llama_cpp
export AIRGAP_INFERENCE__MODEL_PATH=/var/lib/airgap-agent/models/your-model.gguf
airgap-agent verify-bundle --models-dir /var/lib/airgap-agent/models
airgap-agent health
airgap-agent run "Analyze logs in workspace and propose remediation steps"If you run vLLM or llama.cpp server on loopback:
inference:
backend: openai_compat
base_url: "http://127.0.0.1:8080/v1"export AIRGAP_INFERENCE__BACKEND=openai_compat
airgap-agent run "Your task"airgap-agent serve --host 127.0.0.1 --port 8741 --dev
curl -s http://127.0.0.1:8741/health | jq .
curl -s -X POST http://127.0.0.1:8741/v1/agent/run \
-H 'Content-Type: application/json' \
-d '{"task":"List workspace files"}' | jq .The API can enforce two layers:
- Bearer auth (
AIRGAP_API_TOKEN) - HMAC-signed per-request capability token (
AIRGAP_API_HMAC_KEY+X-Airgap-Capability-Token)
Example:
export AIRGAP_API_TOKEN=$(openssl rand -hex 32)
export AIRGAP_API_HMAC_KEY=$(openssl rand -hex 32)
airgap-agent serve --host 127.0.0.1 --port 8741
CAP_TOKEN=$(airgap-agent mint-token --cap fs.read --cap fs.list --ttl 300 --max-read-bytes 1048576)
SESSION_TOKEN=$(airgap-agent mint-token --path /v1/sessions --cap fs.read --ttl 300)
curl -s -X POST http://127.0.0.1:8741/v1/agent/run \
-H "Authorization: Bearer $AIRGAP_API_TOKEN" \
-H "X-Airgap-Capability-Token: $CAP_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"task":"List workspace files"}' | jq .Build on a connected machine, save the image, load on the isolated host:
docker build -t airgap-agent:0.1.0 .
docker save airgap-agent:0.1.0 | gzip > airgap-agent-0.1.0.tar.gz
# transfer tar.gz, then on airgapped host:
docker load < airgap-agent-0.1.0.tar.gz
docker compose -f docker-compose.airgap.yml upMount models read-only and a dedicated workspace volume.
See config/default.yaml. Override with env vars (AIRGAP_INFERENCE__BACKEND, etc.) or --config.
Policies: policies/default.yaml.
flowchart LR
CLI[CLI / loopback API] --> Harness[Agent Harness]
Harness --> Policy[Policy Engine]
Harness --> Tools[Tool Registry]
Tools --> Sandbox[Python / FS Sandbox]
Harness --> Inference[Inference Backend]
Inference --> Llama[llama.cpp GGUF]
Inference --> OAI[OpenAI-compat loopback]
Harness --> Audit[Audit JSONL]
Deploy[Bundle Verify] --> Inference
GPL-3.0