nobulex 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.
nobulex-0.1.0/PKG-INFO ADDED
@@ -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,93 @@
1
+ # nobulex
2
+
3
+ **Trust Capital for AI agents. Tamper-proof receipts for everything your AI agent does.**
4
+
5
+ Credit scores exist for people. They don't exist for machines. Until now.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install nobulex
11
+ ```
12
+
13
+ ## Quick Start (5 minutes)
14
+
15
+ ```python
16
+ from nobulex import Agent
17
+
18
+ # Create an agent with a cryptographic identity
19
+ agent = Agent("my-agent")
20
+
21
+ # Every action generates a signed receipt
22
+ receipt = agent.act("send_email", scope="user@example.com")
23
+
24
+ # Receipts are cryptographically verifiable
25
+ assert receipt.verify() # Ed25519 signature check
26
+ print(receipt.action_ref) # SHA-256 hash of the action
27
+
28
+ # Trust Capital builds with every verified action
29
+ print(agent.trust_score) # 13.86
30
+
31
+ # Denied actions prove the system caught violations
32
+ agent.deny("delete_database", scope="production")
33
+ print(agent.trust_score) # 15.22
34
+ ```
35
+
36
+ ## What is this?
37
+
38
+ Every time an AI agent does something, Nobulex generates a **cryptographic receipt**:
39
+
40
+ - **WHO** acted (agent_id)
41
+ - **WHAT** they did (action_type)
42
+ - **ON WHAT** (scope)
43
+ - **WHEN** (timestamp_ms)
44
+ - **WHETHER** it was allowed (verdict)
45
+ - **PROOF** it happened (Ed25519 signature + SHA-256 hash)
46
+
47
+ 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.
48
+
49
+ 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.
50
+
51
+ ## Use Cases
52
+
53
+ - **Audit trails**: Prove what your agent did to regulators (EU AI Act Article 12)
54
+ - **Agent-to-agent trust**: Agents verify each other's track records before collaborating
55
+ - **Compliance**: Tamper-evident records for financial, healthcare, and legal agents
56
+ - **Accountability**: When something goes wrong, receipts prove what happened
57
+
58
+ ## API
59
+
60
+ ### Agent
61
+
62
+ ```python
63
+ from nobulex import Agent
64
+
65
+ agent = Agent("my-agent") # Create agent identity
66
+ receipt = agent.act("tool_call", # Record an action
67
+ scope="api.stripe.com")
68
+ agent.deny("unauthorized_action", # Record a caught violation
69
+ scope="admin_panel")
70
+ print(agent.trust_score) # Get Trust Capital score
71
+ print(agent.receipts) # Get all receipts
72
+ ```
73
+
74
+ ### Receipt
75
+
76
+ ```python
77
+ from nobulex import Receipt, KeyPair
78
+
79
+ keys = KeyPair()
80
+ receipt = Receipt.create(
81
+ agent_id="agent-1",
82
+ action_type="send_email",
83
+ scope="user@example.com",
84
+ keys=keys,
85
+ )
86
+ assert receipt.verify() # Verify signature
87
+ print(receipt.action_ref) # Content-addressable hash
88
+ print(receipt.to_json()) # JSON serialization
89
+ ```
90
+
91
+ ## License
92
+
93
+ MIT — Arian Gogani (@nobulexlabs)
@@ -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"]
@@ -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()
@@ -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
+ )
@@ -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
+ }
@@ -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)