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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proofledger
3
- Version: 0.2.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 RunHandle, ProofLedgerClient, iso_now
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 (Phase 6) — needs the optional `cryptography` extra.
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__ = ["ProofLedgerClient", "RunHandle", "iso_now"]
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__ = ["create_agent_identity", "sign_message", "verify_message", "signing_available"]
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.2.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