vizier-guard 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.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: vizier-guard
3
+ Version: 0.3.0
4
+ Summary: Deterministic authorization and audit guard for AI agent actions
5
+ Author-email: Vassiliy Lakhonin <vassiliy.lakhonin@gmail.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/vassiliylakhonin/vizier
8
+ Project-URL: Documentation, https://vizier.vassiliy-lakhonin.workers.dev/docs
9
+ Project-URL: Repository, https://github.com/vassiliylakhonin/vizier
10
+ Keywords: ai,agents,security,authorization,guardrails,langchain,mcp
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ Provides-Extra: pydantic
26
+ Requires-Dist: pydantic>=2.0; extra == "pydantic"
27
+ Provides-Extra: httpx
28
+ Requires-Dist: httpx>=0.24.0; extra == "httpx"
29
+ Provides-Extra: langchain
30
+ Requires-Dist: langchain-core>=0.1.0; extra == "langchain"
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest>=7.0.0; extra == "test"
33
+
34
+ # Vizier Guard (Python SDK)
35
+
36
+ Deterministic Authorization & Audit Firewall for AI Agent Actions.
37
+
38
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
39
+ [![Python](https://img.shields.io/badge/python-3.9+-green.svg)](pyproject.toml)
40
+
41
+ Vizier is an ultra-low-latency (sub-2ms) deterministic policy kernel that evaluates proposed AI agent actions before they cause external side-effects (payments, database mutations, external messages, code execution, worker deployments).
42
+
43
+ ---
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install vizier-guard
49
+ ```
50
+
51
+ Zero external dependencies required out of the box (uses Python standard library).
52
+
53
+ ---
54
+
55
+ ## 30-Second Quickstart: `@vizier_guard` Decorator
56
+
57
+ Wrap any dangerous tool or Python function with deterministic policy rules:
58
+
59
+ ```python
60
+ from vizier import VizierClient, vizier_guard
61
+
62
+ client = VizierClient(
63
+ base_url="https://vizier.vassiliy-lakhonin.workers.dev",
64
+ api_key="your-vizier-api-key"
65
+ )
66
+
67
+ @vizier_guard(
68
+ client=client,
69
+ action_type="purchase",
70
+ max_amount=500.0,
71
+ currency="USD",
72
+ allowed_targets=["approved-hotel.com", "supplier-corp.com"]
73
+ )
74
+ def book_hotel(amount: float, target: str):
75
+ # This only runs if Vizier returns ALLOW
76
+ print(f"Booking confirmed at {target} for ${amount}")
77
+ return {"status": "booked", "amount": amount}
78
+
79
+ # Allowed: $350 <= $500 to approved target
80
+ book_hotel(amount=350.0, target="approved-hotel.com")
81
+
82
+ # Blocked: $1200 > $500 (raises ActionBlockedError)
83
+ book_hotel(amount=1200.0, target="approved-hotel.com")
84
+ ```
85
+
86
+ ---
87
+
88
+ ## LangChain Integration
89
+
90
+ Protect any LangChain `BaseTool` from agent hallucinations or runaway spending:
91
+
92
+ ```python
93
+ from vizier import VizierClient
94
+ from vizier.integrations.langchain import VizierLangChainToolGuard
95
+ from langchain_community.tools import DuckDuckGoSearchRun
96
+
97
+ client = VizierClient(api_key="...")
98
+
99
+ # Wrap your tool
100
+ safe_search = VizierLangChainToolGuard(
101
+ tool=DuckDuckGoSearchRun(),
102
+ client=client,
103
+ allowed_actions=["search"],
104
+ max_amount=0.0
105
+ )
106
+
107
+ # Pass safe_search into your LangChain or LangGraph agent
108
+ agent = create_react_agent(llm, tools=[safe_search])
109
+ ```
110
+
111
+ ---
112
+
113
+ ## CrewAI Integration
114
+
115
+ ```python
116
+ from vizier.integrations.crewai import VizierCrewAIToolGuard
117
+
118
+ guarded_tool = VizierCrewAIToolGuard(
119
+ tool=my_dangerous_payment_tool,
120
+ max_amount=250.0,
121
+ agent_id="finance_agent"
122
+ )
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Standalone Verification
128
+
129
+ ```python
130
+ from vizier import VizierClient
131
+
132
+ vizier = VizierClient(api_key="...")
133
+
134
+ decision = vizier.check(
135
+ action_type="deploy_worker",
136
+ target="worker:payment-service",
137
+ parameters={"git_commit": "abcdef123..."},
138
+ allowed_actions=["deploy_worker"]
139
+ )
140
+
141
+ if decision.is_allowed:
142
+ print(f"Action permitted! Receipt ID: {decision.receipt.id}")
143
+ else:
144
+ print(f"Action rejected ({decision.decision}): {decision.explanation}")
145
+ ```
@@ -0,0 +1,112 @@
1
+ # Vizier Guard (Python SDK)
2
+
3
+ Deterministic Authorization & Audit Firewall for AI Agent Actions.
4
+
5
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
6
+ [![Python](https://img.shields.io/badge/python-3.9+-green.svg)](pyproject.toml)
7
+
8
+ Vizier is an ultra-low-latency (sub-2ms) deterministic policy kernel that evaluates proposed AI agent actions before they cause external side-effects (payments, database mutations, external messages, code execution, worker deployments).
9
+
10
+ ---
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install vizier-guard
16
+ ```
17
+
18
+ Zero external dependencies required out of the box (uses Python standard library).
19
+
20
+ ---
21
+
22
+ ## 30-Second Quickstart: `@vizier_guard` Decorator
23
+
24
+ Wrap any dangerous tool or Python function with deterministic policy rules:
25
+
26
+ ```python
27
+ from vizier import VizierClient, vizier_guard
28
+
29
+ client = VizierClient(
30
+ base_url="https://vizier.vassiliy-lakhonin.workers.dev",
31
+ api_key="your-vizier-api-key"
32
+ )
33
+
34
+ @vizier_guard(
35
+ client=client,
36
+ action_type="purchase",
37
+ max_amount=500.0,
38
+ currency="USD",
39
+ allowed_targets=["approved-hotel.com", "supplier-corp.com"]
40
+ )
41
+ def book_hotel(amount: float, target: str):
42
+ # This only runs if Vizier returns ALLOW
43
+ print(f"Booking confirmed at {target} for ${amount}")
44
+ return {"status": "booked", "amount": amount}
45
+
46
+ # Allowed: $350 <= $500 to approved target
47
+ book_hotel(amount=350.0, target="approved-hotel.com")
48
+
49
+ # Blocked: $1200 > $500 (raises ActionBlockedError)
50
+ book_hotel(amount=1200.0, target="approved-hotel.com")
51
+ ```
52
+
53
+ ---
54
+
55
+ ## LangChain Integration
56
+
57
+ Protect any LangChain `BaseTool` from agent hallucinations or runaway spending:
58
+
59
+ ```python
60
+ from vizier import VizierClient
61
+ from vizier.integrations.langchain import VizierLangChainToolGuard
62
+ from langchain_community.tools import DuckDuckGoSearchRun
63
+
64
+ client = VizierClient(api_key="...")
65
+
66
+ # Wrap your tool
67
+ safe_search = VizierLangChainToolGuard(
68
+ tool=DuckDuckGoSearchRun(),
69
+ client=client,
70
+ allowed_actions=["search"],
71
+ max_amount=0.0
72
+ )
73
+
74
+ # Pass safe_search into your LangChain or LangGraph agent
75
+ agent = create_react_agent(llm, tools=[safe_search])
76
+ ```
77
+
78
+ ---
79
+
80
+ ## CrewAI Integration
81
+
82
+ ```python
83
+ from vizier.integrations.crewai import VizierCrewAIToolGuard
84
+
85
+ guarded_tool = VizierCrewAIToolGuard(
86
+ tool=my_dangerous_payment_tool,
87
+ max_amount=250.0,
88
+ agent_id="finance_agent"
89
+ )
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Standalone Verification
95
+
96
+ ```python
97
+ from vizier import VizierClient
98
+
99
+ vizier = VizierClient(api_key="...")
100
+
101
+ decision = vizier.check(
102
+ action_type="deploy_worker",
103
+ target="worker:payment-service",
104
+ parameters={"git_commit": "abcdef123..."},
105
+ allowed_actions=["deploy_worker"]
106
+ )
107
+
108
+ if decision.is_allowed:
109
+ print(f"Action permitted! Receipt ID: {decision.receipt.id}")
110
+ else:
111
+ print(f"Action rejected ({decision.decision}): {decision.explanation}")
112
+ ```
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vizier-guard"
7
+ version = "0.3.0"
8
+ description = "Deterministic authorization and audit guard for AI agent actions"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [
13
+ { name = "Vassiliy Lakhonin", email = "vassiliy.lakhonin@gmail.com" }
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: Apache Software License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
26
+ "Topic :: Security",
27
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
28
+ ]
29
+ keywords = ["ai", "agents", "security", "authorization", "guardrails", "langchain", "mcp"]
30
+ dependencies = []
31
+
32
+ [project.optional-dependencies]
33
+ pydantic = ["pydantic>=2.0"]
34
+ httpx = ["httpx>=0.24.0"]
35
+ langchain = ["langchain-core>=0.1.0"]
36
+ test = [
37
+ "pytest>=7.0.0",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/vassiliylakhonin/vizier"
42
+ Documentation = "https://vizier.vassiliy-lakhonin.workers.dev/docs"
43
+ Repository = "https://github.com/vassiliylakhonin/vizier"
44
+
45
+ [tool.pytest.ini_options]
46
+ pythonpath = ["src"]
47
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,43 @@
1
+ """
2
+ Vizier: Deterministic Authorization & Audit Kernel for AI Agents.
3
+ """
4
+
5
+ from .canonical import canonicalize, sha256_canonical_json
6
+ from .client import VizierClient, VizierError
7
+ from .decorators import vizier_guard, ActionBlockedError
8
+ from .models import (
9
+ Action,
10
+ Agent,
11
+ Authority,
12
+ AuthorityConstraints,
13
+ Context,
14
+ Decision,
15
+ PolicyResult,
16
+ Principal,
17
+ Receipt,
18
+ ReceiptGrant,
19
+ VerificationRequest,
20
+ VerificationResponse,
21
+ )
22
+
23
+ __version__ = "0.3.0"
24
+ __all__ = [
25
+ "VizierClient",
26
+ "VizierError",
27
+ "vizier_guard",
28
+ "ActionBlockedError",
29
+ "VerificationRequest",
30
+ "VerificationResponse",
31
+ "Decision",
32
+ "Agent",
33
+ "Principal",
34
+ "Action",
35
+ "Authority",
36
+ "AuthorityConstraints",
37
+ "Context",
38
+ "PolicyResult",
39
+ "Receipt",
40
+ "ReceiptGrant",
41
+ "canonicalize",
42
+ "sha256_canonical_json",
43
+ ]
@@ -0,0 +1,35 @@
1
+ import hashlib
2
+ import json
3
+ from typing import Any
4
+
5
+ def canonicalize(value: Any) -> str:
6
+ """
7
+ RFC 8785-like deterministic JSON canonicalization matching Vizier TS kernel:
8
+ - Booleans, strings, nulls, and numbers are serialized cleanly
9
+ - Float values with integral value format without trailing .0 (matching ECMAScript JSON.stringify)
10
+ - Arrays preserve order with canonical elements
11
+ - Dictionaries sort keys lexicographically
12
+ """
13
+ if value is None:
14
+ return "null"
15
+ if isinstance(value, bool):
16
+ return "true" if value else "false"
17
+ if isinstance(value, (int, float)):
18
+ # ECMAScript JSON.stringify formats integral numbers without .0
19
+ if isinstance(value, float) and value.is_integer():
20
+ return str(int(value))
21
+ return json.dumps(value, separators=(",", ":"))
22
+ if isinstance(value, str):
23
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
24
+ if isinstance(value, (list, tuple)):
25
+ return "[" + ",".join(canonicalize(item) for item in value) + "]"
26
+ if isinstance(value, dict):
27
+ sorted_keys = sorted(value.keys())
28
+ items = [f"{json.dumps(k, ensure_ascii=False)}:{canonicalize(value[k])}" for k in sorted_keys]
29
+ return "{" + ",".join(items) + "}"
30
+ raise TypeError(f"Value of type {type(value)} is not canonicalizable JSON.")
31
+
32
+ def sha256_canonical_json(value: Any) -> str:
33
+ """Compute SHA-256 hex digest of canonicalized JSON."""
34
+ raw = canonicalize(value).encode("utf-8")
35
+ return hashlib.sha256(raw).hexdigest()
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+ import json
3
+ import urllib.request
4
+ import urllib.error
5
+ from typing import Any, Dict, List, Optional, Union
6
+
7
+ from .models import (
8
+ Action,
9
+ Agent,
10
+ Authority,
11
+ AuthorityConstraints,
12
+ Context,
13
+ Principal,
14
+ VerificationRequest,
15
+ VerificationResponse,
16
+ )
17
+ from .canonical import sha256_canonical_json
18
+
19
+ class VizierError(Exception):
20
+ def __init__(self, message: str, status: int = 0, code: str = "ERROR", details: Any = None):
21
+ super().__init__(f"[{code}] {message} (HTTP {status})")
22
+ self.message = message
23
+ self.status = status
24
+ self.code = code
25
+ self.details = details
26
+
27
+ class VizierClient:
28
+ """
29
+ Official Vizier Authorization & Governance Client for AI Agents.
30
+ Zero external dependencies required (uses Python standard library).
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ base_url: str = "https://vizier.vassiliy-lakhonin.workers.dev",
36
+ api_key: Optional[str] = None,
37
+ timeout: float = 5.0,
38
+ ):
39
+ self.base_url = base_url.rstrip("/")
40
+ self.api_key = api_key
41
+ self.timeout = timeout
42
+
43
+ def _request(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
44
+ url = f"{self.base_url}{path}"
45
+ headers = {
46
+ "Content-Type": "application/json",
47
+ "User-Agent": "vizier-guard-python/0.3.0",
48
+ }
49
+ if self.api_key:
50
+ headers["Authorization"] = f"Bearer {self.api_key}"
51
+
52
+ payload_bytes = json.dumps(data).encode("utf-8")
53
+ req = urllib.request.Request(url, data=payload_bytes, headers=headers, method="POST")
54
+
55
+ try:
56
+ with urllib.request.urlopen(req, timeout=self.timeout) as response:
57
+ body = response.read().decode("utf-8")
58
+ return json.loads(body)
59
+ except urllib.error.HTTPError as err:
60
+ try:
61
+ body = err.read().decode("utf-8")
62
+ err_data = json.loads(body)
63
+ err_info = err_data.get("error", {})
64
+ msg = err_info.get("message", "Request failed")
65
+ code = err_info.get("code", "REQUEST_FAILED")
66
+ details = err_info.get("details")
67
+ except Exception:
68
+ msg = err.reason
69
+ code = "HTTP_ERROR"
70
+ details = None
71
+ raise VizierError(message=msg, status=err.code, code=code, details=details) from err
72
+ except urllib.error.URLError as err:
73
+ raise VizierError(message=str(err.reason), status=0, code="CONNECTION_FAILED") from err
74
+ except Exception as err:
75
+ raise VizierError(message=str(err), status=0, code="UNKNOWN_ERROR") from err
76
+
77
+ def verify(
78
+ self,
79
+ request: Union[VerificationRequest, Dict[str, Any]],
80
+ verify_receipt_hash: bool = True,
81
+ ) -> VerificationResponse:
82
+ """
83
+ Submit a proposed agent action to Vizier for deterministic authorization.
84
+ Returns VerificationResponse with decision: ALLOW, REVIEW, or BLOCK.
85
+ """
86
+ payload: Dict[str, Any]
87
+ if isinstance(request, VerificationRequest):
88
+ payload = request.to_dict()
89
+ elif isinstance(request, dict):
90
+ payload = request
91
+ else:
92
+ raise TypeError("request must be VerificationRequest or dict")
93
+
94
+ # Force source="rest" for client calls
95
+ if "context" not in payload or payload["context"] is None:
96
+ payload["context"] = {"source": "rest"}
97
+ else:
98
+ payload["context"]["source"] = "rest"
99
+
100
+ raw_response = self._request("/v1/verify", payload)
101
+ resp = VerificationResponse.from_dict(raw_response)
102
+
103
+ # Non-repudiation integrity check
104
+ if verify_receipt_hash:
105
+ expected_hash = sha256_canonical_json(payload)
106
+ if resp.receipt.request_hash != expected_hash:
107
+ raise VizierError(
108
+ message=f"Receipt hash mismatch: expected {expected_hash}, got {resp.receipt.request_hash}",
109
+ status=200,
110
+ code="HASH_MISMATCH",
111
+ )
112
+
113
+ return resp
114
+
115
+ def check(
116
+ self,
117
+ action_type: str,
118
+ target: str,
119
+ parameters: Optional[Dict[str, Any]] = None,
120
+ allowed_actions: Optional[List[str]] = None,
121
+ max_amount: Optional[float] = None,
122
+ currency: Optional[str] = None,
123
+ allowed_targets: Optional[List[str]] = None,
124
+ blocked_targets: Optional[List[str]] = None,
125
+ agent_id: str = "agent",
126
+ principal_id: str = "principal",
127
+ is_reversible: Optional[bool] = None,
128
+ grant: Optional[str] = None,
129
+ ) -> VerificationResponse:
130
+ """
131
+ Convenience method: verify an action in a single line of code.
132
+ """
133
+ req = VerificationRequest(
134
+ agent=Agent(id=agent_id),
135
+ principal=Principal(id=principal_id),
136
+ action=Action(
137
+ type=action_type,
138
+ target=target,
139
+ parameters=parameters or {},
140
+ is_reversible=is_reversible,
141
+ ),
142
+ authority=Authority(
143
+ allowed_actions=allowed_actions or [action_type],
144
+ constraints=AuthorityConstraints(
145
+ max_amount=max_amount,
146
+ currency=currency,
147
+ allowed_targets=allowed_targets,
148
+ blocked_targets=blocked_targets,
149
+ ),
150
+ ),
151
+ grant=grant,
152
+ )
153
+ return self.verify(req)
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+ import functools
3
+ import inspect
4
+ from typing import Any, Callable, Dict, List, Optional, Union
5
+
6
+ from .client import VizierClient
7
+ from .models import VerificationResponse
8
+
9
+ class ActionBlockedError(Exception):
10
+ def __init__(self, response: VerificationResponse):
11
+ super().__init__(
12
+ f"Action blocked by Vizier ({response.decision}): {response.explanation}. Reason codes: {response.reason_codes}"
13
+ )
14
+ self.response = response
15
+
16
+ def vizier_guard(
17
+ client: Optional[VizierClient] = None,
18
+ action_type: Optional[str] = None,
19
+ target: Optional[Union[str, Callable[..., str]]] = None,
20
+ allowed_actions: Optional[List[str]] = None,
21
+ max_amount: Optional[Union[float, Callable[..., Optional[float]]]] = None,
22
+ currency: Optional[str] = None,
23
+ allowed_targets: Optional[List[str]] = None,
24
+ blocked_targets: Optional[List[str]] = None,
25
+ agent_id: str = "agent",
26
+ principal_id: str = "principal",
27
+ on_block: str = "raise", # "raise" or "callback"
28
+ on_block_callback: Optional[Callable[[VerificationResponse], Any]] = None,
29
+ ):
30
+ """
31
+ Decorator to protect any Python function / agent tool with Vizier deterministic authorization.
32
+ Before the function executes, Vizier checks the proposed action, parameters, and constraints.
33
+ If ALLOW, the function executes.
34
+ If BLOCK or REVIEW, execution is halted immediately.
35
+ """
36
+ viz_client = client or VizierClient()
37
+
38
+ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
39
+ act_type = action_type or fn.__name__
40
+
41
+ @functools.wraps(fn)
42
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
43
+ # Bind arguments to function signature
44
+ sig = inspect.signature(fn)
45
+ bound = sig.bind(*args, **kwargs)
46
+ bound.apply_defaults()
47
+ params = dict(bound.arguments)
48
+
49
+ # Resolve target
50
+ if callable(target):
51
+ resolved_target = target(*args, **kwargs)
52
+ elif isinstance(target, str):
53
+ resolved_target = target
54
+ else:
55
+ resolved_target = params.get("target") or params.get("destination") or "default_target"
56
+
57
+ # Resolve max amount if dynamic
58
+ resolved_max_amount: Optional[float]
59
+ if callable(max_amount):
60
+ resolved_max_amount = max_amount(*args, **kwargs)
61
+ else:
62
+ resolved_max_amount = max_amount
63
+
64
+ # Check if amount and currency are inside parameters
65
+ amount = params.get("amount") or params.get("price")
66
+ curr = params.get("currency") or currency
67
+ if curr and "currency" not in params:
68
+ params["currency"] = curr
69
+
70
+ verification = viz_client.check(
71
+ action_type=act_type,
72
+ target=str(resolved_target),
73
+ parameters=params,
74
+ allowed_actions=allowed_actions or [act_type],
75
+ max_amount=resolved_max_amount,
76
+ currency=curr,
77
+ allowed_targets=allowed_targets,
78
+ blocked_targets=blocked_targets,
79
+ agent_id=agent_id,
80
+ principal_id=principal_id,
81
+ )
82
+
83
+ if not verification.is_allowed:
84
+ if on_block == "callback" and on_block_callback:
85
+ return on_block_callback(verification)
86
+ raise ActionBlockedError(verification)
87
+
88
+ # Store the receipt in a context attribute if possible
89
+ result = fn(*args, **kwargs)
90
+ return result
91
+
92
+ # Attach reference to verification check on the wrapper
93
+ wrapper.__vizier_client__ = viz_client # type: ignore
94
+ return wrapper
95
+
96
+ return decorator
@@ -0,0 +1,4 @@
1
+ from .langchain import VizierLangChainToolGuard
2
+ from .crewai import VizierCrewAIToolGuard
3
+
4
+ __all__ = ["VizierLangChainToolGuard", "VizierCrewAIToolGuard"]
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Callable, Dict, List, Optional
3
+
4
+ from ..client import VizierClient
5
+ from ..models import VerificationResponse
6
+
7
+ class VizierCrewAIToolGuard:
8
+ """
9
+ CrewAI Tool wrapper. Protects agents in CrewAI from unauthorized or out-of-budget actions.
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ tool: Any,
15
+ client: Optional[VizierClient] = None,
16
+ max_amount: Optional[float] = None,
17
+ allowed_targets: Optional[List[str]] = None,
18
+ agent_id: str = "crewai_agent",
19
+ principal_id: str = "principal",
20
+ ):
21
+ self.tool = tool
22
+ self.client = client or VizierClient()
23
+ self.max_amount = max_amount
24
+ self.allowed_targets = allowed_targets
25
+ self.agent_id = agent_id
26
+ self.principal_id = principal_id
27
+
28
+ self.name = getattr(tool, "name", "guarded_tool")
29
+ self.description = getattr(tool, "description", "")
30
+
31
+ def _run(self, *args: Any, **kwargs: Any) -> Any:
32
+ params: Dict[str, Any] = kwargs if kwargs else (args[0] if args and isinstance(args[0], dict) else {"input": args})
33
+ target = str(params.get("target") or self.name)
34
+
35
+ res = self.client.check(
36
+ action_type=self.name,
37
+ target=target,
38
+ parameters=params,
39
+ allowed_actions=[self.name],
40
+ max_amount=self.max_amount,
41
+ allowed_targets=self.allowed_targets,
42
+ agent_id=self.agent_id,
43
+ principal_id=self.principal_id,
44
+ )
45
+
46
+ if not res.is_allowed:
47
+ return f"Action blocked by Vizier ({res.decision}): {res.explanation}. Reason codes: {res.reason_codes}"
48
+
49
+ if hasattr(self.tool, "_run"):
50
+ return self.tool._run(*args, **kwargs)
51
+ if callable(self.tool):
52
+ return self.tool(*args, **kwargs)
53
+ return "Tool executed"
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Callable, Dict, List, Optional
3
+
4
+ from ..client import VizierClient
5
+ from ..decorators import ActionBlockedError
6
+ from ..models import VerificationResponse
7
+
8
+ class VizierLangChainToolGuard:
9
+ """
10
+ Wrapper for LangChain BaseTool instances.
11
+ Intercepts tool calls, verifies with Vizier, and blocks execution on BLOCK or REVIEW.
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ tool: Any,
17
+ client: Optional[VizierClient] = None,
18
+ allowed_actions: Optional[List[str]] = None,
19
+ max_amount: Optional[float] = None,
20
+ currency: Optional[str] = None,
21
+ allowed_targets: Optional[List[str]] = None,
22
+ blocked_targets: Optional[List[str]] = None,
23
+ agent_id: str = "langchain_agent",
24
+ principal_id: str = "principal",
25
+ ):
26
+ self.tool = tool
27
+ self.client = client or VizierClient()
28
+ self.allowed_actions = allowed_actions or [getattr(tool, "name", "tool")]
29
+ self.max_amount = max_amount
30
+ self.currency = currency
31
+ self.allowed_targets = allowed_targets
32
+ self.blocked_targets = blocked_targets
33
+ self.agent_id = agent_id
34
+ self.principal_id = principal_id
35
+
36
+ # Preserve LangChain tool metadata
37
+ self.name = getattr(tool, "name", "guarded_tool")
38
+ self.description = getattr(tool, "description", "")
39
+ self.args_schema = getattr(tool, "args_schema", None)
40
+
41
+ def _verify(self, tool_input: Any) -> VerificationResponse:
42
+ params: Dict[str, Any]
43
+ if isinstance(tool_input, dict):
44
+ params = tool_input
45
+ else:
46
+ params = {"input": tool_input}
47
+
48
+ target = str(params.get("target") or params.get("destination") or self.name)
49
+ return self.client.check(
50
+ action_type=self.name,
51
+ target=target,
52
+ parameters=params,
53
+ allowed_actions=self.allowed_actions,
54
+ max_amount=self.max_amount,
55
+ currency=self.currency,
56
+ allowed_targets=self.allowed_targets,
57
+ blocked_targets=self.blocked_targets,
58
+ agent_id=self.agent_id,
59
+ principal_id=self.principal_id,
60
+ )
61
+
62
+ def run(self, *args: Any, **kwargs: Any) -> Any:
63
+ tool_input = kwargs if kwargs else (args[0] if args else {})
64
+ res = self._verify(tool_input)
65
+ if not res.is_allowed:
66
+ return f"Error: Tool execution blocked by security policy ({res.decision}): {res.explanation}"
67
+ return self.tool.run(*args, **kwargs)
68
+
69
+ async def arun(self, *args: Any, **kwargs: Any) -> Any:
70
+ tool_input = kwargs if kwargs else (args[0] if args else {})
71
+ res = self._verify(tool_input)
72
+ if not res.is_allowed:
73
+ return f"Error: Tool execution blocked by security policy ({res.decision}): {res.explanation}"
74
+ return await self.tool.arun(*args, **kwargs)
75
+
76
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
77
+ return self.run(*args, **kwargs)
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field, asdict
3
+ from typing import Any, Dict, List, Literal, Optional, Union
4
+
5
+ Decision = Literal["ALLOW", "REVIEW", "BLOCK"]
6
+ Source = Literal["a2a", "mcp", "rest", "internal", "unknown"]
7
+ AuthorityProvenance = Literal["principal_signed", "trusted_integration", "unverified"]
8
+
9
+ @dataclass
10
+ class Agent:
11
+ id: str
12
+ owner: Optional[str] = None
13
+
14
+ @dataclass
15
+ class Principal:
16
+ id: str
17
+
18
+ @dataclass
19
+ class Action:
20
+ type: str
21
+ target: str
22
+ parameters: Dict[str, Any] = field(default_factory=dict)
23
+ is_reversible: Optional[bool] = None
24
+
25
+ @dataclass
26
+ class AuthorityConstraints:
27
+ max_amount: Optional[float] = None
28
+ currency: Optional[str] = None
29
+ allowed_targets: Optional[List[str]] = None
30
+ blocked_targets: Optional[List[str]] = None
31
+ allowed_sensitive_actions: Optional[List[str]] = None
32
+ require_review_for_irreversible: Optional[bool] = None
33
+
34
+ @dataclass
35
+ class Authority:
36
+ allowed_actions: List[str] = field(default_factory=list)
37
+ constraints: AuthorityConstraints = field(default_factory=AuthorityConstraints)
38
+
39
+ @dataclass
40
+ class Context:
41
+ request_id: Optional[str] = None
42
+ timestamp: Optional[str] = None
43
+ source: Source = "rest"
44
+
45
+ @dataclass
46
+ class VerificationRequest:
47
+ agent: Agent
48
+ principal: Optional[Principal]
49
+ action: Action
50
+ authority: Authority
51
+ context: Context = field(default_factory=Context)
52
+ grant: Optional[str] = None
53
+
54
+ def to_dict(self) -> Dict[str, Any]:
55
+ d: Dict[str, Any] = {
56
+ "agent": {"id": self.agent.id, "owner": self.agent.owner},
57
+ "principal": {"id": self.principal.id} if self.principal else None,
58
+ "action": {
59
+ "type": self.action.type,
60
+ "target": self.action.target,
61
+ "parameters": self.action.parameters,
62
+ },
63
+ "authority": {
64
+ "allowed_actions": self.authority.allowed_actions,
65
+ "constraints": {},
66
+ },
67
+ "context": {
68
+ "request_id": self.context.request_id,
69
+ "timestamp": self.context.timestamp,
70
+ "source": self.context.source,
71
+ },
72
+ }
73
+ if self.action.is_reversible is not None:
74
+ d["action"]["is_reversible"] = self.action.is_reversible
75
+
76
+ c = self.authority.constraints
77
+ cd = d["authority"]["constraints"]
78
+ if c.max_amount is not None:
79
+ cd["max_amount"] = c.max_amount
80
+ if c.currency is not None:
81
+ cd["currency"] = c.currency
82
+ if c.allowed_targets is not None:
83
+ cd["allowed_targets"] = c.allowed_targets
84
+ if c.blocked_targets is not None:
85
+ cd["blocked_targets"] = c.blocked_targets
86
+ if c.allowed_sensitive_actions is not None:
87
+ cd["allowed_sensitive_actions"] = c.allowed_sensitive_actions
88
+ if c.require_review_for_irreversible is not None:
89
+ cd["require_review_for_irreversible"] = c.require_review_for_irreversible
90
+
91
+ if self.grant is not None:
92
+ d["grant"] = self.grant
93
+ return d
94
+
95
+ @dataclass
96
+ class PolicyResult:
97
+ rule_id: str
98
+ result: Literal["PASS", "REVIEW", "FAIL"]
99
+ reason_code: Optional[str]
100
+ details: Dict[str, Any] = field(default_factory=dict)
101
+
102
+ @dataclass
103
+ class ReceiptGrant:
104
+ jti: str
105
+ issuer: str
106
+ subject: str
107
+ key_id: str
108
+ expires_at: str
109
+
110
+ @dataclass
111
+ class Receipt:
112
+ id: str
113
+ created_at: str
114
+ request_hash: str
115
+ decision: Decision
116
+ risk_score: float
117
+ policy_rule_ids: List[str]
118
+ reason_codes: List[str]
119
+ authority_provenance: AuthorityProvenance
120
+ grant: Optional[ReceiptGrant] = None
121
+
122
+ @dataclass
123
+ class VerificationResponse:
124
+ decision: Decision
125
+ risk_score: float
126
+ reason_codes: List[str]
127
+ explanation: str
128
+ policy_results: List[PolicyResult]
129
+ receipt: Receipt
130
+
131
+ @property
132
+ def is_allowed(self) -> bool:
133
+ return self.decision == "ALLOW"
134
+
135
+ @property
136
+ def is_blocked(self) -> bool:
137
+ return self.decision == "BLOCK"
138
+
139
+ @property
140
+ def requires_review(self) -> bool:
141
+ return self.decision == "REVIEW"
142
+
143
+ @classmethod
144
+ def from_dict(cls, data: Dict[str, Any]) -> VerificationResponse:
145
+ results = [
146
+ PolicyResult(
147
+ rule_id=r["rule_id"],
148
+ result=r["result"],
149
+ reason_code=r.get("reason_code"),
150
+ details=r.get("details", {}),
151
+ )
152
+ for r in data.get("policy_results", [])
153
+ ]
154
+ rc = data["receipt"]
155
+ grant_data = rc.get("grant")
156
+ grant = ReceiptGrant(**grant_data) if grant_data else None
157
+
158
+ receipt = Receipt(
159
+ id=rc["id"],
160
+ created_at=rc["created_at"],
161
+ request_hash=rc["request_hash"],
162
+ decision=rc["decision"],
163
+ risk_score=float(rc["risk_score"]),
164
+ policy_rule_ids=list(rc.get("policy_rule_ids", [])),
165
+ reason_codes=list(rc.get("reason_codes", [])),
166
+ authority_provenance=rc.get("authority_provenance", "unverified"),
167
+ grant=grant,
168
+ )
169
+
170
+ return cls(
171
+ decision=data["decision"],
172
+ risk_score=float(data["risk_score"]),
173
+ reason_codes=list(data.get("reason_codes", [])),
174
+ explanation=data.get("explanation", ""),
175
+ policy_results=results,
176
+ receipt=receipt,
177
+ )
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: vizier-guard
3
+ Version: 0.3.0
4
+ Summary: Deterministic authorization and audit guard for AI agent actions
5
+ Author-email: Vassiliy Lakhonin <vassiliy.lakhonin@gmail.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/vassiliylakhonin/vizier
8
+ Project-URL: Documentation, https://vizier.vassiliy-lakhonin.workers.dev/docs
9
+ Project-URL: Repository, https://github.com/vassiliylakhonin/vizier
10
+ Keywords: ai,agents,security,authorization,guardrails,langchain,mcp
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ Provides-Extra: pydantic
26
+ Requires-Dist: pydantic>=2.0; extra == "pydantic"
27
+ Provides-Extra: httpx
28
+ Requires-Dist: httpx>=0.24.0; extra == "httpx"
29
+ Provides-Extra: langchain
30
+ Requires-Dist: langchain-core>=0.1.0; extra == "langchain"
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest>=7.0.0; extra == "test"
33
+
34
+ # Vizier Guard (Python SDK)
35
+
36
+ Deterministic Authorization & Audit Firewall for AI Agent Actions.
37
+
38
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
39
+ [![Python](https://img.shields.io/badge/python-3.9+-green.svg)](pyproject.toml)
40
+
41
+ Vizier is an ultra-low-latency (sub-2ms) deterministic policy kernel that evaluates proposed AI agent actions before they cause external side-effects (payments, database mutations, external messages, code execution, worker deployments).
42
+
43
+ ---
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install vizier-guard
49
+ ```
50
+
51
+ Zero external dependencies required out of the box (uses Python standard library).
52
+
53
+ ---
54
+
55
+ ## 30-Second Quickstart: `@vizier_guard` Decorator
56
+
57
+ Wrap any dangerous tool or Python function with deterministic policy rules:
58
+
59
+ ```python
60
+ from vizier import VizierClient, vizier_guard
61
+
62
+ client = VizierClient(
63
+ base_url="https://vizier.vassiliy-lakhonin.workers.dev",
64
+ api_key="your-vizier-api-key"
65
+ )
66
+
67
+ @vizier_guard(
68
+ client=client,
69
+ action_type="purchase",
70
+ max_amount=500.0,
71
+ currency="USD",
72
+ allowed_targets=["approved-hotel.com", "supplier-corp.com"]
73
+ )
74
+ def book_hotel(amount: float, target: str):
75
+ # This only runs if Vizier returns ALLOW
76
+ print(f"Booking confirmed at {target} for ${amount}")
77
+ return {"status": "booked", "amount": amount}
78
+
79
+ # Allowed: $350 <= $500 to approved target
80
+ book_hotel(amount=350.0, target="approved-hotel.com")
81
+
82
+ # Blocked: $1200 > $500 (raises ActionBlockedError)
83
+ book_hotel(amount=1200.0, target="approved-hotel.com")
84
+ ```
85
+
86
+ ---
87
+
88
+ ## LangChain Integration
89
+
90
+ Protect any LangChain `BaseTool` from agent hallucinations or runaway spending:
91
+
92
+ ```python
93
+ from vizier import VizierClient
94
+ from vizier.integrations.langchain import VizierLangChainToolGuard
95
+ from langchain_community.tools import DuckDuckGoSearchRun
96
+
97
+ client = VizierClient(api_key="...")
98
+
99
+ # Wrap your tool
100
+ safe_search = VizierLangChainToolGuard(
101
+ tool=DuckDuckGoSearchRun(),
102
+ client=client,
103
+ allowed_actions=["search"],
104
+ max_amount=0.0
105
+ )
106
+
107
+ # Pass safe_search into your LangChain or LangGraph agent
108
+ agent = create_react_agent(llm, tools=[safe_search])
109
+ ```
110
+
111
+ ---
112
+
113
+ ## CrewAI Integration
114
+
115
+ ```python
116
+ from vizier.integrations.crewai import VizierCrewAIToolGuard
117
+
118
+ guarded_tool = VizierCrewAIToolGuard(
119
+ tool=my_dangerous_payment_tool,
120
+ max_amount=250.0,
121
+ agent_id="finance_agent"
122
+ )
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Standalone Verification
128
+
129
+ ```python
130
+ from vizier import VizierClient
131
+
132
+ vizier = VizierClient(api_key="...")
133
+
134
+ decision = vizier.check(
135
+ action_type="deploy_worker",
136
+ target="worker:payment-service",
137
+ parameters={"git_commit": "abcdef123..."},
138
+ allowed_actions=["deploy_worker"]
139
+ )
140
+
141
+ if decision.is_allowed:
142
+ print(f"Action permitted! Receipt ID: {decision.receipt.id}")
143
+ else:
144
+ print(f"Action rejected ({decision.decision}): {decision.explanation}")
145
+ ```
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/vizier/__init__.py
4
+ src/vizier/canonical.py
5
+ src/vizier/client.py
6
+ src/vizier/decorators.py
7
+ src/vizier/models.py
8
+ src/vizier/integrations/__init__.py
9
+ src/vizier/integrations/crewai.py
10
+ src/vizier/integrations/langchain.py
11
+ src/vizier_guard.egg-info/PKG-INFO
12
+ src/vizier_guard.egg-info/SOURCES.txt
13
+ src/vizier_guard.egg-info/dependency_links.txt
14
+ src/vizier_guard.egg-info/requires.txt
15
+ src/vizier_guard.egg-info/top_level.txt
16
+ tests/test_canonical.py
17
+ tests/test_decorators.py
18
+ tests/test_models.py
@@ -0,0 +1,12 @@
1
+
2
+ [httpx]
3
+ httpx>=0.24.0
4
+
5
+ [langchain]
6
+ langchain-core>=0.1.0
7
+
8
+ [pydantic]
9
+ pydantic>=2.0
10
+
11
+ [test]
12
+ pytest>=7.0.0
@@ -0,0 +1,31 @@
1
+ import pytest
2
+ from vizier.canonical import canonicalize, sha256_canonical_json
3
+
4
+ def test_primitives():
5
+ assert canonicalize(None) == "null"
6
+ assert canonicalize(True) == "true"
7
+ assert canonicalize(False) == "false"
8
+ assert canonicalize(42) == "42"
9
+ assert canonicalize("hello") == '"hello"'
10
+
11
+ def test_dict_key_sorting():
12
+ d1 = {"z": 1, "a": 2, "m": 3}
13
+ d2 = {"a": 2, "m": 3, "z": 1}
14
+ assert canonicalize(d1) == '{"a":2,"m":3,"z":1}'
15
+ assert canonicalize(d1) == canonicalize(d2)
16
+ assert sha256_canonical_json(d1) == sha256_canonical_json(d2)
17
+
18
+ def test_nested_structures():
19
+ data = {
20
+ "user": {"name": "alice", "id": 1},
21
+ "tags": ["prod", "agent"],
22
+ "active": True
23
+ }
24
+ expected = '{"active":true,"tags":["prod","agent"],"user":{"id":1,"name":"alice"}}'
25
+ assert canonicalize(data) == expected
26
+
27
+ def test_hash_consistency():
28
+ obj = {"action": "purchase", "amount": 100}
29
+ h = sha256_canonical_json(obj)
30
+ assert len(h) == 64
31
+ assert h == sha256_canonical_json({"amount": 100, "action": "purchase"})
@@ -0,0 +1,75 @@
1
+ import pytest
2
+ from unittest.mock import MagicMock
3
+
4
+ from vizier import vizier_guard, ActionBlockedError, VerificationResponse
5
+ from vizier.models import Receipt, PolicyResult
6
+
7
+ def make_mock_response(decision: str, explanation: str = "ok"):
8
+ return VerificationResponse(
9
+ decision=decision,
10
+ risk_score=0.0 if decision == "ALLOW" else 0.8,
11
+ reason_codes=[] if decision == "ALLOW" else ["AMOUNT_LIMIT_EXCEEDED"],
12
+ explanation=explanation,
13
+ policy_results=[
14
+ PolicyResult(rule_id="amount.limit", result="PASS" if decision == "ALLOW" else "FAIL", reason_code=None)
15
+ ],
16
+ receipt=Receipt(
17
+ id="vrf_test_123",
18
+ created_at="2026-09-11T00:00:00Z",
19
+ request_hash="a" * 64,
20
+ decision=decision,
21
+ risk_score=0.0,
22
+ policy_rule_ids=["amount.limit"],
23
+ reason_codes=[],
24
+ authority_provenance="trusted_integration",
25
+ ),
26
+ )
27
+
28
+ def test_decorator_allows_valid_action():
29
+ mock_client = MagicMock()
30
+ mock_client.check.return_value = make_mock_response("ALLOW")
31
+
32
+ @vizier_guard(client=mock_client, action_type="test_action", max_amount=100.0)
33
+ def do_work(amount: float, target: str):
34
+ return f"worked on {target} with {amount}"
35
+
36
+ result = do_work(50.0, target="server-1")
37
+ assert result == "worked on server-1 with 50.0"
38
+ assert mock_client.check.called
39
+ kwargs = mock_client.check.call_args[1]
40
+ assert kwargs["action_type"] == "test_action"
41
+ assert kwargs["target"] == "server-1"
42
+ assert kwargs["max_amount"] == 100.0
43
+
44
+ def test_decorator_blocks_invalid_action():
45
+ mock_client = MagicMock()
46
+ mock_client.check.return_value = make_mock_response("BLOCK", explanation="Amount exceeded limit")
47
+
48
+ @vizier_guard(client=mock_client, action_type="transfer", max_amount=100.0)
49
+ def transfer(amount: float, target: str):
50
+ return "transferred"
51
+
52
+ with pytest.raises(ActionBlockedError) as exc_info:
53
+ transfer(500.0, target="untrusted_account")
54
+
55
+ assert "blocked by Vizier (BLOCK)" in str(exc_info.value)
56
+ assert exc_info.value.response.decision == "BLOCK"
57
+
58
+ def test_decorator_callback_mode():
59
+ mock_client = MagicMock()
60
+ mock_client.check.return_value = make_mock_response("REVIEW", explanation="Manual approval required")
61
+
62
+ def handle_block(resp):
63
+ return {"status": "paused_for_human", "decision": resp.decision}
64
+
65
+ @vizier_guard(
66
+ client=mock_client,
67
+ action_type="delete_db",
68
+ on_block="callback",
69
+ on_block_callback=handle_block
70
+ )
71
+ def delete_data(db_name: str):
72
+ return "deleted"
73
+
74
+ res = delete_data("prod_users")
75
+ assert res == {"status": "paused_for_human", "decision": "REVIEW"}
@@ -0,0 +1,59 @@
1
+ import pytest
2
+ from vizier.models import (
3
+ Agent, Principal, Action, Authority, AuthorityConstraints,
4
+ VerificationRequest, VerificationResponse
5
+ )
6
+
7
+ def test_verification_request_to_dict():
8
+ req = VerificationRequest(
9
+ agent=Agent(id="ag_1", owner="corp"),
10
+ principal=Principal(id="corp"),
11
+ action=Action(type="deploy", target="worker:1", parameters={"v": 2}),
12
+ authority=Authority(
13
+ allowed_actions=["deploy"],
14
+ constraints=AuthorityConstraints(max_amount=50.0, currency="USD", allowed_targets=["worker:1"])
15
+ )
16
+ )
17
+ d = req.to_dict()
18
+ assert d["agent"] == {"id": "ag_1", "owner": "corp"}
19
+ assert d["principal"] == {"id": "corp"}
20
+ assert d["action"]["type"] == "deploy"
21
+ assert d["authority"]["allowed_actions"] == ["deploy"]
22
+ assert d["authority"]["constraints"]["max_amount"] == 50.0
23
+ assert d["authority"]["constraints"]["allowed_targets"] == ["worker:1"]
24
+ assert d["context"]["source"] == "rest"
25
+
26
+ def test_verification_response_from_dict():
27
+ raw = {
28
+ "decision": "ALLOW",
29
+ "risk_score": 0.0,
30
+ "reason_codes": [],
31
+ "explanation": "Action allowed",
32
+ "policy_results": [
33
+ {"rule_id": "rule.1", "result": "PASS", "reason_code": None, "details": {}}
34
+ ],
35
+ "receipt": {
36
+ "id": "vrf_1",
37
+ "created_at": "2026-09-11T00:00:00Z",
38
+ "request_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
39
+ "decision": "ALLOW",
40
+ "risk_score": 0.0,
41
+ "policy_rule_ids": ["rule.1"],
42
+ "reason_codes": [],
43
+ "authority_provenance": "principal_signed",
44
+ "grant": {
45
+ "jti": "g_1",
46
+ "issuer": "corp",
47
+ "subject": "ag_1",
48
+ "key_id": "k_1",
49
+ "expires_at": "2026-09-12T00:00:00Z"
50
+ }
51
+ }
52
+ }
53
+ resp = VerificationResponse.from_dict(raw)
54
+ assert resp.is_allowed is True
55
+ assert resp.is_blocked is False
56
+ assert resp.receipt.id == "vrf_1"
57
+ assert resp.receipt.grant is not None
58
+ assert resp.receipt.grant.key_id == "k_1"
59
+ assert resp.policy_results[0].rule_id == "rule.1"