"""NPE sidecar adapter (fail-closed). The live binary is ``npe ``. There is **no** ``npe seal --to`` verb. Production HPKE is ``npe send`` with a sender ``.seed`` and recipient ``.npeid``. ``crypto.mode=npe`` must not fall back to lab-xor. ``PFC_REQUIRE_NPE=1`` on pfc-py-admin is a bus-wide fail-close; do not set it on ns1 until every NATS client uses ``npe send``. """ from __future__ import annotations # annotations as strings import json # encode sidecar request import os # NPE_BIN / NPE_SENDER_SEED import shutil # look up npe on PATH import subprocess # run sidecar from pathlib import Path # /opt/pfc/bin/npe from typing import Any, Dict, Mapping # types class NpeRequired(RuntimeError): """Raised when NPE is required but the sidecar is not usable.""" def npe_bin() -> str: """Return path to npe or raise. Checks NPE_BIN, PATH, then /opt/pfc/bin/npe.""" cands = [os.environ.get("NPE_BIN") or "", shutil.which("npe") or "", "/opt/pfc/bin/npe"] for path in cands: # first executable wins if path and Path(path).is_file() and os.access(path, os.X_OK): return path # found raise NpeRequired("npe binary not on PATH or /opt/pfc/bin/npe; attach sidecar") # fail closed def probe() -> Dict[str, Any]: """Does the real CLI exist and speak send/keygen? Never enables fail-close.""" try: # binary lookup path = npe_bin() # may raise except NpeRequired as exc: # missing return {"ok": False, "usable": False, "error": str(exc)} proc = subprocess.run([path], capture_output=True, timeout=5, check=False) # usage on stderr text = (proc.stderr or proc.stdout).decode(errors="replace") # usage line usable = "send" in text and "keygen" in text # real npe CLI return { "ok": usable, "usable": usable, "bin": path, "cli": "npe ", "usage": text.strip()[:240], "require_npe_env": os.environ.get("PFC_REQUIRE_NPE", ""), } def seal_npe(to: str, body: Mapping[str, Any]) -> str: """HPKE-seal via ``npe send``. Returns a handle, never lab-xor. Requires ``NPE_SENDER_SEED`` (sender .seed) and ``to`` as a path to a recipient ``.npeid``. This publishes on NATS; it is not a hex-only sidecar. Missing keys → NpeRequired (fail closed). """ sender = os.environ.get("NPE_SENDER_SEED", "") # host-only seed if not sender or not Path(sender).is_file(): # no identity raise NpeRequired("NPE_SENDER_SEED missing; npe send needs sender .seed + recipient .npeid") to_path = to # dest is .npeid path for the real CLI if not Path(to_path).is_file(): # mailbox id is not a file raise NpeRequired("npe send --to expects a .npeid file, not a mailbox string") raw = json.dumps(body, sort_keys=True, separators=(",", ":")).encode() # canonical body proc = subprocess.run( # real CLI [npe_bin(), "send", "--id", sender, "--to", to_path, "--data", "-"], input=raw, capture_output=True, timeout=15, check=False, ) if proc.returncode != 0: # sidecar failed raise NpeRequired("npe send failed: " + proc.stderr.decode(errors="replace")[:200]) return proc.stdout.strip().decode() or "npe-send-ok" # token from sidecar