fix(npu): expose advisory gateway on docker bridge
This commit is contained in:
@@ -133,7 +133,7 @@ Host/user services:
|
||||
- `openvino-router-classifier.service` — `:18819`, local-only dry-run Atlas/Hermes message classifier; advisory only
|
||||
- `openvino-genai-npu-worker.service` — `:18820`, local-only bounded GenAI worker for small background generation jobs
|
||||
- `openvino-doc-image-triage.service` — `:18829`, local-only document/image triage HTTP wrapper with allowed-root enforcement
|
||||
- `openvino-advisory-gateway.service` — `:18830`, local-only advisory envelope wrapper over classifier, GenAI, and doc/image triage; explicit no-authority contract
|
||||
- `openvino-advisory-gateway.service` — `172.19.0.1:18830`, Docker-bridge advisory envelope wrapper over classifier, GenAI, and doc/image triage for `n8n-agent`; explicit no-authority contract
|
||||
|
||||
Local-only OpenVINO NPU sidecars:
|
||||
|
||||
@@ -143,9 +143,9 @@ Local-only OpenVINO NPU sidecars:
|
||||
| `18819` | router/classifier | live user service; dry-run only | no Hermes/Atlas routing, memory writes, service restarts, or outbound messages |
|
||||
| `18820` | bounded GenAI worker | live user service | background jobs only; not primary Atlas/Hermes model routing |
|
||||
| `18829` | document/image triage | live localhost server | allowed-root limited; no private directory processing unless explicitly approved; NPU stage is embeddings via `:18817` |
|
||||
| `18830` | advisory gateway | live user service; host-local Hermes-accessible wrapper | returns `openvino_advisory_v1` envelopes only; no routing, memory writes, external sends, tool execution, restarts, or process-root broadening from request payloads; n8n bridge access intentionally disabled unless separately approved |
|
||||
| `18830` | advisory gateway | live user service; bound to `172.19.0.1` for `n8n-agent` bridge access | returns `openvino_advisory_v1` envelopes only; no routing, memory writes, external sends, tool execution, restarts, or process-root broadening from request payloads; refuses wildcard binds |
|
||||
|
||||
These sidecars bind to `127.0.0.1` by default and must not be wired into live Atlas/Hermes routing, memory writes, broad private document processing, or primary model paths without explicit Will approval. Any NPU claim requires a positive `/sys/class/accel/accel0/device/npu_busy_time_us` delta before/after inference or service-reported equivalent. HTTP 200 alone is not proof.
|
||||
These sidecars bind to `127.0.0.1` by default, except `openvino-advisory-gateway.service`, which is explicitly approved on the Docker bridge IP `172.19.0.1` so `n8n-agent` can call it. They must not be wired into live Atlas/Hermes routing, memory writes, broad private document processing, external sends, tool execution, service restarts, or primary model paths without explicit Will approval. Any NPU claim requires a positive `/sys/class/accel/accel0/device/npu_busy_time_us` delta before/after inference or service-reported equivalent. HTTP 200 alone is not proof.
|
||||
|
||||
### 5. Obsidian and RAG
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]] = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user