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.
- governor/__init__.py +109 -0
- governor/__main__.py +4 -0
- governor/cli.py +210 -0
- governor/client.py +369 -0
- governor/decorator.py +121 -0
- governor/integrations/__init__.py +47 -0
- governor/integrations/_compat.py +17 -0
- governor/integrations/autogpt/__init__.py +133 -0
- governor/integrations/autogpt/commands.py +522 -0
- governor/integrations/autogpt/plugin.py +730 -0
- governor/integrations/azure/__init__.py +221 -0
- governor/integrations/azure/agent.py +970 -0
- governor/integrations/azure/auth.py +310 -0
- governor/integrations/azure/guardrails.py +1073 -0
- governor/integrations/bedrock/__init__.py +150 -0
- governor/integrations/bedrock/agent.py +736 -0
- governor/integrations/bedrock/auth.py +617 -0
- governor/integrations/bedrock/interceptor.py +835 -0
- governor/integrations/bedrock/tools.py +613 -0
- governor/integrations/crewai/__init__.py +144 -0
- governor/integrations/crewai/agent.py +298 -0
- governor/integrations/crewai/callbacks.py +386 -0
- governor/integrations/crewai/crew.py +417 -0
- governor/integrations/crewai/task.py +349 -0
- governor/integrations/databricks/__init__.py +92 -0
- governor/integrations/databricks/agent.py +710 -0
- governor/integrations/databricks/auth.py +534 -0
- governor/integrations/databricks/mlflow.py +787 -0
- governor/integrations/databricks/unity.py +684 -0
- governor/integrations/gcp/__init__.py +145 -0
- governor/integrations/gcp/adk.py +565 -0
- governor/integrations/gcp/agent.py +621 -0
- governor/integrations/gcp/auth.py +410 -0
- governor/integrations/gcp/tools.py +501 -0
- governor/integrations/langchain/__init__.py +80 -0
- governor/integrations/langchain/agent.py +624 -0
- governor/integrations/langchain/callback.py +474 -0
- governor/integrations/langchain/tools.py +437 -0
- governor/integrations/langgraph/__init__.py +25 -0
- governor/integrations/langgraph/tool_node.py +271 -0
- governor/integrations/llamaindex/__init__.py +62 -0
- governor/integrations/llamaindex/query_engine.py +394 -0
- governor/integrations/llamaindex/tools.py +638 -0
- governor_sdk-0.2.0.dist-info/METADATA +50 -0
- governor_sdk-0.2.0.dist-info/RECORD +48 -0
- governor_sdk-0.2.0.dist-info/WHEEL +5 -0
- governor_sdk-0.2.0.dist-info/entry_points.txt +2 -0
- 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
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
|