proofledger 0.2.0__tar.gz → 0.3.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.3.0}/PKG-INFO +1 -1
- {proofledger-0.2.0 → proofledger-0.3.0}/proofledger/__init__.py +87 -2
- {proofledger-0.2.0 → proofledger-0.3.0}/proofledger/client.py +224 -1
- {proofledger-0.2.0 → proofledger-0.3.0}/proofledger/signing.py +48 -1
- {proofledger-0.2.0 → proofledger-0.3.0}/proofledger/transport.py +128 -0
- {proofledger-0.2.0 → proofledger-0.3.0}/pyproject.toml +1 -1
- {proofledger-0.2.0 → proofledger-0.3.0}/.gitignore +0 -0
- {proofledger-0.2.0 → proofledger-0.3.0}/LICENSE +0 -0
- {proofledger-0.2.0 → proofledger-0.3.0}/README.md +0 -0
- {proofledger-0.2.0 → proofledger-0.3.0}/examples/basic.py +0 -0
- {proofledger-0.2.0 → proofledger-0.3.0}/proofledger/adapters.py +0 -0
- {proofledger-0.2.0 → proofledger-0.3.0}/proofledger/hashing.py +0 -0
- {proofledger-0.2.0 → proofledger-0.3.0}/proofledger/ids.py +0 -0
- {proofledger-0.2.0 → proofledger-0.3.0}/proofledger/pricing.py +0 -0
- {proofledger-0.2.0 → proofledger-0.3.0}/tests/test_adapters.py +0 -0
- {proofledger-0.2.0 → proofledger-0.3.0}/tests/test_client.py +0 -0
- {proofledger-0.2.0 → proofledger-0.3.0}/tests/test_hashing.py +0 -0
- {proofledger-0.2.0 → proofledger-0.3.0}/tests/test_pricing.py +0 -0
- {proofledger-0.2.0 → proofledger-0.3.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.3.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,7 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
from typing import Any, Callable, Dict, Optional, TypeVar
|
|
20
20
|
|
|
21
|
-
from .client import RunHandle, ProofLedgerClient, iso_now
|
|
21
|
+
from .client import RunHandle, ProofLedgerClient, PolicyBlockedError, iso_now
|
|
22
22
|
from .hashing import (
|
|
23
23
|
GENESIS_HASH,
|
|
24
24
|
ChainVerificationResult,
|
|
@@ -49,6 +49,20 @@ __all__ = [
|
|
|
49
49
|
"require_approval",
|
|
50
50
|
"register_agent_identity",
|
|
51
51
|
"send_agent_message",
|
|
52
|
+
# Phase 1 AI Trust Platform
|
|
53
|
+
"register_agent",
|
|
54
|
+
"identify_agent",
|
|
55
|
+
"log_decision",
|
|
56
|
+
"log_tool_call",
|
|
57
|
+
"log_api_call",
|
|
58
|
+
"log_workflow_step",
|
|
59
|
+
"log_trust_event",
|
|
60
|
+
"evaluate_policy",
|
|
61
|
+
"sign_event",
|
|
62
|
+
"verify_event",
|
|
63
|
+
"get_trust_score",
|
|
64
|
+
"flush",
|
|
65
|
+
"PolicyBlockedError",
|
|
52
66
|
# Adapters
|
|
53
67
|
"instrument",
|
|
54
68
|
"wrap_openai",
|
|
@@ -106,6 +120,13 @@ def enable(
|
|
|
106
120
|
local_dir: Optional[str] = None,
|
|
107
121
|
silent: bool = False,
|
|
108
122
|
disabled: bool = False,
|
|
123
|
+
agent_name: Optional[str] = None,
|
|
124
|
+
owner: Optional[str] = None,
|
|
125
|
+
framework: Optional[str] = None,
|
|
126
|
+
model: Optional[str] = None,
|
|
127
|
+
version: Optional[str] = None,
|
|
128
|
+
auto_capture: bool = True,
|
|
129
|
+
policy_mode: str = "monitor",
|
|
109
130
|
) -> ProofLedgerClient:
|
|
110
131
|
"""Initialize the global client and return it for advanced use."""
|
|
111
132
|
global _global_client
|
|
@@ -118,6 +139,13 @@ def enable(
|
|
|
118
139
|
local_dir=local_dir,
|
|
119
140
|
silent=silent,
|
|
120
141
|
disabled=disabled,
|
|
142
|
+
agent_name=agent_name,
|
|
143
|
+
owner=owner,
|
|
144
|
+
framework=framework,
|
|
145
|
+
model=model,
|
|
146
|
+
version=version,
|
|
147
|
+
auto_capture=auto_capture,
|
|
148
|
+
policy_mode=policy_mode,
|
|
121
149
|
)
|
|
122
150
|
return _global_client
|
|
123
151
|
|
|
@@ -185,13 +213,70 @@ def send_agent_message(**kwargs: Any) -> Dict[str, Any]:
|
|
|
185
213
|
return _get_client().send_agent_message(**kwargs)
|
|
186
214
|
|
|
187
215
|
|
|
216
|
+
# --- Phase 1: AI Trust Platform ----------------------------------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def register_agent(**kwargs: Any) -> Dict[str, Any]:
|
|
220
|
+
"""Register this agent's identity. See ProofLedgerClient.register_agent."""
|
|
221
|
+
return _get_client().register_agent(**kwargs)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def identify_agent(agent_id: str, private_key: Optional[str] = None) -> None:
|
|
225
|
+
"""Set the active agent identity (and optional signing key)."""
|
|
226
|
+
return _get_client().identify_agent(agent_id, private_key)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def log_trust_event(**kwargs: Any) -> Dict[str, Any]:
|
|
230
|
+
"""Log one event through the trust pipeline."""
|
|
231
|
+
return _get_client().log_trust_event(**kwargs)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def log_decision(**kwargs: Any) -> Dict[str, Any]:
|
|
235
|
+
"""Log an agent decision."""
|
|
236
|
+
return _get_client().log_decision(**kwargs)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def log_tool_call(tool_name: str, **kwargs: Any) -> Dict[str, Any]:
|
|
240
|
+
"""Log a tool/MCP call."""
|
|
241
|
+
return _get_client().log_tool_call(tool_name, **kwargs)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def log_api_call(api_endpoint: str, **kwargs: Any) -> Dict[str, Any]:
|
|
245
|
+
"""Log an outbound API call."""
|
|
246
|
+
return _get_client().log_api_call(api_endpoint, **kwargs)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def log_workflow_step(
|
|
250
|
+
workflow_id: str, status: Optional[str] = None, **kwargs: Any
|
|
251
|
+
) -> Dict[str, Any]:
|
|
252
|
+
"""Log a workflow step (status="completed"/"failed" closes the workflow)."""
|
|
253
|
+
return _get_client().log_workflow_step(workflow_id, status, **kwargs)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def evaluate_policy(**kwargs: Any) -> Dict[str, Any]:
|
|
257
|
+
"""Dry-run the policy engine without logging or changing trust."""
|
|
258
|
+
return _get_client().evaluate_policy(**kwargs)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_trust_score(agent_id: Optional[str] = None) -> Dict[str, Any]:
|
|
262
|
+
"""Fetch the agent's live trust score and level."""
|
|
263
|
+
return _get_client().get_trust_score(agent_id)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def flush() -> None:
|
|
267
|
+
"""Await pending sends (no-op in the sync Python SDK; API parity)."""
|
|
268
|
+
return _get_client().flush()
|
|
269
|
+
|
|
270
|
+
|
|
188
271
|
# Adapters (imported lazily-safe; they resolve the global client at call time).
|
|
189
272
|
from .adapters import instrument, wrap_openai, create_langchain_handler # noqa: E402
|
|
190
273
|
|
|
191
|
-
# Signing
|
|
274
|
+
# Signing — needs the optional `cryptography` extra.
|
|
192
275
|
from .signing import ( # noqa: E402
|
|
193
276
|
create_agent_identity,
|
|
194
277
|
sign_message,
|
|
195
278
|
verify_message,
|
|
279
|
+
sign_event,
|
|
280
|
+
verify_event,
|
|
196
281
|
signing_available,
|
|
197
282
|
)
|
|
@@ -22,7 +22,7 @@ from .hashing import (
|
|
|
22
22
|
from .pricing import estimate_cost
|
|
23
23
|
from .transport import HttpTransport, LocalTransport, Transport
|
|
24
24
|
|
|
25
|
-
__all__ = ["ProofLedgerClient", "RunHandle", "iso_now"]
|
|
25
|
+
__all__ = ["ProofLedgerClient", "RunHandle", "PolicyBlockedError", "iso_now"]
|
|
26
26
|
|
|
27
27
|
T = TypeVar("T")
|
|
28
28
|
|
|
@@ -90,6 +90,13 @@ class ProofLedgerClient:
|
|
|
90
90
|
local_dir: Optional[str] = None,
|
|
91
91
|
silent: bool = False,
|
|
92
92
|
disabled: bool = False,
|
|
93
|
+
agent_name: Optional[str] = None,
|
|
94
|
+
owner: Optional[str] = None,
|
|
95
|
+
framework: Optional[str] = None,
|
|
96
|
+
model: Optional[str] = None,
|
|
97
|
+
version: Optional[str] = None,
|
|
98
|
+
auto_capture: bool = True,
|
|
99
|
+
policy_mode: str = "monitor",
|
|
93
100
|
) -> None:
|
|
94
101
|
use_local = local is True or not api_key
|
|
95
102
|
self.config: Dict[str, Any] = {
|
|
@@ -99,8 +106,21 @@ class ProofLedgerClient:
|
|
|
99
106
|
"base_url": base_url,
|
|
100
107
|
"disabled": disabled is True,
|
|
101
108
|
"silent": silent is True,
|
|
109
|
+
# Phase 1 trust platform defaults.
|
|
110
|
+
"agent_name": agent_name,
|
|
111
|
+
"owner": owner,
|
|
112
|
+
"framework": framework,
|
|
113
|
+
"model": model,
|
|
114
|
+
"version": version,
|
|
115
|
+
"auto_capture": auto_capture is not False,
|
|
116
|
+
"policy_mode": policy_mode if policy_mode in ("monitor", "enforce") else "monitor",
|
|
102
117
|
}
|
|
103
118
|
|
|
119
|
+
#: Active agent identity for trust events. The private key lives in
|
|
120
|
+
#: memory only — never sent to the backend, never logged.
|
|
121
|
+
self._current_agent_id: Optional[str] = agent_name
|
|
122
|
+
self._current_private_key: Optional[str] = None
|
|
123
|
+
|
|
104
124
|
self.transport: Transport = (
|
|
105
125
|
LocalTransport(local_dir or ".proofledger")
|
|
106
126
|
if use_local
|
|
@@ -551,6 +571,209 @@ class ProofLedgerClient:
|
|
|
551
571
|
}
|
|
552
572
|
)
|
|
553
573
|
|
|
574
|
+
# --- Phase 1: AI Trust Platform ------------------------------------------
|
|
575
|
+
|
|
576
|
+
def _require_agent_id(self, agent_id: Optional[str] = None) -> str:
|
|
577
|
+
resolved = agent_id or self._current_agent_id or self.config.get("agent_name")
|
|
578
|
+
if not resolved:
|
|
579
|
+
raise ValueError(
|
|
580
|
+
"ProofLedger: no agent_id. Pass one, call identify_agent(), "
|
|
581
|
+
"or set agent_name in enable()."
|
|
582
|
+
)
|
|
583
|
+
return resolved
|
|
584
|
+
|
|
585
|
+
def register_agent(self, **kwargs: Any) -> Dict[str, Any]:
|
|
586
|
+
"""Register (or update) this agent's identity in the registry.
|
|
587
|
+
|
|
588
|
+
Uses ``enable()`` config as defaults. With ``generate_keys=True`` (the
|
|
589
|
+
default when no public key is supplied) the server returns the private
|
|
590
|
+
key ONCE; the client keeps it in memory for event signing.
|
|
591
|
+
"""
|
|
592
|
+
agent_id = self._require_agent_id(kwargs.get("agent_id"))
|
|
593
|
+
payload = {
|
|
594
|
+
"projectId": self.config["project_id"],
|
|
595
|
+
"agentId": agent_id,
|
|
596
|
+
"name": kwargs.get("name") or self.config.get("agent_name") or agent_id,
|
|
597
|
+
"description": kwargs.get("description"),
|
|
598
|
+
"owner": kwargs.get("owner") or self.config.get("owner"),
|
|
599
|
+
"framework": kwargs.get("framework") or self.config.get("framework"),
|
|
600
|
+
"model": kwargs.get("model") or self.config.get("model"),
|
|
601
|
+
"version": kwargs.get("version") or self.config.get("version"),
|
|
602
|
+
"environment": kwargs.get("environment") or self.config["environment"],
|
|
603
|
+
"publicKey": kwargs.get("public_key"),
|
|
604
|
+
"generateKeys": kwargs.get(
|
|
605
|
+
"generate_keys", kwargs.get("public_key") is None
|
|
606
|
+
),
|
|
607
|
+
}
|
|
608
|
+
result = self.transport.register_agent(payload)
|
|
609
|
+
self._current_agent_id = agent_id
|
|
610
|
+
if result.get("privateKey"):
|
|
611
|
+
self._current_private_key = result["privateKey"]
|
|
612
|
+
return result
|
|
613
|
+
|
|
614
|
+
def identify_agent(self, agent_id: str, private_key: Optional[str] = None) -> None:
|
|
615
|
+
"""Set the active agent identity (and optional signing key)."""
|
|
616
|
+
self._current_agent_id = agent_id
|
|
617
|
+
if private_key:
|
|
618
|
+
self._current_private_key = private_key
|
|
619
|
+
|
|
620
|
+
def sign_event(self, event: Dict[str, Any], private_key: Optional[str] = None) -> str:
|
|
621
|
+
"""Sign an event body (eventId, agentId, eventType, action, timestamp)."""
|
|
622
|
+
from .signing import sign_event as _sign_event
|
|
623
|
+
|
|
624
|
+
key = private_key or self._current_private_key
|
|
625
|
+
if not key:
|
|
626
|
+
raise ValueError(
|
|
627
|
+
"ProofLedger: no private key. register_agent() with generate_keys "
|
|
628
|
+
"or identify_agent(agent_id, private_key)."
|
|
629
|
+
)
|
|
630
|
+
return _sign_event(key, event)
|
|
631
|
+
|
|
632
|
+
def verify_event(
|
|
633
|
+
self, public_key: str, event: Dict[str, Any], signature: str
|
|
634
|
+
) -> bool:
|
|
635
|
+
"""Verify an event signature against a public key (offline check)."""
|
|
636
|
+
from .signing import verify_event as _verify_event
|
|
637
|
+
|
|
638
|
+
return _verify_event(public_key, event, signature)
|
|
639
|
+
|
|
640
|
+
def log_trust_event(self, **kwargs: Any) -> Dict[str, Any]:
|
|
641
|
+
"""Log one event through the trust pipeline (policy → security → trust
|
|
642
|
+
→ hash chain). Signs automatically when a private key is held. In
|
|
643
|
+
enforce mode raises :class:`PolicyBlockedError` on a block decision.
|
|
644
|
+
"""
|
|
645
|
+
from .signing import signing_available
|
|
646
|
+
|
|
647
|
+
agent_id = self._require_agent_id(kwargs.get("agent_id"))
|
|
648
|
+
action = kwargs.get("action")
|
|
649
|
+
if not action:
|
|
650
|
+
raise ValueError("ProofLedger: action is required")
|
|
651
|
+
event_id = kwargs.get("event_id") or ids.new_id("evt")
|
|
652
|
+
timestamp = iso_now()
|
|
653
|
+
event_type = kwargs.get("event_type") or kwargs.get("event_category") or "custom"
|
|
654
|
+
signature = None
|
|
655
|
+
if (
|
|
656
|
+
not kwargs.get("unsigned")
|
|
657
|
+
and self._current_private_key
|
|
658
|
+
and signing_available()
|
|
659
|
+
and agent_id == (self._current_agent_id or agent_id)
|
|
660
|
+
):
|
|
661
|
+
signature = self.sign_event(
|
|
662
|
+
{
|
|
663
|
+
"eventId": event_id,
|
|
664
|
+
"agentId": agent_id,
|
|
665
|
+
"eventType": event_type,
|
|
666
|
+
"action": action,
|
|
667
|
+
"timestamp": timestamp,
|
|
668
|
+
}
|
|
669
|
+
)
|
|
670
|
+
if self.config["disabled"]:
|
|
671
|
+
return {
|
|
672
|
+
"event": {"eventId": event_id, "agentId": agent_id, "action": action},
|
|
673
|
+
"policy": {"decision": "allow", "matched": [], "reason": "SDK disabled"},
|
|
674
|
+
"trust": {"before": 100, "after": 100, "level": "trusted"},
|
|
675
|
+
}
|
|
676
|
+
payload = {
|
|
677
|
+
"projectId": self.config["project_id"],
|
|
678
|
+
"agentId": agent_id,
|
|
679
|
+
"eventId": event_id,
|
|
680
|
+
"eventType": event_type,
|
|
681
|
+
"eventCategory": kwargs.get("event_category"),
|
|
682
|
+
"action": action,
|
|
683
|
+
"inputSummary": kwargs.get("input_summary"),
|
|
684
|
+
"outputSummary": kwargs.get("output_summary"),
|
|
685
|
+
"toolName": kwargs.get("tool_name"),
|
|
686
|
+
"toolType": kwargs.get("tool_type"),
|
|
687
|
+
"mcpServer": kwargs.get("mcp_server"),
|
|
688
|
+
"apiEndpoint": kwargs.get("api_endpoint"),
|
|
689
|
+
"workflowId": kwargs.get("workflow_id"),
|
|
690
|
+
"workflowName": kwargs.get("workflow_name"),
|
|
691
|
+
"userId": kwargs.get("user_id"),
|
|
692
|
+
"sensitiveAction": kwargs.get("sensitive_action"),
|
|
693
|
+
"metadata": kwargs.get("metadata"),
|
|
694
|
+
"timestamp": timestamp,
|
|
695
|
+
"signature": signature,
|
|
696
|
+
}
|
|
697
|
+
result = self.transport.send_trust_event(payload)
|
|
698
|
+
policy = result.get("policy") or {}
|
|
699
|
+
if self.config["policy_mode"] == "enforce" and policy.get("decision") == "block":
|
|
700
|
+
raise PolicyBlockedError(policy)
|
|
701
|
+
return result
|
|
702
|
+
|
|
703
|
+
def log_decision(self, **kwargs: Any) -> Dict[str, Any]:
|
|
704
|
+
"""Log an agent decision (reasoning step, plan, choice)."""
|
|
705
|
+
kwargs.update(event_type="decision", event_category="decision")
|
|
706
|
+
return self.log_trust_event(**kwargs)
|
|
707
|
+
|
|
708
|
+
def log_tool_call(self, tool_name: str, **kwargs: Any) -> Dict[str, Any]:
|
|
709
|
+
"""Log a tool/MCP call (unknown tools auto-register as unapproved)."""
|
|
710
|
+
kwargs.setdefault("action", f"Tool call: {tool_name}")
|
|
711
|
+
kwargs.update(
|
|
712
|
+
tool_name=tool_name, event_type="tool_call", event_category="tool_call"
|
|
713
|
+
)
|
|
714
|
+
return self.log_trust_event(**kwargs)
|
|
715
|
+
|
|
716
|
+
def log_api_call(self, api_endpoint: str, **kwargs: Any) -> Dict[str, Any]:
|
|
717
|
+
"""Log an outbound API call."""
|
|
718
|
+
kwargs.setdefault("action", f"API call: {api_endpoint}")
|
|
719
|
+
kwargs.update(
|
|
720
|
+
api_endpoint=api_endpoint, event_type="api_call", event_category="api_call"
|
|
721
|
+
)
|
|
722
|
+
return self.log_trust_event(**kwargs)
|
|
723
|
+
|
|
724
|
+
def log_workflow_step(
|
|
725
|
+
self, workflow_id: str, status: Optional[str] = None, **kwargs: Any
|
|
726
|
+
) -> Dict[str, Any]:
|
|
727
|
+
"""Log a workflow step; pass status="completed"/"failed" to close it."""
|
|
728
|
+
event_type = (
|
|
729
|
+
"workflow_completed"
|
|
730
|
+
if status == "completed"
|
|
731
|
+
else "workflow_failed"
|
|
732
|
+
if status == "failed"
|
|
733
|
+
else kwargs.pop("event_type", None) or "workflow_step"
|
|
734
|
+
)
|
|
735
|
+
kwargs.update(
|
|
736
|
+
workflow_id=workflow_id, event_type=event_type, event_category="workflow"
|
|
737
|
+
)
|
|
738
|
+
return self.log_trust_event(**kwargs)
|
|
739
|
+
|
|
740
|
+
def evaluate_policy(self, **kwargs: Any) -> Dict[str, Any]:
|
|
741
|
+
"""Dry-run the policy engine WITHOUT logging or changing trust."""
|
|
742
|
+
if self.config["disabled"]:
|
|
743
|
+
return {"decision": "allow", "matched": [], "reason": "SDK disabled"}
|
|
744
|
+
return self.transport.evaluate_policy(
|
|
745
|
+
{
|
|
746
|
+
"projectId": self.config["project_id"],
|
|
747
|
+
"agentId": self._require_agent_id(kwargs.get("agent_id")),
|
|
748
|
+
"eventType": kwargs.get("event_type"),
|
|
749
|
+
"eventCategory": kwargs.get("event_category"),
|
|
750
|
+
"toolName": kwargs.get("tool_name"),
|
|
751
|
+
"mcpServer": kwargs.get("mcp_server"),
|
|
752
|
+
"apiEndpoint": kwargs.get("api_endpoint"),
|
|
753
|
+
"sensitiveAction": kwargs.get("sensitive_action"),
|
|
754
|
+
}
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
def get_trust_score(self, agent_id: Optional[str] = None) -> Dict[str, Any]:
|
|
758
|
+
"""Fetch the agent's live trust score and level."""
|
|
759
|
+
return self.transport.get_trust_score(
|
|
760
|
+
self.config["project_id"], self._require_agent_id(agent_id)
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
def flush(self) -> None:
|
|
764
|
+
"""No-op for API parity — the Python SDK sends synchronously."""
|
|
765
|
+
return None
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
class PolicyBlockedError(RuntimeError):
|
|
769
|
+
"""Raised by log_* methods in enforce mode when policy blocks the action."""
|
|
770
|
+
|
|
771
|
+
def __init__(self, evaluation: Dict[str, Any]) -> None:
|
|
772
|
+
super().__init__(
|
|
773
|
+
f"ProofLedger policy blocked this action: {evaluation.get('reason')}"
|
|
774
|
+
)
|
|
775
|
+
self.evaluation = evaluation
|
|
776
|
+
|
|
554
777
|
|
|
555
778
|
def _now_ms() -> float:
|
|
556
779
|
"""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.3.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
|