pramagent 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. pramagent/__init__.py +97 -0
  2. pramagent/anchoring/__init__.py +5 -0
  3. pramagent/anchoring/ethereum.py +204 -0
  4. pramagent/api/__init__.py +7 -0
  5. pramagent/api/app.py +733 -0
  6. pramagent/audit/__init__.py +198 -0
  7. pramagent/auth.py +175 -0
  8. pramagent/backends/__init__.py +21 -0
  9. pramagent/backends/migrations.py +172 -0
  10. pramagent/backends/redis_backend.py +519 -0
  11. pramagent/classifier.py +414 -0
  12. pramagent/cli.py +333 -0
  13. pramagent/compliance.py +253 -0
  14. pramagent/config.py +211 -0
  15. pramagent/core.py +325 -0
  16. pramagent/errors.py +210 -0
  17. pramagent/hitl/__init__.py +28 -0
  18. pramagent/hitl/adapters.py +372 -0
  19. pramagent/hitl/slack.py +292 -0
  20. pramagent/hitl/workflow.py +334 -0
  21. pramagent/layers/__init__.py +265 -0
  22. pramagent/layers/isolation.py +178 -0
  23. pramagent/layers/llm_judge.py +316 -0
  24. pramagent/layers/observability.py +58 -0
  25. pramagent/layers/tool_guard.py +885 -0
  26. pramagent/otel.py +53 -0
  27. pramagent/providers/__init__.py +354 -0
  28. pramagent/ratelimit.py +52 -0
  29. pramagent/rca.py +169 -0
  30. pramagent/redteam.py +341 -0
  31. pramagent/store.py +248 -0
  32. pramagent/store_encrypted.py +221 -0
  33. pramagent/store_postgres.py +435 -0
  34. pramagent/store_s3.py +214 -0
  35. pramagent/telemetry.py +240 -0
  36. pramagent/types.py +119 -0
  37. pramagent/usage.py +600 -0
  38. pramagent-0.5.0.data/data/share/pramagent/CHANGELOG.md +161 -0
  39. pramagent-0.5.0.data/data/share/pramagent/README.md +257 -0
  40. pramagent-0.5.0.data/data/share/pramagent/docs/COMPLIANCE_MAPPING.md +37 -0
  41. pramagent-0.5.0.data/data/share/pramagent/docs/DEMO_SCRIPT.md +58 -0
  42. pramagent-0.5.0.data/data/share/pramagent/docs/DEPLOYMENT.md +177 -0
  43. pramagent-0.5.0.data/data/share/pramagent/docs/HARDENING_GUIDE.md +129 -0
  44. pramagent-0.5.0.data/data/share/pramagent/docs/IMPLEMENTATION_STATUS.md +98 -0
  45. pramagent-0.5.0.data/data/share/pramagent/docs/LIVE_TEST_RESULTS.md +175 -0
  46. pramagent-0.5.0.data/data/share/pramagent/docs/LOAD_TEST.md +57 -0
  47. pramagent-0.5.0.data/data/share/pramagent/docs/LOAD_TEST_RESULTS.md +118 -0
  48. pramagent-0.5.0.data/data/share/pramagent/docs/REDTEAM_RESULTS.md +45 -0
  49. pramagent-0.5.0.data/data/share/pramagent/docs/RELEASE.md +108 -0
  50. pramagent-0.5.0.dist-info/METADATA +333 -0
  51. pramagent-0.5.0.dist-info/RECORD +55 -0
  52. pramagent-0.5.0.dist-info/WHEEL +5 -0
  53. pramagent-0.5.0.dist-info/entry_points.txt +2 -0
  54. pramagent-0.5.0.dist-info/licenses/LICENSE +201 -0
  55. pramagent-0.5.0.dist-info/top_level.txt +1 -0
pramagent/__init__.py ADDED
@@ -0,0 +1,97 @@
1
+ r"""
2
+ Pramagent - trust middleware for AI agents: deterministic guardrails, HITL,
3
+ tool policy, and tamper-evident traces.
4
+
5
+ Quick start
6
+ -----------
7
+ import asyncio
8
+ from pramagent import Pramagent
9
+ from pramagent.layers import SafetyLayer, Rule
10
+ from pramagent.types import Verdict
11
+
12
+ armor = Pramagent(
13
+ safety=SafetyLayer(rules=[
14
+ Rule("no_account_disclosure", Verdict.BLOCK, pattern=r"acct[-_ ]?\d{6,}"),
15
+ ])
16
+ )
17
+ resp = asyncio.run(armor.run("Hello", tenant_id="acme", session_id="s1"))
18
+ print(resp.output)
19
+ print(resp.trace.this_hash)
20
+ """
21
+ from .core import Pramagent
22
+ from .layers import (ComplianceLayer, HITLLayer, IsolationLayer,
23
+ ObservabilityLayer, ReliabilityLayer, Rule, SafetyLayer,
24
+ ToolDecision, ToolGuardLayer, ToolPolicy)
25
+ from .providers import (AnthropicProvider, BaseProvider, FallbackProvider,
26
+ GeminiProvider, MockProvider, OllamaProvider,
27
+ OpenAICompatibleProvider, OpenAIProvider)
28
+ from .store import MemoryStore, SQLiteStore
29
+ from .auth import APIKeyRegistry, JWTManager
30
+ from .otel import OpenTelemetryExporter, OpenTelemetryNotInstalled
31
+ from .anchoring import EthereumAnchor, EthereumAnchorReceipt
32
+ from .redteam import RedTeamReport, run_injection_benchmark
33
+ from .types import AgentResponse, HITLStatus, TraceEvent, Verdict
34
+ from .usage import (
35
+ InMemoryUsageLedger,
36
+ InMemoryUsageSink,
37
+ UsageLedgerEntry,
38
+ UsageDecision,
39
+ UsageEvent,
40
+ UsageEventSink,
41
+ UsageLimits,
42
+ UsageSnapshot,
43
+ UsageTracker,
44
+ WebhookUsageSink,
45
+ )
46
+
47
+ __version__ = "0.5.0"
48
+ __all__ = [
49
+ "Pramagent",
50
+ "AgentResponse",
51
+ "TraceEvent",
52
+ "Verdict",
53
+ "HITLStatus",
54
+ "IsolationLayer",
55
+ "ObservabilityLayer",
56
+ "ComplianceLayer",
57
+ "SafetyLayer",
58
+ "ReliabilityLayer",
59
+ "HITLLayer",
60
+ "ToolGuardLayer",
61
+ "ToolPolicy",
62
+ "ToolDecision",
63
+ "Rule",
64
+ "MemoryStore",
65
+ "SQLiteStore",
66
+ "APIKeyRegistry",
67
+ "JWTManager",
68
+ "UsageTracker",
69
+ "UsageLimits",
70
+ "UsageSnapshot",
71
+ "UsageDecision",
72
+ "UsageEvent",
73
+ "UsageEventSink",
74
+ "UsageLedgerEntry",
75
+ "InMemoryUsageLedger",
76
+ "InMemoryUsageSink",
77
+ "WebhookUsageSink",
78
+ "run_injection_benchmark",
79
+ "RedTeamReport",
80
+ "OpenTelemetryExporter",
81
+ "OpenTelemetryNotInstalled",
82
+ "EthereumAnchor",
83
+ "EthereumAnchorReceipt",
84
+ "BaseProvider",
85
+ "MockProvider",
86
+ "AnthropicProvider",
87
+ "OpenAIProvider",
88
+ "OpenAICompatibleProvider",
89
+ "GeminiProvider",
90
+ "OllamaProvider",
91
+ "FallbackProvider",
92
+ "__version__",
93
+ ]
94
+ from .classifier import (
95
+ build_classifier, EmbeddingInjectionClassifier, KeywordFallbackClassifier,
96
+ INJECTION_EXEMPLARS, BENIGN_EXEMPLARS,
97
+ )
@@ -0,0 +1,5 @@
1
+ """Optional external audit anchoring integrations."""
2
+
3
+ from .ethereum import EthereumAnchor, EthereumAnchorReceipt
4
+
5
+ __all__ = ["EthereumAnchor", "EthereumAnchorReceipt"]
@@ -0,0 +1,204 @@
1
+ """
2
+ pramagent.anchoring.ethereum
3
+ ============================
4
+ Real Ethereum/Sepolia anchoring for Pramagent audit heads.
5
+
6
+ The default strategy does not require a custom smart contract: it sends a
7
+ zero-value transaction to the configured contract address, or back to the
8
+ signing account when no contract is supplied, with the trace hash in calldata.
9
+ That gives a public timestamped receipt without forcing Solidity into the MVP.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from typing import Any, Optional
15
+
16
+
17
+ SEPOLIA_CHAIN_ID = 11155111
18
+
19
+
20
+ class EthereumAnchorError(RuntimeError):
21
+ """Raised when Ethereum anchoring cannot complete."""
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class EthereumAnchorReceipt:
26
+ tx_hash: str
27
+ block_number: int
28
+ status: int
29
+ chain_id: int
30
+ anchored_hash: str
31
+
32
+ def to_dict(self) -> dict[str, Any]:
33
+ return {
34
+ "tx_hash": self.tx_hash,
35
+ "block_number": self.block_number,
36
+ "status": self.status,
37
+ "chain_id": self.chain_id,
38
+ "anchored_hash": self.anchored_hash,
39
+ }
40
+
41
+
42
+ class EthereumAnchor:
43
+ """Anchor SHA-256 trace heads to Ethereum-compatible chains.
44
+
45
+ Parameters are explicit so production callers can wire secrets from a real
46
+ secret manager. Tests can pass a fake Web3-like object through ``web3``.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ *,
52
+ rpc_url: str = "",
53
+ private_key: str = "",
54
+ contract_address: str = "",
55
+ chain_id: int = SEPOLIA_CHAIN_ID,
56
+ web3: Optional[Any] = None,
57
+ gas_limit: int = 50_000,
58
+ wait_timeout: int = 300,
59
+ poll_latency: int = 5,
60
+ ) -> None:
61
+ self.rpc_url = rpc_url
62
+ self.private_key = private_key
63
+ self.contract_address = contract_address
64
+ self.chain_id = chain_id
65
+ self.gas_limit = gas_limit
66
+ self.wait_timeout = wait_timeout
67
+ self.poll_latency = poll_latency
68
+ self._w3 = web3 or self._load_web3(rpc_url)
69
+ if not private_key:
70
+ raise EthereumAnchorError("private_key is required for Ethereum anchoring")
71
+ self._account = self._w3.eth.account.from_key(private_key)
72
+
73
+ @staticmethod
74
+ def _load_web3(rpc_url: str) -> Any:
75
+ if not rpc_url:
76
+ raise EthereumAnchorError("rpc_url is required for Ethereum anchoring")
77
+ try:
78
+ from web3 import Web3 # type: ignore
79
+ except ImportError as exc:
80
+ raise EthereumAnchorError(
81
+ "web3 is not installed; install with: pip install 'pramagent[ethereum]'"
82
+ ) from exc
83
+ return Web3(Web3.HTTPProvider(rpc_url))
84
+
85
+ def anchor(self, trace_hash: str) -> EthereumAnchorReceipt:
86
+ clean_hash = _normalize_trace_hash(trace_hash)
87
+ to_address = self.contract_address or self._account.address
88
+ nonce = self._w3.eth.get_transaction_count(self._account.address)
89
+ tx = {
90
+ "to": to_address,
91
+ "value": 0,
92
+ "data": "0x" + clean_hash,
93
+ "nonce": nonce,
94
+ "chainId": self.chain_id,
95
+ "gas": self.gas_limit,
96
+ }
97
+ tx.update(_fee_fields(self._w3))
98
+ signed = self._w3.eth.account.sign_transaction(tx, self.private_key)
99
+ raw = getattr(signed, "raw_transaction", None) or getattr(signed, "rawTransaction")
100
+ tx_hash_raw = self._w3.eth.send_raw_transaction(raw)
101
+ tx_hash = _to_hex(self._w3, tx_hash_raw)
102
+ receipt = self._w3.eth.wait_for_transaction_receipt(
103
+ tx_hash,
104
+ timeout=self.wait_timeout,
105
+ poll_latency=self.poll_latency,
106
+ )
107
+ block_number = _get_receipt_value(receipt, "blockNumber", "block_number", default=0)
108
+ status = _get_receipt_value(receipt, "status", default=0)
109
+ return EthereumAnchorReceipt(
110
+ tx_hash=tx_hash,
111
+ block_number=int(block_number or 0),
112
+ status=int(status or 0),
113
+ chain_id=self.chain_id,
114
+ anchored_hash=clean_hash,
115
+ )
116
+
117
+ def verify_on_chain(
118
+ self,
119
+ tx_hash: str,
120
+ *,
121
+ expected_hash: str = "",
122
+ ) -> EthereumAnchorReceipt:
123
+ normalized_tx = tx_hash.removeprefix("eth:").removeprefix("sepolia:")
124
+ receipt = self._w3.eth.get_transaction_receipt(normalized_tx)
125
+ tx = self._w3.eth.get_transaction(normalized_tx)
126
+ status = int(_get_receipt_value(receipt, "status", default=0) or 0)
127
+ block_number = int(
128
+ _get_receipt_value(receipt, "blockNumber", "block_number", default=0) or 0
129
+ )
130
+ input_data = _calldata_to_hex(_get_receipt_value(tx, "input", "data", default=""))
131
+ anchored_hash = expected_hash.removeprefix("0x").lower()
132
+ if status != 1:
133
+ raise EthereumAnchorError(f"transaction {normalized_tx} did not succeed")
134
+ if anchored_hash and anchored_hash not in input_data.lower():
135
+ raise EthereumAnchorError("transaction calldata does not contain expected hash")
136
+ return EthereumAnchorReceipt(
137
+ tx_hash=normalized_tx,
138
+ block_number=block_number,
139
+ status=status,
140
+ chain_id=self.chain_id,
141
+ anchored_hash=anchored_hash,
142
+ )
143
+
144
+
145
+ def _normalize_trace_hash(trace_hash: str) -> str:
146
+ clean = trace_hash.removeprefix("0x").lower()
147
+ if len(clean) != 64:
148
+ raise EthereumAnchorError("trace_hash must be a 32-byte hex SHA-256 digest")
149
+ try:
150
+ bytes.fromhex(clean)
151
+ except ValueError as exc:
152
+ raise EthereumAnchorError("trace_hash must be valid hex") from exc
153
+ return clean
154
+
155
+
156
+ def _to_hex(w3: Any, value: Any) -> str:
157
+ if isinstance(value, str):
158
+ return value
159
+ if hasattr(w3, "to_hex"):
160
+ return w3.to_hex(value)
161
+ if hasattr(value, "hex"):
162
+ h = value.hex()
163
+ return h if h.startswith("0x") else f"0x{h}"
164
+ raise EthereumAnchorError("could not convert transaction hash to hex")
165
+
166
+
167
+ def _fee_fields(w3: Any) -> dict[str, int]:
168
+ gas_price = int(w3.eth.gas_price)
169
+ try:
170
+ latest = w3.eth.get_block("latest")
171
+ base_fee = _get_receipt_value(latest, "baseFeePerGas", "base_fee_per_gas")
172
+ except Exception:
173
+ base_fee = None
174
+ if base_fee is None:
175
+ return {"gasPrice": max(gas_price, 1)}
176
+
177
+ priority_fee = max(int(gas_price * 0.15), 1_000_000_000)
178
+ max_fee = max(int(gas_price * 2), int(base_fee) * 2 + priority_fee)
179
+ return {
180
+ "maxFeePerGas": max_fee,
181
+ "maxPriorityFeePerGas": priority_fee,
182
+ }
183
+
184
+
185
+ def _calldata_to_hex(value: Any) -> str:
186
+ if value is None:
187
+ return ""
188
+ if isinstance(value, str):
189
+ return value.lower()
190
+ if isinstance(value, bytes):
191
+ return "0x" + value.hex()
192
+ if hasattr(value, "hex"):
193
+ h = value.hex()
194
+ return h.lower() if str(h).startswith("0x") else f"0x{h}".lower()
195
+ return str(value).lower()
196
+
197
+
198
+ def _get_receipt_value(obj: Any, *names: str, default: Any = None) -> Any:
199
+ for name in names:
200
+ if isinstance(obj, dict) and name in obj:
201
+ return obj[name]
202
+ if hasattr(obj, name):
203
+ return getattr(obj, name)
204
+ return default
@@ -0,0 +1,7 @@
1
+ """HTTP sidecar for Pramagent. Import the app factory or the ready-made app.
2
+
3
+ from pramagent.api.app import app, create_app
4
+ """
5
+ from .app import app, create_app
6
+
7
+ __all__ = ["app", "create_app"]