proofledger 0.2.0__tar.gz → 0.4.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.
- {proofledger-0.2.0 → proofledger-0.4.0}/PKG-INFO +1 -1
- {proofledger-0.2.0 → proofledger-0.4.0}/proofledger/__init__.py +98 -2
- {proofledger-0.2.0 → proofledger-0.4.0}/proofledger/client.py +262 -1
- {proofledger-0.2.0 → proofledger-0.4.0}/proofledger/signing.py +48 -1
- {proofledger-0.2.0 → proofledger-0.4.0}/proofledger/transport.py +128 -0
- {proofledger-0.2.0 → proofledger-0.4.0}/pyproject.toml +1 -1
- {proofledger-0.2.0 → proofledger-0.4.0}/.gitignore +0 -0
- {proofledger-0.2.0 → proofledger-0.4.0}/LICENSE +0 -0
- {proofledger-0.2.0 → proofledger-0.4.0}/README.md +0 -0
- {proofledger-0.2.0 → proofledger-0.4.0}/examples/basic.py +0 -0
- {proofledger-0.2.0 → proofledger-0.4.0}/proofledger/adapters.py +0 -0
- {proofledger-0.2.0 → proofledger-0.4.0}/proofledger/hashing.py +0 -0
- {proofledger-0.2.0 → proofledger-0.4.0}/proofledger/ids.py +0 -0
- {proofledger-0.2.0 → proofledger-0.4.0}/proofledger/pricing.py +0 -0
- {proofledger-0.2.0 → proofledger-0.4.0}/tests/test_adapters.py +0 -0
- {proofledger-0.2.0 → proofledger-0.4.0}/tests/test_client.py +0 -0
- {proofledger-0.2.0 → proofledger-0.4.0}/tests/test_hashing.py +0 -0
- {proofledger-0.2.0 → proofledger-0.4.0}/tests/test_pricing.py +0 -0
- {proofledger-0.2.0 → proofledger-0.4.0}/tests/test_signing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofledger
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Framework-agnostic, tamper-evident audit layer for AI agents. Hash chains verify byte-for-byte against the ProofLedger TypeScript SDK and dashboard.
|
|
5
5
|
Project-URL: Homepage, https://proofledger.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/jorama/proofledger
|
|
@@ -18,7 +18,13 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
from typing import Any, Callable, Dict, Optional, TypeVar
|
|
20
20
|
|
|
21
|
-
from .client import
|
|
21
|
+
from .client import (
|
|
22
|
+
RunHandle,
|
|
23
|
+
ProofLedgerClient,
|
|
24
|
+
PolicyBlockedError,
|
|
25
|
+
ApprovalDeniedError,
|
|
26
|
+
iso_now,
|
|
27
|
+
)
|
|
22
28
|
from .hashing import (
|
|
23
29
|
GENESIS_HASH,
|
|
24
30
|
ChainVerificationResult,
|
|
@@ -49,6 +55,21 @@ __all__ = [
|
|
|
49
55
|
"require_approval",
|
|
50
56
|
"register_agent_identity",
|
|
51
57
|
"send_agent_message",
|
|
58
|
+
# Phase 1 AI Trust Platform
|
|
59
|
+
"register_agent",
|
|
60
|
+
"identify_agent",
|
|
61
|
+
"log_decision",
|
|
62
|
+
"log_tool_call",
|
|
63
|
+
"log_api_call",
|
|
64
|
+
"log_workflow_step",
|
|
65
|
+
"log_trust_event",
|
|
66
|
+
"evaluate_policy",
|
|
67
|
+
"sign_event",
|
|
68
|
+
"verify_event",
|
|
69
|
+
"get_trust_score",
|
|
70
|
+
"flush",
|
|
71
|
+
"PolicyBlockedError",
|
|
72
|
+
"ApprovalDeniedError",
|
|
52
73
|
# Adapters
|
|
53
74
|
"instrument",
|
|
54
75
|
"wrap_openai",
|
|
@@ -106,6 +127,15 @@ def enable(
|
|
|
106
127
|
local_dir: Optional[str] = None,
|
|
107
128
|
silent: bool = False,
|
|
108
129
|
disabled: bool = False,
|
|
130
|
+
agent_name: Optional[str] = None,
|
|
131
|
+
owner: Optional[str] = None,
|
|
132
|
+
framework: Optional[str] = None,
|
|
133
|
+
model: Optional[str] = None,
|
|
134
|
+
version: Optional[str] = None,
|
|
135
|
+
auto_capture: bool = True,
|
|
136
|
+
policy_mode: str = "monitor",
|
|
137
|
+
approval_interval_ms: float = 2000,
|
|
138
|
+
approval_timeout_ms: float = 300_000,
|
|
109
139
|
) -> ProofLedgerClient:
|
|
110
140
|
"""Initialize the global client and return it for advanced use."""
|
|
111
141
|
global _global_client
|
|
@@ -118,6 +148,15 @@ def enable(
|
|
|
118
148
|
local_dir=local_dir,
|
|
119
149
|
silent=silent,
|
|
120
150
|
disabled=disabled,
|
|
151
|
+
agent_name=agent_name,
|
|
152
|
+
owner=owner,
|
|
153
|
+
framework=framework,
|
|
154
|
+
model=model,
|
|
155
|
+
version=version,
|
|
156
|
+
auto_capture=auto_capture,
|
|
157
|
+
policy_mode=policy_mode,
|
|
158
|
+
approval_interval_ms=approval_interval_ms,
|
|
159
|
+
approval_timeout_ms=approval_timeout_ms,
|
|
121
160
|
)
|
|
122
161
|
return _global_client
|
|
123
162
|
|
|
@@ -185,13 +224,70 @@ def send_agent_message(**kwargs: Any) -> Dict[str, Any]:
|
|
|
185
224
|
return _get_client().send_agent_message(**kwargs)
|
|
186
225
|
|
|
187
226
|
|
|
227
|
+
# --- Phase 1: AI Trust Platform ----------------------------------------------
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def register_agent(**kwargs: Any) -> Dict[str, Any]:
|
|
231
|
+
"""Register this agent's identity. See ProofLedgerClient.register_agent."""
|
|
232
|
+
return _get_client().register_agent(**kwargs)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def identify_agent(agent_id: str, private_key: Optional[str] = None) -> None:
|
|
236
|
+
"""Set the active agent identity (and optional signing key)."""
|
|
237
|
+
return _get_client().identify_agent(agent_id, private_key)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def log_trust_event(**kwargs: Any) -> Dict[str, Any]:
|
|
241
|
+
"""Log one event through the trust pipeline."""
|
|
242
|
+
return _get_client().log_trust_event(**kwargs)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def log_decision(**kwargs: Any) -> Dict[str, Any]:
|
|
246
|
+
"""Log an agent decision."""
|
|
247
|
+
return _get_client().log_decision(**kwargs)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def log_tool_call(tool_name: str, **kwargs: Any) -> Dict[str, Any]:
|
|
251
|
+
"""Log a tool/MCP call."""
|
|
252
|
+
return _get_client().log_tool_call(tool_name, **kwargs)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def log_api_call(api_endpoint: str, **kwargs: Any) -> Dict[str, Any]:
|
|
256
|
+
"""Log an outbound API call."""
|
|
257
|
+
return _get_client().log_api_call(api_endpoint, **kwargs)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def log_workflow_step(
|
|
261
|
+
workflow_id: str, status: Optional[str] = None, **kwargs: Any
|
|
262
|
+
) -> Dict[str, Any]:
|
|
263
|
+
"""Log a workflow step (status="completed"/"failed" closes the workflow)."""
|
|
264
|
+
return _get_client().log_workflow_step(workflow_id, status, **kwargs)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def evaluate_policy(**kwargs: Any) -> Dict[str, Any]:
|
|
268
|
+
"""Dry-run the policy engine without logging or changing trust."""
|
|
269
|
+
return _get_client().evaluate_policy(**kwargs)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def get_trust_score(agent_id: Optional[str] = None) -> Dict[str, Any]:
|
|
273
|
+
"""Fetch the agent's live trust score and level."""
|
|
274
|
+
return _get_client().get_trust_score(agent_id)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def flush() -> None:
|
|
278
|
+
"""Await pending sends (no-op in the sync Python SDK; API parity)."""
|
|
279
|
+
return _get_client().flush()
|
|
280
|
+
|
|
281
|
+
|
|
188
282
|
# Adapters (imported lazily-safe; they resolve the global client at call time).
|
|
189
283
|
from .adapters import instrument, wrap_openai, create_langchain_handler # noqa: E402
|
|
190
284
|
|
|
191
|
-
# Signing
|
|
285
|
+
# Signing — needs the optional `cryptography` extra.
|
|
192
286
|
from .signing import ( # noqa: E402
|
|
193
287
|
create_agent_identity,
|
|
194
288
|
sign_message,
|
|
195
289
|
verify_message,
|
|
290
|
+
sign_event,
|
|
291
|
+
verify_event,
|
|
196
292
|
signing_available,
|
|
197
293
|
)
|
|
@@ -22,7 +22,13 @@ from .hashing import (
|
|
|
22
22
|
from .pricing import estimate_cost
|
|
23
23
|
from .transport import HttpTransport, LocalTransport, Transport
|
|
24
24
|
|
|
25
|
-
__all__ = [
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ProofLedgerClient",
|
|
27
|
+
"RunHandle",
|
|
28
|
+
"PolicyBlockedError",
|
|
29
|
+
"ApprovalDeniedError",
|
|
30
|
+
"iso_now",
|
|
31
|
+
]
|
|
26
32
|
|
|
27
33
|
T = TypeVar("T")
|
|
28
34
|
|
|
@@ -90,6 +96,15 @@ class ProofLedgerClient:
|
|
|
90
96
|
local_dir: Optional[str] = None,
|
|
91
97
|
silent: bool = False,
|
|
92
98
|
disabled: bool = False,
|
|
99
|
+
agent_name: Optional[str] = None,
|
|
100
|
+
owner: Optional[str] = None,
|
|
101
|
+
framework: Optional[str] = None,
|
|
102
|
+
model: Optional[str] = None,
|
|
103
|
+
version: Optional[str] = None,
|
|
104
|
+
auto_capture: bool = True,
|
|
105
|
+
policy_mode: str = "monitor",
|
|
106
|
+
approval_interval_ms: float = 2000,
|
|
107
|
+
approval_timeout_ms: float = 300_000,
|
|
93
108
|
) -> None:
|
|
94
109
|
use_local = local is True or not api_key
|
|
95
110
|
self.config: Dict[str, Any] = {
|
|
@@ -99,8 +114,23 @@ class ProofLedgerClient:
|
|
|
99
114
|
"base_url": base_url,
|
|
100
115
|
"disabled": disabled is True,
|
|
101
116
|
"silent": silent is True,
|
|
117
|
+
# Phase 1 trust platform defaults.
|
|
118
|
+
"agent_name": agent_name,
|
|
119
|
+
"owner": owner,
|
|
120
|
+
"framework": framework,
|
|
121
|
+
"model": model,
|
|
122
|
+
"version": version,
|
|
123
|
+
"auto_capture": auto_capture is not False,
|
|
124
|
+
"policy_mode": policy_mode if policy_mode in ("monitor", "enforce") else "monitor",
|
|
125
|
+
"approval_interval_ms": approval_interval_ms,
|
|
126
|
+
"approval_timeout_ms": approval_timeout_ms,
|
|
102
127
|
}
|
|
103
128
|
|
|
129
|
+
#: Active agent identity for trust events. The private key lives in
|
|
130
|
+
#: memory only — never sent to the backend, never logged.
|
|
131
|
+
self._current_agent_id: Optional[str] = agent_name
|
|
132
|
+
self._current_private_key: Optional[str] = None
|
|
133
|
+
|
|
104
134
|
self.transport: Transport = (
|
|
105
135
|
LocalTransport(local_dir or ".proofledger")
|
|
106
136
|
if use_local
|
|
@@ -551,6 +581,237 @@ class ProofLedgerClient:
|
|
|
551
581
|
}
|
|
552
582
|
)
|
|
553
583
|
|
|
584
|
+
# --- Phase 1: AI Trust Platform ------------------------------------------
|
|
585
|
+
|
|
586
|
+
def _require_agent_id(self, agent_id: Optional[str] = None) -> str:
|
|
587
|
+
resolved = agent_id or self._current_agent_id or self.config.get("agent_name")
|
|
588
|
+
if not resolved:
|
|
589
|
+
raise ValueError(
|
|
590
|
+
"ProofLedger: no agent_id. Pass one, call identify_agent(), "
|
|
591
|
+
"or set agent_name in enable()."
|
|
592
|
+
)
|
|
593
|
+
return resolved
|
|
594
|
+
|
|
595
|
+
def register_agent(self, **kwargs: Any) -> Dict[str, Any]:
|
|
596
|
+
"""Register (or update) this agent's identity in the registry.
|
|
597
|
+
|
|
598
|
+
Uses ``enable()`` config as defaults. With ``generate_keys=True`` (the
|
|
599
|
+
default when no public key is supplied) the server returns the private
|
|
600
|
+
key ONCE; the client keeps it in memory for event signing.
|
|
601
|
+
"""
|
|
602
|
+
agent_id = self._require_agent_id(kwargs.get("agent_id"))
|
|
603
|
+
payload = {
|
|
604
|
+
"projectId": self.config["project_id"],
|
|
605
|
+
"agentId": agent_id,
|
|
606
|
+
"name": kwargs.get("name") or self.config.get("agent_name") or agent_id,
|
|
607
|
+
"description": kwargs.get("description"),
|
|
608
|
+
"owner": kwargs.get("owner") or self.config.get("owner"),
|
|
609
|
+
"framework": kwargs.get("framework") or self.config.get("framework"),
|
|
610
|
+
"model": kwargs.get("model") or self.config.get("model"),
|
|
611
|
+
"version": kwargs.get("version") or self.config.get("version"),
|
|
612
|
+
"environment": kwargs.get("environment") or self.config["environment"],
|
|
613
|
+
"publicKey": kwargs.get("public_key"),
|
|
614
|
+
"generateKeys": kwargs.get(
|
|
615
|
+
"generate_keys", kwargs.get("public_key") is None
|
|
616
|
+
),
|
|
617
|
+
}
|
|
618
|
+
result = self.transport.register_agent(payload)
|
|
619
|
+
self._current_agent_id = agent_id
|
|
620
|
+
if result.get("privateKey"):
|
|
621
|
+
self._current_private_key = result["privateKey"]
|
|
622
|
+
return result
|
|
623
|
+
|
|
624
|
+
def identify_agent(self, agent_id: str, private_key: Optional[str] = None) -> None:
|
|
625
|
+
"""Set the active agent identity (and optional signing key)."""
|
|
626
|
+
self._current_agent_id = agent_id
|
|
627
|
+
if private_key:
|
|
628
|
+
self._current_private_key = private_key
|
|
629
|
+
|
|
630
|
+
def sign_event(self, event: Dict[str, Any], private_key: Optional[str] = None) -> str:
|
|
631
|
+
"""Sign an event body (eventId, agentId, eventType, action, timestamp)."""
|
|
632
|
+
from .signing import sign_event as _sign_event
|
|
633
|
+
|
|
634
|
+
key = private_key or self._current_private_key
|
|
635
|
+
if not key:
|
|
636
|
+
raise ValueError(
|
|
637
|
+
"ProofLedger: no private key. register_agent() with generate_keys "
|
|
638
|
+
"or identify_agent(agent_id, private_key)."
|
|
639
|
+
)
|
|
640
|
+
return _sign_event(key, event)
|
|
641
|
+
|
|
642
|
+
def verify_event(
|
|
643
|
+
self, public_key: str, event: Dict[str, Any], signature: str
|
|
644
|
+
) -> bool:
|
|
645
|
+
"""Verify an event signature against a public key (offline check)."""
|
|
646
|
+
from .signing import verify_event as _verify_event
|
|
647
|
+
|
|
648
|
+
return _verify_event(public_key, event, signature)
|
|
649
|
+
|
|
650
|
+
def log_trust_event(self, **kwargs: Any) -> Dict[str, Any]:
|
|
651
|
+
"""Log one event through the trust pipeline (policy → security → trust
|
|
652
|
+
→ hash chain). Signs automatically when a private key is held. In
|
|
653
|
+
enforce mode raises :class:`PolicyBlockedError` on a block decision.
|
|
654
|
+
"""
|
|
655
|
+
from .signing import signing_available
|
|
656
|
+
|
|
657
|
+
agent_id = self._require_agent_id(kwargs.get("agent_id"))
|
|
658
|
+
action = kwargs.get("action")
|
|
659
|
+
if not action:
|
|
660
|
+
raise ValueError("ProofLedger: action is required")
|
|
661
|
+
event_id = kwargs.get("event_id") or ids.new_id("evt")
|
|
662
|
+
timestamp = iso_now()
|
|
663
|
+
event_type = kwargs.get("event_type") or kwargs.get("event_category") or "custom"
|
|
664
|
+
signature = None
|
|
665
|
+
if (
|
|
666
|
+
not kwargs.get("unsigned")
|
|
667
|
+
and self._current_private_key
|
|
668
|
+
and signing_available()
|
|
669
|
+
and agent_id == (self._current_agent_id or agent_id)
|
|
670
|
+
):
|
|
671
|
+
signature = self.sign_event(
|
|
672
|
+
{
|
|
673
|
+
"eventId": event_id,
|
|
674
|
+
"agentId": agent_id,
|
|
675
|
+
"eventType": event_type,
|
|
676
|
+
"action": action,
|
|
677
|
+
"timestamp": timestamp,
|
|
678
|
+
}
|
|
679
|
+
)
|
|
680
|
+
if self.config["disabled"]:
|
|
681
|
+
return {
|
|
682
|
+
"event": {"eventId": event_id, "agentId": agent_id, "action": action},
|
|
683
|
+
"policy": {"decision": "allow", "matched": [], "reason": "SDK disabled"},
|
|
684
|
+
"trust": {"before": 100, "after": 100, "level": "trusted"},
|
|
685
|
+
}
|
|
686
|
+
payload = {
|
|
687
|
+
"projectId": self.config["project_id"],
|
|
688
|
+
"agentId": agent_id,
|
|
689
|
+
"eventId": event_id,
|
|
690
|
+
"eventType": event_type,
|
|
691
|
+
"eventCategory": kwargs.get("event_category"),
|
|
692
|
+
"action": action,
|
|
693
|
+
"inputSummary": kwargs.get("input_summary"),
|
|
694
|
+
"outputSummary": kwargs.get("output_summary"),
|
|
695
|
+
"toolName": kwargs.get("tool_name"),
|
|
696
|
+
"toolType": kwargs.get("tool_type"),
|
|
697
|
+
"mcpServer": kwargs.get("mcp_server"),
|
|
698
|
+
"apiEndpoint": kwargs.get("api_endpoint"),
|
|
699
|
+
"workflowId": kwargs.get("workflow_id"),
|
|
700
|
+
"workflowName": kwargs.get("workflow_name"),
|
|
701
|
+
"userId": kwargs.get("user_id"),
|
|
702
|
+
"sensitiveAction": kwargs.get("sensitive_action"),
|
|
703
|
+
"metadata": kwargs.get("metadata"),
|
|
704
|
+
"timestamp": timestamp,
|
|
705
|
+
"signature": signature,
|
|
706
|
+
}
|
|
707
|
+
result = self.transport.send_trust_event(payload)
|
|
708
|
+
policy = result.get("policy") or {}
|
|
709
|
+
if self.config["policy_mode"] == "enforce":
|
|
710
|
+
if policy.get("decision") == "block":
|
|
711
|
+
raise PolicyBlockedError(policy)
|
|
712
|
+
# require_approval GATES the action: block until a human decides.
|
|
713
|
+
# Fail closed — denied or timed-out-pending both raise.
|
|
714
|
+
approval = result.get("approval") or {}
|
|
715
|
+
if policy.get("decision") == "require_approval" and approval.get("id"):
|
|
716
|
+
status = self.await_approval(
|
|
717
|
+
approval["id"],
|
|
718
|
+
interval_ms=self.config["approval_interval_ms"],
|
|
719
|
+
timeout_ms=self.config["approval_timeout_ms"],
|
|
720
|
+
)
|
|
721
|
+
if status != "approved":
|
|
722
|
+
raise ApprovalDeniedError(approval["id"], status, action)
|
|
723
|
+
approval["status"] = "approved"
|
|
724
|
+
return result
|
|
725
|
+
|
|
726
|
+
def log_decision(self, **kwargs: Any) -> Dict[str, Any]:
|
|
727
|
+
"""Log an agent decision (reasoning step, plan, choice)."""
|
|
728
|
+
kwargs.update(event_type="decision", event_category="decision")
|
|
729
|
+
return self.log_trust_event(**kwargs)
|
|
730
|
+
|
|
731
|
+
def log_tool_call(self, tool_name: str, **kwargs: Any) -> Dict[str, Any]:
|
|
732
|
+
"""Log a tool/MCP call (unknown tools auto-register as unapproved)."""
|
|
733
|
+
kwargs.setdefault("action", f"Tool call: {tool_name}")
|
|
734
|
+
kwargs.update(
|
|
735
|
+
tool_name=tool_name, event_type="tool_call", event_category="tool_call"
|
|
736
|
+
)
|
|
737
|
+
return self.log_trust_event(**kwargs)
|
|
738
|
+
|
|
739
|
+
def log_api_call(self, api_endpoint: str, **kwargs: Any) -> Dict[str, Any]:
|
|
740
|
+
"""Log an outbound API call."""
|
|
741
|
+
kwargs.setdefault("action", f"API call: {api_endpoint}")
|
|
742
|
+
kwargs.update(
|
|
743
|
+
api_endpoint=api_endpoint, event_type="api_call", event_category="api_call"
|
|
744
|
+
)
|
|
745
|
+
return self.log_trust_event(**kwargs)
|
|
746
|
+
|
|
747
|
+
def log_workflow_step(
|
|
748
|
+
self, workflow_id: str, status: Optional[str] = None, **kwargs: Any
|
|
749
|
+
) -> Dict[str, Any]:
|
|
750
|
+
"""Log a workflow step; pass status="completed"/"failed" to close it."""
|
|
751
|
+
event_type = (
|
|
752
|
+
"workflow_completed"
|
|
753
|
+
if status == "completed"
|
|
754
|
+
else "workflow_failed"
|
|
755
|
+
if status == "failed"
|
|
756
|
+
else kwargs.pop("event_type", None) or "workflow_step"
|
|
757
|
+
)
|
|
758
|
+
kwargs.update(
|
|
759
|
+
workflow_id=workflow_id, event_type=event_type, event_category="workflow"
|
|
760
|
+
)
|
|
761
|
+
return self.log_trust_event(**kwargs)
|
|
762
|
+
|
|
763
|
+
def evaluate_policy(self, **kwargs: Any) -> Dict[str, Any]:
|
|
764
|
+
"""Dry-run the policy engine WITHOUT logging or changing trust."""
|
|
765
|
+
if self.config["disabled"]:
|
|
766
|
+
return {"decision": "allow", "matched": [], "reason": "SDK disabled"}
|
|
767
|
+
return self.transport.evaluate_policy(
|
|
768
|
+
{
|
|
769
|
+
"projectId": self.config["project_id"],
|
|
770
|
+
"agentId": self._require_agent_id(kwargs.get("agent_id")),
|
|
771
|
+
"eventType": kwargs.get("event_type"),
|
|
772
|
+
"eventCategory": kwargs.get("event_category"),
|
|
773
|
+
"toolName": kwargs.get("tool_name"),
|
|
774
|
+
"mcpServer": kwargs.get("mcp_server"),
|
|
775
|
+
"apiEndpoint": kwargs.get("api_endpoint"),
|
|
776
|
+
"sensitiveAction": kwargs.get("sensitive_action"),
|
|
777
|
+
}
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
def get_trust_score(self, agent_id: Optional[str] = None) -> Dict[str, Any]:
|
|
781
|
+
"""Fetch the agent's live trust score and level."""
|
|
782
|
+
return self.transport.get_trust_score(
|
|
783
|
+
self.config["project_id"], self._require_agent_id(agent_id)
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
def flush(self) -> None:
|
|
787
|
+
"""No-op for API parity — the Python SDK sends synchronously."""
|
|
788
|
+
return None
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
class PolicyBlockedError(RuntimeError):
|
|
792
|
+
"""Raised by log_* methods in enforce mode when policy blocks the action."""
|
|
793
|
+
|
|
794
|
+
def __init__(self, evaluation: Dict[str, Any]) -> None:
|
|
795
|
+
super().__init__(
|
|
796
|
+
f"ProofLedger policy blocked this action: {evaluation.get('reason')}"
|
|
797
|
+
)
|
|
798
|
+
self.evaluation = evaluation
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
class ApprovalDeniedError(RuntimeError):
|
|
802
|
+
"""Raised in enforce mode when a require_approval action is denied — or
|
|
803
|
+
still pending when the wait times out (fail-closed)."""
|
|
804
|
+
|
|
805
|
+
def __init__(self, approval_id: str, status: str, title: Optional[str] = None) -> None:
|
|
806
|
+
suffix = f": {title}" if title else ""
|
|
807
|
+
super().__init__(
|
|
808
|
+
f"ProofLedger approval timed out while pending{suffix}"
|
|
809
|
+
if status == "pending"
|
|
810
|
+
else f"ProofLedger approval denied{suffix}"
|
|
811
|
+
)
|
|
812
|
+
self.approval_id = approval_id
|
|
813
|
+
self.status = status
|
|
814
|
+
|
|
554
815
|
|
|
555
816
|
def _now_ms() -> float:
|
|
556
817
|
"""Epoch milliseconds, matching JS ``Date.now()``."""
|
|
@@ -21,7 +21,14 @@ try: # optional dependency
|
|
|
21
21
|
except Exception: # pragma: no cover - exercised only without the extra
|
|
22
22
|
_HAS_CRYPTO = False
|
|
23
23
|
|
|
24
|
-
__all__ = [
|
|
24
|
+
__all__ = [
|
|
25
|
+
"create_agent_identity",
|
|
26
|
+
"sign_message",
|
|
27
|
+
"verify_message",
|
|
28
|
+
"sign_event",
|
|
29
|
+
"verify_event",
|
|
30
|
+
"signing_available",
|
|
31
|
+
]
|
|
25
32
|
|
|
26
33
|
|
|
27
34
|
def signing_available() -> bool:
|
|
@@ -64,6 +71,46 @@ def sign_message(private_key_b64: str, envelope: Dict[str, Any]) -> str:
|
|
|
64
71
|
return base64.b64encode(priv.sign(data)).decode("ascii")
|
|
65
72
|
|
|
66
73
|
|
|
74
|
+
def _signable_event_body(event: Dict[str, Any]) -> str:
|
|
75
|
+
"""Canonical body signed for a Phase 1 trust event.
|
|
76
|
+
|
|
77
|
+
Must stay in lockstep with the backend's ``signableEventBody`` and the TS
|
|
78
|
+
SDK's ``signEvent`` — the server verifies against exactly this JSON.
|
|
79
|
+
"""
|
|
80
|
+
return canonicalize(
|
|
81
|
+
{
|
|
82
|
+
"eventId": event["eventId"],
|
|
83
|
+
"agentId": event["agentId"],
|
|
84
|
+
"eventType": event["eventType"],
|
|
85
|
+
"action": event["action"],
|
|
86
|
+
"timestamp": event["timestamp"],
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def sign_event(private_key_b64: str, event: Dict[str, Any]) -> str:
|
|
92
|
+
"""Sign a trust-platform event body. Returns a base64 signature.
|
|
93
|
+
|
|
94
|
+
``event`` must contain eventId, agentId, eventType, action, timestamp.
|
|
95
|
+
"""
|
|
96
|
+
_require()
|
|
97
|
+
priv = serialization.load_der_private_key(base64.b64decode(private_key_b64), password=None)
|
|
98
|
+
return base64.b64encode(priv.sign(_signable_event_body(event).encode("utf-8"))).decode("ascii")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def verify_event(public_key_b64: str, event: Dict[str, Any], signature_b64: str) -> bool:
|
|
102
|
+
"""Verify a trust-platform event signature against an agent's public key."""
|
|
103
|
+
if not _HAS_CRYPTO:
|
|
104
|
+
return False
|
|
105
|
+
try:
|
|
106
|
+
pub = serialization.load_der_public_key(base64.b64decode(public_key_b64))
|
|
107
|
+
data = _signable_event_body(event).encode("utf-8")
|
|
108
|
+
pub.verify(base64.b64decode(signature_b64), data) # type: ignore[union-attr]
|
|
109
|
+
return True
|
|
110
|
+
except Exception:
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
67
114
|
def verify_message(public_key_b64: str, envelope: Dict[str, Any], signature_b64: str) -> bool:
|
|
68
115
|
"""Verify a signature against an agent's public key."""
|
|
69
116
|
if not _HAS_CRYPTO:
|
|
@@ -64,6 +64,20 @@ class Transport(abc.ABC):
|
|
|
64
64
|
@abc.abstractmethod
|
|
65
65
|
def send_agent_message(self, payload: Dict[str, Any]) -> Dict[str, Any]: ...
|
|
66
66
|
|
|
67
|
+
# --- Phase 1 trust platform (non-abstract for backward compatibility) ----
|
|
68
|
+
|
|
69
|
+
def register_agent(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
|
|
72
|
+
def send_trust_event(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
73
|
+
raise NotImplementedError
|
|
74
|
+
|
|
75
|
+
def evaluate_policy(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
76
|
+
raise NotImplementedError
|
|
77
|
+
|
|
78
|
+
def get_trust_score(self, project_id: str, agent_id: str) -> Dict[str, Any]:
|
|
79
|
+
raise NotImplementedError
|
|
80
|
+
|
|
67
81
|
|
|
68
82
|
class HttpTransport(Transport):
|
|
69
83
|
"""HTTP transport using only ``urllib`` from the standard library."""
|
|
@@ -145,6 +159,28 @@ class HttpTransport(Transport):
|
|
|
145
159
|
data = self._request("/api/proofledger/messages", "POST", _drop_none(payload))
|
|
146
160
|
return (data or {}).get("message", {})
|
|
147
161
|
|
|
162
|
+
# --- Phase 1 trust platform ----------------------------------------------
|
|
163
|
+
|
|
164
|
+
def register_agent(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
165
|
+
return self._request("/api/agents", "POST", _drop_none(payload)) or {}
|
|
166
|
+
|
|
167
|
+
def send_trust_event(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
168
|
+
return self._request("/api/events", "POST", _drop_none(payload)) or {}
|
|
169
|
+
|
|
170
|
+
def evaluate_policy(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
171
|
+
data = self._request("/api/policies/evaluate", "POST", _drop_none(payload))
|
|
172
|
+
return (data or {}).get("evaluation", {})
|
|
173
|
+
|
|
174
|
+
def get_trust_score(self, project_id: str, agent_id: str) -> Dict[str, Any]:
|
|
175
|
+
quoted_agent = urllib.parse.quote(agent_id, safe="")
|
|
176
|
+
quoted_project = urllib.parse.quote(project_id, safe="")
|
|
177
|
+
return (
|
|
178
|
+
self._request(
|
|
179
|
+
f"/api/agents/{quoted_agent}/trust?projectId={quoted_project}", "GET"
|
|
180
|
+
)
|
|
181
|
+
or {}
|
|
182
|
+
)
|
|
183
|
+
|
|
148
184
|
|
|
149
185
|
class LocalTransport(Transport):
|
|
150
186
|
"""Local-first transport: in-memory store + best-effort JSONL log.
|
|
@@ -208,6 +244,98 @@ class LocalTransport(Transport):
|
|
|
208
244
|
verified = bool(public_key) and verify_message(public_key, envelope, payload["signature"])
|
|
209
245
|
return {"id": new_id("msg"), "verified": verified}
|
|
210
246
|
|
|
247
|
+
# --- Phase 1 trust platform (local approximations) ------------------------
|
|
248
|
+
# Local mode has no policy store or registry: everything is allowed, trust
|
|
249
|
+
# stays at 100, and signatures verify against locally-registered keys.
|
|
250
|
+
|
|
251
|
+
def register_agent(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
252
|
+
if not hasattr(self, "_trust_agents"):
|
|
253
|
+
self._trust_agents: Dict[str, Dict[str, Any]] = {}
|
|
254
|
+
agent_id = payload["agentId"]
|
|
255
|
+
existing = self._trust_agents.get(agent_id)
|
|
256
|
+
public_key = payload.get("publicKey") or (existing or {}).get("publicKey")
|
|
257
|
+
private_key = None
|
|
258
|
+
if not public_key and payload.get("generateKeys"):
|
|
259
|
+
from .signing import create_agent_identity, signing_available
|
|
260
|
+
|
|
261
|
+
if signing_available():
|
|
262
|
+
pair = create_agent_identity()
|
|
263
|
+
public_key = pair["publicKey"]
|
|
264
|
+
private_key = pair["privateKey"]
|
|
265
|
+
agent = {
|
|
266
|
+
"agentId": agent_id,
|
|
267
|
+
"name": payload.get("name") or agent_id,
|
|
268
|
+
"owner": payload.get("owner"),
|
|
269
|
+
"framework": payload.get("framework"),
|
|
270
|
+
"model": payload.get("model"),
|
|
271
|
+
"version": payload.get("version"),
|
|
272
|
+
"environment": payload.get("environment") or "development",
|
|
273
|
+
"publicKey": public_key,
|
|
274
|
+
"status": "active",
|
|
275
|
+
"trustScore": 100,
|
|
276
|
+
"trustLevel": "trusted",
|
|
277
|
+
}
|
|
278
|
+
self._trust_agents[agent_id] = agent
|
|
279
|
+
if public_key:
|
|
280
|
+
self._identities[agent_id] = public_key
|
|
281
|
+
result: Dict[str, Any] = {"agent": agent, "created": existing is None}
|
|
282
|
+
if private_key:
|
|
283
|
+
result["privateKey"] = private_key
|
|
284
|
+
return result
|
|
285
|
+
|
|
286
|
+
def send_trust_event(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
287
|
+
from .signing import verify_event
|
|
288
|
+
|
|
289
|
+
agent_id = payload["agentId"]
|
|
290
|
+
signature_valid = None
|
|
291
|
+
if payload.get("signature"):
|
|
292
|
+
public_key = self._identities.get(agent_id)
|
|
293
|
+
signature_valid = bool(public_key) and verify_event(
|
|
294
|
+
public_key,
|
|
295
|
+
{
|
|
296
|
+
"eventId": payload.get("eventId"),
|
|
297
|
+
"agentId": agent_id,
|
|
298
|
+
"eventType": payload.get("eventType"),
|
|
299
|
+
"action": payload.get("action"),
|
|
300
|
+
"timestamp": payload.get("timestamp"),
|
|
301
|
+
},
|
|
302
|
+
payload["signature"],
|
|
303
|
+
)
|
|
304
|
+
return {
|
|
305
|
+
"event": {
|
|
306
|
+
"eventId": payload.get("eventId") or new_id("evt"),
|
|
307
|
+
"agentId": agent_id,
|
|
308
|
+
"eventType": payload.get("eventType"),
|
|
309
|
+
"eventCategory": payload.get("eventCategory") or "system",
|
|
310
|
+
"action": payload.get("action"),
|
|
311
|
+
"policyDecision": "allow",
|
|
312
|
+
"signatureValid": signature_valid,
|
|
313
|
+
"hash": "",
|
|
314
|
+
"previousHash": "",
|
|
315
|
+
},
|
|
316
|
+
"policy": {
|
|
317
|
+
"decision": "allow",
|
|
318
|
+
"matched": [],
|
|
319
|
+
"reason": "Local mode — policies are not evaluated",
|
|
320
|
+
},
|
|
321
|
+
"trust": {"before": 100, "after": 100, "level": "trusted"},
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
def evaluate_policy(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
325
|
+
return {
|
|
326
|
+
"decision": "allow",
|
|
327
|
+
"matched": [],
|
|
328
|
+
"reason": "Local mode — policies are not evaluated",
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
def get_trust_score(self, project_id: str, agent_id: str) -> Dict[str, Any]:
|
|
332
|
+
return {
|
|
333
|
+
"agentId": agent_id,
|
|
334
|
+
"trustScore": 100,
|
|
335
|
+
"trustLevel": "trusted",
|
|
336
|
+
"status": "active",
|
|
337
|
+
}
|
|
338
|
+
|
|
211
339
|
def _persist(self, event: Dict[str, Any]) -> None:
|
|
212
340
|
try:
|
|
213
341
|
if not self._dir_ready:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proofledger"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Framework-agnostic, tamper-evident audit layer for AI agents. Hash chains verify byte-for-byte against the ProofLedger TypeScript SDK and dashboard."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|