governor-sdk 0.2.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 (48) hide show
  1. governor/__init__.py +109 -0
  2. governor/__main__.py +4 -0
  3. governor/cli.py +210 -0
  4. governor/client.py +369 -0
  5. governor/decorator.py +121 -0
  6. governor/integrations/__init__.py +47 -0
  7. governor/integrations/_compat.py +17 -0
  8. governor/integrations/autogpt/__init__.py +133 -0
  9. governor/integrations/autogpt/commands.py +522 -0
  10. governor/integrations/autogpt/plugin.py +730 -0
  11. governor/integrations/azure/__init__.py +221 -0
  12. governor/integrations/azure/agent.py +970 -0
  13. governor/integrations/azure/auth.py +310 -0
  14. governor/integrations/azure/guardrails.py +1073 -0
  15. governor/integrations/bedrock/__init__.py +150 -0
  16. governor/integrations/bedrock/agent.py +736 -0
  17. governor/integrations/bedrock/auth.py +617 -0
  18. governor/integrations/bedrock/interceptor.py +835 -0
  19. governor/integrations/bedrock/tools.py +613 -0
  20. governor/integrations/crewai/__init__.py +144 -0
  21. governor/integrations/crewai/agent.py +298 -0
  22. governor/integrations/crewai/callbacks.py +386 -0
  23. governor/integrations/crewai/crew.py +417 -0
  24. governor/integrations/crewai/task.py +349 -0
  25. governor/integrations/databricks/__init__.py +92 -0
  26. governor/integrations/databricks/agent.py +710 -0
  27. governor/integrations/databricks/auth.py +534 -0
  28. governor/integrations/databricks/mlflow.py +787 -0
  29. governor/integrations/databricks/unity.py +684 -0
  30. governor/integrations/gcp/__init__.py +145 -0
  31. governor/integrations/gcp/adk.py +565 -0
  32. governor/integrations/gcp/agent.py +621 -0
  33. governor/integrations/gcp/auth.py +410 -0
  34. governor/integrations/gcp/tools.py +501 -0
  35. governor/integrations/langchain/__init__.py +80 -0
  36. governor/integrations/langchain/agent.py +624 -0
  37. governor/integrations/langchain/callback.py +474 -0
  38. governor/integrations/langchain/tools.py +437 -0
  39. governor/integrations/langgraph/__init__.py +25 -0
  40. governor/integrations/langgraph/tool_node.py +271 -0
  41. governor/integrations/llamaindex/__init__.py +62 -0
  42. governor/integrations/llamaindex/query_engine.py +394 -0
  43. governor/integrations/llamaindex/tools.py +638 -0
  44. governor_sdk-0.2.0.dist-info/METADATA +50 -0
  45. governor_sdk-0.2.0.dist-info/RECORD +48 -0
  46. governor_sdk-0.2.0.dist-info/WHEEL +5 -0
  47. governor_sdk-0.2.0.dist-info/entry_points.txt +2 -0
  48. governor_sdk-0.2.0.dist-info/top_level.txt +1 -0
governor/__init__.py ADDED
@@ -0,0 +1,109 @@
1
+ """GovernorAI SDK - Python client for GovernorAI AI governance.
2
+
3
+ Quickstart:
4
+ pip install governor-sdk
5
+ export GOVERNOR_URL=https://gateway.governorai.ai
6
+ export GOVERNOR_API_KEY=gov_xxx
7
+
8
+ # One-line governance for any supported framework:
9
+ from governor import govern
10
+ governed_agent = govern(your_agent)
11
+ """
12
+
13
+ from .client import (
14
+ AsyncGovernorAIClient,
15
+ GovernorAIApprovalRequired,
16
+ GovernorAIClient,
17
+ GovernorAIDenied,
18
+ GovernorAIError,
19
+ )
20
+ from .decorator import governed, governed_tool_call
21
+
22
+ __version__ = "0.2.0"
23
+ __all__ = [
24
+ "AsyncGovernorAIClient",
25
+ "GovernorAIClient",
26
+ "GovernorAIError",
27
+ "GovernorAIDenied",
28
+ "GovernorAIApprovalRequired",
29
+ "governed",
30
+ "governed_tool_call",
31
+ "govern",
32
+ ]
33
+
34
+
35
+ def govern(agent, *, governor_url=None, api_key=None, agent_id=None, namespace="default"):
36
+ """Universal one-line governance wrapper.
37
+
38
+ Auto-detects the framework and wraps the agent/executor with GovernorAI
39
+ governance. Supports LangChain, CrewAI, LangGraph, and LlamaIndex.
40
+
41
+ Args:
42
+ agent: The agent/executor/crew/engine to govern
43
+ governor_url: Gateway URL (falls back to GOVERNOR_URL env var)
44
+ api_key: API key (falls back to GOVERNOR_API_KEY env var)
45
+ agent_id: Agent identifier (auto-derived if omitted)
46
+ namespace: Namespace for policy lookup
47
+
48
+ Returns:
49
+ A governed wrapper that intercepts tool calls through GovernorAI.
50
+
51
+ Example:
52
+ from governor import govern
53
+ governed = govern(your_langchain_executor)
54
+ result = governed.invoke({"input": "query"})
55
+ """
56
+ agent_type = type(agent).__name__
57
+ module = type(agent).__module__ or ""
58
+
59
+ # LangChain AgentExecutor or Runnable
60
+ if "langchain" in module or agent_type in ("AgentExecutor", "RunnableSequence"):
61
+ from .integrations.langchain import wrap_agent
62
+ return wrap_agent(
63
+ agent,
64
+ governor_url=governor_url,
65
+ api_key=api_key,
66
+ agent_id=agent_id,
67
+ namespace=namespace,
68
+ )
69
+
70
+ # CrewAI Crew
71
+ if "crewai" in module or agent_type == "Crew":
72
+ from .integrations.crewai import GovernorAICrew
73
+ client = GovernorAIClient(base_url=governor_url, api_key=api_key)
74
+ return GovernorAICrew(
75
+ crew=agent,
76
+ governor_client=client,
77
+ crew_id=agent_id or "crewai-crew",
78
+ namespace=namespace,
79
+ )
80
+
81
+ # LangGraph ToolNode
82
+ if "langgraph" in module or agent_type == "ToolNode":
83
+ from .integrations.langgraph import GovernedToolNode
84
+ return GovernedToolNode(
85
+ tools=getattr(agent, "tools_by_name", {}).values() if hasattr(agent, "tools_by_name") else [],
86
+ governor_url=governor_url,
87
+ api_key=api_key,
88
+ agent_id=agent_id or "langgraph-agent",
89
+ )
90
+
91
+ # LlamaIndex QueryEngine
92
+ if "llama_index" in module or "llamaindex" in module or "QueryEngine" in agent_type:
93
+ from .integrations.llamaindex import GovernorAIQueryEngine
94
+ client = GovernorAIClient(base_url=governor_url, api_key=api_key)
95
+ return GovernorAIQueryEngine(
96
+ query_engine=agent,
97
+ governor_client=client,
98
+ agent_id=agent_id or "llamaindex-agent",
99
+ namespace=namespace,
100
+ )
101
+
102
+ raise TypeError(
103
+ f"Cannot auto-detect framework for {agent_type} from {module}. "
104
+ f"Use framework-specific wrappers instead:\n"
105
+ f" from governor.integrations.langchain import wrap_agent\n"
106
+ f" from governor.integrations.crewai import GovernorAICrew\n"
107
+ f" from governor.integrations.langgraph import GovernedToolNode\n"
108
+ f" from governor.integrations.llamaindex import GovernedQueryEngine"
109
+ )
governor/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ """Allow running GovernorAI CLI as: python -m governor"""
2
+ from .cli import main
3
+
4
+ main()
governor/cli.py ADDED
@@ -0,0 +1,210 @@
1
+ """GovernorAI CLI — terminal-first developer experience.
2
+
3
+ Usage:
4
+ python -m governor init # Configure access
5
+ python -m governor test # Send a test governed action
6
+ python -m governor status # Check connection and policy status
7
+ """
8
+
9
+ import argparse
10
+ import json
11
+ import os
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ from .client import GovernorAIClient, GovernorAIError
16
+
17
+
18
+ CONFIG_FILE = ".governor.env"
19
+ ENV_URL = "GOVERNOR_URL"
20
+ ENV_KEY = "GOVERNOR_API_KEY"
21
+
22
+
23
+ def get_client():
24
+ """Create a client from env vars or config file."""
25
+ url = os.environ.get(ENV_URL)
26
+ key = os.environ.get(ENV_KEY)
27
+
28
+ if not url or not key:
29
+ # Try .env-style config file (KEY=VALUE format, no YAML dependency)
30
+ config_path = Path(CONFIG_FILE)
31
+ if config_path.exists():
32
+ with open(config_path) as f:
33
+ for line in f:
34
+ line = line.strip()
35
+ if line.startswith("#") or "=" not in line:
36
+ continue
37
+ k, v = line.split("=", 1)
38
+ k, v = k.strip(), v.strip()
39
+ if k == "GOVERNOR_URL" and not url:
40
+ url = v
41
+ elif k == "GOVERNOR_API_KEY" and not key:
42
+ key = v
43
+
44
+ if not url:
45
+ print(f"Error: Set {ENV_URL} or create {CONFIG_FILE}")
46
+ sys.exit(1)
47
+ if not key:
48
+ print(f"Error: Set {ENV_KEY} or create {CONFIG_FILE}")
49
+ sys.exit(1)
50
+
51
+ return GovernorAIClient(base_url=url, api_key=key)
52
+
53
+
54
+ def cmd_init(args):
55
+ """Configure GovernorAI access."""
56
+ url = args.url or input(f"GovernorAI Gateway URL [{os.environ.get(ENV_URL, 'https://gateway.governorai.ai')}]: ").strip()
57
+ if not url:
58
+ url = os.environ.get(ENV_URL, "https://gateway.governorai.ai")
59
+
60
+ key = args.key or input("API Key: ").strip()
61
+ if not key:
62
+ print("Error: API key is required.")
63
+ sys.exit(1)
64
+
65
+ # Test connectivity
66
+ print(f"\nTesting connection to {url}...")
67
+ client = GovernorAIClient(base_url=url, api_key=key)
68
+ try:
69
+ # Try a simple health check or list policies
70
+ result = client.execute(
71
+ agent_id="governorai-cli-test",
72
+ tool="cli.init.ping",
73
+ args={"purpose": "connectivity_test"},
74
+ )
75
+ print(f"✓ Connected! Decision: {result.get('decision', 'ALLOW')}")
76
+ except GovernorAIError as e:
77
+ # Even a DENY is a successful connection
78
+ if "denied" in str(e).lower() or "blocked" in str(e).lower():
79
+ print(f"✓ Connected! (Policy denied test action — governance is working)")
80
+ else:
81
+ print(f"✗ Connection failed: {e}")
82
+ sys.exit(1)
83
+ except Exception as e:
84
+ print(f"✗ Connection failed: {e}")
85
+ sys.exit(1)
86
+
87
+ # Save config (.env format — no YAML dependency)
88
+ print(f"\nSaving to {CONFIG_FILE}...")
89
+ with open(CONFIG_FILE, "w") as f:
90
+ f.write(f"# GovernorAI configuration\n")
91
+ f.write(f"GOVERNOR_URL={url}\n")
92
+ f.write(f"GOVERNOR_API_KEY={key}\n")
93
+ print(f"✓ Config saved to {CONFIG_FILE}")
94
+ print(f"\nYou can also set environment variables:")
95
+ print(f" export {ENV_URL}={url}")
96
+ print(f" export {ENV_KEY}={key}")
97
+
98
+
99
+ def cmd_test(args):
100
+ """Send a test governed action."""
101
+ client = get_client()
102
+ agent_id = args.agent or "cli-test-agent"
103
+ tool = args.tool or "cli.test.action"
104
+ raw_args = args.args
105
+
106
+ test_args = {}
107
+ if raw_args:
108
+ try:
109
+ test_args = json.loads(raw_args)
110
+ except json.JSONDecodeError:
111
+ print(f"Error: --args must be valid JSON, got: {raw_args}")
112
+ sys.exit(1)
113
+ else:
114
+ # Default test payload that should trigger a deny with the starter policy
115
+ test_args = {
116
+ "destination_external": True,
117
+ "contains_pii": True,
118
+ "payload_preview": "Test PII data",
119
+ }
120
+
121
+ print(f"Sending governed action...")
122
+ print(f" Agent: {agent_id}")
123
+ print(f" Tool: {tool}")
124
+ print(f" Args: {json.dumps(test_args, indent=2)}")
125
+ print()
126
+
127
+ try:
128
+ result = client.execute(
129
+ agent_id=agent_id,
130
+ tool=tool,
131
+ args=test_args,
132
+ )
133
+ decision = result.get("decision", "UNKNOWN")
134
+ reason = result.get("reason", "")
135
+ latency = result.get("latency_ms", "?")
136
+
137
+ if decision == "ALLOW":
138
+ print(f" ✓ Decision: ALLOW")
139
+ elif decision == "DENY":
140
+ print(f" ✗ Decision: DENY")
141
+ else:
142
+ print(f" ⚡ Decision: {decision}")
143
+
144
+ if reason:
145
+ print(f" Reason: {reason}")
146
+ print(f" Latency: {latency}ms")
147
+ print(f"\nView in dashboard → Events page")
148
+
149
+ except GovernorAIError as e:
150
+ print(f" ✗ Error: {e}")
151
+ sys.exit(1)
152
+
153
+
154
+ def cmd_status(args):
155
+ """Check connection and governance status."""
156
+ client = get_client()
157
+
158
+ print(f"GovernorAI Status")
159
+ print(f" URL: {client.base_url}")
160
+ print(f" Key: {client.api_key[:10]}..." if client.api_key else " Key: not set")
161
+ print()
162
+
163
+ # Test connectivity
164
+ try:
165
+ result = client.execute(
166
+ agent_id="governorai-cli-status",
167
+ tool="cli.status.ping",
168
+ args={"purpose": "status_check"},
169
+ )
170
+ print(f" Connection: ✓ OK")
171
+ print(f" Decision: {result.get('decision', 'ALLOW')}")
172
+ except GovernorAIError as e:
173
+ if "denied" in str(e).lower():
174
+ print(f" Connection: ✓ OK (policy active)")
175
+ else:
176
+ print(f" Connection: ✗ Failed ({e})")
177
+ except Exception as e:
178
+ print(f" Connection: ✗ Failed ({e})")
179
+
180
+
181
+ def main():
182
+ parser = argparse.ArgumentParser(
183
+ prog="governorai",
184
+ description="GovernorAI CLI — AI governance from your terminal",
185
+ )
186
+ subparsers = parser.add_subparsers(dest="command", required=True)
187
+
188
+ # init
189
+ init_parser = subparsers.add_parser("init", help="Configure GovernorAI access")
190
+ init_parser.add_argument("--url", help="Gateway URL")
191
+ init_parser.add_argument("--key", help="API key")
192
+ init_parser.set_defaults(func=cmd_init)
193
+
194
+ # test
195
+ test_parser = subparsers.add_parser("test", help="Send a test governed action")
196
+ test_parser.add_argument("--agent", help="Agent ID (default: cli-test-agent)")
197
+ test_parser.add_argument("--tool", help="Tool name (default: cli.test.action)")
198
+ test_parser.add_argument("--args", help="JSON args (default: PII test payload)")
199
+ test_parser.set_defaults(func=cmd_test)
200
+
201
+ # status
202
+ status_parser = subparsers.add_parser("status", help="Check connection status")
203
+ status_parser.set_defaults(func=cmd_status)
204
+
205
+ args = parser.parse_args()
206
+ args.func(args)
207
+
208
+
209
+ if __name__ == "__main__":
210
+ main()
governor/client.py ADDED
@@ -0,0 +1,369 @@
1
+ """GovernorAI API client."""
2
+
3
+ import os
4
+ import uuid
5
+ from typing import Any, Optional
6
+
7
+ import requests
8
+
9
+
10
+ class GovernorAIClient:
11
+ """Client for interacting with GovernorAI Gateway.
12
+
13
+ Auto-configures from environment variables when explicit values are not passed:
14
+ GOVERNOR_URL — Gateway URL (default: http://localhost:8080)
15
+ GOVERNOR_API_KEY — API key for authentication
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ base_url: Optional[str] = None,
21
+ api_key: Optional[str] = None,
22
+ timeout: int = 30,
23
+ ):
24
+ """
25
+ Initialize the GovernorAI client.
26
+
27
+ Args:
28
+ base_url: GovernorAI Gateway URL. Falls back to GOVERNOR_URL env var.
29
+ api_key: API key for authentication. Falls back to GOVERNOR_API_KEY env var.
30
+ timeout: Request timeout in seconds
31
+ """
32
+ self.base_url = (base_url or os.environ.get("GOVERNOR_URL", "http://localhost:8080")).rstrip("/")
33
+ self.api_key = api_key or os.environ.get("GOVERNOR_API_KEY")
34
+ self.timeout = timeout
35
+ self.session = requests.Session()
36
+
37
+ def _request_headers(self, extra: Optional[dict[str, str]] = None) -> dict[str, str]:
38
+ """Build per-request headers without mutating shared session state."""
39
+ headers: dict[str, str] = {}
40
+ if self.api_key:
41
+ headers["Authorization"] = f"Bearer {self.api_key}"
42
+ if extra:
43
+ headers.update(extra)
44
+ return headers
45
+
46
+ def execute(
47
+ self,
48
+ tool: str,
49
+ args: dict[str, Any],
50
+ agent_id: str,
51
+ session_id: Optional[str] = None,
52
+ namespace: str = "default",
53
+ trace_id: Optional[str] = None,
54
+ principal_id: Optional[str] = None,
55
+ framework_context: Optional[dict[str, Any]] = None,
56
+ target_context: Optional[dict[str, Any]] = None,
57
+ ) -> dict[str, Any]:
58
+ """
59
+ Execute a tool action through GovernorAI.
60
+
61
+ Args:
62
+ tool: Tool name (e.g., "erp.process_payment")
63
+ args: Tool arguments
64
+ agent_id: Agent identifier
65
+ session_id: Session identifier (auto-generated if not provided)
66
+ namespace: Namespace for policy lookup
67
+ trace_id: Trace ID for distributed tracing
68
+ principal_id: UAP principal identifier
69
+ framework_context: Agent framework metadata
70
+ target_context: Target system metadata
71
+
72
+ Returns:
73
+ Response from GovernorAI including decision and result
74
+
75
+ Raises:
76
+ GovernorAIError: If the request fails or action is denied
77
+ """
78
+ if session_id is None:
79
+ session_id = str(uuid.uuid4())
80
+
81
+ if trace_id is None:
82
+ trace_id = str(uuid.uuid4())
83
+
84
+ payload = {
85
+ "agent_id": agent_id,
86
+ "session_id": session_id,
87
+ "namespace": namespace,
88
+ "tool": tool,
89
+ "args": args,
90
+ "trace_id": trace_id,
91
+ }
92
+ if principal_id:
93
+ payload["principal_id"] = principal_id
94
+ if framework_context:
95
+ payload["framework_context"] = framework_context
96
+ if target_context:
97
+ payload["target_context"] = target_context
98
+
99
+ response = self.session.post(
100
+ f"{self.base_url}/api/v1/gateway/execute",
101
+ json=payload,
102
+ headers=self._request_headers(
103
+ {
104
+ "X-Governor-Agent-ID": agent_id,
105
+ "X-Governor-Session-ID": session_id,
106
+ }
107
+ ),
108
+ timeout=self.timeout,
109
+ )
110
+
111
+ result = response.json()
112
+
113
+ if result.get("decision") == "deny":
114
+ raise GovernorAIDenied(
115
+ result.get("reason", "Action denied"),
116
+ rule_id=result.get("rule_id"),
117
+ explain=result.get("explain"),
118
+ explain_code=result.get("explain_code"),
119
+ policy_id=result.get("policy_id"),
120
+ )
121
+
122
+ if result.get("decision") == "pause":
123
+ raise GovernorAIApprovalRequired(
124
+ result.get("reason", "Approval required"),
125
+ approval_id=result.get("approval_id"),
126
+ approval_url=result.get("approval_url"),
127
+ )
128
+
129
+ return result
130
+
131
+ def check_kill_switch(self, agent_id: str, tool: str, namespace: str) -> bool:
132
+ """Check if a kill switch is active."""
133
+ try:
134
+ response = self.session.get(
135
+ f"{self.base_url}/api/v1/killswitch/status",
136
+ headers=self._request_headers(),
137
+ timeout=self.timeout,
138
+ )
139
+ result = response.json() or {}
140
+ except Exception:
141
+ return False
142
+
143
+ for ks in result.get("kill_switches", []) or []:
144
+ if ks["scope"] == "agent" and ks["target"] == agent_id:
145
+ return True
146
+ if ks["scope"] == "tool" and ks["target"] == tool:
147
+ return True
148
+ if ks["scope"] == "namespace" and ks["target"] == namespace:
149
+ return True
150
+
151
+ return False
152
+
153
+
154
+ class AsyncGovernorAIClient:
155
+ """Async client for interacting with GovernorAI Gateway.
156
+
157
+ Uses ``aiohttp`` for non-blocking HTTP calls. Install with::
158
+
159
+ pip install aiohttp
160
+
161
+ Example::
162
+
163
+ async with AsyncGovernorAIClient(base_url="http://localhost:8080") as client:
164
+ result = await client.execute(
165
+ tool="erp.process_payment",
166
+ args={"amount": 100},
167
+ agent_id="payment-agent",
168
+ )
169
+ """
170
+
171
+ def __init__(
172
+ self,
173
+ base_url: str = "http://localhost:8080",
174
+ api_key: Optional[str] = None,
175
+ timeout: int = 30,
176
+ ):
177
+ """
178
+ Initialize the async GovernorAI client.
179
+
180
+ Args:
181
+ base_url: GovernorAI Gateway URL.
182
+ api_key: API key for authentication.
183
+ timeout: Request timeout in seconds.
184
+
185
+ Raises:
186
+ ImportError: If ``aiohttp`` is not installed.
187
+ """
188
+ try:
189
+ import aiohttp # noqa: F401
190
+ except ImportError:
191
+ raise ImportError(
192
+ "aiohttp is required for AsyncGovernorAIClient. "
193
+ "Install it with: pip install aiohttp"
194
+ )
195
+
196
+ self.base_url = base_url.rstrip("/")
197
+ self.api_key = api_key
198
+ self.timeout = timeout
199
+ self._session: Optional[Any] = None
200
+
201
+ async def _get_session(self) -> Any:
202
+ """Return the shared ``aiohttp.ClientSession``, creating it lazily."""
203
+ if self._session is None or self._session.closed:
204
+ import aiohttp
205
+
206
+ headers: dict[str, str] = {}
207
+ if self.api_key:
208
+ headers["Authorization"] = f"Bearer {self.api_key}"
209
+ timeout = aiohttp.ClientTimeout(total=self.timeout)
210
+ self._session = aiohttp.ClientSession(headers=headers, timeout=timeout)
211
+ return self._session
212
+
213
+ async def execute(
214
+ self,
215
+ tool: str,
216
+ args: dict[str, Any],
217
+ agent_id: str,
218
+ session_id: Optional[str] = None,
219
+ namespace: str = "default",
220
+ trace_id: Optional[str] = None,
221
+ principal_id: Optional[str] = None,
222
+ framework_context: Optional[dict[str, Any]] = None,
223
+ target_context: Optional[dict[str, Any]] = None,
224
+ ) -> dict[str, Any]:
225
+ """
226
+ Execute a tool action through GovernorAI asynchronously.
227
+
228
+ Args:
229
+ tool: Tool name (e.g., "erp.process_payment").
230
+ args: Tool arguments.
231
+ agent_id: Agent identifier.
232
+ session_id: Session identifier (auto-generated if not provided).
233
+ namespace: Namespace for policy lookup.
234
+ trace_id: Trace ID for distributed tracing.
235
+ principal_id: UAP principal identifier.
236
+ framework_context: Agent framework metadata.
237
+ target_context: Target system metadata.
238
+
239
+ Returns:
240
+ Response from GovernorAI including decision and result.
241
+
242
+ Raises:
243
+ GovernorAIError: If the request fails or action is denied.
244
+ """
245
+ if session_id is None:
246
+ session_id = str(uuid.uuid4())
247
+
248
+ if trace_id is None:
249
+ trace_id = str(uuid.uuid4())
250
+
251
+ payload = {
252
+ "agent_id": agent_id,
253
+ "session_id": session_id,
254
+ "namespace": namespace,
255
+ "tool": tool,
256
+ "args": args,
257
+ "trace_id": trace_id,
258
+ }
259
+ if principal_id:
260
+ payload["principal_id"] = principal_id
261
+ if framework_context:
262
+ payload["framework_context"] = framework_context
263
+ if target_context:
264
+ payload["target_context"] = target_context
265
+
266
+ request_headers = {
267
+ "X-Governor-Agent-ID": agent_id,
268
+ "X-Governor-Session-ID": session_id,
269
+ "Content-Type": "application/json",
270
+ }
271
+
272
+ session = await self._get_session()
273
+ async with session.post(
274
+ f"{self.base_url}/api/v1/gateway/execute",
275
+ json=payload,
276
+ headers=request_headers,
277
+ ) as response:
278
+ result = await response.json()
279
+
280
+ if result.get("decision") == "deny":
281
+ raise GovernorAIDenied(
282
+ result.get("reason", "Action denied"),
283
+ rule_id=result.get("rule_id"),
284
+ explain=result.get("explain"),
285
+ explain_code=result.get("explain_code"),
286
+ policy_id=result.get("policy_id"),
287
+ )
288
+
289
+ if result.get("decision") == "pause":
290
+ raise GovernorAIApprovalRequired(
291
+ result.get("reason", "Approval required"),
292
+ approval_id=result.get("approval_id"),
293
+ approval_url=result.get("approval_url"),
294
+ )
295
+
296
+ return result
297
+
298
+ async def check_kill_switch(self, agent_id: str, tool: str, namespace: str) -> bool:
299
+ """Async check if a kill switch is active."""
300
+ try:
301
+ session = await self._get_session()
302
+ async with session.get(
303
+ f"{self.base_url}/api/v1/killswitch/status",
304
+ ) as response:
305
+ result = (await response.json()) or {}
306
+ except Exception:
307
+ return False
308
+
309
+ for ks in result.get("kill_switches", []) or []:
310
+ if ks["scope"] == "agent" and ks["target"] == agent_id:
311
+ return True
312
+ if ks["scope"] == "tool" and ks["target"] == tool:
313
+ return True
314
+ if ks["scope"] == "namespace" and ks["target"] == namespace:
315
+ return True
316
+
317
+ return False
318
+
319
+ async def close(self) -> None:
320
+ """Close the underlying HTTP session."""
321
+ if self._session is not None and not self._session.closed:
322
+ await self._session.close()
323
+ self._session = None
324
+
325
+ async def __aenter__(self) -> "AsyncGovernorAIClient":
326
+ """Enter async context manager."""
327
+ return self
328
+
329
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
330
+ """Exit async context manager and close the session."""
331
+ await self.close()
332
+
333
+
334
+ class GovernorAIError(Exception):
335
+ """Base exception for GovernorAI errors."""
336
+
337
+ pass
338
+
339
+
340
+ class GovernorAIDenied(GovernorAIError):
341
+ """Raised when an action is denied by policy."""
342
+
343
+ def __init__(
344
+ self,
345
+ message: str,
346
+ rule_id: Optional[str] = None,
347
+ explain: Optional[str] = None,
348
+ explain_code: Optional[str] = None,
349
+ policy_id: Optional[str] = None,
350
+ ):
351
+ super().__init__(message)
352
+ self.rule_id = rule_id
353
+ self.explain = explain
354
+ self.explain_code = explain_code
355
+ self.policy_id = policy_id
356
+
357
+
358
+ class GovernorAIApprovalRequired(GovernorAIError):
359
+ """Raised when an action requires approval."""
360
+
361
+ def __init__(
362
+ self,
363
+ message: str,
364
+ approval_id: Optional[str] = None,
365
+ approval_url: Optional[str] = None,
366
+ ):
367
+ super().__init__(message)
368
+ self.approval_id = approval_id
369
+ self.approval_url = approval_url