faramesh-sdk 0.3.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.
faramesh/__init__.py ADDED
@@ -0,0 +1,178 @@
1
+ """Faramesh Python SDK - Production-ready client for the Faramesh Execution Governor API.
2
+
3
+ Quick Start:
4
+ >>> from faramesh import configure, submit_action, approve_action
5
+ >>> configure(base_url="http://localhost:8000", token="dev-token")
6
+ >>> action = submit_action("my-agent", "http", "get", {"url": "https://example.com"})
7
+ >>> print(f"Action {action['id']} status: {action['status']}")
8
+ """
9
+
10
+ from .client import (
11
+ # Configuration
12
+ configure,
13
+ ClientConfig,
14
+
15
+ # Core functions
16
+ submit_action,
17
+ submit_actions,
18
+ submit_actions_bulk,
19
+ submit_and_wait,
20
+ block_until_approved,
21
+ get_action,
22
+ list_actions,
23
+ approve_action,
24
+ deny_action,
25
+ start_action,
26
+ replay_action,
27
+ wait_for_completion,
28
+ apply,
29
+ tail_events,
30
+ stream_events,
31
+
32
+ # Convenience aliases
33
+ allow,
34
+ deny,
35
+
36
+ # Exceptions
37
+ FarameshError,
38
+ FarameshAuthError,
39
+ FarameshNotFoundError,
40
+ FarameshPolicyError,
41
+ FarameshTimeoutError,
42
+ FarameshConnectionError,
43
+ FarameshValidationError,
44
+ FarameshServerError,
45
+ FarameshBatchError,
46
+ FarameshDeniedError,
47
+
48
+ # Legacy class-based API (for backward compatibility)
49
+ ExecutionGovernorClient,
50
+ GovernorConfig,
51
+ GovernorError,
52
+ GovernorTimeoutError,
53
+ GovernorAuthError,
54
+ GovernorConnectionError,
55
+ )
56
+
57
+ # Import new modules
58
+ from .governed_tool import governed_tool
59
+ from .snapshot import ActionSnapshotStore, get_default_store
60
+ from .policy_helpers import validate_policy_file, test_policy_against_action
61
+
62
+ # Import canonicalization helpers
63
+ from .canonicalization import (
64
+ canonicalize,
65
+ canonicalize_action_payload,
66
+ compute_request_hash,
67
+ compute_hash,
68
+ CanonicalizeError,
69
+ )
70
+
71
+ # Import gate helpers
72
+ from .gate import (
73
+ gate_decide,
74
+ gate_decide_dict,
75
+ replay_decision,
76
+ verify_request_hash,
77
+ execute_if_allowed,
78
+ GateDecision,
79
+ ReplayResult,
80
+ )
81
+
82
+ # Import version
83
+ from .client import __version__
84
+
85
+ # Import policy models
86
+ from .policy import (
87
+ Policy,
88
+ PolicyRule,
89
+ MatchCondition,
90
+ RiskRule,
91
+ RiskLevel,
92
+ create_policy,
93
+ )
94
+
95
+ __all__ = [
96
+ # Configuration
97
+ "configure",
98
+ "ClientConfig",
99
+
100
+ # Core functions
101
+ "submit_action",
102
+ "submit_actions",
103
+ "submit_actions_bulk",
104
+ "submit_and_wait",
105
+ "block_until_approved",
106
+ "get_action",
107
+ "list_actions",
108
+ "approve_action",
109
+ "deny_action",
110
+ "start_action",
111
+ "replay_action",
112
+ "wait_for_completion",
113
+ "apply",
114
+ "tail_events",
115
+ "stream_events",
116
+
117
+ # Convenience aliases
118
+ "allow",
119
+ "deny",
120
+
121
+ # Policy models
122
+ "Policy",
123
+ "PolicyRule",
124
+ "MatchCondition",
125
+ "RiskRule",
126
+ "RiskLevel",
127
+ "create_policy",
128
+
129
+ # Policy helpers
130
+ "validate_policy_file",
131
+ "test_policy_against_action",
132
+
133
+ # Canonicalization helpers
134
+ "canonicalize",
135
+ "canonicalize_action_payload",
136
+ "compute_request_hash",
137
+ "compute_hash",
138
+ "CanonicalizeError",
139
+
140
+ # Gate/Replay helpers
141
+ "gate_decide",
142
+ "gate_decide_dict",
143
+ "replay_decision",
144
+ "verify_request_hash",
145
+ "execute_if_allowed",
146
+ "GateDecision",
147
+ "ReplayResult",
148
+
149
+ # Decorators
150
+ "governed_tool",
151
+
152
+ # Utilities
153
+ "ActionSnapshotStore",
154
+ "get_default_store",
155
+
156
+ # Exceptions
157
+ "FarameshError",
158
+ "FarameshAuthError",
159
+ "FarameshNotFoundError",
160
+ "FarameshPolicyError",
161
+ "FarameshTimeoutError",
162
+ "FarameshConnectionError",
163
+ "FarameshValidationError",
164
+ "FarameshServerError",
165
+ "FarameshBatchError",
166
+ "FarameshDeniedError",
167
+
168
+ # Legacy API
169
+ "ExecutionGovernorClient",
170
+ "GovernorConfig",
171
+ "GovernorError",
172
+ "GovernorTimeoutError",
173
+ "GovernorAuthError",
174
+ "GovernorConnectionError",
175
+
176
+ # Version
177
+ "__version__",
178
+ ]
@@ -0,0 +1,281 @@
1
+ """
2
+ Deterministic canonicalization for Faramesh client-side hashing.
3
+
4
+ This module mirrors the canonicalization logic in faramesh-core to enable
5
+ clients to compute request_hash locally before submitting actions.
6
+
7
+ Usage:
8
+ >>> from faramesh.canonicalization import compute_request_hash
9
+ >>> payload = {"agent_id": "test", "tool": "http", "operation": "get", "params": {}}
10
+ >>> hash_value = compute_request_hash(payload)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import copy
16
+ import hashlib
17
+ import math
18
+ from decimal import Decimal, InvalidOperation
19
+ from typing import Any, Dict, List, Optional, Union
20
+
21
+
22
+ class CanonicalizeError(Exception):
23
+ """
24
+ Raised when canonicalization fails.
25
+
26
+ This is a fail-closed error: if canonicalization cannot complete,
27
+ the action should not proceed.
28
+ """
29
+ pass
30
+
31
+
32
+ # Fields to exclude from action payloads (ephemeral/internal)
33
+ _ACTION_EXCLUDE_FIELDS = frozenset({
34
+ "id",
35
+ "approval_token",
36
+ "created_at",
37
+ "updated_at",
38
+ "tenant_id",
39
+ "project_id",
40
+ "version",
41
+ "decision",
42
+ "status",
43
+ "reason",
44
+ "risk_level",
45
+ "policy_version",
46
+ "policy_hash",
47
+ "runtime_version",
48
+ "profile_id",
49
+ "profile_version",
50
+ "profile_hash",
51
+ "provenance_id",
52
+ "outcome",
53
+ "reason_code",
54
+ "reason_details",
55
+ "request_hash",
56
+ })
57
+
58
+
59
+ def _normalize_float(value: float) -> str:
60
+ """
61
+ Normalize a float to canonical string representation.
62
+
63
+ Rules:
64
+ - No exponent notation (1e3 → 1000)
65
+ - No trailing zeros (1.50 → 1.5)
66
+ - Whole numbers have no decimal point (1.0 → 1)
67
+ - NaN, Infinity, -Infinity → CanonicalizeError
68
+ """
69
+ # FAIL CLOSED on non-finite values
70
+ if math.isnan(value):
71
+ raise CanonicalizeError("NaN is not allowed in canonical JSON")
72
+ if math.isinf(value):
73
+ raise CanonicalizeError("Infinity is not allowed in canonical JSON")
74
+
75
+ # Handle zero explicitly
76
+ if value == 0.0:
77
+ return "0"
78
+
79
+ try:
80
+ dec = Decimal(repr(value))
81
+ dec = dec.normalize()
82
+ sign, digits, exponent = dec.as_tuple()
83
+ digit_str = ''.join(str(d) for d in digits)
84
+
85
+ if exponent >= 0:
86
+ result = digit_str + ('0' * exponent)
87
+ elif -exponent >= len(digit_str):
88
+ result = '0.' + ('0' * (-exponent - len(digit_str))) + digit_str
89
+ else:
90
+ decimal_pos = len(digit_str) + exponent
91
+ result = digit_str[:decimal_pos] + '.' + digit_str[decimal_pos:]
92
+
93
+ if sign:
94
+ result = '-' + result
95
+
96
+ return result
97
+
98
+ except (InvalidOperation, ValueError) as e:
99
+ raise CanonicalizeError(f"Failed to normalize float {value!r}: {e}")
100
+
101
+
102
+ def _serialize_string(value: str) -> str:
103
+ """Serialize a string to canonical JSON representation."""
104
+ result = ['"']
105
+
106
+ for char in value:
107
+ code = ord(char)
108
+
109
+ if char == '"':
110
+ result.append('\\"')
111
+ elif char == '\\':
112
+ result.append('\\\\')
113
+ elif char == '\b':
114
+ result.append('\\b')
115
+ elif char == '\f':
116
+ result.append('\\f')
117
+ elif char == '\n':
118
+ result.append('\\n')
119
+ elif char == '\r':
120
+ result.append('\\r')
121
+ elif char == '\t':
122
+ result.append('\\t')
123
+ elif code < 0x20:
124
+ result.append(f'\\u{code:04x}')
125
+ else:
126
+ result.append(char)
127
+
128
+ result.append('"')
129
+ return ''.join(result)
130
+
131
+
132
+ def _serialize_dict(value: dict) -> str:
133
+ """Serialize a dict to canonical JSON with sorted keys."""
134
+ for key in value:
135
+ if not isinstance(key, str):
136
+ raise CanonicalizeError(
137
+ f"Dict keys must be strings, got {type(key).__name__}: {key!r}"
138
+ )
139
+
140
+ sorted_keys = sorted(value.keys())
141
+ parts = []
142
+ for key in sorted_keys:
143
+ key_str = _serialize_string(key)
144
+ val_str = _serialize_value(value[key])
145
+ parts.append(f"{key_str}:{val_str}")
146
+
147
+ return "{" + ",".join(parts) + "}"
148
+
149
+
150
+ def _serialize_list(value: Union[list, tuple]) -> str:
151
+ """Serialize a list/tuple to canonical JSON (order preserved)."""
152
+ parts = [_serialize_value(item) for item in value]
153
+ return "[" + ",".join(parts) + "]"
154
+
155
+
156
+ def _serialize_value(value: Any) -> str:
157
+ """Serialize a value to canonical JSON string."""
158
+ if value is None:
159
+ return "null"
160
+
161
+ if isinstance(value, bool):
162
+ return "true" if value else "false"
163
+
164
+ if isinstance(value, int) and not isinstance(value, bool):
165
+ return str(value)
166
+
167
+ if isinstance(value, float):
168
+ return _normalize_float(value)
169
+
170
+ if isinstance(value, str):
171
+ return _serialize_string(value)
172
+
173
+ if isinstance(value, dict):
174
+ return _serialize_dict(value)
175
+
176
+ if isinstance(value, (list, tuple)):
177
+ return _serialize_list(value)
178
+
179
+ raise CanonicalizeError(
180
+ f"Cannot canonicalize value of type {type(value).__name__}: {value!r}"
181
+ )
182
+
183
+
184
+ def canonicalize(obj: Any) -> str:
185
+ """
186
+ Canonicalize an object to a deterministic JSON string.
187
+
188
+ Guarantees:
189
+ - Dict keys sorted lexicographically
190
+ - Lists preserve order
191
+ - UTF-8 compatible (ensure_ascii=False)
192
+ - No NaN, Infinity, -Infinity
193
+ - Floats normalized: no exponent, no trailing zeros
194
+ - Input is never mutated
195
+
196
+ Args:
197
+ obj: Any JSON-serializable Python object
198
+
199
+ Returns:
200
+ Canonical JSON string
201
+
202
+ Raises:
203
+ CanonicalizeError: If canonicalization fails
204
+ """
205
+ obj_copy = copy.deepcopy(obj)
206
+ return _serialize_value(obj_copy)
207
+
208
+
209
+ def canonicalize_action_payload(payload: dict) -> bytes:
210
+ """
211
+ Canonicalize an action payload for hashing.
212
+
213
+ Drops ephemeral/internal fields (id, approval_token, timestamps, etc.)
214
+ and produces canonical bytes.
215
+
216
+ Args:
217
+ payload: Action payload dict with agent_id, tool, operation, params, context
218
+
219
+ Returns:
220
+ Canonical bytes representation
221
+
222
+ Raises:
223
+ CanonicalizeError: If canonicalization fails
224
+ """
225
+ clean_payload: Dict[str, Any] = {}
226
+
227
+ for key, value in payload.items():
228
+ if key in _ACTION_EXCLUDE_FIELDS or key.startswith("_"):
229
+ continue
230
+ clean_payload[key] = value
231
+
232
+ return canonicalize(clean_payload).encode("utf-8")
233
+
234
+
235
+ def compute_request_hash(payload: dict) -> str:
236
+ """
237
+ Compute SHA-256 hash of canonicalized action payload.
238
+
239
+ This produces the same hash as the Faramesh server, enabling
240
+ clients to verify request_hash matches before/after submission.
241
+
242
+ Args:
243
+ payload: Action payload dict (agent_id, tool, operation, params, context)
244
+
245
+ Returns:
246
+ SHA-256 hex digest (64 characters)
247
+
248
+ Raises:
249
+ CanonicalizeError: If canonicalization fails
250
+
251
+ Example:
252
+ >>> payload = {
253
+ ... "agent_id": "test",
254
+ ... "tool": "http",
255
+ ... "operation": "get",
256
+ ... "params": {"url": "https://example.com"},
257
+ ... "context": {}
258
+ ... }
259
+ >>> hash_value = compute_request_hash(payload)
260
+ >>> print(hash_value) # 64-char hex string
261
+ """
262
+ canonical_bytes = canonicalize_action_payload(payload)
263
+ return hashlib.sha256(canonical_bytes).hexdigest()
264
+
265
+
266
+ def compute_hash(obj: Any) -> str:
267
+ """
268
+ Compute SHA-256 hash of the canonical JSON representation.
269
+
270
+ Args:
271
+ obj: Any JSON-serializable Python object
272
+
273
+ Returns:
274
+ SHA-256 hex digest (64 characters)
275
+
276
+ Raises:
277
+ CanonicalizeError: If canonicalization fails
278
+ """
279
+ canonical_str = canonicalize(obj)
280
+ canonical_bytes = canonical_str.encode("utf-8")
281
+ return hashlib.sha256(canonical_bytes).hexdigest()