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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ nobulex