nobulex 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.
- nobulex/__init__.py +30 -0
- nobulex/__main__.py +81 -0
- nobulex/agent.py +90 -0
- nobulex/chain.py +156 -0
- nobulex/crewai.py +93 -0
- nobulex/crypto.py +86 -0
- nobulex/decorator.py +105 -0
- nobulex/langchain.py +113 -0
- nobulex/receipt.py +162 -0
- nobulex/trust.py +68 -0
- nobulex-0.1.0.dist-info/METADATA +118 -0
- nobulex-0.1.0.dist-info/RECORD +14 -0
- nobulex-0.1.0.dist-info/WHEEL +5 -0
- nobulex-0.1.0.dist-info/top_level.txt +1 -0
nobulex/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Nobulex - Trust Capital for AI Agents
|
|
3
|
+
|
|
4
|
+
Tamper-proof receipts for everything your AI agent does.
|
|
5
|
+
Credit scores for machines.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from nobulex import Agent, Receipt
|
|
9
|
+
|
|
10
|
+
# Create an agent identity
|
|
11
|
+
agent = Agent("my-agent")
|
|
12
|
+
|
|
13
|
+
# Generate a receipt for any action
|
|
14
|
+
receipt = agent.act("send_email", scope="user@example.com")
|
|
15
|
+
|
|
16
|
+
# Verify any receipt
|
|
17
|
+
assert receipt.verify()
|
|
18
|
+
|
|
19
|
+
# Get trust score
|
|
20
|
+
print(agent.trust_score)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from nobulex.agent import Agent
|
|
24
|
+
from nobulex.receipt import Receipt
|
|
25
|
+
from nobulex.trust import TrustLedger
|
|
26
|
+
from nobulex.crypto import KeyPair
|
|
27
|
+
from nobulex.decorator import track
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
__all__ = ["Agent", "Receipt", "TrustLedger", "KeyPair", "track"]
|
nobulex/__main__.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Nobulex CLI - Verify receipts from the command line.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python -m nobulex verify receipt.json
|
|
7
|
+
python -m nobulex demo
|
|
8
|
+
python -m nobulex keygen
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import json
|
|
13
|
+
from nobulex import Agent, Receipt, KeyPair
|
|
14
|
+
from nobulex.chain import ReceiptChain
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmd_demo():
|
|
18
|
+
"""Run a quick demo showing receipt generation and verification."""
|
|
19
|
+
print("nobulex demo")
|
|
20
|
+
print("=" * 40)
|
|
21
|
+
agent = Agent("demo-agent")
|
|
22
|
+
r1 = agent.act("send_email", scope="user@example.com")
|
|
23
|
+
r2 = agent.act("api_call", scope="stripe.com")
|
|
24
|
+
r3 = agent.deny("delete_db", scope="production")
|
|
25
|
+
print(f"generated 3 receipts")
|
|
26
|
+
print(f" allow: {r1.action_ref[:24]}... verified={r1.verify()}")
|
|
27
|
+
print(f" allow: {r2.action_ref[:24]}... verified={r2.verify()}")
|
|
28
|
+
print(f" deny: {r3.action_ref[:24]}... verified={r3.verify()}")
|
|
29
|
+
print(f"trust score: {agent.trust_score}")
|
|
30
|
+
print(f"\ntamper test:")
|
|
31
|
+
r1.scope = "TAMPERED"
|
|
32
|
+
print(f" modified receipt verified={r1.verify()} (tamper detected)")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def cmd_verify(filepath):
|
|
36
|
+
"""Verify a receipt JSON file."""
|
|
37
|
+
try:
|
|
38
|
+
with open(filepath) as f:
|
|
39
|
+
data = json.load(f)
|
|
40
|
+
receipt = Receipt.from_dict(data)
|
|
41
|
+
ok = receipt.verify()
|
|
42
|
+
print(f"receipt: {filepath}")
|
|
43
|
+
print(f" agent: {receipt.agent_id}")
|
|
44
|
+
print(f" action: {receipt.action_type}")
|
|
45
|
+
print(f" scope: {receipt.scope}")
|
|
46
|
+
print(f" verdict: {receipt.verdict}")
|
|
47
|
+
print(f" action_ref: {receipt.action_ref[:32]}...")
|
|
48
|
+
print(f" verified: {ok}")
|
|
49
|
+
sys.exit(0 if ok else 1)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print(f"error: {e}")
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def cmd_keygen():
|
|
56
|
+
"""Generate a new Ed25519 key pair."""
|
|
57
|
+
keys = KeyPair()
|
|
58
|
+
print(f"public_key: {keys.public_hex}")
|
|
59
|
+
print("(private key held in memory only)")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main():
|
|
63
|
+
if len(sys.argv) < 2:
|
|
64
|
+
print("usage: python -m nobulex [demo|verify|keygen]")
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
|
|
67
|
+
cmd = sys.argv[1]
|
|
68
|
+
if cmd == "demo":
|
|
69
|
+
cmd_demo()
|
|
70
|
+
elif cmd == "verify" and len(sys.argv) >= 3:
|
|
71
|
+
cmd_verify(sys.argv[2])
|
|
72
|
+
elif cmd == "keygen":
|
|
73
|
+
cmd_keygen()
|
|
74
|
+
else:
|
|
75
|
+
print(f"unknown command: {cmd}")
|
|
76
|
+
print("usage: python -m nobulex [demo|verify|keygen]")
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|
nobulex/agent.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Agent identity and receipt management for Nobulex."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from nobulex.crypto import KeyPair
|
|
7
|
+
from nobulex.receipt import Receipt
|
|
8
|
+
from nobulex.trust import TrustLedger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Agent:
|
|
12
|
+
"""
|
|
13
|
+
An AI agent with a cryptographic identity and trust history.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
agent = Agent("my-agent")
|
|
17
|
+
receipt = agent.act("send_email", scope="user@example.com")
|
|
18
|
+
assert receipt.verify()
|
|
19
|
+
print(agent.trust_score)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, agent_id: str, keys: Optional[KeyPair] = None):
|
|
23
|
+
self.agent_id = agent_id
|
|
24
|
+
self.keys = keys or KeyPair()
|
|
25
|
+
self._ledger = TrustLedger()
|
|
26
|
+
self._receipts: List[Receipt] = []
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def public_key(self) -> str:
|
|
30
|
+
return self.keys.public_hex
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def trust_score(self) -> float:
|
|
34
|
+
return self._ledger.score(self.agent_id)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def receipts(self) -> List[Receipt]:
|
|
38
|
+
return list(self._receipts)
|
|
39
|
+
|
|
40
|
+
def act(
|
|
41
|
+
self,
|
|
42
|
+
action_type: str,
|
|
43
|
+
scope: str,
|
|
44
|
+
verdict: str = "ALLOW",
|
|
45
|
+
metadata: Optional[dict] = None,
|
|
46
|
+
) -> Receipt:
|
|
47
|
+
"""
|
|
48
|
+
Record an agent action and generate a signed receipt.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
action_type: What the agent did (e.g., "send_email")
|
|
52
|
+
scope: What resource was acted on
|
|
53
|
+
verdict: "ALLOW" or "DENY"
|
|
54
|
+
metadata: Optional extra data
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
A signed, verifiable Receipt
|
|
58
|
+
"""
|
|
59
|
+
receipt = Receipt.create(
|
|
60
|
+
agent_id=self.agent_id,
|
|
61
|
+
action_type=action_type,
|
|
62
|
+
scope=scope,
|
|
63
|
+
keys=self.keys,
|
|
64
|
+
verdict=verdict,
|
|
65
|
+
metadata=metadata,
|
|
66
|
+
)
|
|
67
|
+
self._receipts.append(receipt)
|
|
68
|
+
self._ledger.record(receipt)
|
|
69
|
+
return receipt
|
|
70
|
+
|
|
71
|
+
def deny(
|
|
72
|
+
self, action_type: str, scope: str, metadata: Optional[dict] = None
|
|
73
|
+
) -> Receipt:
|
|
74
|
+
"""Record a denied action. Proves the system caught a violation."""
|
|
75
|
+
return self.act(action_type, scope, verdict="DENY", metadata=metadata)
|
|
76
|
+
|
|
77
|
+
def verify_receipt(self, receipt: Receipt) -> bool:
|
|
78
|
+
"""Verify a receipt was signed by this agent."""
|
|
79
|
+
return (
|
|
80
|
+
receipt.verify()
|
|
81
|
+
and receipt.signer_public_key == self.public_key
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
n = len(self._receipts)
|
|
86
|
+
return (
|
|
87
|
+
f"Agent(id={self.agent_id!r}, "
|
|
88
|
+
f"receipts={n}, "
|
|
89
|
+
f"trust_score={self.trust_score:.2f})"
|
|
90
|
+
)
|
nobulex/chain.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Receipt chain for tamper-evident audit trails.
|
|
2
|
+
|
|
3
|
+
Links receipts together with hash pointers so tampering
|
|
4
|
+
with any single receipt breaks the entire chain.
|
|
5
|
+
|
|
6
|
+
from nobulex.chain import ReceiptChain
|
|
7
|
+
|
|
8
|
+
chain = ReceiptChain(agent_id="my-agent")
|
|
9
|
+
chain.append("send_email", scope="user@example.com")
|
|
10
|
+
chain.append("api_call", scope="stripe.com")
|
|
11
|
+
|
|
12
|
+
assert chain.verify() # Verify entire chain
|
|
13
|
+
|
|
14
|
+
# Export the audit trail
|
|
15
|
+
chain.export("audit-trail.json")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from typing import List, Optional
|
|
20
|
+
from nobulex.agent import Agent
|
|
21
|
+
from nobulex.receipt import Receipt
|
|
22
|
+
from nobulex.crypto import sha256_hex, jcs_canonicalize
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ReceiptChain:
|
|
26
|
+
"""
|
|
27
|
+
A hash-linked chain of receipts.
|
|
28
|
+
Tampering with any receipt breaks the chain.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, agent_id: str = "chain-agent"):
|
|
32
|
+
self.agent = Agent(agent_id)
|
|
33
|
+
self._chain: List[dict] = []
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def length(self) -> int:
|
|
37
|
+
return len(self._chain)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def head_hash(self) -> Optional[str]:
|
|
41
|
+
if not self._chain:
|
|
42
|
+
return None
|
|
43
|
+
return self._chain[-1]["chain_hash"]
|
|
44
|
+
|
|
45
|
+
def append(
|
|
46
|
+
self,
|
|
47
|
+
action_type: str,
|
|
48
|
+
scope: str,
|
|
49
|
+
verdict: str = "ALLOW",
|
|
50
|
+
metadata: Optional[dict] = None,
|
|
51
|
+
) -> Receipt:
|
|
52
|
+
"""Add an action to the chain."""
|
|
53
|
+
if verdict == "ALLOW":
|
|
54
|
+
receipt = self.agent.act(action_type, scope=scope, metadata=metadata)
|
|
55
|
+
else:
|
|
56
|
+
receipt = self.agent.deny(action_type, scope=scope, metadata=metadata)
|
|
57
|
+
|
|
58
|
+
# Compute chain hash: SHA-256(prev_hash + receipt.action_ref)
|
|
59
|
+
prev_hash = self.head_hash or "0" * 64
|
|
60
|
+
chain_input = prev_hash + receipt.action_ref
|
|
61
|
+
chain_hash = sha256_hex(chain_input)
|
|
62
|
+
|
|
63
|
+
self._chain.append({
|
|
64
|
+
"index": len(self._chain),
|
|
65
|
+
"receipt": receipt,
|
|
66
|
+
"prev_hash": prev_hash,
|
|
67
|
+
"chain_hash": chain_hash,
|
|
68
|
+
})
|
|
69
|
+
return receipt
|
|
70
|
+
|
|
71
|
+
def verify(self) -> bool:
|
|
72
|
+
"""Verify the entire chain is intact."""
|
|
73
|
+
if not self._chain:
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
for i, entry in enumerate(self._chain):
|
|
77
|
+
# Verify receipt signature
|
|
78
|
+
if not entry["receipt"].verify():
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
# Verify chain hash
|
|
82
|
+
prev = self._chain[i - 1]["chain_hash"] if i > 0 else "0" * 64
|
|
83
|
+
expected = sha256_hex(prev + entry["receipt"].action_ref)
|
|
84
|
+
if entry["chain_hash"] != expected:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
def export(self, filepath: str) -> None:
|
|
90
|
+
"""Export the chain as a JSON audit trail."""
|
|
91
|
+
data = {
|
|
92
|
+
"agent_id": self.agent.agent_id,
|
|
93
|
+
"chain_length": len(self._chain),
|
|
94
|
+
"head_hash": self.head_hash,
|
|
95
|
+
"verified": self.verify(),
|
|
96
|
+
"entries": [
|
|
97
|
+
{
|
|
98
|
+
"index": e["index"],
|
|
99
|
+
"action_type": e["receipt"].action_type,
|
|
100
|
+
"scope": e["receipt"].scope,
|
|
101
|
+
"verdict": e["receipt"].verdict,
|
|
102
|
+
"action_ref": e["receipt"].action_ref,
|
|
103
|
+
"timestamp_ms": e["receipt"].timestamp_ms,
|
|
104
|
+
"prev_hash": e["prev_hash"],
|
|
105
|
+
"chain_hash": e["chain_hash"],
|
|
106
|
+
}
|
|
107
|
+
for e in self._chain
|
|
108
|
+
],
|
|
109
|
+
}
|
|
110
|
+
with open(filepath, "w") as f:
|
|
111
|
+
json.dump(data, f, indent=2)
|
|
112
|
+
|
|
113
|
+
def __repr__(self) -> str:
|
|
114
|
+
v = "verified" if self.verify() else "BROKEN"
|
|
115
|
+
return f"ReceiptChain(agent={self.agent.agent_id!r}, length={self.length}, status={v})"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def verify_audit_trail(filepath: str) -> dict:
|
|
119
|
+
"""Verify an exported audit trail JSON file.
|
|
120
|
+
|
|
121
|
+
Checks hash chain integrity (prev_hash linkage).
|
|
122
|
+
Returns a verification report.
|
|
123
|
+
"""
|
|
124
|
+
import json
|
|
125
|
+
import hashlib
|
|
126
|
+
|
|
127
|
+
with open(filepath) as f:
|
|
128
|
+
data = json.load(f)
|
|
129
|
+
|
|
130
|
+
entries = data.get("entries", [])
|
|
131
|
+
results = []
|
|
132
|
+
all_valid = True
|
|
133
|
+
prev = "0" * 64
|
|
134
|
+
|
|
135
|
+
for entry in entries:
|
|
136
|
+
# Verify chain linkage
|
|
137
|
+
chain_ok = entry.get("prev_hash") == prev
|
|
138
|
+
if not chain_ok:
|
|
139
|
+
all_valid = False
|
|
140
|
+
|
|
141
|
+
results.append({
|
|
142
|
+
"index": entry.get("index", len(results)),
|
|
143
|
+
"action_type": entry.get("action_type", ""),
|
|
144
|
+
"verdict": entry.get("verdict", ""),
|
|
145
|
+
"action_ref": entry.get("action_ref", "")[:32] + "...",
|
|
146
|
+
"chain_valid": chain_ok,
|
|
147
|
+
})
|
|
148
|
+
prev = entry.get("chain_hash", "")
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
"file": filepath,
|
|
152
|
+
"total_receipts": len(results),
|
|
153
|
+
"chain_intact": all_valid,
|
|
154
|
+
"head_hash": data.get("head_hash", ""),
|
|
155
|
+
"receipts": results,
|
|
156
|
+
}
|
nobulex/crewai.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""CrewAI integration for Nobulex receipts.
|
|
2
|
+
|
|
3
|
+
Add tamper-proof receipts to any CrewAI crew:
|
|
4
|
+
|
|
5
|
+
from nobulex.crewai import NobuCrewTracker
|
|
6
|
+
tracker = NobuCrewTracker("my-crew")
|
|
7
|
+
|
|
8
|
+
# Track agent actions
|
|
9
|
+
tracker.on_task_start(agent_name="researcher", task="find data")
|
|
10
|
+
tracker.on_task_complete(agent_name="researcher", task="find data", result="found 10 results")
|
|
11
|
+
tracker.on_task_fail(agent_name="writer", task="write report", error="timeout")
|
|
12
|
+
|
|
13
|
+
print(tracker.trust_scores) # Per-agent trust scores
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Dict, List, Optional
|
|
17
|
+
from nobulex.agent import Agent
|
|
18
|
+
from nobulex.receipt import Receipt
|
|
19
|
+
from nobulex.trust import TrustLedger
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NobuCrewTracker:
|
|
23
|
+
"""Track trust across a crew of AI agents."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, crew_id: str = "crewai-crew"):
|
|
26
|
+
self.crew_id = crew_id
|
|
27
|
+
self._agents: Dict[str, Agent] = {}
|
|
28
|
+
self._all_receipts: List[Receipt] = []
|
|
29
|
+
|
|
30
|
+
def _get_agent(self, agent_name: str) -> Agent:
|
|
31
|
+
if agent_name not in self._agents:
|
|
32
|
+
agent_id = f"{self.crew_id}:{agent_name}"
|
|
33
|
+
self._agents[agent_name] = Agent(agent_id)
|
|
34
|
+
return self._agents[agent_name]
|
|
35
|
+
|
|
36
|
+
def on_task_start(
|
|
37
|
+
self, agent_name: str, task: str, metadata: Optional[dict] = None
|
|
38
|
+
) -> Receipt:
|
|
39
|
+
agent = self._get_agent(agent_name)
|
|
40
|
+
receipt = agent.act(
|
|
41
|
+
action_type=f"task:{task}",
|
|
42
|
+
scope=f"crew:{self.crew_id}",
|
|
43
|
+
metadata=metadata or {},
|
|
44
|
+
)
|
|
45
|
+
self._all_receipts.append(receipt)
|
|
46
|
+
return receipt
|
|
47
|
+
|
|
48
|
+
def on_task_complete(
|
|
49
|
+
self, agent_name: str, task: str, result: str = "", metadata: Optional[dict] = None
|
|
50
|
+
) -> Receipt:
|
|
51
|
+
agent = self._get_agent(agent_name)
|
|
52
|
+
meta = metadata or {}
|
|
53
|
+
meta["result_preview"] = result[:200]
|
|
54
|
+
receipt = agent.act(
|
|
55
|
+
action_type=f"complete:{task}",
|
|
56
|
+
scope=f"crew:{self.crew_id}",
|
|
57
|
+
metadata=meta,
|
|
58
|
+
)
|
|
59
|
+
self._all_receipts.append(receipt)
|
|
60
|
+
return receipt
|
|
61
|
+
|
|
62
|
+
def on_task_fail(
|
|
63
|
+
self, agent_name: str, task: str, error: str = "", metadata: Optional[dict] = None
|
|
64
|
+
) -> Receipt:
|
|
65
|
+
agent = self._get_agent(agent_name)
|
|
66
|
+
meta = metadata or {}
|
|
67
|
+
meta["error"] = error[:200]
|
|
68
|
+
receipt = agent.deny(
|
|
69
|
+
action_type=f"fail:{task}",
|
|
70
|
+
scope=f"crew:{self.crew_id}",
|
|
71
|
+
metadata=meta,
|
|
72
|
+
)
|
|
73
|
+
self._all_receipts.append(receipt)
|
|
74
|
+
return receipt
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def trust_scores(self) -> Dict[str, float]:
|
|
78
|
+
return {name: a.trust_score for name, a in self._agents.items()}
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def receipts(self) -> List[Receipt]:
|
|
82
|
+
return list(self._all_receipts)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def agent_names(self) -> List[str]:
|
|
86
|
+
return list(self._agents.keys())
|
|
87
|
+
|
|
88
|
+
def summary(self) -> str:
|
|
89
|
+
lines = [f"Crew: {self.crew_id}", f"Agents: {len(self._agents)}", f"Total receipts: {len(self._all_receipts)}", ""]
|
|
90
|
+
for name, agent in self._agents.items():
|
|
91
|
+
n = len(agent.receipts)
|
|
92
|
+
lines.append(f" {name}: {n} receipts, trust={agent.trust_score:.1f}")
|
|
93
|
+
return "\n".join(lines)
|
nobulex/crypto.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Cryptographic primitives for Nobulex receipts."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import rfc8785
|
|
8
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
9
|
+
Ed25519PrivateKey,
|
|
10
|
+
Ed25519PublicKey,
|
|
11
|
+
)
|
|
12
|
+
from cryptography.hazmat.primitives import serialization
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class KeyPair:
|
|
16
|
+
"""Ed25519 key pair for signing and verifying receipts."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, private_key: Optional[Ed25519PrivateKey] = None):
|
|
19
|
+
if private_key is None:
|
|
20
|
+
private_key = Ed25519PrivateKey.generate()
|
|
21
|
+
self._private = private_key
|
|
22
|
+
self._public = private_key.public_key()
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def public_bytes(self) -> bytes:
|
|
26
|
+
return self._public.public_bytes(
|
|
27
|
+
serialization.Encoding.Raw,
|
|
28
|
+
serialization.PublicFormat.Raw,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def public_hex(self) -> str:
|
|
33
|
+
return self.public_bytes.hex()
|
|
34
|
+
|
|
35
|
+
def sign(self, data: bytes) -> bytes:
|
|
36
|
+
return self._private.sign(data)
|
|
37
|
+
|
|
38
|
+
def sign_hex(self, data: bytes) -> str:
|
|
39
|
+
return self.sign(data).hex()
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def verify_signature(
|
|
43
|
+
public_bytes: bytes, signature: bytes, data: bytes
|
|
44
|
+
) -> bool:
|
|
45
|
+
try:
|
|
46
|
+
pub = Ed25519PublicKey.from_public_bytes(public_bytes)
|
|
47
|
+
pub.verify(signature, data)
|
|
48
|
+
return True
|
|
49
|
+
except Exception:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_seed(cls, seed: bytes) -> "KeyPair":
|
|
54
|
+
"""Create a key pair from a 32-byte seed (deterministic)."""
|
|
55
|
+
private_key = Ed25519PrivateKey.from_private_bytes(seed)
|
|
56
|
+
return cls(private_key)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def jcs_canonicalize(obj: dict) -> str:
|
|
60
|
+
"""
|
|
61
|
+
JSON Canonicalization Scheme (RFC 8785).
|
|
62
|
+
Uses the rfc8785 package for strict conformance.
|
|
63
|
+
"""
|
|
64
|
+
return rfc8785.dumps(obj).decode("utf-8")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def sha256_hex(data: str) -> str:
|
|
68
|
+
"""SHA-256 hash of a string, returned as lowercase hex."""
|
|
69
|
+
return hashlib.sha256(data.encode("utf-8")).hexdigest()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def compute_action_ref(
|
|
73
|
+
agent_id: str, action_type: str, scope: str, timestamp_ms: int
|
|
74
|
+
) -> str:
|
|
75
|
+
"""
|
|
76
|
+
Compute the action_ref hash from receipt preimage fields.
|
|
77
|
+
action_ref = SHA-256(JCS({agent_id, action_type, scope, timestamp_ms}))
|
|
78
|
+
"""
|
|
79
|
+
preimage = {
|
|
80
|
+
"agent_id": agent_id,
|
|
81
|
+
"action_type": action_type,
|
|
82
|
+
"scope": scope,
|
|
83
|
+
"timestamp_ms": timestamp_ms,
|
|
84
|
+
}
|
|
85
|
+
canonical = jcs_canonicalize(preimage)
|
|
86
|
+
return sha256_hex(canonical)
|
nobulex/decorator.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Universal decorator for adding receipts to any function.
|
|
2
|
+
|
|
3
|
+
Works with any Python function, any framework, any agent:
|
|
4
|
+
|
|
5
|
+
from nobulex.decorator import track
|
|
6
|
+
|
|
7
|
+
@track(agent_id="my-agent")
|
|
8
|
+
def send_email(to, subject, body):
|
|
9
|
+
# your code here
|
|
10
|
+
return "sent"
|
|
11
|
+
|
|
12
|
+
# Every call now generates a tamper-proof receipt
|
|
13
|
+
result = send_email("user@example.com", "Hello", "World")
|
|
14
|
+
|
|
15
|
+
# Access receipts
|
|
16
|
+
print(send_email.receipts)
|
|
17
|
+
print(send_email.trust_score)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import functools
|
|
21
|
+
from typing import Optional, Callable, Any
|
|
22
|
+
from nobulex.agent import Agent
|
|
23
|
+
from nobulex.receipt import Receipt
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def track(
|
|
27
|
+
agent_id: str = "tracked-agent",
|
|
28
|
+
action_type: Optional[str] = None,
|
|
29
|
+
) -> Callable:
|
|
30
|
+
"""
|
|
31
|
+
Decorator that generates a receipt for every function call.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
agent_id: Identifier for the agent
|
|
35
|
+
action_type: Override action name (defaults to function name)
|
|
36
|
+
|
|
37
|
+
The decorated function gains these attributes:
|
|
38
|
+
.receipts: List of all receipts
|
|
39
|
+
.trust_score: Current Trust Capital score
|
|
40
|
+
.agent: The underlying Agent object
|
|
41
|
+
.last_receipt: Most recent receipt
|
|
42
|
+
|
|
43
|
+
Usage:
|
|
44
|
+
@track("my-bot")
|
|
45
|
+
def process_order(order_id):
|
|
46
|
+
return do_something(order_id)
|
|
47
|
+
|
|
48
|
+
result = process_order("ORD-123")
|
|
49
|
+
print(process_order.last_receipt) # Receipt(...)
|
|
50
|
+
print(process_order.trust_score) # 13.86
|
|
51
|
+
"""
|
|
52
|
+
agent = Agent(agent_id)
|
|
53
|
+
|
|
54
|
+
def decorator(func: Callable) -> Callable:
|
|
55
|
+
@functools.wraps(func)
|
|
56
|
+
def wrapper(*args, **kwargs):
|
|
57
|
+
action = action_type or func.__name__
|
|
58
|
+
scope = f"args:{str(args)[:100]}"
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
result = func(*args, **kwargs)
|
|
62
|
+
receipt = agent.act(action, scope=scope)
|
|
63
|
+
wrapper.last_receipt = receipt
|
|
64
|
+
return result
|
|
65
|
+
except Exception as e:
|
|
66
|
+
receipt = agent.deny(
|
|
67
|
+
action, scope=f"error:{str(e)[:100]}"
|
|
68
|
+
)
|
|
69
|
+
wrapper.last_receipt = receipt
|
|
70
|
+
raise
|
|
71
|
+
|
|
72
|
+
wrapper.agent = agent
|
|
73
|
+
wrapper.receipts = agent.receipts
|
|
74
|
+
wrapper.trust_score = property(lambda self: agent.trust_score)
|
|
75
|
+
|
|
76
|
+
# Make trust_score accessible as attribute
|
|
77
|
+
class TrackedFunc:
|
|
78
|
+
def __call__(self, *args, **kwargs):
|
|
79
|
+
return wrapper(*args, **kwargs)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def receipts(self):
|
|
83
|
+
return agent.receipts
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def trust_score(self):
|
|
87
|
+
return agent.trust_score
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def agent(self):
|
|
91
|
+
return agent
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def last_receipt(self):
|
|
95
|
+
return getattr(wrapper, 'last_receipt', None)
|
|
96
|
+
|
|
97
|
+
def __getattr__(self, name):
|
|
98
|
+
return getattr(func, name)
|
|
99
|
+
|
|
100
|
+
tracked = TrackedFunc()
|
|
101
|
+
tracked.__name__ = func.__name__
|
|
102
|
+
tracked.__doc__ = func.__doc__
|
|
103
|
+
return tracked
|
|
104
|
+
|
|
105
|
+
return decorator
|
nobulex/langchain.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""LangChain middleware for Nobulex receipts.
|
|
2
|
+
|
|
3
|
+
Add tamper-proof receipts to any LangChain agent in 2 lines:
|
|
4
|
+
|
|
5
|
+
from nobulex.integrations.langchain import NobuReceipts
|
|
6
|
+
agent = NobuReceipts.wrap(your_agent, agent_id="my-agent")
|
|
7
|
+
|
|
8
|
+
Every tool call now generates a cryptographic receipt.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
12
|
+
from nobulex.agent import Agent
|
|
13
|
+
from nobulex.receipt import Receipt
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NobuReceipts:
|
|
17
|
+
"""
|
|
18
|
+
LangChain callback handler that generates receipts
|
|
19
|
+
for every tool call an agent makes.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
from langchain.agents import create_tool_calling_agent
|
|
23
|
+
from nobulex.integrations.langchain import NobuReceipts
|
|
24
|
+
|
|
25
|
+
tracker = NobuReceipts(agent_id="my-agent")
|
|
26
|
+
|
|
27
|
+
# Option 1: Use as a callback
|
|
28
|
+
agent.invoke(input, config={"callbacks": [tracker]})
|
|
29
|
+
|
|
30
|
+
# Option 2: Wrap the agent
|
|
31
|
+
wrapped = NobuReceipts.wrap(agent, "my-agent")
|
|
32
|
+
wrapped.invoke(input)
|
|
33
|
+
|
|
34
|
+
# Get receipts
|
|
35
|
+
print(tracker.receipts)
|
|
36
|
+
print(tracker.trust_score)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, agent_id: str = "langchain-agent"):
|
|
40
|
+
self.agent = Agent(agent_id)
|
|
41
|
+
self._pending: Dict[str, dict] = {}
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def receipts(self) -> List[Receipt]:
|
|
45
|
+
return self.agent.receipts
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def trust_score(self) -> float:
|
|
49
|
+
return self.agent.trust_score
|
|
50
|
+
|
|
51
|
+
def on_tool_start(
|
|
52
|
+
self, tool_name: str, tool_input: Any, **kwargs
|
|
53
|
+
) -> Receipt:
|
|
54
|
+
"""Generate a receipt when a tool call starts."""
|
|
55
|
+
scope = str(tool_input)[:200] # Truncate long inputs
|
|
56
|
+
receipt = self.agent.act(
|
|
57
|
+
action_type=f"tool:{tool_name}",
|
|
58
|
+
scope=scope,
|
|
59
|
+
)
|
|
60
|
+
return receipt
|
|
61
|
+
|
|
62
|
+
def on_tool_error(
|
|
63
|
+
self, tool_name: str, error: str, **kwargs
|
|
64
|
+
) -> Receipt:
|
|
65
|
+
"""Generate a DENY receipt when a tool call fails."""
|
|
66
|
+
receipt = self.agent.deny(
|
|
67
|
+
action_type=f"tool:{tool_name}",
|
|
68
|
+
scope=f"error:{error[:200]}",
|
|
69
|
+
)
|
|
70
|
+
return receipt
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def wrap(cls, runnable: Any, agent_id: str = "langchain-agent"):
|
|
74
|
+
"""
|
|
75
|
+
Wrap any LangChain runnable with receipt tracking.
|
|
76
|
+
|
|
77
|
+
Returns a wrapper that generates receipts for every
|
|
78
|
+
tool call while passing through all other behavior.
|
|
79
|
+
"""
|
|
80
|
+
tracker = cls(agent_id=agent_id)
|
|
81
|
+
|
|
82
|
+
class ReceiptWrapper:
|
|
83
|
+
def __init__(self, inner, tracker):
|
|
84
|
+
self._inner = inner
|
|
85
|
+
self.tracker = tracker
|
|
86
|
+
|
|
87
|
+
def invoke(self, input: Any, **kwargs) -> Any:
|
|
88
|
+
result = self._inner.invoke(input, **kwargs)
|
|
89
|
+
# Generate receipt for the invocation
|
|
90
|
+
action = "invoke"
|
|
91
|
+
scope = str(input)[:200]
|
|
92
|
+
self.tracker.agent.act(action, scope=scope)
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
async def ainvoke(self, input: Any, **kwargs) -> Any:
|
|
96
|
+
result = await self._inner.ainvoke(input, **kwargs)
|
|
97
|
+
action = "ainvoke"
|
|
98
|
+
scope = str(input)[:200]
|
|
99
|
+
self.tracker.agent.act(action, scope=scope)
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def receipts(self):
|
|
104
|
+
return self.tracker.receipts
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def trust_score(self):
|
|
108
|
+
return self.tracker.trust_score
|
|
109
|
+
|
|
110
|
+
def __getattr__(self, name):
|
|
111
|
+
return getattr(self._inner, name)
|
|
112
|
+
|
|
113
|
+
return ReceiptWrapper(runnable, tracker)
|
nobulex/receipt.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Receipt generation and verification for Nobulex."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass, field, asdict
|
|
5
|
+
from typing import Optional, Literal
|
|
6
|
+
|
|
7
|
+
from nobulex.crypto import (
|
|
8
|
+
KeyPair,
|
|
9
|
+
jcs_canonicalize,
|
|
10
|
+
sha256_hex,
|
|
11
|
+
compute_action_ref,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Receipt:
|
|
17
|
+
"""
|
|
18
|
+
A tamper-proof cryptographic receipt for an AI agent action.
|
|
19
|
+
|
|
20
|
+
Every receipt proves:
|
|
21
|
+
- WHO acted (agent_id)
|
|
22
|
+
- WHAT they did (action_type)
|
|
23
|
+
- ON WHAT (scope)
|
|
24
|
+
- WHEN (timestamp_ms)
|
|
25
|
+
- WHETHER it was allowed (verdict)
|
|
26
|
+
- WHO verified it (signer)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
agent_id: str
|
|
30
|
+
action_type: str
|
|
31
|
+
scope: str
|
|
32
|
+
timestamp_ms: int
|
|
33
|
+
verdict: Literal["ALLOW", "DENY"] = "ALLOW"
|
|
34
|
+
action_ref: str = ""
|
|
35
|
+
signature: str = ""
|
|
36
|
+
signer_public_key: str = ""
|
|
37
|
+
version: str = "nobulex-receipt-v0.1"
|
|
38
|
+
policy_version: str = ""
|
|
39
|
+
attempt_id: str = ""
|
|
40
|
+
metadata: dict = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict:
|
|
43
|
+
"""Convert receipt to dictionary (excludes empty fields)."""
|
|
44
|
+
d = asdict(self)
|
|
45
|
+
return {k: v for k, v in d.items() if v != "" and v != {}}
|
|
46
|
+
|
|
47
|
+
def to_canonical(self) -> str:
|
|
48
|
+
"""JCS-canonical JSON representation (for hashing/signing)."""
|
|
49
|
+
signable = {
|
|
50
|
+
"agent_id": self.agent_id,
|
|
51
|
+
"action_type": self.action_type,
|
|
52
|
+
"scope": self.scope,
|
|
53
|
+
"timestamp_ms": self.timestamp_ms,
|
|
54
|
+
"verdict": self.verdict,
|
|
55
|
+
"action_ref": self.action_ref,
|
|
56
|
+
"version": self.version,
|
|
57
|
+
}
|
|
58
|
+
if self.policy_version:
|
|
59
|
+
signable["policy_version"] = self.policy_version
|
|
60
|
+
if self.attempt_id:
|
|
61
|
+
signable["attempt_id"] = self.attempt_id
|
|
62
|
+
if self.metadata:
|
|
63
|
+
signable["metadata"] = self.metadata
|
|
64
|
+
return jcs_canonicalize(signable)
|
|
65
|
+
|
|
66
|
+
def verify(self) -> bool:
|
|
67
|
+
"""Verify the receipt's cryptographic signature."""
|
|
68
|
+
if not self.signature or not self.signer_public_key:
|
|
69
|
+
return False
|
|
70
|
+
try:
|
|
71
|
+
canonical = self.to_canonical()
|
|
72
|
+
return KeyPair.verify_signature(
|
|
73
|
+
bytes.fromhex(self.signer_public_key),
|
|
74
|
+
bytes.fromhex(self.signature),
|
|
75
|
+
canonical.encode("utf-8"),
|
|
76
|
+
)
|
|
77
|
+
except Exception:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def to_json(self) -> str:
|
|
81
|
+
"""Serialize receipt to JSON string."""
|
|
82
|
+
import json
|
|
83
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_dict(cls, data: dict) -> "Receipt":
|
|
87
|
+
"""Create a Receipt from a dictionary."""
|
|
88
|
+
return cls(
|
|
89
|
+
agent_id=data["agent_id"],
|
|
90
|
+
action_type=data["action_type"],
|
|
91
|
+
scope=data["scope"],
|
|
92
|
+
timestamp_ms=data["timestamp_ms"],
|
|
93
|
+
verdict=data.get("verdict", "ALLOW"),
|
|
94
|
+
action_ref=data.get("action_ref", ""),
|
|
95
|
+
signature=data.get("signature", ""),
|
|
96
|
+
signer_public_key=data.get("signer_public_key", ""),
|
|
97
|
+
version=data.get("version", "nobulex-receipt-v0.1"),
|
|
98
|
+
policy_version=data.get("policy_version", ""),
|
|
99
|
+
attempt_id=data.get("attempt_id", ""),
|
|
100
|
+
metadata=data.get("metadata", {}),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def create(
|
|
105
|
+
cls,
|
|
106
|
+
agent_id: str,
|
|
107
|
+
action_type: str,
|
|
108
|
+
scope: str,
|
|
109
|
+
keys: KeyPair,
|
|
110
|
+
verdict: str = "ALLOW",
|
|
111
|
+
timestamp_ms: Optional[int] = None,
|
|
112
|
+
metadata: Optional[dict] = None,
|
|
113
|
+
policy_version: str = "",
|
|
114
|
+
attempt_id: str = "",
|
|
115
|
+
) -> "Receipt":
|
|
116
|
+
"""
|
|
117
|
+
Create and sign a new receipt.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
agent_id: The agent's identifier
|
|
121
|
+
action_type: What the agent did (e.g., "send_email", "api_call")
|
|
122
|
+
scope: What resource was acted on
|
|
123
|
+
keys: KeyPair used to sign the receipt
|
|
124
|
+
verdict: "ALLOW" or "DENY"
|
|
125
|
+
timestamp_ms: Unix epoch milliseconds (auto-generated if omitted)
|
|
126
|
+
metadata: Optional additional data
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
A signed Receipt
|
|
130
|
+
"""
|
|
131
|
+
if timestamp_ms is None:
|
|
132
|
+
timestamp_ms = int(time.time() * 1000)
|
|
133
|
+
|
|
134
|
+
action_ref = compute_action_ref(
|
|
135
|
+
agent_id, action_type, scope, timestamp_ms
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
receipt = cls(
|
|
139
|
+
agent_id=agent_id,
|
|
140
|
+
action_type=action_type,
|
|
141
|
+
scope=scope,
|
|
142
|
+
timestamp_ms=timestamp_ms,
|
|
143
|
+
verdict=verdict,
|
|
144
|
+
action_ref=action_ref,
|
|
145
|
+
signer_public_key=keys.public_hex,
|
|
146
|
+
policy_version=policy_version,
|
|
147
|
+
attempt_id=attempt_id,
|
|
148
|
+
metadata=metadata or {},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
canonical = receipt.to_canonical()
|
|
152
|
+
receipt.signature = keys.sign_hex(canonical.encode("utf-8"))
|
|
153
|
+
return receipt
|
|
154
|
+
|
|
155
|
+
def __repr__(self) -> str:
|
|
156
|
+
status = "verified" if self.verify() else "unverified"
|
|
157
|
+
return (
|
|
158
|
+
f"Receipt(agent={self.agent_id!r}, "
|
|
159
|
+
f"action={self.action_type!r}, "
|
|
160
|
+
f"verdict={self.verdict}, "
|
|
161
|
+
f"status={status})"
|
|
162
|
+
)
|
nobulex/trust.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Trust Capital scoring for Nobulex agents."""
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Dict, List, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from nobulex.receipt import Receipt
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TrustLedger:
|
|
11
|
+
"""
|
|
12
|
+
Computes Trust Capital scores from receipt history.
|
|
13
|
+
|
|
14
|
+
Trust Capital is the credit score for machines.
|
|
15
|
+
It's earned through verified behavior, not granted.
|
|
16
|
+
You can copy the code, but you can't copy the score.
|
|
17
|
+
|
|
18
|
+
Scoring rules:
|
|
19
|
+
- Each ALLOW receipt adds trust (+1.0)
|
|
20
|
+
- Each DENY receipt proves the system caught a
|
|
21
|
+
violation, slight trust boost (+0.2)
|
|
22
|
+
- Score decays over time without new receipts
|
|
23
|
+
- Score is bounded [0.0, 100.0]
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self._receipts: Dict[str, list] = defaultdict(list)
|
|
28
|
+
|
|
29
|
+
def record(self, receipt: "Receipt") -> None:
|
|
30
|
+
"""Record a receipt in the ledger."""
|
|
31
|
+
self._receipts[receipt.agent_id].append(receipt)
|
|
32
|
+
|
|
33
|
+
def score(self, agent_id: str) -> float:
|
|
34
|
+
"""
|
|
35
|
+
Compute Trust Capital score for an agent.
|
|
36
|
+
|
|
37
|
+
Returns a score between 0.0 and 100.0.
|
|
38
|
+
"""
|
|
39
|
+
receipts = self._receipts.get(agent_id, [])
|
|
40
|
+
if not receipts:
|
|
41
|
+
return 0.0
|
|
42
|
+
|
|
43
|
+
raw = 0.0
|
|
44
|
+
for r in receipts:
|
|
45
|
+
if r.verdict == "ALLOW":
|
|
46
|
+
raw += 1.0
|
|
47
|
+
elif r.verdict == "DENY":
|
|
48
|
+
raw += 0.2 # Caught violation = slight trust
|
|
49
|
+
|
|
50
|
+
# Logarithmic scaling: diminishing returns
|
|
51
|
+
import math
|
|
52
|
+
scaled = 20.0 * math.log1p(raw)
|
|
53
|
+
|
|
54
|
+
return min(100.0, round(scaled, 2))
|
|
55
|
+
|
|
56
|
+
def history(self, agent_id: str) -> List["Receipt"]:
|
|
57
|
+
"""Get all receipts for an agent."""
|
|
58
|
+
return list(self._receipts.get(agent_id, []))
|
|
59
|
+
|
|
60
|
+
def agent_count(self) -> int:
|
|
61
|
+
"""Number of agents tracked."""
|
|
62
|
+
return len(self._receipts)
|
|
63
|
+
|
|
64
|
+
def receipt_count(self, agent_id: str = "") -> int:
|
|
65
|
+
"""Total receipts, or receipts for a specific agent."""
|
|
66
|
+
if agent_id:
|
|
67
|
+
return len(self._receipts.get(agent_id, []))
|
|
68
|
+
return sum(len(r) for r in self._receipts.values())
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nobulex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Trust Capital for AI agents. Tamper-proof receipts for everything your AI agent does.
|
|
5
|
+
Author-email: Arian Gogani <nobulex.dev@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://nobulex.com
|
|
8
|
+
Project-URL: Repository, https://github.com/arian-gogani/nobulex
|
|
9
|
+
Project-URL: Documentation, https://nobulex.com/docs
|
|
10
|
+
Keywords: ai,agents,trust,accountability,receipts,verification
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Security :: Cryptography
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: cryptography>=41.0
|
|
24
|
+
Requires-Dist: rfc8785>=0.1.2
|
|
25
|
+
|
|
26
|
+
# nobulex
|
|
27
|
+
|
|
28
|
+
**Trust Capital for AI agents. Tamper-proof receipts for everything your AI agent does.**
|
|
29
|
+
|
|
30
|
+
Credit scores exist for people. They don't exist for machines. Until now.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install nobulex
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start (5 minutes)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from nobulex import Agent
|
|
42
|
+
|
|
43
|
+
# Create an agent with a cryptographic identity
|
|
44
|
+
agent = Agent("my-agent")
|
|
45
|
+
|
|
46
|
+
# Every action generates a signed receipt
|
|
47
|
+
receipt = agent.act("send_email", scope="user@example.com")
|
|
48
|
+
|
|
49
|
+
# Receipts are cryptographically verifiable
|
|
50
|
+
assert receipt.verify() # Ed25519 signature check
|
|
51
|
+
print(receipt.action_ref) # SHA-256 hash of the action
|
|
52
|
+
|
|
53
|
+
# Trust Capital builds with every verified action
|
|
54
|
+
print(agent.trust_score) # 13.86
|
|
55
|
+
|
|
56
|
+
# Denied actions prove the system caught violations
|
|
57
|
+
agent.deny("delete_database", scope="production")
|
|
58
|
+
print(agent.trust_score) # 15.22
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## What is this?
|
|
62
|
+
|
|
63
|
+
Every time an AI agent does something, Nobulex generates a **cryptographic receipt**:
|
|
64
|
+
|
|
65
|
+
- **WHO** acted (agent_id)
|
|
66
|
+
- **WHAT** they did (action_type)
|
|
67
|
+
- **ON WHAT** (scope)
|
|
68
|
+
- **WHEN** (timestamp_ms)
|
|
69
|
+
- **WHETHER** it was allowed (verdict)
|
|
70
|
+
- **PROOF** it happened (Ed25519 signature + SHA-256 hash)
|
|
71
|
+
|
|
72
|
+
Receipts are tamper-proof. You can't edit them after the fact. You can't fake them. An independent verifier can check any receipt without trusting the agent.
|
|
73
|
+
|
|
74
|
+
Over time, receipts build into **Trust Capital** — a portable trust score that follows the agent across deployments. You can copy an agent's code, but you can't copy its credit score. The copy starts at zero.
|
|
75
|
+
|
|
76
|
+
## Use Cases
|
|
77
|
+
|
|
78
|
+
- **Audit trails**: Prove what your agent did to regulators (EU AI Act Article 12)
|
|
79
|
+
- **Agent-to-agent trust**: Agents verify each other's track records before collaborating
|
|
80
|
+
- **Compliance**: Tamper-evident records for financial, healthcare, and legal agents
|
|
81
|
+
- **Accountability**: When something goes wrong, receipts prove what happened
|
|
82
|
+
|
|
83
|
+
## API
|
|
84
|
+
|
|
85
|
+
### Agent
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from nobulex import Agent
|
|
89
|
+
|
|
90
|
+
agent = Agent("my-agent") # Create agent identity
|
|
91
|
+
receipt = agent.act("tool_call", # Record an action
|
|
92
|
+
scope="api.stripe.com")
|
|
93
|
+
agent.deny("unauthorized_action", # Record a caught violation
|
|
94
|
+
scope="admin_panel")
|
|
95
|
+
print(agent.trust_score) # Get Trust Capital score
|
|
96
|
+
print(agent.receipts) # Get all receipts
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Receipt
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from nobulex import Receipt, KeyPair
|
|
103
|
+
|
|
104
|
+
keys = KeyPair()
|
|
105
|
+
receipt = Receipt.create(
|
|
106
|
+
agent_id="agent-1",
|
|
107
|
+
action_type="send_email",
|
|
108
|
+
scope="user@example.com",
|
|
109
|
+
keys=keys,
|
|
110
|
+
)
|
|
111
|
+
assert receipt.verify() # Verify signature
|
|
112
|
+
print(receipt.action_ref) # Content-addressable hash
|
|
113
|
+
print(receipt.to_json()) # JSON serialization
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT — Arian Gogani (@nobulexlabs)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
nobulex/__init__.py,sha256=80yrzy0_bmpS1mClYfsxaRAzpzUJappMrVAMA9rUdfM,719
|
|
2
|
+
nobulex/__main__.py,sha256=JuqAQ-y3xh7CR-LQ4VxstaLPq-I4C_oG4YABzriavks,2357
|
|
3
|
+
nobulex/agent.py,sha256=Wy6Y6zFb355-yzlw5kMdaKovGQAzIaERJbgYdFTP9Y8,2545
|
|
4
|
+
nobulex/chain.py,sha256=aQAn4PhyAkhblTvrxvTxbzaEX7PW3S_q-A99quHoDmc,4773
|
|
5
|
+
nobulex/crewai.py,sha256=T5erdPb9B8EoMNqLlJ6WnYHaCrVecrkZCXk8nYNlCuE,3173
|
|
6
|
+
nobulex/crypto.py,sha256=JUmASxF5ASVJclGsAqQofRVMB5WB87KylXlgJS89lmk,2451
|
|
7
|
+
nobulex/decorator.py,sha256=kIN6hAEe4XNGtWMTFugKXHSyxglwOKLWoEq1MVmtDoo,3041
|
|
8
|
+
nobulex/langchain.py,sha256=Zyj1CaIGp9tKO9XS0KQgQXdp_Q--dE3aDMCbCQbxXtM,3514
|
|
9
|
+
nobulex/receipt.py,sha256=IgQ_pRkxUVjjeevRFYSCzGc9RsVomLVfOXJ3UWrT_Pc,5079
|
|
10
|
+
nobulex/trust.py,sha256=dhCX3MN1qtosX6i7pCvNdLpo5245-ZyGdi6oOmIPDIs,2091
|
|
11
|
+
nobulex-0.1.0.dist-info/METADATA,sha256=H5dYlwTW9umQ4GQIWUd8mmFz--qtrNOAJ4jYZAavo0c,3789
|
|
12
|
+
nobulex-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
13
|
+
nobulex-0.1.0.dist-info/top_level.txt,sha256=rdY9aOxWT0d-wwdxsuJ9YqK3jm8n3c85N47m7NC2K3U,8
|
|
14
|
+
nobulex-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nobulex
|