fix(npu): expose advisory gateway on docker bridge

This commit is contained in:
William Valentin
2026-06-04 16:19:22 -07:00
parent 59c5fd3e57
commit aeb3c9f8fb
5 changed files with 57 additions and 14 deletions
+12 -6
View File
@@ -1,8 +1,8 @@
# OpenVINO NPU advisory gateway
Local-only bounded wrapper for the classifier, GenAI worker, and doc/image triage sidecars.
Bounded Docker-bridge wrapper for the classifier, GenAI worker, and doc/image triage sidecars.
- HTTP bind: `127.0.0.1:18830` only; Docker/n8n bridge access is intentionally not enabled by default
- HTTP bind: `172.19.0.1:18830` for `n8n-agent` on the `swarm_default` Docker bridge
- Service: `openvino-advisory-gateway.service`
- Mode: advisory/shadow/draft only
- Metadata log: `~/.local/state/openvino-advisory-gateway/events.sqlite`
@@ -36,7 +36,7 @@ POST /v1/advisory/triage
### Classifier shadow call
```bash
curl -fsS http://127.0.0.1:18830/v1/advisory/classify \
curl -fsS http://172.19.0.1:18830/v1/advisory/classify \
-H 'Content-Type: application/json' \
-d '{"trace_id":"smoke","text":"Urgent: inspect service health and systemd status."}' | jq .
```
@@ -46,7 +46,7 @@ curl -fsS http://127.0.0.1:18830/v1/advisory/classify \
Allowed jobs: `title`, `summary`, `notification`, `memory_candidate`.
```bash
curl -fsS http://127.0.0.1:18830/v1/advisory/generate \
curl -fsS http://172.19.0.1:18830/v1/advisory/generate \
-H 'Content-Type: application/json' \
-d '{"job":"title","input":"Summarize a local health check.","max_new_tokens":24}' | jq .
```
@@ -54,7 +54,7 @@ curl -fsS http://127.0.0.1:18830/v1/advisory/generate \
### Explicit-file doc/image triage
```bash
curl -fsS http://127.0.0.1:18830/v1/advisory/triage \
curl -fsS http://172.19.0.1:18830/v1/advisory/triage \
-H 'Content-Type: application/json' \
-d '{"path":"/home/will/lab/swarm/openvino-doc-image-triage-npu/samples/synthetic_invoice.png","allowed_roots":["/home/will/lab/swarm/openvino-doc-image-triage-npu"]}' | jq .
```
@@ -75,7 +75,13 @@ systemctl --user enable --now openvino-advisory-gateway.service
systemctl --user status openvino-advisory-gateway.service --no-pager
```
`--allowed-root` may be repeated in the systemd unit when additional non-private fixture/review directories are approved. Keep the service bound to `127.0.0.1` unless Will explicitly approves a Docker-bridge exposure plan.
`--allowed-root` may be repeated in the systemd unit when additional non-private fixture/review directories are approved. Docker bridge exposure must use `--allow-docker-bridge` and the approved bridge IP `172.19.0.1`; the service still refuses wildcard binds such as `0.0.0.0`.
From `n8n-agent`, verify bridge reachability with:
```bash
docker exec n8n-agent wget -qO- -T 8 http://172.19.0.1:18830/healthz
```
## Tests
+26 -2
View File
@@ -10,6 +10,7 @@ from __future__ import annotations
import argparse
import hashlib
import ipaddress
import json
import os
import sqlite3
@@ -21,6 +22,7 @@ from typing import Any, Callable
from urllib.parse import urlparse
HOST = "127.0.0.1"
DOCKER_BRIDGE_HOST = "172.19.0.1"
PORT = 18830
CLASSIFIER_URL = "http://127.0.0.1:18819/v1/classify"
GENAI_URL = "http://127.0.0.1:18820/v1/worker/generate"
@@ -40,6 +42,20 @@ AUTHORITY = {
}
def validate_bind_host(host: str, *, allow_docker_bridge: bool = False) -> None:
"""Restrict service exposure to localhost or the explicitly approved Docker bridge bind."""
if host == "127.0.0.1":
return
if not allow_docker_bridge:
raise ValueError("refusing non-local bind without --allow-docker-bridge")
try:
addr = ipaddress.ip_address(host)
except ValueError as exc:
raise ValueError("bind host must be a literal IP address") from exc
if host != DOCKER_BRIDGE_HOST or not (addr.version == 4 and addr.is_private and not addr.is_loopback and not addr.is_unspecified):
raise ValueError(f"Docker bridge bind must use approved bridge IP {DOCKER_BRIDGE_HOST}")
def sha256_text(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
@@ -335,9 +351,17 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--port", type=int, default=int(os.environ.get("NPU_ADVISORY_PORT", str(PORT))))
parser.add_argument("--log-db", default=str(DEFAULT_LOG_DB))
parser.add_argument("--allowed-root", action="append", dest="allowed_roots", default=None, help="Configured file root allowed for advisory doc/image triage. May be repeated.")
parser.add_argument(
"--allow-docker-bridge",
action="store_true",
default=os.environ.get("NPU_ADVISORY_ALLOW_DOCKER_BRIDGE", "").lower() in {"1", "true", "yes"},
help="Permit binding to a private Docker bridge IP instead of 127.0.0.1.",
)
args = parser.parse_args(argv)
if args.host != "127.0.0.1":
raise SystemExit("refusing non-local bind")
try:
validate_bind_host(args.host, allow_docker_bridge=args.allow_docker_bridge)
except ValueError as exc:
raise SystemExit(str(exc)) from exc
configured_roots = [Path(p).expanduser().resolve() for p in (args.allowed_roots or DEFAULT_ALLOWED_ROOTS)]
logger = AdvisoryLogger(args.log_db)
server = ThreadingHTTPServer((args.host, args.port), make_handler(logger, configured_roots))
@@ -1,15 +1,16 @@
[Unit]
Description=OpenVINO NPU advisory gateway (local-only, port 18830)
Description=OpenVINO NPU advisory gateway (Docker bridge, port 18830)
After=network.target openvino-router-classifier.service openvino-genai-npu-worker.service openvino-doc-image-triage.service
Wants=openvino-router-classifier.service openvino-genai-npu-worker.service openvino-doc-image-triage.service
[Service]
Type=simple
WorkingDirectory=/home/will/lab/swarm/openvino-advisory-gateway
Environment=NPU_ADVISORY_HOST=127.0.0.1
Environment=NPU_ADVISORY_HOST=172.19.0.1
Environment=NPU_ADVISORY_PORT=18830
Environment=NPU_ADVISORY_ALLOW_DOCKER_BRIDGE=true
Environment=NPU_ADVISORY_LOG_DB=/home/will/.local/state/openvino-advisory-gateway/events.sqlite
ExecStart=/home/will/.venvs/npu/bin/python /home/will/lab/swarm/openvino-advisory-gateway/gateway.py --host 127.0.0.1 --port 18830 --allowed-root /home/will/lab/swarm/openvino-doc-image-triage-npu
ExecStart=/home/will/.venvs/npu/bin/python /home/will/lab/swarm/openvino-advisory-gateway/gateway.py --host 172.19.0.1 --port 18830 --allow-docker-bridge --allowed-root /home/will/lab/swarm/openvino-doc-image-triage-npu
Restart=on-failure
RestartSec=5
@@ -34,6 +34,18 @@ def test_authority_envelope_is_advisory_and_forbids_side_effects() -> None:
assert env["npu_proof"] == {"required": True, "ok": True, "npu_busy_delta_us": 123}
def test_bind_host_requires_explicit_docker_bridge_approval() -> None:
gateway.validate_bind_host("127.0.0.1")
with pytest.raises(ValueError, match="without --allow-docker-bridge"):
gateway.validate_bind_host("172.19.0.1")
gateway.validate_bind_host("172.19.0.1", allow_docker_bridge=True)
with pytest.raises(ValueError, match="approved bridge IP"):
gateway.validate_bind_host("0.0.0.0", allow_docker_bridge=True)
def test_classify_calls_sidecar_and_logs_metadata_only(tmp_path: Path) -> None:
calls: list[tuple[str, dict]] = []