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.
- pramagent/__init__.py +97 -0
- pramagent/anchoring/__init__.py +5 -0
- pramagent/anchoring/ethereum.py +204 -0
- pramagent/api/__init__.py +7 -0
- pramagent/api/app.py +733 -0
- pramagent/audit/__init__.py +198 -0
- pramagent/auth.py +175 -0
- pramagent/backends/__init__.py +21 -0
- pramagent/backends/migrations.py +172 -0
- pramagent/backends/redis_backend.py +519 -0
- pramagent/classifier.py +414 -0
- pramagent/cli.py +333 -0
- pramagent/compliance.py +253 -0
- pramagent/config.py +211 -0
- pramagent/core.py +325 -0
- pramagent/errors.py +210 -0
- pramagent/hitl/__init__.py +28 -0
- pramagent/hitl/adapters.py +372 -0
- pramagent/hitl/slack.py +292 -0
- pramagent/hitl/workflow.py +334 -0
- pramagent/layers/__init__.py +265 -0
- pramagent/layers/isolation.py +178 -0
- pramagent/layers/llm_judge.py +316 -0
- pramagent/layers/observability.py +58 -0
- pramagent/layers/tool_guard.py +885 -0
- pramagent/otel.py +53 -0
- pramagent/providers/__init__.py +354 -0
- pramagent/ratelimit.py +52 -0
- pramagent/rca.py +169 -0
- pramagent/redteam.py +341 -0
- pramagent/store.py +248 -0
- pramagent/store_encrypted.py +221 -0
- pramagent/store_postgres.py +435 -0
- pramagent/store_s3.py +214 -0
- pramagent/telemetry.py +240 -0
- pramagent/types.py +119 -0
- pramagent/usage.py +600 -0
- pramagent-0.5.0.data/data/share/pramagent/CHANGELOG.md +161 -0
- pramagent-0.5.0.data/data/share/pramagent/README.md +257 -0
- pramagent-0.5.0.data/data/share/pramagent/docs/COMPLIANCE_MAPPING.md +37 -0
- pramagent-0.5.0.data/data/share/pramagent/docs/DEMO_SCRIPT.md +58 -0
- pramagent-0.5.0.data/data/share/pramagent/docs/DEPLOYMENT.md +177 -0
- pramagent-0.5.0.data/data/share/pramagent/docs/HARDENING_GUIDE.md +129 -0
- pramagent-0.5.0.data/data/share/pramagent/docs/IMPLEMENTATION_STATUS.md +98 -0
- pramagent-0.5.0.data/data/share/pramagent/docs/LIVE_TEST_RESULTS.md +175 -0
- pramagent-0.5.0.data/data/share/pramagent/docs/LOAD_TEST.md +57 -0
- pramagent-0.5.0.data/data/share/pramagent/docs/LOAD_TEST_RESULTS.md +118 -0
- pramagent-0.5.0.data/data/share/pramagent/docs/REDTEAM_RESULTS.md +45 -0
- pramagent-0.5.0.data/data/share/pramagent/docs/RELEASE.md +108 -0
- pramagent-0.5.0.dist-info/METADATA +333 -0
- pramagent-0.5.0.dist-info/RECORD +55 -0
- pramagent-0.5.0.dist-info/WHEEL +5 -0
- pramagent-0.5.0.dist-info/entry_points.txt +2 -0
- pramagent-0.5.0.dist-info/licenses/LICENSE +201 -0
- 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,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
|