nxd 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nxd/__init__.py +65 -0
- nxd/_crypto.py +46 -0
- nxd/audit.py +86 -0
- nxd/bind.py +50 -0
- nxd/blur.py +41 -0
- nxd/channel.py +63 -0
- nxd/checkpoint.py +75 -0
- nxd/fhe.py +106 -0
- nxd/handoff.py +30 -0
- nxd/redact.py +42 -0
- nxd/seal.py +49 -0
- nxd/search.py +72 -0
- nxd/shield.py +26 -0
- nxd/sign.py +83 -0
- nxd/split.py +60 -0
- nxd/tokenize.py +68 -0
- nxd/vault.py +101 -0
- nxd/verify.py +112 -0
- nxd-0.1.0.dist-info/METADATA +120 -0
- nxd-0.1.0.dist-info/RECORD +23 -0
- nxd-0.1.0.dist-info/WHEEL +5 -0
- nxd-0.1.0.dist-info/licenses/LICENSE +21 -0
- nxd-0.1.0.dist-info/top_level.txt +1 -0
nxd/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NXD — encrypted agent operating system.
|
|
3
|
+
|
|
4
|
+
Three guarantees:
|
|
5
|
+
1. The agent works fully
|
|
6
|
+
2. The agent sees nothing
|
|
7
|
+
3. The operator holds the keys
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
from nxd.audit import export as audit_export
|
|
15
|
+
from nxd.audit import log as audit_log
|
|
16
|
+
from nxd.audit import verify as audit_verify
|
|
17
|
+
from nxd.bind import BindingError, bind, open
|
|
18
|
+
from nxd.blur import blur, blur_dataset, blur_guide, blur_report
|
|
19
|
+
from nxd.channel import Channel, ChannelClosedError, channel
|
|
20
|
+
from nxd.checkpoint import checkpoint
|
|
21
|
+
from nxd.fhe import aggregate, match, score
|
|
22
|
+
from nxd.handoff import Handoff
|
|
23
|
+
from nxd.redact import deredact, redact
|
|
24
|
+
from nxd.search import SearchIndex, build_index, retrieve, search
|
|
25
|
+
from nxd.seal import seal, seal_text, unseal, unseal_text
|
|
26
|
+
from nxd.shield import send_to_model, shield, unshield
|
|
27
|
+
from nxd.sign import sign, sign_record, verify_record, verify_signature
|
|
28
|
+
from nxd.split import InsufficientSharesError, distribute, reconstruct_strict, split
|
|
29
|
+
from nxd.tokenize import detokenize, tokenize, tokenize_record
|
|
30
|
+
from nxd.vault import Vault
|
|
31
|
+
from nxd.verify import LockedError, register, unlock, verify
|
|
32
|
+
|
|
33
|
+
Mode = Literal["auto", "fast", "secure"]
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"score", "match", "aggregate", "receive",
|
|
37
|
+
"Vault", "Handoff",
|
|
38
|
+
"shield", "unshield", "send_to_model",
|
|
39
|
+
"build_index", "search", "retrieve", "SearchIndex",
|
|
40
|
+
"register", "verify", "unlock", "LockedError",
|
|
41
|
+
"tokenize", "detokenize", "tokenize_record",
|
|
42
|
+
"redact", "deredact",
|
|
43
|
+
"seal", "unseal", "seal_text", "unseal_text",
|
|
44
|
+
"channel", "Channel", "ChannelClosedError",
|
|
45
|
+
"checkpoint",
|
|
46
|
+
"sign", "verify_signature", "sign_record", "verify_record",
|
|
47
|
+
"blur", "blur_dataset", "blur_report", "blur_guide",
|
|
48
|
+
"split", "reconstruct_strict", "distribute", "InsufficientSharesError",
|
|
49
|
+
"bind", "open", "BindingError",
|
|
50
|
+
"audit_log", "audit_verify", "audit_export",
|
|
51
|
+
"Mode",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def receive(model: Any, token: str, handoff: Handoff, mode: Mode = "auto") -> list[int]:
|
|
56
|
+
"""Agent B scores encrypted handoff context."""
|
|
57
|
+
context = handoff.unpack(token)
|
|
58
|
+
clients = context if isinstance(context, list) else [context]
|
|
59
|
+
return score(model, clients, mode=mode)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class audit:
|
|
63
|
+
log = staticmethod(audit_log)
|
|
64
|
+
verify = staticmethod(audit_verify)
|
|
65
|
+
export = staticmethod(audit_export)
|
nxd/_crypto.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Shared cryptography utilities for NXD."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from cryptography.fernet import Fernet
|
|
10
|
+
|
|
11
|
+
DEFAULT_DATA_DIR = Path.home() / ".nxd"
|
|
12
|
+
MASTER_KEY_FILE = "master.key"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def data_dir() -> Path:
|
|
16
|
+
import os
|
|
17
|
+
path = Path(os.environ.get("NXD_DATA_DIR", DEFAULT_DATA_DIR))
|
|
18
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
return path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def master_key_path() -> Path:
|
|
23
|
+
return data_dir() / MASTER_KEY_FILE
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_fernet(key_path: Path | None = None) -> Fernet:
|
|
27
|
+
path = key_path or master_key_path()
|
|
28
|
+
if path.exists():
|
|
29
|
+
key = path.read_bytes()
|
|
30
|
+
else:
|
|
31
|
+
key = Fernet.generate_key()
|
|
32
|
+
path.write_bytes(key)
|
|
33
|
+
os.chmod(path, 0o600)
|
|
34
|
+
return Fernet(key)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_hmac_key() -> bytes:
|
|
38
|
+
return hashlib.sha256(master_key_path().read_bytes()).digest()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def encrypt_bytes(data: bytes, fernet: Fernet | None = None) -> bytes:
|
|
42
|
+
return (fernet or load_fernet()).encrypt(data)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def decrypt_bytes(token: bytes, fernet: Fernet | None = None) -> bytes:
|
|
46
|
+
return (fernet or load_fernet()).decrypt(token)
|
nxd/audit.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Tamper-proof audit chain for all NXD operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from nxd._crypto import data_dir
|
|
12
|
+
|
|
13
|
+
AUDIT_FILE = "audit_chain.jsonl"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuditChain:
|
|
17
|
+
def __init__(self, path: Path | None = None):
|
|
18
|
+
self.path = path or (data_dir() / AUDIT_FILE)
|
|
19
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
if not self.path.exists():
|
|
21
|
+
self.path.write_text("")
|
|
22
|
+
|
|
23
|
+
def _last_hash(self) -> str:
|
|
24
|
+
lines = [l for l in self.path.read_text().splitlines() if l.strip()]
|
|
25
|
+
if not lines:
|
|
26
|
+
return "genesis"
|
|
27
|
+
return json.loads(lines[-1])["hash"]
|
|
28
|
+
|
|
29
|
+
def log(self, operation: str, **meta: Any) -> dict:
|
|
30
|
+
prev = self._last_hash()
|
|
31
|
+
entry = {
|
|
32
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
33
|
+
"operation": operation,
|
|
34
|
+
"meta": meta,
|
|
35
|
+
"prev_hash": prev,
|
|
36
|
+
}
|
|
37
|
+
payload = json.dumps(entry, sort_keys=True)
|
|
38
|
+
entry["hash"] = hashlib.sha256((prev + payload).encode()).hexdigest()
|
|
39
|
+
with self.path.open("a") as f:
|
|
40
|
+
f.write(json.dumps(entry) + "\n")
|
|
41
|
+
return entry
|
|
42
|
+
|
|
43
|
+
def entries(self) -> list[dict]:
|
|
44
|
+
return [json.loads(l) for l in self.path.read_text().splitlines() if l.strip()]
|
|
45
|
+
|
|
46
|
+
def verify(self) -> tuple[bool, int | None]:
|
|
47
|
+
lines = self.entries()
|
|
48
|
+
prev = "genesis"
|
|
49
|
+
for i, entry in enumerate(lines):
|
|
50
|
+
stored = entry.pop("hash")
|
|
51
|
+
payload = json.dumps(entry, sort_keys=True)
|
|
52
|
+
expected = hashlib.sha256((prev + payload).encode()).hexdigest()
|
|
53
|
+
if stored != expected:
|
|
54
|
+
entry["hash"] = stored
|
|
55
|
+
return False, i
|
|
56
|
+
prev = stored
|
|
57
|
+
entry["hash"] = stored
|
|
58
|
+
return True, None
|
|
59
|
+
|
|
60
|
+
def export(self, out_path: str | Path) -> Path:
|
|
61
|
+
out = Path(out_path)
|
|
62
|
+
report = {"chain_valid": self.verify()[0], "entries": self.entries()}
|
|
63
|
+
out.write_text(json.dumps(report, indent=2))
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_audit = AuditChain()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def log(operation: str, **meta: Any) -> dict:
|
|
71
|
+
return _audit.log(operation, **meta)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def verify() -> tuple[bool, int | None]:
|
|
75
|
+
return _audit.verify()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def export(out_path: str | Path) -> Path:
|
|
79
|
+
return _audit.export(out_path)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def reset_for_tests(path: Path) -> None:
|
|
83
|
+
global _audit
|
|
84
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
path.write_text("")
|
|
86
|
+
_audit = AuditChain(path)
|
nxd/bind.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Data binding — encrypt locked to specific recipient and purpose."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from cryptography.fernet import Fernet
|
|
9
|
+
from cryptography.hazmat.primitives import hashes
|
|
10
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
11
|
+
|
|
12
|
+
from nxd._crypto import master_key_path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BindingError(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _derive_key(recipient: str, purpose: str = "") -> Fernet:
|
|
20
|
+
seed = master_key_path().read_bytes()
|
|
21
|
+
derived = HKDF(
|
|
22
|
+
algorithm=hashes.SHA256(),
|
|
23
|
+
length=32,
|
|
24
|
+
salt=b"nxd-bind",
|
|
25
|
+
info=f"{recipient}:{purpose}".encode(),
|
|
26
|
+
).derive(seed)
|
|
27
|
+
key = base64.urlsafe_b64encode(derived)
|
|
28
|
+
return Fernet(key)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def bind(data: str, recipient: str, purpose: str = "") -> str:
|
|
32
|
+
from nxd import audit
|
|
33
|
+
payload = json.dumps({"data": data, "recipient": recipient, "purpose": purpose})
|
|
34
|
+
token = _derive_key(recipient, purpose).encrypt(payload.encode()).decode()
|
|
35
|
+
audit.log("bind", recipient=recipient, purpose=purpose)
|
|
36
|
+
return token
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def open(locked: str, caller: str, purpose: str = "") -> str:
|
|
40
|
+
from nxd import audit
|
|
41
|
+
try:
|
|
42
|
+
payload = json.loads(_derive_key(caller, purpose).decrypt(locked.encode()).decode())
|
|
43
|
+
except Exception as e:
|
|
44
|
+
raise BindingError(f"Access denied for {caller}") from e
|
|
45
|
+
if payload["recipient"] != caller:
|
|
46
|
+
raise BindingError(f"Bound to {payload['recipient']}, not {caller}")
|
|
47
|
+
if purpose and payload.get("purpose") and payload["purpose"] != purpose:
|
|
48
|
+
raise BindingError(f"Bound to purpose '{payload['purpose']}', not '{purpose}'")
|
|
49
|
+
audit.log("open", caller=caller, purpose=purpose)
|
|
50
|
+
return payload["data"]
|
nxd/blur.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Differential privacy — blur aggregates for safe sharing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def blur(value: float, sensitivity: float = 1.0, epsilon: float = 1.0) -> float:
|
|
9
|
+
from nxd import audit
|
|
10
|
+
scale = sensitivity / epsilon
|
|
11
|
+
noise = np.random.laplace(0, scale)
|
|
12
|
+
result = float(value + noise)
|
|
13
|
+
audit.log("blur", epsilon=epsilon)
|
|
14
|
+
return result
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def blur_dataset(values: list[float], sensitivity: float = 1.0, epsilon: float = 1.0) -> list[float]:
|
|
18
|
+
from nxd import audit
|
|
19
|
+
result = [blur(v, sensitivity, epsilon) for v in values]
|
|
20
|
+
audit.log("blur_dataset", count=len(values))
|
|
21
|
+
return result
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def blur_report(records: list[dict], fields: list[str], epsilon: float = 1.0) -> dict:
|
|
25
|
+
from nxd import audit
|
|
26
|
+
report = {}
|
|
27
|
+
for field in fields:
|
|
28
|
+
vals = [float(r[field]) for r in records if field in r]
|
|
29
|
+
if vals:
|
|
30
|
+
noisy = blur_dataset(vals, sensitivity=max(vals) * 0.1, epsilon=epsilon)
|
|
31
|
+
report[field] = {"mean": float(np.mean(noisy)), "std": float(np.std(noisy))}
|
|
32
|
+
audit.log("blur_report", fields=fields)
|
|
33
|
+
return report
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def blur_guide() -> str:
|
|
37
|
+
return (
|
|
38
|
+
"epsilon=0.1: very strong privacy, less accuracy\n"
|
|
39
|
+
"epsilon=1.0: balanced\n"
|
|
40
|
+
"epsilon=10.0: weak privacy, high accuracy"
|
|
41
|
+
)
|
nxd/channel.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Encrypted channels — E2E messaging between agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from cryptography.fernet import Fernet
|
|
10
|
+
from cryptography.hazmat.primitives import hashes
|
|
11
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
12
|
+
|
|
13
|
+
from nxd._crypto import master_key_path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChannelClosedError(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _session_fernet(sender: str, receiver: str) -> Fernet:
|
|
21
|
+
seed = master_key_path().read_bytes()
|
|
22
|
+
derived = HKDF(
|
|
23
|
+
algorithm=hashes.SHA256(),
|
|
24
|
+
length=32,
|
|
25
|
+
salt=b"nxd-channel",
|
|
26
|
+
info=f"{sender}:{receiver}".encode(),
|
|
27
|
+
).derive(seed)
|
|
28
|
+
key = base64.urlsafe_b64encode(derived)
|
|
29
|
+
return Fernet(key)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Channel:
|
|
33
|
+
def __init__(self, sender: str, receiver: str):
|
|
34
|
+
self.sender = sender
|
|
35
|
+
self.receiver = receiver
|
|
36
|
+
self._closed = False
|
|
37
|
+
self._fernet = _session_fernet(sender, receiver)
|
|
38
|
+
|
|
39
|
+
def send(self, message: str) -> str:
|
|
40
|
+
from nxd import audit
|
|
41
|
+
if self._closed:
|
|
42
|
+
raise ChannelClosedError("Channel is closed")
|
|
43
|
+
packet = json.dumps({"ts": time.time(), "msg": message}).encode()
|
|
44
|
+
token = self._fernet.encrypt(packet).decode()
|
|
45
|
+
audit.log("channel_send", sender=self.sender, receiver=self.receiver)
|
|
46
|
+
return token
|
|
47
|
+
|
|
48
|
+
def receive(self, packet: str) -> str:
|
|
49
|
+
from nxd import audit
|
|
50
|
+
if self._closed:
|
|
51
|
+
raise ChannelClosedError("Channel is closed")
|
|
52
|
+
payload = json.loads(self._fernet.decrypt(packet.encode()).decode())
|
|
53
|
+
audit.log("channel_receive", receiver=self.receiver)
|
|
54
|
+
return payload["msg"]
|
|
55
|
+
|
|
56
|
+
def close(self) -> None:
|
|
57
|
+
from nxd import audit
|
|
58
|
+
self._closed = True
|
|
59
|
+
audit.log("channel_close", sender=self.sender, receiver=self.receiver)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def channel(sender: str, receiver: str) -> Channel:
|
|
63
|
+
return Channel(sender, receiver)
|
nxd/checkpoint.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Agent state persistence — encrypted checkpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import gzip
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from nxd._crypto import data_dir, load_fernet
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CheckpointStore:
|
|
14
|
+
def __init__(self, path: Path | None = None):
|
|
15
|
+
self.dir = path or (data_dir() / "checkpoints")
|
|
16
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
def _file(self, agent_id: str) -> Path:
|
|
19
|
+
return self.dir / f"{agent_id}.nxd"
|
|
20
|
+
|
|
21
|
+
def save(self, agent_id: str, state: dict) -> Path:
|
|
22
|
+
from nxd import audit
|
|
23
|
+
f = load_fernet()
|
|
24
|
+
meta = {"agent_id": agent_id, "timestamp": time.time(), "keys": list(state.keys())}
|
|
25
|
+
payload = gzip.compress(json.dumps({"meta": meta, "state": state}).encode())
|
|
26
|
+
out = self._file(agent_id)
|
|
27
|
+
out.write_bytes(f.encrypt(payload))
|
|
28
|
+
audit.log("checkpoint_save", agent_id=agent_id)
|
|
29
|
+
return out
|
|
30
|
+
|
|
31
|
+
def load(self, agent_id: str) -> dict:
|
|
32
|
+
from nxd import audit
|
|
33
|
+
out = self._file(agent_id)
|
|
34
|
+
if not out.exists():
|
|
35
|
+
raise FileNotFoundError(f"No checkpoint for {agent_id}")
|
|
36
|
+
f = load_fernet()
|
|
37
|
+
payload = gzip.decompress(f.decrypt(out.read_bytes()))
|
|
38
|
+
data = json.loads(payload.decode())
|
|
39
|
+
audit.log("checkpoint_load", agent_id=agent_id)
|
|
40
|
+
return data["state"]
|
|
41
|
+
|
|
42
|
+
def list_checkpoints(self) -> list[str]:
|
|
43
|
+
entries = []
|
|
44
|
+
for p in self.dir.glob("*.nxd"):
|
|
45
|
+
try:
|
|
46
|
+
f = load_fernet()
|
|
47
|
+
payload = gzip.decompress(f.decrypt(p.read_bytes()))
|
|
48
|
+
meta = json.loads(payload.decode())["meta"]
|
|
49
|
+
ts = time.strftime("%Y-%m-%d %H:%M", time.localtime(meta["timestamp"]))
|
|
50
|
+
entries.append(f"{meta['agent_id']} | {ts} | {len(meta['keys'])} keys")
|
|
51
|
+
except Exception:
|
|
52
|
+
entries.append(p.stem)
|
|
53
|
+
return entries
|
|
54
|
+
|
|
55
|
+
def purge(self, agent_id: str) -> None:
|
|
56
|
+
from nxd import audit
|
|
57
|
+
out = self._file(agent_id)
|
|
58
|
+
if out.exists():
|
|
59
|
+
out.unlink()
|
|
60
|
+
audit.log("checkpoint_purge", agent_id=agent_id)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_store = CheckpointStore()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class checkpoint:
|
|
67
|
+
save = staticmethod(lambda agent_id, state: _store.save(agent_id, state))
|
|
68
|
+
load = staticmethod(lambda agent_id: _store.load(agent_id))
|
|
69
|
+
list = staticmethod(lambda: _store.list_checkpoints())
|
|
70
|
+
purge = staticmethod(lambda agent_id: _store.purge(agent_id))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def reset_for_tests(path: Path) -> None:
|
|
74
|
+
global _store
|
|
75
|
+
_store = CheckpointStore(path)
|
nxd/fhe.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""FHE compute primitives — score, match, aggregate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import multiprocessing as mp
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
Mode = Literal["auto", "fast", "secure"]
|
|
13
|
+
PARALLEL_THRESHOLD = 64
|
|
14
|
+
MIN_COMPILE_ROWS = 64
|
|
15
|
+
|
|
16
|
+
_compiled_models: set[int] = set()
|
|
17
|
+
_worker_model: Any = None
|
|
18
|
+
_worker_fhe: str = "execute"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _to_float64(data: Any) -> np.ndarray:
|
|
22
|
+
if isinstance(data, pd.DataFrame):
|
|
23
|
+
arr = data.values
|
|
24
|
+
elif isinstance(data, np.ndarray):
|
|
25
|
+
arr = data
|
|
26
|
+
elif isinstance(data, dict):
|
|
27
|
+
arr = np.array(list(data.values()), dtype=np.float64)
|
|
28
|
+
elif isinstance(data, list):
|
|
29
|
+
if not data:
|
|
30
|
+
return np.empty((0, 0), dtype=np.float64)
|
|
31
|
+
if isinstance(data[0], dict):
|
|
32
|
+
arr = pd.DataFrame(data).values
|
|
33
|
+
else:
|
|
34
|
+
arr = np.array(data, dtype=np.float64)
|
|
35
|
+
else:
|
|
36
|
+
arr = np.asarray(data, dtype=np.float64)
|
|
37
|
+
return np.asarray(arr, dtype=np.float64)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _compile_set(X: np.ndarray) -> np.ndarray:
|
|
41
|
+
if len(X) >= MIN_COMPILE_ROWS:
|
|
42
|
+
return X
|
|
43
|
+
repeats = (MIN_COMPILE_ROWS // max(len(X), 1)) + 1
|
|
44
|
+
return np.tile(X, (repeats, 1))[:MIN_COMPILE_ROWS]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _resolve_fhe(mode: Mode) -> str:
|
|
48
|
+
return "simulate" if mode == "fast" else "execute"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _ensure_compiled(model: Any, X: np.ndarray) -> None:
|
|
52
|
+
key = id(model)
|
|
53
|
+
if key not in _compiled_models:
|
|
54
|
+
model.compile(_compile_set(X))
|
|
55
|
+
_compiled_models.add(key)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _worker_predict(X_batch: np.ndarray) -> np.ndarray:
|
|
59
|
+
return _worker_model.predict(X_batch, fhe=_worker_fhe)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _predict(model: Any, X: np.ndarray, mode: Mode) -> np.ndarray:
|
|
63
|
+
_ensure_compiled(model, X)
|
|
64
|
+
fhe = _resolve_fhe(mode)
|
|
65
|
+
use_parallel = mode == "auto" and fhe == "execute" and len(X) >= PARALLEL_THRESHOLD
|
|
66
|
+
if use_parallel:
|
|
67
|
+
global _worker_model, _worker_fhe
|
|
68
|
+
_worker_model = model
|
|
69
|
+
_worker_fhe = fhe
|
|
70
|
+
n_workers = min(os.cpu_count() or 8, len(X))
|
|
71
|
+
batches = [b for b in np.array_split(X, n_workers) if len(b) > 0]
|
|
72
|
+
ctx = mp.get_context("fork")
|
|
73
|
+
with ctx.Pool(len(batches)) as pool:
|
|
74
|
+
parts = pool.map(_worker_predict, batches)
|
|
75
|
+
return np.concatenate(parts)
|
|
76
|
+
return model.predict(X, fhe=fhe)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def score(model: Any, clients: Any, mode: Mode = "auto") -> list[int]:
|
|
80
|
+
from nxd import audit
|
|
81
|
+
X = _to_float64(clients)
|
|
82
|
+
if X.ndim == 1:
|
|
83
|
+
X = X.reshape(1, -1)
|
|
84
|
+
result = [int(v) for v in _predict(model, X, mode)]
|
|
85
|
+
audit.log("score", count=len(result), mode=mode)
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def match(model: Any, record_a: Any, record_b: Any) -> bool:
|
|
90
|
+
from nxd import audit
|
|
91
|
+
a = _to_float64(record_a).ravel()
|
|
92
|
+
b = _to_float64(record_b).ravel()
|
|
93
|
+
X = np.concatenate([a, b]).reshape(1, -1)
|
|
94
|
+
result = bool(int(_predict(model, X, mode="secure")[0]) == 1)
|
|
95
|
+
audit.log("match", result=result)
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def aggregate(model: Any, records: Any) -> float:
|
|
100
|
+
from nxd import audit
|
|
101
|
+
X = _to_float64(records)
|
|
102
|
+
if X.ndim == 1:
|
|
103
|
+
X = X.reshape(-1, 1)
|
|
104
|
+
result = float(np.mean(_predict(model, X, mode="auto")))
|
|
105
|
+
audit.log("aggregate", count=len(X))
|
|
106
|
+
return result
|
nxd/handoff.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Encrypted context handoff between agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from cryptography.fernet import Fernet
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Handoff:
|
|
12
|
+
def __init__(self, key: bytes | None = None):
|
|
13
|
+
self._key = key or Fernet.generate_key()
|
|
14
|
+
self._fernet = Fernet(self._key)
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def key(self) -> bytes:
|
|
18
|
+
return self._key
|
|
19
|
+
|
|
20
|
+
def pack(self, context: Any) -> str:
|
|
21
|
+
from nxd import audit
|
|
22
|
+
token = self._fernet.encrypt(json.dumps(context).encode()).decode()
|
|
23
|
+
audit.log("handoff_pack")
|
|
24
|
+
return token
|
|
25
|
+
|
|
26
|
+
def unpack(self, token: str) -> Any:
|
|
27
|
+
from nxd import audit
|
|
28
|
+
data = json.loads(self._fernet.decrypt(token.encode()).decode())
|
|
29
|
+
audit.log("handoff_unpack")
|
|
30
|
+
return data
|
nxd/redact.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""PII redaction — mask sensitive text before AI models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
_PATTERNS = [
|
|
9
|
+
(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "EMAIL"),
|
|
10
|
+
(r"\b\d{3}-\d{2}-\d{4}\b", "SSN"),
|
|
11
|
+
(r"\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b", "CARD"),
|
|
12
|
+
(r"\bsk_live_[A-Za-z0-9]+\b", "APIKEY"),
|
|
13
|
+
(r"\bBearer\s+[A-Za-z0-9._-]+\b", "BEARER"),
|
|
14
|
+
(r"\b\d{2}/\d{2}/\d{4}\b", "DATE"),
|
|
15
|
+
(r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b", "PHONE"),
|
|
16
|
+
(r"\b[A-Z][a-z]+ [A-Z][a-z]+\b", "NAME"),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def redact(text: str) -> tuple[str, dict[str, str]]:
|
|
21
|
+
from nxd import audit
|
|
22
|
+
mapping: dict[str, str] = {}
|
|
23
|
+
result = text
|
|
24
|
+
counters: dict[str, int] = {}
|
|
25
|
+
for pattern, label in _PATTERNS:
|
|
26
|
+
for match in re.finditer(pattern, result):
|
|
27
|
+
val = match.group()
|
|
28
|
+
counters[label] = counters.get(label, 0) + 1
|
|
29
|
+
placeholder = f"[{label}_{counters[label]}]"
|
|
30
|
+
mapping[placeholder] = val
|
|
31
|
+
result = result.replace(val, placeholder, 1)
|
|
32
|
+
audit.log("redact", placeholders=len(mapping))
|
|
33
|
+
return result, mapping
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def deredact(text: str, mapping: dict[str, str]) -> str:
|
|
37
|
+
from nxd import audit
|
|
38
|
+
result = text
|
|
39
|
+
for placeholder, value in mapping.items():
|
|
40
|
+
result = result.replace(placeholder, value)
|
|
41
|
+
audit.log("deredact", restored=len(mapping))
|
|
42
|
+
return result
|
nxd/seal.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Document sealing — encrypt files for storage and transit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from nxd._crypto import load_fernet
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def seal(path: str | Path, operator_id: str = "operator") -> Path:
|
|
13
|
+
from nxd import audit
|
|
14
|
+
src = Path(path)
|
|
15
|
+
f = load_fernet()
|
|
16
|
+
header = {"filename": src.name, "timestamp": time.time(), "operator": operator_id}
|
|
17
|
+
payload = json.dumps(header).encode() + b"\n---\n" + src.read_bytes()
|
|
18
|
+
out = src.with_suffix(src.suffix + ".nxd")
|
|
19
|
+
out.write_bytes(f.encrypt(payload))
|
|
20
|
+
audit.log("seal", file=src.name)
|
|
21
|
+
return out
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def unseal(path: str | Path, out_dir: str | Path | None = None) -> Path:
|
|
25
|
+
from nxd import audit
|
|
26
|
+
src = Path(path)
|
|
27
|
+
f = load_fernet()
|
|
28
|
+
payload = f.decrypt(src.read_bytes())
|
|
29
|
+
header_raw, data = payload.split(b"\n---\n", 1)
|
|
30
|
+
header = json.loads(header_raw.decode())
|
|
31
|
+
dest_dir = Path(out_dir) if out_dir else src.parent
|
|
32
|
+
out = dest_dir / header["filename"]
|
|
33
|
+
out.write_bytes(data)
|
|
34
|
+
audit.log("unseal", file=header["filename"])
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def seal_text(text: str) -> str:
|
|
39
|
+
from nxd import audit
|
|
40
|
+
token = load_fernet().encrypt(text.encode()).decode()
|
|
41
|
+
audit.log("seal_text", length=len(text))
|
|
42
|
+
return token
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def unseal_text(token: str) -> str:
|
|
46
|
+
from nxd import audit
|
|
47
|
+
text = load_fernet().decrypt(token.encode()).decode()
|
|
48
|
+
audit.log("unseal_text", length=len(text))
|
|
49
|
+
return text
|