nxd 0.1.0__tar.gz
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-0.1.0/LICENSE +21 -0
- nxd-0.1.0/PKG-INFO +120 -0
- nxd-0.1.0/README.md +89 -0
- nxd-0.1.0/nxd/__init__.py +65 -0
- nxd-0.1.0/nxd/_crypto.py +46 -0
- nxd-0.1.0/nxd/audit.py +86 -0
- nxd-0.1.0/nxd/bind.py +50 -0
- nxd-0.1.0/nxd/blur.py +41 -0
- nxd-0.1.0/nxd/channel.py +63 -0
- nxd-0.1.0/nxd/checkpoint.py +75 -0
- nxd-0.1.0/nxd/fhe.py +106 -0
- nxd-0.1.0/nxd/handoff.py +30 -0
- nxd-0.1.0/nxd/redact.py +42 -0
- nxd-0.1.0/nxd/seal.py +49 -0
- nxd-0.1.0/nxd/search.py +72 -0
- nxd-0.1.0/nxd/shield.py +26 -0
- nxd-0.1.0/nxd/sign.py +83 -0
- nxd-0.1.0/nxd/split.py +60 -0
- nxd-0.1.0/nxd/tokenize.py +68 -0
- nxd-0.1.0/nxd/vault.py +101 -0
- nxd-0.1.0/nxd/verify.py +112 -0
- nxd-0.1.0/nxd.egg-info/PKG-INFO +120 -0
- nxd-0.1.0/nxd.egg-info/SOURCES.txt +26 -0
- nxd-0.1.0/nxd.egg-info/dependency_links.txt +1 -0
- nxd-0.1.0/nxd.egg-info/requires.txt +10 -0
- nxd-0.1.0/nxd.egg-info/top_level.txt +1 -0
- nxd-0.1.0/pyproject.toml +42 -0
- nxd-0.1.0/setup.cfg +4 -0
nxd-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nexplora Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
nxd-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nxd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Encrypted compute layer for AI agents
|
|
5
|
+
Author: Nexplora Labs
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Nexploraai/nxd
|
|
8
|
+
Project-URL: Repository, https://github.com/Nexploraai/nxd
|
|
9
|
+
Project-URL: Issues, https://github.com/Nexploraai/nxd/issues
|
|
10
|
+
Keywords: fhe,encryption,ai-agents,privacy,homomorphic-encryption
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Topic :: Security :: Cryptography
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: <3.12,>=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: concrete-ml==1.9.0
|
|
22
|
+
Requires-Dist: cryptography>=42.0.0
|
|
23
|
+
Requires-Dist: numpy>=1.26.0
|
|
24
|
+
Requires-Dist: pandas>=2.0.0
|
|
25
|
+
Requires-Dist: scikit-learn>=1.5.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest; extra == "dev"
|
|
28
|
+
Requires-Dist: build; extra == "dev"
|
|
29
|
+
Requires-Dist: twine; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# NXD
|
|
33
|
+
|
|
34
|
+
NXD is an encrypted compute layer for AI agents. It wraps fully homomorphic encryption, credential vaulting, and privacy primitives behind a single Python import — so developers can run agents on sensitive data without exposing client records, credentials, or proprietary code to models, clouds, or MCP servers.
|
|
35
|
+
|
|
36
|
+
## Three guarantees
|
|
37
|
+
|
|
38
|
+
1. **The agent works fully** — capability unchanged; scores, matches, charges, and aggregates complete normally.
|
|
39
|
+
2. **The agent sees nothing** — sensitive values stay encrypted; agents handle opaque tokens and references only.
|
|
40
|
+
3. **The operator holds the keys** — keys stay local, auditable, and revocable.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install nxd
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requires Python 3.10 or 3.11 (Concrete ML FHE dependency).
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import nxd
|
|
54
|
+
|
|
55
|
+
# FHE compute on encrypted data
|
|
56
|
+
results = nxd.score(model, clients)
|
|
57
|
+
matched = nxd.match(model, record_a, record_b)
|
|
58
|
+
average = nxd.aggregate(model, records)
|
|
59
|
+
|
|
60
|
+
# Credentials — agent never sees plaintext keys
|
|
61
|
+
vault = nxd.Vault(agent_id="billing-agent")
|
|
62
|
+
vault.store("stripe_key", "sk_live_xxxx")
|
|
63
|
+
result = vault.use("stripe_key", stripe_charge_fn)
|
|
64
|
+
vault.audit_log()
|
|
65
|
+
|
|
66
|
+
# Agent-to-agent encrypted context
|
|
67
|
+
handoff = nxd.Handoff()
|
|
68
|
+
token = handoff.pack(clients)
|
|
69
|
+
scores = nxd.receive(model, token, handoff)
|
|
70
|
+
|
|
71
|
+
# Code and text privacy before any AI call
|
|
72
|
+
protected = nxd.shield(source_code)
|
|
73
|
+
original = nxd.unshield(protected)
|
|
74
|
+
|
|
75
|
+
# Encrypted search, identity, tokenization, PII redaction
|
|
76
|
+
index = nxd.build_index(records)
|
|
77
|
+
token, hits = nxd.search(index, "diabetes")
|
|
78
|
+
nxd.register("user_123", "credential")
|
|
79
|
+
nxd.verify("user_123", candidate)
|
|
80
|
+
safe = nxd.redact("Patient John Smith, SSN 432-12-6789")
|
|
81
|
+
token = nxd.tokenize("4532-1234-5678-9010")
|
|
82
|
+
|
|
83
|
+
# Documents, channels, state, signatures
|
|
84
|
+
nxd.seal("contract.pdf")
|
|
85
|
+
ch = nxd.channel("agent-a", "agent-b")
|
|
86
|
+
nxd.checkpoint.save("agent-123", state)
|
|
87
|
+
nxd.sign("agent-a", "approve payment")
|
|
88
|
+
|
|
89
|
+
# Privacy analytics, key control, audit
|
|
90
|
+
nxd.blur(47230.0, sensitivity=1000, epsilon=1.0)
|
|
91
|
+
shares = nxd.split("master_key", n=5, m=3)
|
|
92
|
+
locked = nxd.bind(data, recipient="agent-compliance-7")
|
|
93
|
+
nxd.audit.verify()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Benchmarks (MacBook Air, Python 3.11, Concrete ML 1.9.0)
|
|
97
|
+
|
|
98
|
+
| Operation | Latency | Notes |
|
|
99
|
+
|-----------|---------|-------|
|
|
100
|
+
| FHE score (1 record) | ~183 ms | First-call cold start |
|
|
101
|
+
| FHE score (1k records, parallel) | **1.6 s** | 8 cores, ~1.6 ms/record |
|
|
102
|
+
| FHE match (single pair) | **352 ms** | Cross-system comparison |
|
|
103
|
+
| FHE aggregate (1k records, parallel) | **1.8 s** | ~0.009% quantization error |
|
|
104
|
+
| Credential vault use | <1 ms | Decrypt in memory only |
|
|
105
|
+
| Proof suite | **85/85 passed** | `python3 prove.py` |
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/Nexploraai/nxd
|
|
111
|
+
cd nxd
|
|
112
|
+
pip install -e ".[dev]"
|
|
113
|
+
python3 prove.py
|
|
114
|
+
python3 agent.py
|
|
115
|
+
python3 demo.py
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT — see [LICENSE](LICENSE).
|
nxd-0.1.0/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# NXD
|
|
2
|
+
|
|
3
|
+
NXD is an encrypted compute layer for AI agents. It wraps fully homomorphic encryption, credential vaulting, and privacy primitives behind a single Python import — so developers can run agents on sensitive data without exposing client records, credentials, or proprietary code to models, clouds, or MCP servers.
|
|
4
|
+
|
|
5
|
+
## Three guarantees
|
|
6
|
+
|
|
7
|
+
1. **The agent works fully** — capability unchanged; scores, matches, charges, and aggregates complete normally.
|
|
8
|
+
2. **The agent sees nothing** — sensitive values stay encrypted; agents handle opaque tokens and references only.
|
|
9
|
+
3. **The operator holds the keys** — keys stay local, auditable, and revocable.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install nxd
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Python 3.10 or 3.11 (Concrete ML FHE dependency).
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import nxd
|
|
23
|
+
|
|
24
|
+
# FHE compute on encrypted data
|
|
25
|
+
results = nxd.score(model, clients)
|
|
26
|
+
matched = nxd.match(model, record_a, record_b)
|
|
27
|
+
average = nxd.aggregate(model, records)
|
|
28
|
+
|
|
29
|
+
# Credentials — agent never sees plaintext keys
|
|
30
|
+
vault = nxd.Vault(agent_id="billing-agent")
|
|
31
|
+
vault.store("stripe_key", "sk_live_xxxx")
|
|
32
|
+
result = vault.use("stripe_key", stripe_charge_fn)
|
|
33
|
+
vault.audit_log()
|
|
34
|
+
|
|
35
|
+
# Agent-to-agent encrypted context
|
|
36
|
+
handoff = nxd.Handoff()
|
|
37
|
+
token = handoff.pack(clients)
|
|
38
|
+
scores = nxd.receive(model, token, handoff)
|
|
39
|
+
|
|
40
|
+
# Code and text privacy before any AI call
|
|
41
|
+
protected = nxd.shield(source_code)
|
|
42
|
+
original = nxd.unshield(protected)
|
|
43
|
+
|
|
44
|
+
# Encrypted search, identity, tokenization, PII redaction
|
|
45
|
+
index = nxd.build_index(records)
|
|
46
|
+
token, hits = nxd.search(index, "diabetes")
|
|
47
|
+
nxd.register("user_123", "credential")
|
|
48
|
+
nxd.verify("user_123", candidate)
|
|
49
|
+
safe = nxd.redact("Patient John Smith, SSN 432-12-6789")
|
|
50
|
+
token = nxd.tokenize("4532-1234-5678-9010")
|
|
51
|
+
|
|
52
|
+
# Documents, channels, state, signatures
|
|
53
|
+
nxd.seal("contract.pdf")
|
|
54
|
+
ch = nxd.channel("agent-a", "agent-b")
|
|
55
|
+
nxd.checkpoint.save("agent-123", state)
|
|
56
|
+
nxd.sign("agent-a", "approve payment")
|
|
57
|
+
|
|
58
|
+
# Privacy analytics, key control, audit
|
|
59
|
+
nxd.blur(47230.0, sensitivity=1000, epsilon=1.0)
|
|
60
|
+
shares = nxd.split("master_key", n=5, m=3)
|
|
61
|
+
locked = nxd.bind(data, recipient="agent-compliance-7")
|
|
62
|
+
nxd.audit.verify()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Benchmarks (MacBook Air, Python 3.11, Concrete ML 1.9.0)
|
|
66
|
+
|
|
67
|
+
| Operation | Latency | Notes |
|
|
68
|
+
|-----------|---------|-------|
|
|
69
|
+
| FHE score (1 record) | ~183 ms | First-call cold start |
|
|
70
|
+
| FHE score (1k records, parallel) | **1.6 s** | 8 cores, ~1.6 ms/record |
|
|
71
|
+
| FHE match (single pair) | **352 ms** | Cross-system comparison |
|
|
72
|
+
| FHE aggregate (1k records, parallel) | **1.8 s** | ~0.009% quantization error |
|
|
73
|
+
| Credential vault use | <1 ms | Decrypt in memory only |
|
|
74
|
+
| Proof suite | **85/85 passed** | `python3 prove.py` |
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
git clone https://github.com/Nexploraai/nxd
|
|
80
|
+
cd nxd
|
|
81
|
+
pip install -e ".[dev]"
|
|
82
|
+
python3 prove.py
|
|
83
|
+
python3 agent.py
|
|
84
|
+
python3 demo.py
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -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-0.1.0/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-0.1.0/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-0.1.0/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-0.1.0/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-0.1.0/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)
|
|
@@ -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)
|