① 내부 내보내기가 현재 점수·현재 사용 표시 대신 decisionSnapshot 을 읽는다. 드리프트는 currentRiskScore·scoredVersion 열로 드러낸다. 스냅샷 없는 옛 행은 no-snapshot. ② 큐 열 '위험' → '검토 우선'(침해 판정 아님), 점수 구성 패널에 범위 한 줄, 가이드 명시. ③ docs/operations/operating-modes.md: 전체 egress / API 호스트만 / 완전 폐쇄에서 점수가 뜻하는 것. 완전 폐쇄는 키를 빼야 한다(남겨 두면 큐 전체가 실패 하한 30). verify.ps1: ruff clean, 681 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
169 lines
6.5 KiB
Markdown
169 lines
6.5 KiB
Markdown
# Offline (air-gapped / 폐쇄망) install
|
|
|
|
The deployment target has **no internet**. All Python dependencies must be
|
|
carried across the air gap as pre-downloaded wheels. The build/staging machine
|
|
(which has internet) does the downloading; the target only installs.
|
|
|
|
## Prerequisites
|
|
|
|
- Same OS + CPU architecture + **Python 3.13** on build and target machines
|
|
(wheels are platform- and version-specific).
|
|
- The repository itself transferred to the target.
|
|
|
|
## 1. On the build machine (has internet)
|
|
|
|
```bash
|
|
# Runtime only (--only-binary keeps sdists out so nothing has to build on the
|
|
# offline target; also carry pip/setuptools/wheel themselves):
|
|
pip download --only-binary=:all: -r requirements.txt -d wheelhouse/
|
|
pip download --only-binary=:all: pip setuptools wheel -d wheelhouse/
|
|
|
|
# Or runtime + test tooling:
|
|
pip download --only-binary=:all: -r requirements-dev.txt -d wheelhouse/
|
|
```
|
|
|
|
This fills `wheelhouse/` with every wheel (including transitive deps).
|
|
|
|
### Hash-verified lock (recommended)
|
|
|
|
Pin the full transitive tree with hashes so a tampered/substituted wheel is
|
|
rejected at install time:
|
|
|
|
```bash
|
|
# On the build machine (pip-tools):
|
|
pip-compile --generate-hashes -o requirements.lock requirements.txt
|
|
```
|
|
|
|
Then install on the target with `--require-hashes` (step 3).
|
|
|
|
### Model weights (`models/`)
|
|
|
|
Some analysis paths use pre-trained weights that are **not** pip packages and are
|
|
**not** committed to the repository (`models/` is gitignored, same treatment as
|
|
`wheelhouse/`). Fetch them on the build machine:
|
|
|
|
```bash
|
|
mkdir -p models
|
|
# YuNet face detector (OpenCV Zoo, MIT). Used by face/person presence detection;
|
|
# cv2.FaceDetectorYN itself ships inside opencv-python-headless, only the weights
|
|
# are external.
|
|
curl -sSL -o models/face_detection_yunet_2023mar.onnx \
|
|
https://github.com/opencv/opencv_zoo/raw/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx
|
|
```
|
|
|
|
Verify before transferring — a truncated or substituted model must not cross the
|
|
air gap unnoticed:
|
|
|
|
| file | bytes | sha256 |
|
|
|---|---:|---|
|
|
| `face_detection_yunet_2023mar.onnx` | 232589 | `8f2383e4dd3cfbb4553ea8718107fc0423210dc964f9f4280604804ed2552fa4` |
|
|
|
|
```bash
|
|
python - <<'PY'
|
|
import hashlib, pathlib
|
|
p = pathlib.Path("models/face_detection_yunet_2023mar.onnx")
|
|
print(len(p.read_bytes()), hashlib.sha256(p.read_bytes()).hexdigest())
|
|
PY
|
|
```
|
|
|
|
If `models/` is absent on the target the server still starts: face detection
|
|
falls back to the bundled Haar cascades and logs the downgrade once. It does not
|
|
fail closed, because a missing optional weight must not stop an operator from
|
|
reviewing.
|
|
|
|
## 2. Transfer
|
|
|
|
Copy the repository, the `wheelhouse/` directory, and the `models/` directory to
|
|
the target via the approved air-gap transfer process (e.g. removable media).
|
|
|
|
## 3. On the target machine (no internet)
|
|
|
|
```bash
|
|
python -m venv .venv
|
|
. .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
pip install --no-index --find-links wheelhouse/ -r requirements.txt
|
|
```
|
|
|
|
`--no-index` guarantees pip never reaches out to the internet; `--find-links`
|
|
points it at the local wheelhouse.
|
|
|
|
## 4. Verify offline
|
|
|
|
```bash
|
|
# Confirm no network access is attempted and imports resolve:
|
|
python -c "import numpy, cv2, PIL; print('deps ok')"
|
|
python -m pytest -q # if test deps were installed
|
|
```
|
|
|
|
## Optional: Ollama LLM model (evidence narration)
|
|
|
|
The LLM assistant is optional. If you want it on the air-gapped target, the
|
|
model weights must be carried across too — they are **not** part of the Python
|
|
wheelhouse:
|
|
|
|
Carry the **`ollama` binary itself** as well as the weights — neither is in the
|
|
Python wheelhouse.
|
|
|
|
Use `qwen2.5:7b` (about 4.7 GB), the code default. Do **not** substitute
|
|
`qwen2.5:0.5b-instruct`: it was measured against this prompt at 55.7 s and it
|
|
broke the output contract, reprinting the prompt's format example as if it were
|
|
a finding. A 3B-class model is the floor for reading evidence and citing it.
|
|
|
|
```bash
|
|
# Build machine (has internet):
|
|
ollama pull qwen2.5:7b # or your pinned OLLAMA_MODEL
|
|
# Copy the model store across the air gap:
|
|
# default: ~/.ollama/models (or set OLLAMA_MODELS to a directory you transfer)
|
|
|
|
# Target machine (no internet):
|
|
export OLLAMA_MODELS=/path/to/transferred/models
|
|
ollama serve &
|
|
# Smoke test:
|
|
ollama run qwen2.5:7b "hello"
|
|
```
|
|
|
|
Set `OLLAMA_BASE_URL` / `OLLAMA_MODEL` in `.env`. Leaving them unset does **not**
|
|
turn the LLM off — the client falls back to `http://127.0.0.1:11434` and the code
|
|
default model, so a target without Ollama spends about 2 s per case before
|
|
writing a readable failure row (scores are unaffected either way). To stop the
|
|
attempts, disable the `llm` provider in the operator console.
|
|
|
|
Hardware: the 7B default was timed on a GPU host (about 2 s warm, 7 s cold). On a
|
|
CPU-only target, raise `OLLAMA_TIMEOUT` (default 120 s) and expect narration to
|
|
dominate seed time.
|
|
|
|
## Optional: Playwright Chromium (E2E tests)
|
|
|
|
The operator-GUI E2E suite needs a Chromium binary, which Playwright normally
|
|
downloads. For the air gap, fetch it on the build machine and carry it:
|
|
|
|
```bash
|
|
# Build machine:
|
|
PLAYWRIGHT_BROWSERS_PATH=./pw-browsers playwright install chromium
|
|
# Transfer the pw-browsers/ directory to the target, then:
|
|
|
|
# Target machine:
|
|
export PLAYWRIGHT_BROWSERS_PATH=/path/to/pw-browsers
|
|
python -m pytest tests/operator_gui -q
|
|
```
|
|
|
|
## Notes
|
|
|
|
- **Which network mode?** Read `operating-modes.md` before deploying: what
|
|
scores mean with full egress, API hosts only, or no egress at all (and why a
|
|
fully closed site must remove the API keys rather than just block the network).
|
|
- The operator GUI (`web/operator-gui/`) is fully self-contained: all
|
|
scripts/styles are local and the Pretendard font is bundled
|
|
(`assets/fonts/PretendardVariable.woff2`). No CDN fetch at runtime.
|
|
- External **search** APIs (Google / Naver / Cloud Vision) are an allowed
|
|
runtime exception and require outbound access to those providers. They
|
|
are optional; missing credentials disable the provider (see the startup
|
|
provider-readiness log).
|
|
- **Broader egress:** beyond the API hosts, the image-comparison feature also
|
|
fetches arbitrary public result-page / image / stylesheet URLs returned by
|
|
those searches (SSRF-guarded: private/loopback/CGNAT/link-local ranges are
|
|
blocked). Restricting egress to the API hosts only disables image comparison
|
|
of external results; it does not break core review, but it weakens the web-copy
|
|
signal silently (no failure row) - see `operating-modes.md` mode B.
|
|
- `COPYRIGHTER_AUTH_TOKEN` should be set in `.env` for any multi-operator or
|
|
networked deployment so the data routes require a shared token.
|