hexgate 0.2.6__tar.gz → 0.2.8__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.
- hexgate-0.2.8/PKG-INFO +168 -0
- hexgate-0.2.8/README.md +127 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/__init__.py +28 -16
- hexgate-0.2.8/hexgate/adapters/google/mcp.py +95 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/google/runner.py +17 -2
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/google/tools.py +27 -3
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/google/wrapper.py +20 -7
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/langchain/agent.py +23 -1
- hexgate-0.2.8/hexgate/adapters/langchain/mcp.py +47 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/langchain/tools.py +7 -25
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/langchain/wrapper.py +7 -1
- hexgate-0.2.8/hexgate/adapters/openai/mcp.py +97 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/openai/runner.py +50 -5
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/openai/tools.py +28 -3
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/openai/wrapper.py +12 -3
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/agent.py +22 -1
- hexgate-0.2.8/hexgate/adapters/pydantic_ai/mcp.py +60 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/tools.py +27 -3
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/wrapper.py +18 -6
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/agents/__init__.py +6 -14
- hexgate-0.2.8/hexgate/agents/approvals.py +53 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/agents/factory.py +36 -6
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/agents/loader.py +53 -111
- hexgate-0.2.8/hexgate/audit.py +168 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/bootstrap.py +2 -1
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/_common.py +7 -41
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/chat.py +2 -3
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/policy/main.py +68 -18
- hexgate-0.2.8/hexgate/cli/register/__init__.py +11 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/register/register.py +2 -2
- hexgate-0.2.8/hexgate/cli/serve.py +692 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cloud/__init__.py +2 -2
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cloud/client.py +18 -1
- hexgate-0.2.8/hexgate/manifest/__init__.py +23 -0
- hexgate-0.2.6/hexgate/cli/register/manifest.py → hexgate-0.2.8/hexgate/manifest/builder.py +6 -6
- {hexgate-0.2.6/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/google.py +64 -11
- {hexgate-0.2.6/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/langchain.py +1 -1
- hexgate-0.2.6/hexgate/cli/register/hexgate.py → hexgate-0.2.8/hexgate/manifest/native.py +1 -1
- {hexgate-0.2.6/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/openai.py +1 -1
- {hexgate-0.2.6/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/pydantic_ai.py +2 -2
- hexgate-0.2.8/hexgate/mcp/__init__.py +61 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/mcp/proxy.py +162 -52
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/__init__.py +44 -1
- hexgate-0.2.8/hexgate/security/bans.py +330 -0
- hexgate-0.2.8/hexgate/security/builder.py +203 -0
- hexgate-0.2.8/hexgate/security/constraints.py +862 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/enforcer.py +2 -1
- hexgate-0.2.8/hexgate/security/errors.py +39 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/models.py +20 -2
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/policy.py +15 -2
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/policy_set.py +28 -1
- hexgate-0.2.8/hexgate/security/rego.py +710 -0
- hexgate-0.2.8/hexgate/security/testing.py +82 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/__init__.py +8 -6
- hexgate-0.2.8/hexgate/tools/_relocated.py +22 -0
- hexgate-0.2.6/hexgate/audit.py → hexgate-0.2.8/hexgate/tracing/_senders.py +74 -166
- hexgate-0.2.8/hexgate/tracing/usage.py +70 -0
- hexgate-0.2.8/hexgate.egg-info/PKG-INFO +168 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate.egg-info/SOURCES.txt +19 -15
- {hexgate-0.2.6 → hexgate-0.2.8}/pyproject.toml +1 -9
- {hexgate-0.2.6 → hexgate-0.2.8}/tests/test_bootstrap.py +9 -8
- hexgate-0.2.6/PKG-INFO +0 -1589
- hexgate-0.2.6/README.md +0 -1548
- hexgate-0.2.6/hexgate/agents/builtin/__init__.py +0 -1
- hexgate-0.2.6/hexgate/agents/builtin/researcher/agent.yaml +0 -7
- hexgate-0.2.6/hexgate/agents/builtin/researcher/policy.yaml +0 -10
- hexgate-0.2.6/hexgate/agents/builtin/researcher/system.md +0 -5
- hexgate-0.2.6/hexgate/agents/prompts/agent_system.md +0 -14
- hexgate-0.2.6/hexgate/cli/register/__init__.py +0 -8
- hexgate-0.2.6/hexgate/cli/serve.py +0 -337
- hexgate-0.2.6/hexgate/mcp/__init__.py +0 -47
- hexgate-0.2.6/hexgate/security/constraints.py +0 -252
- hexgate-0.2.6/hexgate/security/errors.py +0 -11
- hexgate-0.2.6/hexgate/security/rego.py +0 -334
- hexgate-0.2.6/hexgate/tools/fetch.py +0 -72
- hexgate-0.2.6/hexgate/tools/refund.py +0 -53
- hexgate-0.2.6/hexgate/tools/websearch.py +0 -72
- hexgate-0.2.6/hexgate.egg-info/PKG-INFO +0 -1589
- {hexgate-0.2.6 → hexgate-0.2.8}/LICENSE +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/__init__.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/google/__init__.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/langchain/__init__.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/openai/__init__.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/__init__.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/agents/models.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/__init__.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/policy/__init__.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/register/main.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/state.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cloud/attenuate.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cloud/biscuit.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/config/__init__.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/config/env.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/config/settings.py +0 -0
- {hexgate-0.2.6/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/models.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/mcp/client.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/mcp/config.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/runtime/__init__.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/runtime/command_policy.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/runtime/context.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/runtime/sandbox_runtime.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/runtime/srt.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/runtime/workspace.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/binding.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/bundle.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/decision.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/file_scope.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/rego_wasm.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/signing.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/source.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/wasm_engine.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/streaming/__init__.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/streaming/events.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/streaming/normalize.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/bash.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/decorators.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/__init__.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/_common.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/edit_file.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/glob.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/grep.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/read_file.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/write_file.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tracing/__init__.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tracing/langfuse.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/utils/__init__.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/utils/retry.py +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate.egg-info/dependency_links.txt +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate.egg-info/entry_points.txt +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate.egg-info/requires.txt +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/hexgate.egg-info/top_level.txt +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/setup.cfg +0 -0
- {hexgate-0.2.6 → hexgate-0.2.8}/tests/test_demo.py +0 -0
hexgate-0.2.8/PKG-INFO
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hexgate
|
|
3
|
+
Version: 0.2.8
|
|
4
|
+
Summary: Hexgate — authorization infrastructure for AI agents (agent runtime + cloud client).
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: bashlex>=0.18
|
|
10
|
+
Requires-Dist: biscuit-python>=0.4
|
|
11
|
+
Requires-Dist: cryptography>=42
|
|
12
|
+
Requires-Dist: httpx>=0.28.1
|
|
13
|
+
Requires-Dist: langchain
|
|
14
|
+
Requires-Dist: langchain-openai
|
|
15
|
+
Requires-Dist: langchain-core
|
|
16
|
+
Requires-Dist: langfuse
|
|
17
|
+
Requires-Dist: pydantic>=2.12.4
|
|
18
|
+
Requires-Dist: python-dotenv>=1.1.1
|
|
19
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
20
|
+
Requires-Dist: rich>=13.9.4
|
|
21
|
+
Requires-Dist: websockets>=13.0
|
|
22
|
+
Requires-Dist: openai-agents>=0.0.10
|
|
23
|
+
Requires-Dist: langgraph>=0.2
|
|
24
|
+
Requires-Dist: nest_asyncio>=1.6
|
|
25
|
+
Requires-Dist: openinference-instrumentation-openai-agents>=0.1
|
|
26
|
+
Requires-Dist: google-adk>=1.0
|
|
27
|
+
Requires-Dist: google-genai>=1.0
|
|
28
|
+
Requires-Dist: litellm>=1.50
|
|
29
|
+
Requires-Dist: openinference-instrumentation-google-adk>=0.1.11
|
|
30
|
+
Requires-Dist: pydantic-ai-slim>=1.88.0
|
|
31
|
+
Requires-Dist: wasmtime>=20.0
|
|
32
|
+
Requires-Dist: mcp>=1.0
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: ipykernel; extra == "dev"
|
|
35
|
+
Requires-Dist: jupyter; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest>=8.4.1; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-asyncio>=1.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-cov>=6.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: ruff>=0.12.2; extra == "dev"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
<div align="center">
|
|
43
|
+
|
|
44
|
+
<img src="./icon.svg" alt="Hexgate" width="96" height="96" />
|
|
45
|
+
|
|
46
|
+
# Hexgate
|
|
47
|
+
|
|
48
|
+
**Runtime authorization for AI agents.**
|
|
49
|
+
On every tool call, Hexgate decides whether *this user*, in *this role*, may run *this tool* with *these arguments* — allow, deny, or require approval. For OpenAI Agents, LangChain, Google ADK, Pydantic AI, or a native runtime.
|
|
50
|
+
|
|
51
|
+
[**Website**](https://hexgate.ai) · [**Docs**](https://docs.hexgate.ai) · [PyPI](https://pypi.org/project/hexgate/)
|
|
52
|
+
[](https://pypi.org/project/hexgate/)
|
|
53
|
+
[](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml)
|
|
54
|
+
[](https://codecov.io/gh/HexamindOrganisation/hexgate)
|
|
55
|
+
[](https://pypi.org/project/hexgate/)
|
|
56
|
+
[](LICENSE)
|
|
57
|
+
|
|
58
|
+
<br />
|
|
59
|
+
|
|
60
|
+
<img src="./assets/hero.png" alt="Control what your agents do — not just what they say. Policy decisions streaming live from the PolicyEnforcer." />
|
|
61
|
+
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## What is Hexgate?
|
|
67
|
+
|
|
68
|
+
Hexgate is two things that move together:
|
|
69
|
+
|
|
70
|
+
- **`hexgate` — the SDK.** A Python runtime that gates every tool call through a typed `Decision` (allow / deny / approval-required), resolving the caller's role at call time to apply that role's rules. Wrap an existing agent without rewriting it, or build one natively — every decision is traced and audited with the caller's identity. [See supported frameworks →](https://docs.hexgate.ai/adapters/openai)
|
|
71
|
+
- **The Hexgate platform** *(optional)* — a FastAPI control plane + React dashboard for editing policy in a browser, minting per-project tokens, watching live decisions stream from a serving agent, and shipping signed WASM policy bundles to production. Available as **[Hexgate Cloud](https://app.hexgate.ai)** (hosted — set one env var, no infra) or self-hosted.
|
|
72
|
+
|
|
73
|
+
You can use the SDK three ways: **local** (YAML/bundle on disk, no platform), **Hexgate Cloud** (remote enforcement + audit — just set `HEXGATE_API_KEY`), or **self-hosted** (run the control plane yourself). `HEXGATE_API_URL` defaults to `https://app.hexgate.ai`, so remote enforcement is one env var away.
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
end user (id + role) tool call (name + args)
|
|
77
|
+
└───────────────┬────────────────┘
|
|
78
|
+
▼
|
|
79
|
+
PolicyEnforcer.decide() ◄── policy (local YAML / bundle
|
|
80
|
+
▼ or signed cloud bundle)
|
|
81
|
+
allow · deny · approval
|
|
82
|
+
│
|
|
83
|
+
▼
|
|
84
|
+
audit log — who called what, and whether it was allowed
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Quickstart
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pip install hexgate
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**See it enforce — no API keys.** Save a policy that gives two roles different
|
|
94
|
+
limits on the *same* `refund_order` tool:
|
|
95
|
+
|
|
96
|
+
<!-- Keep this refund_order policy example in sync with docs/quickstart.mdx -->
|
|
97
|
+
```yaml
|
|
98
|
+
# policy.yaml
|
|
99
|
+
version: 1
|
|
100
|
+
roles:
|
|
101
|
+
support: # small USD refunds only
|
|
102
|
+
default_policy: { mode: deny }
|
|
103
|
+
tools:
|
|
104
|
+
refund_order:
|
|
105
|
+
mode: allow
|
|
106
|
+
constraints:
|
|
107
|
+
- args.amount <= 50
|
|
108
|
+
- args.currency == "USD"
|
|
109
|
+
billing: # larger refunds, major currencies
|
|
110
|
+
default_policy: { mode: deny }
|
|
111
|
+
tools:
|
|
112
|
+
refund_order:
|
|
113
|
+
mode: allow
|
|
114
|
+
constraints:
|
|
115
|
+
- args.amount <= 500
|
|
116
|
+
- args.currency in ["USD", "EUR"]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`hexgate policy test` decides the **same $400 refund** for each role offline — no model, no keys:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
hexgate policy test policy.yaml --role support \
|
|
123
|
+
--tool refund_order --args '{"amount": 400, "currency": "USD"}'
|
|
124
|
+
# ✗ DENY · support → refund_order({"amount": 400, "currency": "USD"})
|
|
125
|
+
# reason: Policy on "refund_order" denied: constraint failed — args.amount <= 50
|
|
126
|
+
|
|
127
|
+
hexgate policy test policy.yaml --role billing \
|
|
128
|
+
--tool refund_order --args '{"amount": 400, "currency": "USD"}'
|
|
129
|
+
# ✓ ALLOW · billing → refund_order({"amount": 400, "currency": "USD"})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Same tool, same request — **the caller's role and the arguments decide**, enforced
|
|
133
|
+
outside the model. The [full quickstart →](https://docs.hexgate.ai/quickstart) puts
|
|
134
|
+
this in front of a live agent.
|
|
135
|
+
|
|
136
|
+
## Documentation
|
|
137
|
+
|
|
138
|
+
Full documentation lives at **[docs.hexgate.ai](https://docs.hexgate.ai)**.
|
|
139
|
+
|
|
140
|
+
| | |
|
|
141
|
+
|---|---|
|
|
142
|
+
| [Build an agent](https://docs.hexgate.ai/guides/build-an-agent) | The two shapes — wrap an existing agent, or let the platform own the YAML. |
|
|
143
|
+
| [Framework adapters](https://docs.hexgate.ai/adapters/openai) | OpenAI Agents, LangChain/LangGraph, Google ADK, Pydantic AI. |
|
|
144
|
+
| [Policy](https://docs.hexgate.ai/policy/yaml-shape) | YAML shape, constraints, WASM bundles, signing, local override. |
|
|
145
|
+
| [User scope + roles](https://docs.hexgate.ai/concepts/user-scope) | Per-request identity, role resolution, biscuit attenuation. |
|
|
146
|
+
| [CLI](https://docs.hexgate.ai/cli/chat) | `chat`, `serve`, `register`, `policy`. |
|
|
147
|
+
| [MCP servers](https://docs.hexgate.ai/concepts/mcp) | Wrap any Model Context Protocol server as policy-enforced tools. |
|
|
148
|
+
| [Hexgate Cloud (hosted)](https://docs.hexgate.ai/platform/hosted) | Remote policy enforcement + audit with zero infra — get a key, set one env var. |
|
|
149
|
+
| [Platform (self-hosted)](https://docs.hexgate.ai/platform/overview) | Run the control plane, dashboard, ClickHouse audit, and Resend email yourself. |
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
Contributor setup, `make` targets, and the test suites are documented in
|
|
154
|
+
[Development & testing](https://docs.hexgate.ai/internals/development). The short
|
|
155
|
+
version:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
make install-dev # uv sync --extra dev (first time only)
|
|
159
|
+
make check # lint + fmt-check + test (matches CI)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT — see [LICENSE](LICENSE).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
If Hexgate looks useful, [give it a ⭐ on GitHub](https://github.com/HexamindOrganisation/hexgate) — it helps more than you'd think. Built by [Hexamind](https://hexgate.ai).
|
hexgate-0.2.8/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="./icon.svg" alt="Hexgate" width="96" height="96" />
|
|
4
|
+
|
|
5
|
+
# Hexgate
|
|
6
|
+
|
|
7
|
+
**Runtime authorization for AI agents.**
|
|
8
|
+
On every tool call, Hexgate decides whether *this user*, in *this role*, may run *this tool* with *these arguments* — allow, deny, or require approval. For OpenAI Agents, LangChain, Google ADK, Pydantic AI, or a native runtime.
|
|
9
|
+
|
|
10
|
+
[**Website**](https://hexgate.ai) · [**Docs**](https://docs.hexgate.ai) · [PyPI](https://pypi.org/project/hexgate/)
|
|
11
|
+
[](https://pypi.org/project/hexgate/)
|
|
12
|
+
[](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml)
|
|
13
|
+
[](https://codecov.io/gh/HexamindOrganisation/hexgate)
|
|
14
|
+
[](https://pypi.org/project/hexgate/)
|
|
15
|
+
[](LICENSE)
|
|
16
|
+
|
|
17
|
+
<br />
|
|
18
|
+
|
|
19
|
+
<img src="./assets/hero.png" alt="Control what your agents do — not just what they say. Policy decisions streaming live from the PolicyEnforcer." />
|
|
20
|
+
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## What is Hexgate?
|
|
26
|
+
|
|
27
|
+
Hexgate is two things that move together:
|
|
28
|
+
|
|
29
|
+
- **`hexgate` — the SDK.** A Python runtime that gates every tool call through a typed `Decision` (allow / deny / approval-required), resolving the caller's role at call time to apply that role's rules. Wrap an existing agent without rewriting it, or build one natively — every decision is traced and audited with the caller's identity. [See supported frameworks →](https://docs.hexgate.ai/adapters/openai)
|
|
30
|
+
- **The Hexgate platform** *(optional)* — a FastAPI control plane + React dashboard for editing policy in a browser, minting per-project tokens, watching live decisions stream from a serving agent, and shipping signed WASM policy bundles to production. Available as **[Hexgate Cloud](https://app.hexgate.ai)** (hosted — set one env var, no infra) or self-hosted.
|
|
31
|
+
|
|
32
|
+
You can use the SDK three ways: **local** (YAML/bundle on disk, no platform), **Hexgate Cloud** (remote enforcement + audit — just set `HEXGATE_API_KEY`), or **self-hosted** (run the control plane yourself). `HEXGATE_API_URL` defaults to `https://app.hexgate.ai`, so remote enforcement is one env var away.
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
end user (id + role) tool call (name + args)
|
|
36
|
+
└───────────────┬────────────────┘
|
|
37
|
+
▼
|
|
38
|
+
PolicyEnforcer.decide() ◄── policy (local YAML / bundle
|
|
39
|
+
▼ or signed cloud bundle)
|
|
40
|
+
allow · deny · approval
|
|
41
|
+
│
|
|
42
|
+
▼
|
|
43
|
+
audit log — who called what, and whether it was allowed
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quickstart
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install hexgate
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**See it enforce — no API keys.** Save a policy that gives two roles different
|
|
53
|
+
limits on the *same* `refund_order` tool:
|
|
54
|
+
|
|
55
|
+
<!-- Keep this refund_order policy example in sync with docs/quickstart.mdx -->
|
|
56
|
+
```yaml
|
|
57
|
+
# policy.yaml
|
|
58
|
+
version: 1
|
|
59
|
+
roles:
|
|
60
|
+
support: # small USD refunds only
|
|
61
|
+
default_policy: { mode: deny }
|
|
62
|
+
tools:
|
|
63
|
+
refund_order:
|
|
64
|
+
mode: allow
|
|
65
|
+
constraints:
|
|
66
|
+
- args.amount <= 50
|
|
67
|
+
- args.currency == "USD"
|
|
68
|
+
billing: # larger refunds, major currencies
|
|
69
|
+
default_policy: { mode: deny }
|
|
70
|
+
tools:
|
|
71
|
+
refund_order:
|
|
72
|
+
mode: allow
|
|
73
|
+
constraints:
|
|
74
|
+
- args.amount <= 500
|
|
75
|
+
- args.currency in ["USD", "EUR"]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`hexgate policy test` decides the **same $400 refund** for each role offline — no model, no keys:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
hexgate policy test policy.yaml --role support \
|
|
82
|
+
--tool refund_order --args '{"amount": 400, "currency": "USD"}'
|
|
83
|
+
# ✗ DENY · support → refund_order({"amount": 400, "currency": "USD"})
|
|
84
|
+
# reason: Policy on "refund_order" denied: constraint failed — args.amount <= 50
|
|
85
|
+
|
|
86
|
+
hexgate policy test policy.yaml --role billing \
|
|
87
|
+
--tool refund_order --args '{"amount": 400, "currency": "USD"}'
|
|
88
|
+
# ✓ ALLOW · billing → refund_order({"amount": 400, "currency": "USD"})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Same tool, same request — **the caller's role and the arguments decide**, enforced
|
|
92
|
+
outside the model. The [full quickstart →](https://docs.hexgate.ai/quickstart) puts
|
|
93
|
+
this in front of a live agent.
|
|
94
|
+
|
|
95
|
+
## Documentation
|
|
96
|
+
|
|
97
|
+
Full documentation lives at **[docs.hexgate.ai](https://docs.hexgate.ai)**.
|
|
98
|
+
|
|
99
|
+
| | |
|
|
100
|
+
|---|---|
|
|
101
|
+
| [Build an agent](https://docs.hexgate.ai/guides/build-an-agent) | The two shapes — wrap an existing agent, or let the platform own the YAML. |
|
|
102
|
+
| [Framework adapters](https://docs.hexgate.ai/adapters/openai) | OpenAI Agents, LangChain/LangGraph, Google ADK, Pydantic AI. |
|
|
103
|
+
| [Policy](https://docs.hexgate.ai/policy/yaml-shape) | YAML shape, constraints, WASM bundles, signing, local override. |
|
|
104
|
+
| [User scope + roles](https://docs.hexgate.ai/concepts/user-scope) | Per-request identity, role resolution, biscuit attenuation. |
|
|
105
|
+
| [CLI](https://docs.hexgate.ai/cli/chat) | `chat`, `serve`, `register`, `policy`. |
|
|
106
|
+
| [MCP servers](https://docs.hexgate.ai/concepts/mcp) | Wrap any Model Context Protocol server as policy-enforced tools. |
|
|
107
|
+
| [Hexgate Cloud (hosted)](https://docs.hexgate.ai/platform/hosted) | Remote policy enforcement + audit with zero infra — get a key, set one env var. |
|
|
108
|
+
| [Platform (self-hosted)](https://docs.hexgate.ai/platform/overview) | Run the control plane, dashboard, ClickHouse audit, and Resend email yourself. |
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
Contributor setup, `make` targets, and the test suites are documented in
|
|
113
|
+
[Development & testing](https://docs.hexgate.ai/internals/development). The short
|
|
114
|
+
version:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
make install-dev # uv sync --extra dev (first time only)
|
|
118
|
+
make check # lint + fmt-check + test (matches CI)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT — see [LICENSE](LICENSE).
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
If Hexgate looks useful, [give it a ⭐ on GitHub](https://github.com/HexamindOrganisation/hexgate) — it helps more than you'd think. Built by [Hexamind](https://hexgate.ai).
|
|
@@ -10,21 +10,27 @@ from hexgate.agents.factory import (
|
|
|
10
10
|
from hexgate.agents.loader import (
|
|
11
11
|
clear_registered_agents,
|
|
12
12
|
list_available_agents,
|
|
13
|
-
list_builtin_agents,
|
|
14
13
|
list_local_agents,
|
|
15
14
|
list_registered_agents,
|
|
16
15
|
load_agent,
|
|
17
|
-
load_builtin_agent,
|
|
18
16
|
load_hexgate_agent,
|
|
19
17
|
load_local_agent,
|
|
20
18
|
load_registered_agent,
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
register_agent_factory,
|
|
20
|
+
unregister_agent_factory,
|
|
23
21
|
)
|
|
24
|
-
from hexgate.cli.register import AgentManifest, create_manifest
|
|
25
22
|
from hexgate.cloud import HexgateClient, HexgateConfig
|
|
23
|
+
from hexgate.manifest import AgentManifest, create_manifest
|
|
26
24
|
from hexgate.runtime import LocalWorkspace, ToolUseContext, User, Workspace
|
|
27
|
-
from hexgate.security import
|
|
25
|
+
from hexgate.security import (
|
|
26
|
+
AgentPolicy,
|
|
27
|
+
C,
|
|
28
|
+
PolicyBuilder,
|
|
29
|
+
RolePolicyBuilder,
|
|
30
|
+
assert_allows,
|
|
31
|
+
assert_denies,
|
|
32
|
+
assert_needs_approval,
|
|
33
|
+
)
|
|
28
34
|
from hexgate.tools import (
|
|
29
35
|
agent_tool,
|
|
30
36
|
bash,
|
|
@@ -32,15 +38,18 @@ from hexgate.tools import (
|
|
|
32
38
|
glob,
|
|
33
39
|
grep,
|
|
34
40
|
read_file,
|
|
35
|
-
refund_order,
|
|
36
41
|
write_file,
|
|
37
42
|
)
|
|
38
|
-
from hexgate.tools.fetch import fetch
|
|
39
|
-
from hexgate.tools.websearch import web_search
|
|
40
43
|
|
|
41
44
|
__all__ = [
|
|
42
45
|
"AgentManifest",
|
|
43
46
|
"AgentPolicy",
|
|
47
|
+
"C",
|
|
48
|
+
"PolicyBuilder",
|
|
49
|
+
"RolePolicyBuilder",
|
|
50
|
+
"assert_allows",
|
|
51
|
+
"assert_denies",
|
|
52
|
+
"assert_needs_approval",
|
|
44
53
|
"HexgateClient",
|
|
45
54
|
"HexgateConfig",
|
|
46
55
|
"LocalWorkspace",
|
|
@@ -54,25 +63,28 @@ __all__ = [
|
|
|
54
63
|
"create_agent",
|
|
55
64
|
"create_manifest",
|
|
56
65
|
"enforce_policy",
|
|
57
|
-
"fetch",
|
|
58
66
|
"glob",
|
|
59
67
|
"grep",
|
|
60
68
|
"invoke_agent",
|
|
61
69
|
"list_available_agents",
|
|
62
|
-
"list_builtin_agents",
|
|
63
70
|
"list_local_agents",
|
|
64
71
|
"list_registered_agents",
|
|
65
72
|
"load_agent",
|
|
66
|
-
"load_builtin_agent",
|
|
67
73
|
"load_hexgate_agent",
|
|
68
74
|
"load_local_agent",
|
|
69
75
|
"load_registered_agent",
|
|
70
|
-
"
|
|
76
|
+
"register_agent_factory",
|
|
71
77
|
"read_file",
|
|
72
|
-
"refund_order",
|
|
73
78
|
"stream_agent",
|
|
74
79
|
"stream_agent_raw",
|
|
75
|
-
"
|
|
76
|
-
"web_search",
|
|
80
|
+
"unregister_agent_factory",
|
|
77
81
|
"write_file",
|
|
78
82
|
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def __getattr__(name: str) -> object:
|
|
86
|
+
from hexgate.tools._relocated import RELOCATED_TOOLS, relocated_import_error
|
|
87
|
+
|
|
88
|
+
if name in RELOCATED_TOOLS:
|
|
89
|
+
raise relocated_import_error(name)
|
|
90
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Google ADK adapter for :class:`~hexgate.mcp.MCPToolset`.
|
|
2
|
+
|
|
3
|
+
Every :class:`~hexgate.mcp.MCPToolProxy` produced by the toolset becomes
|
|
4
|
+
a :class:`google.adk.tools.BaseTool` subclass whose ``_get_declaration``
|
|
5
|
+
returns a ``FunctionDeclaration`` carrying the MCP tool's raw
|
|
6
|
+
JSON Schema (via ``parametersJsonSchema``) and whose ``run_async``
|
|
7
|
+
forwards to the proxy's ``call``. Once wrapped, the resulting tools
|
|
8
|
+
are indistinguishable from ADK-native :class:`FunctionTool` instances
|
|
9
|
+
to the rest of the Google ADK path — attach them to an ``Agent``, then
|
|
10
|
+
wrap via :func:`~hexgate.adapters.google.wrap_google_agent` so the
|
|
11
|
+
existing per-tool policy gate covers MCP invocations too.
|
|
12
|
+
|
|
13
|
+
Usage::
|
|
14
|
+
|
|
15
|
+
from google.adk.agents import Agent
|
|
16
|
+
from hexgate.adapters.google import wrap_google_agent
|
|
17
|
+
from hexgate.adapters.google.mcp import wrap_mcp_toolset
|
|
18
|
+
from hexgate.mcp import MCPServerConfig, MCPToolset
|
|
19
|
+
|
|
20
|
+
slack = MCPServerConfig(name="slack", transport="stdio", command="slack-mcp")
|
|
21
|
+
async with MCPToolset(slack) as mcp:
|
|
22
|
+
agent = Agent(
|
|
23
|
+
name="bot",
|
|
24
|
+
tools=[*wrap_mcp_toolset(mcp), *native],
|
|
25
|
+
)
|
|
26
|
+
wrapped, binding = wrap_google_agent(agent, api_key=api_key)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from google.adk.tools import BaseTool
|
|
34
|
+
from google.adk.tools.tool_context import ToolContext
|
|
35
|
+
from google.genai import types as genai_types
|
|
36
|
+
|
|
37
|
+
from hexgate.mcp.proxy import MCPToolProxy, MCPToolset
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _MCPProxyTool(BaseTool):
|
|
41
|
+
"""One :class:`BaseTool` that forwards to an :class:`MCPToolProxy`.
|
|
42
|
+
|
|
43
|
+
ADK's :class:`FunctionTool` derives its schema from a Python
|
|
44
|
+
callable's signature via reflection, which can't express the shapes
|
|
45
|
+
MCP servers advertise (partial ``required``, ``anyOf`` unions,
|
|
46
|
+
nested objects with dynamic keys). Subclassing :class:`BaseTool`
|
|
47
|
+
and returning a hand-built :class:`FunctionDeclaration` from
|
|
48
|
+
``_get_declaration`` bypasses the reflection path and hands the
|
|
49
|
+
server's raw JSON Schema straight to the model — via ADK's
|
|
50
|
+
``parametersJsonSchema`` alias which accepts JSON Schema dicts.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, proxy: MCPToolProxy) -> None:
|
|
54
|
+
super().__init__(name=proxy.qualified_name, description=proxy.description)
|
|
55
|
+
# Store the schema + call as private attrs — ADK's BaseTool has
|
|
56
|
+
# no field for them, so we ride on the object dict.
|
|
57
|
+
self._input_schema = proxy.input_schema
|
|
58
|
+
self._call = proxy.call
|
|
59
|
+
|
|
60
|
+
def _get_declaration(self) -> genai_types.FunctionDeclaration:
|
|
61
|
+
# Gemini's FunctionDeclaration validator rejects any
|
|
62
|
+
# parametersJsonSchema missing a top-level `type` — literal `{}`,
|
|
63
|
+
# or a partial like `{"properties": {...}}`, or a bare
|
|
64
|
+
# `{"anyOf": [...]}`. All three shapes cause the whole tool
|
|
65
|
+
# list to fail at declaration time, so the agent never gets to
|
|
66
|
+
# run. Fill in `type: "object"` (the only shape Gemini accepts
|
|
67
|
+
# for a function-args container) whenever it's absent, and
|
|
68
|
+
# ensure a `properties` map so the LLM sees an argument surface
|
|
69
|
+
# rather than an opaque object. LangChain and OpenAI Agents
|
|
70
|
+
# both tolerate the missing `type` unchanged.
|
|
71
|
+
schema = self._input_schema if isinstance(self._input_schema, dict) else {}
|
|
72
|
+
if "type" not in schema:
|
|
73
|
+
schema = {"type": "object", "properties": {}, **schema}
|
|
74
|
+
return genai_types.FunctionDeclaration(
|
|
75
|
+
name=self.name,
|
|
76
|
+
description=self.description,
|
|
77
|
+
parametersJsonSchema=schema,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async def run_async(
|
|
81
|
+
self, *, args: dict[str, Any], tool_context: ToolContext
|
|
82
|
+
) -> Any:
|
|
83
|
+
return await self._call(**(args or {}))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def wrap_mcp_toolset(toolset: MCPToolset) -> list[BaseTool]:
|
|
87
|
+
"""Wrap every proxy in ``toolset`` as a Google ADK :class:`BaseTool`.
|
|
88
|
+
|
|
89
|
+
The returned tools share the toolset's connection lifecycle — they
|
|
90
|
+
stop working (returning a ``use_after_close`` envelope) once the
|
|
91
|
+
``async with MCPToolset(...)`` block exits. Combine with
|
|
92
|
+
:func:`~hexgate.adapters.google.wrap_google_agent` to gate every
|
|
93
|
+
invocation through :class:`~hexgate.security.PolicyEnforcer`.
|
|
94
|
+
"""
|
|
95
|
+
return [_MCPProxyTool(p) for p in toolset.proxies]
|
|
@@ -16,8 +16,11 @@ from langfuse import get_client, propagate_attributes
|
|
|
16
16
|
from openinference.instrumentation.google_adk import GoogleADKInstrumentor
|
|
17
17
|
|
|
18
18
|
from hexgate.adapters.google.wrapper import wrap_google_agent
|
|
19
|
+
from hexgate.agents.factory import ApprovalHandler
|
|
20
|
+
from hexgate.cloud.client import HexgateClient, HexgateConfig
|
|
19
21
|
from hexgate.config.env import resolve_api_key
|
|
20
22
|
from hexgate.runtime import User
|
|
23
|
+
from hexgate.security.bans import resolve_ban_gate
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
class HexgateRunner:
|
|
@@ -30,6 +33,7 @@ class HexgateRunner:
|
|
|
30
33
|
app_name: str,
|
|
31
34
|
session_service: BaseSessionService,
|
|
32
35
|
api_key: str | None = None,
|
|
36
|
+
approval_handler: ApprovalHandler | None = None,
|
|
33
37
|
**runner_kwargs: Any,
|
|
34
38
|
):
|
|
35
39
|
self.api_key = resolve_api_key(api_key)
|
|
@@ -39,9 +43,13 @@ class HexgateRunner:
|
|
|
39
43
|
)
|
|
40
44
|
# Policy resolves at construction (the loud-failure point); the
|
|
41
45
|
# Runner is built once — refresh swaps the enforcer's policy
|
|
42
|
-
# without touching it.
|
|
46
|
+
# without touching it. One client is shared with the ban resolver.
|
|
47
|
+
client = HexgateClient(HexgateConfig.from_env(api_key=self.api_key))
|
|
43
48
|
self._wrapped_agent, self._binding = wrap_google_agent(
|
|
44
|
-
agent,
|
|
49
|
+
agent,
|
|
50
|
+
api_key=self.api_key,
|
|
51
|
+
approval_handler=approval_handler,
|
|
52
|
+
client=client,
|
|
45
53
|
)
|
|
46
54
|
self._runner = Runner(
|
|
47
55
|
agent=self._wrapped_agent,
|
|
@@ -50,6 +58,9 @@ class HexgateRunner:
|
|
|
50
58
|
**runner_kwargs,
|
|
51
59
|
)
|
|
52
60
|
self._agent_name = getattr(agent, "name", "default")
|
|
61
|
+
self._ban_gate = resolve_ban_gate(
|
|
62
|
+
self._agent_name, api_key=self.api_key, client=client
|
|
63
|
+
)
|
|
53
64
|
|
|
54
65
|
def _setup_observability(self):
|
|
55
66
|
"""Install Langfuse + GoogleADKInstrumentor (idempotent)."""
|
|
@@ -88,6 +99,8 @@ class HexgateRunner:
|
|
|
88
99
|
"""
|
|
89
100
|
self._setup_observability()
|
|
90
101
|
self._binding.refresh() # per-run policy pull; 304 when unchanged
|
|
102
|
+
if self._ban_gate is not None:
|
|
103
|
+
self._ban_gate.check(user)
|
|
91
104
|
with user.sync_scope(), self._propagate(user):
|
|
92
105
|
agen = self._runner.run_async(
|
|
93
106
|
user_id=user.user_id,
|
|
@@ -116,6 +129,8 @@ class HexgateRunner:
|
|
|
116
129
|
"""Run the Google ADK agent asynchronously, yielding events."""
|
|
117
130
|
self._setup_observability()
|
|
118
131
|
await self._binding.refresh_async() # per-run policy pull; 304 when unchanged
|
|
132
|
+
if self._ban_gate is not None:
|
|
133
|
+
await self._ban_gate.check_async(user)
|
|
119
134
|
async with user:
|
|
120
135
|
with self._propagate(user):
|
|
121
136
|
async for event in self._runner.run_async(
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"""Google ADK adapter: wrap ``BaseTool`` so ``run_async`` consults a
|
|
2
2
|
:class:`PolicyEnforcer` first. Non-allow outcomes render as markered
|
|
3
3
|
strings the model sees as tool output.
|
|
4
|
+
|
|
5
|
+
When a caller supplies ``approval_handler``, a ``NEEDS_APPROVAL``
|
|
6
|
+
decision fires the callback and runs the original tool on truthy return;
|
|
7
|
+
falsy return (or a missing handler) keeps today's behavior of surfacing
|
|
8
|
+
the ``[approval_required]`` marker to the model.
|
|
4
9
|
"""
|
|
5
10
|
|
|
6
11
|
from __future__ import annotations
|
|
@@ -14,6 +19,9 @@ from google.adk.tools.base_tool import BaseTool
|
|
|
14
19
|
from google.adk.tools.function_tool import FunctionTool
|
|
15
20
|
from google.adk.tools.tool_context import ToolContext
|
|
16
21
|
|
|
22
|
+
from hexgate.agents.approvals import resolve_approval_async
|
|
23
|
+
from hexgate.agents.factory import ApprovalHandler
|
|
24
|
+
from hexgate.security.decision import DecisionOutcome
|
|
17
25
|
from hexgate.security.enforcer import PolicyEnforcer
|
|
18
26
|
|
|
19
27
|
|
|
@@ -32,7 +40,12 @@ def _normalize(tool: ToolEntry) -> BaseTool:
|
|
|
32
40
|
)
|
|
33
41
|
|
|
34
42
|
|
|
35
|
-
def wrap_tool(
|
|
43
|
+
def wrap_tool(
|
|
44
|
+
tool: ToolEntry,
|
|
45
|
+
enforcer: PolicyEnforcer,
|
|
46
|
+
*,
|
|
47
|
+
approval_handler: ApprovalHandler | None = None,
|
|
48
|
+
) -> BaseTool:
|
|
36
49
|
"""Return a copy of ``tool`` with ``run_async`` gated by ``enforcer``."""
|
|
37
50
|
base = _normalize(tool)
|
|
38
51
|
name = base.name
|
|
@@ -45,6 +58,12 @@ def wrap_tool(tool: ToolEntry, enforcer: PolicyEnforcer) -> BaseTool:
|
|
|
45
58
|
decision = enforcer.decide(name, args or {})
|
|
46
59
|
if decision.allowed:
|
|
47
60
|
return await original_run_async(args=args, tool_context=tool_context)
|
|
61
|
+
if (
|
|
62
|
+
decision.outcome is DecisionOutcome.NEEDS_APPROVAL
|
|
63
|
+
and approval_handler is not None
|
|
64
|
+
and await resolve_approval_async(approval_handler, decision)
|
|
65
|
+
):
|
|
66
|
+
return await original_run_async(args=args, tool_context=tool_context)
|
|
48
67
|
return decision.as_error_message()
|
|
49
68
|
|
|
50
69
|
wrapped = copy.copy(base)
|
|
@@ -52,6 +71,11 @@ def wrap_tool(tool: ToolEntry, enforcer: PolicyEnforcer) -> BaseTool:
|
|
|
52
71
|
return wrapped
|
|
53
72
|
|
|
54
73
|
|
|
55
|
-
def wrap_tools(
|
|
74
|
+
def wrap_tools(
|
|
75
|
+
tools: list[ToolEntry],
|
|
76
|
+
enforcer: PolicyEnforcer,
|
|
77
|
+
*,
|
|
78
|
+
approval_handler: ApprovalHandler | None = None,
|
|
79
|
+
) -> list[BaseTool]:
|
|
56
80
|
"""Return a fresh list of policy-gated copies."""
|
|
57
|
-
return [wrap_tool(t, enforcer) for t in tools]
|
|
81
|
+
return [wrap_tool(t, enforcer, approval_handler=approval_handler) for t in tools]
|
|
@@ -10,31 +10,44 @@ is what the runner refreshes per run.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
13
15
|
from google.adk.agents import BaseAgent
|
|
14
16
|
|
|
15
17
|
from hexgate.adapters.google.tools import wrap_tools
|
|
18
|
+
from hexgate.agents.factory import ApprovalHandler
|
|
16
19
|
from hexgate.security.binding import PolicyBinding, resolve_policy
|
|
17
20
|
from hexgate.security.enforcer import build_enforcer
|
|
18
21
|
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from hexgate.cloud.client import HexgateClient
|
|
24
|
+
|
|
19
25
|
|
|
20
26
|
def wrap_google_agent(
|
|
21
|
-
agent: BaseAgent,
|
|
27
|
+
agent: BaseAgent,
|
|
28
|
+
*,
|
|
29
|
+
api_key: str,
|
|
30
|
+
approval_handler: ApprovalHandler | None = None,
|
|
31
|
+
client: HexgateClient | None = None,
|
|
22
32
|
) -> tuple[BaseAgent, PolicyBinding]:
|
|
23
33
|
"""Return a policy-gated clone of ``agent`` plus its refresh binding.
|
|
24
34
|
|
|
25
35
|
Caller must open a :class:`User` scope around the run.
|
|
26
|
-
``NEEDS_APPROVAL`` outcomes
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
``NEEDS_APPROVAL`` outcomes fire ``approval_handler`` (async
|
|
37
|
+
``fn(decision) -> bool`` or ``bool`` shorthand); a truthy return
|
|
38
|
+
runs the tool, falsy or missing handler surfaces the
|
|
39
|
+
``[approval_required]``-prefixed string as tool result.
|
|
40
|
+
``[policy_denied]`` marks plain denials. Refresh the returned
|
|
41
|
+
binding at run boundaries (``HexgateRunner`` does). Fail-loud: an
|
|
42
|
+
unregistered agent (platform 404) raises — register it first with
|
|
30
43
|
``hexgate register``.
|
|
31
44
|
"""
|
|
32
45
|
agent_name = getattr(agent, "name", "default")
|
|
33
46
|
tools = list(getattr(agent, "tools", []) or [])
|
|
34
47
|
|
|
35
|
-
resolved = resolve_policy(agent_name, api_key=api_key)
|
|
48
|
+
resolved = resolve_policy(agent_name, api_key=api_key, client=client)
|
|
36
49
|
enforcer = build_enforcer(resolved.engine, agent_name=agent_name, api_key=api_key)
|
|
37
|
-
guarded_tools = wrap_tools(tools, enforcer)
|
|
50
|
+
guarded_tools = wrap_tools(tools, enforcer, approval_handler=approval_handler)
|
|
38
51
|
return (
|
|
39
52
|
agent.model_copy(update={"tools": guarded_tools}),
|
|
40
53
|
PolicyBinding(enforcer, resolved.source),
|