hexgate 0.2.7__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.7 → hexgate-0.2.8}/hexgate/__init__.py +28 -16
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/google/runner.py +15 -2
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/google/wrapper.py +7 -1
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/langchain/agent.py +23 -1
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/langchain/wrapper.py +7 -1
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/openai/runner.py +27 -1
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/agent.py +22 -1
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/wrapper.py +7 -1
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/agents/__init__.py +6 -14
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/agents/factory.py +36 -6
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/agents/loader.py +53 -111
- hexgate-0.2.8/hexgate/audit.py +168 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/bootstrap.py +2 -1
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/_common.py +7 -41
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/chat.py +2 -3
- {hexgate-0.2.7 → 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.7 → hexgate-0.2.8}/hexgate/cli/register/register.py +2 -2
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cloud/__init__.py +2 -2
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cloud/client.py +18 -1
- hexgate-0.2.8/hexgate/manifest/__init__.py +23 -0
- hexgate-0.2.7/hexgate/cli/register/manifest.py → hexgate-0.2.8/hexgate/manifest/builder.py +6 -6
- {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/google.py +1 -1
- {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/langchain.py +1 -1
- hexgate-0.2.7/hexgate/cli/register/hexgate.py → hexgate-0.2.8/hexgate/manifest/native.py +1 -1
- {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/openai.py +1 -1
- {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/pydantic_ai.py +2 -2
- {hexgate-0.2.7 → 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.7 → hexgate-0.2.8}/hexgate/security/enforcer.py +2 -1
- hexgate-0.2.8/hexgate/security/errors.py +39 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/models.py +20 -2
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/policy.py +15 -2
- {hexgate-0.2.7 → 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.7 → hexgate-0.2.8}/hexgate/tools/__init__.py +8 -6
- hexgate-0.2.8/hexgate/tools/_relocated.py +22 -0
- hexgate-0.2.7/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.7 → hexgate-0.2.8}/hexgate.egg-info/SOURCES.txt +14 -15
- {hexgate-0.2.7 → hexgate-0.2.8}/pyproject.toml +1 -9
- {hexgate-0.2.7 → hexgate-0.2.8}/tests/test_bootstrap.py +9 -8
- hexgate-0.2.7/PKG-INFO +0 -1589
- hexgate-0.2.7/README.md +0 -1548
- hexgate-0.2.7/hexgate/agents/builtin/__init__.py +0 -1
- hexgate-0.2.7/hexgate/agents/builtin/researcher/agent.yaml +0 -7
- hexgate-0.2.7/hexgate/agents/builtin/researcher/policy.yaml +0 -10
- hexgate-0.2.7/hexgate/agents/builtin/researcher/system.md +0 -5
- hexgate-0.2.7/hexgate/agents/prompts/agent_system.md +0 -14
- hexgate-0.2.7/hexgate/cli/register/__init__.py +0 -8
- hexgate-0.2.7/hexgate/security/constraints.py +0 -252
- hexgate-0.2.7/hexgate/security/errors.py +0 -11
- hexgate-0.2.7/hexgate/security/rego.py +0 -334
- hexgate-0.2.7/hexgate/tools/fetch.py +0 -72
- hexgate-0.2.7/hexgate/tools/refund.py +0 -53
- hexgate-0.2.7/hexgate/tools/websearch.py +0 -72
- hexgate-0.2.7/hexgate.egg-info/PKG-INFO +0 -1589
- {hexgate-0.2.7 → hexgate-0.2.8}/LICENSE +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/google/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/google/mcp.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/google/tools.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/langchain/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/langchain/mcp.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/langchain/tools.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/openai/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/openai/mcp.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/openai/tools.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/openai/wrapper.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/mcp.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/tools.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/agents/approvals.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/agents/models.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/policy/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/register/main.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/serve.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/state.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cloud/attenuate.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cloud/biscuit.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/config/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/config/env.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/config/settings.py +0 -0
- {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/models.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/mcp/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/mcp/client.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/mcp/config.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/mcp/proxy.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/runtime/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/runtime/command_policy.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/runtime/context.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/runtime/sandbox_runtime.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/runtime/srt.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/runtime/workspace.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/binding.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/bundle.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/decision.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/file_scope.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/rego_wasm.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/signing.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/source.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/wasm_engine.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/streaming/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/streaming/events.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/streaming/normalize.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/bash.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/decorators.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/_common.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/edit_file.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/glob.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/grep.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/read_file.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/write_file.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tracing/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tracing/langfuse.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/utils/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/utils/retry.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate.egg-info/dependency_links.txt +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate.egg-info/entry_points.txt +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate.egg-info/requires.txt +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/hexgate.egg-info/top_level.txt +0 -0
- {hexgate-0.2.7 → hexgate-0.2.8}/setup.cfg +0 -0
- {hexgate-0.2.7 → 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}")
|
|
@@ -17,8 +17,10 @@ from openinference.instrumentation.google_adk import GoogleADKInstrumentor
|
|
|
17
17
|
|
|
18
18
|
from hexgate.adapters.google.wrapper import wrap_google_agent
|
|
19
19
|
from hexgate.agents.factory import ApprovalHandler
|
|
20
|
+
from hexgate.cloud.client import HexgateClient, HexgateConfig
|
|
20
21
|
from hexgate.config.env import resolve_api_key
|
|
21
22
|
from hexgate.runtime import User
|
|
23
|
+
from hexgate.security.bans import resolve_ban_gate
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
class HexgateRunner:
|
|
@@ -41,9 +43,13 @@ class HexgateRunner:
|
|
|
41
43
|
)
|
|
42
44
|
# Policy resolves at construction (the loud-failure point); the
|
|
43
45
|
# Runner is built once — refresh swaps the enforcer's policy
|
|
44
|
-
# 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))
|
|
45
48
|
self._wrapped_agent, self._binding = wrap_google_agent(
|
|
46
|
-
agent,
|
|
49
|
+
agent,
|
|
50
|
+
api_key=self.api_key,
|
|
51
|
+
approval_handler=approval_handler,
|
|
52
|
+
client=client,
|
|
47
53
|
)
|
|
48
54
|
self._runner = Runner(
|
|
49
55
|
agent=self._wrapped_agent,
|
|
@@ -52,6 +58,9 @@ class HexgateRunner:
|
|
|
52
58
|
**runner_kwargs,
|
|
53
59
|
)
|
|
54
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
|
+
)
|
|
55
64
|
|
|
56
65
|
def _setup_observability(self):
|
|
57
66
|
"""Install Langfuse + GoogleADKInstrumentor (idempotent)."""
|
|
@@ -90,6 +99,8 @@ class HexgateRunner:
|
|
|
90
99
|
"""
|
|
91
100
|
self._setup_observability()
|
|
92
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)
|
|
93
104
|
with user.sync_scope(), self._propagate(user):
|
|
94
105
|
agen = self._runner.run_async(
|
|
95
106
|
user_id=user.user_id,
|
|
@@ -118,6 +129,8 @@ class HexgateRunner:
|
|
|
118
129
|
"""Run the Google ADK agent asynchronously, yielding events."""
|
|
119
130
|
self._setup_observability()
|
|
120
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)
|
|
121
134
|
async with user:
|
|
122
135
|
with self._propagate(user):
|
|
123
136
|
async for event in self._runner.run_async(
|
|
@@ -10,6 +10,8 @@ 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
|
|
@@ -17,12 +19,16 @@ from hexgate.agents.factory import ApprovalHandler
|
|
|
17
19
|
from hexgate.security.binding import PolicyBinding, resolve_policy
|
|
18
20
|
from hexgate.security.enforcer import build_enforcer
|
|
19
21
|
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from hexgate.cloud.client import HexgateClient
|
|
24
|
+
|
|
20
25
|
|
|
21
26
|
def wrap_google_agent(
|
|
22
27
|
agent: BaseAgent,
|
|
23
28
|
*,
|
|
24
29
|
api_key: str,
|
|
25
30
|
approval_handler: ApprovalHandler | None = None,
|
|
31
|
+
client: HexgateClient | None = None,
|
|
26
32
|
) -> tuple[BaseAgent, PolicyBinding]:
|
|
27
33
|
"""Return a policy-gated clone of ``agent`` plus its refresh binding.
|
|
28
34
|
|
|
@@ -39,7 +45,7 @@ def wrap_google_agent(
|
|
|
39
45
|
agent_name = getattr(agent, "name", "default")
|
|
40
46
|
tools = list(getattr(agent, "tools", []) or [])
|
|
41
47
|
|
|
42
|
-
resolved = resolve_policy(agent_name, api_key=api_key)
|
|
48
|
+
resolved = resolve_policy(agent_name, api_key=api_key, client=client)
|
|
43
49
|
enforcer = build_enforcer(resolved.engine, agent_name=agent_name, api_key=api_key)
|
|
44
50
|
guarded_tools = wrap_tools(tools, enforcer, approval_handler=approval_handler)
|
|
45
51
|
return (
|
|
@@ -12,6 +12,7 @@ from langgraph.graph.state import CompiledStateGraph
|
|
|
12
12
|
from hexgate.runtime import User
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
+
from hexgate.security.bans import BanGate
|
|
15
16
|
from hexgate.security.binding import PolicyBinding
|
|
16
17
|
|
|
17
18
|
|
|
@@ -33,9 +34,11 @@ class HexgateLangchainAgent:
|
|
|
33
34
|
api_key: str,
|
|
34
35
|
tool_names: list[str],
|
|
35
36
|
binding: PolicyBinding | None = None,
|
|
37
|
+
ban_gate: BanGate | None = None,
|
|
36
38
|
) -> None:
|
|
37
39
|
self._agent = agent
|
|
38
40
|
self._binding = binding
|
|
41
|
+
self._ban_gate = ban_gate
|
|
39
42
|
self._api_key = api_key
|
|
40
43
|
self._tool_names = tool_names
|
|
41
44
|
self._langfuse = get_client()
|
|
@@ -51,6 +54,15 @@ class HexgateLangchainAgent:
|
|
|
51
54
|
if self._binding is not None:
|
|
52
55
|
self._binding.refresh()
|
|
53
56
|
|
|
57
|
+
async def _check_ban_async(self, user: User) -> None:
|
|
58
|
+
"""Refuse a banned agent/user before running, if a gate is attached."""
|
|
59
|
+
if self._ban_gate is not None:
|
|
60
|
+
await self._ban_gate.check_async(user)
|
|
61
|
+
|
|
62
|
+
def _check_ban(self, user: User) -> None:
|
|
63
|
+
if self._ban_gate is not None:
|
|
64
|
+
self._ban_gate.check(user)
|
|
65
|
+
|
|
54
66
|
def _propagate_kwargs(self, user: User, method: str) -> dict[str, Any]:
|
|
55
67
|
return {
|
|
56
68
|
"tags": [f"langchain.agent.{method}"],
|
|
@@ -78,6 +90,7 @@ class HexgateLangchainAgent:
|
|
|
78
90
|
) -> dict[str, Any]:
|
|
79
91
|
"""Invoke the agent asynchronously inside a User scope."""
|
|
80
92
|
await self._refresh_async()
|
|
93
|
+
await self._check_ban_async(user)
|
|
81
94
|
async with user:
|
|
82
95
|
with propagate_attributes(**self._propagate_kwargs(user, "ainvoke")):
|
|
83
96
|
return await self._agent.ainvoke(
|
|
@@ -94,6 +107,7 @@ class HexgateLangchainAgent:
|
|
|
94
107
|
) -> dict[str, Any]:
|
|
95
108
|
"""Invoke the agent synchronously inside a User scope."""
|
|
96
109
|
self._refresh()
|
|
110
|
+
self._check_ban(user)
|
|
97
111
|
with user.sync_scope():
|
|
98
112
|
with propagate_attributes(**self._propagate_kwargs(user, "invoke")):
|
|
99
113
|
return self._agent.invoke(input, self._with_callbacks(config), **kwargs)
|
|
@@ -108,6 +122,7 @@ class HexgateLangchainAgent:
|
|
|
108
122
|
) -> AsyncIterator[dict[str, Any]]:
|
|
109
123
|
"""Stream the agent asynchronously inside a User scope."""
|
|
110
124
|
await self._refresh_async()
|
|
125
|
+
await self._check_ban_async(user)
|
|
111
126
|
async with user:
|
|
112
127
|
with propagate_attributes(**self._propagate_kwargs(user, "astream")):
|
|
113
128
|
async for chunk in self._agent.astream(
|
|
@@ -125,6 +140,7 @@ class HexgateLangchainAgent:
|
|
|
125
140
|
) -> Iterator[dict[str, Any]]:
|
|
126
141
|
"""Stream the agent synchronously inside a User scope."""
|
|
127
142
|
self._refresh()
|
|
143
|
+
self._check_ban(user)
|
|
128
144
|
with user.sync_scope():
|
|
129
145
|
with propagate_attributes(**self._propagate_kwargs(user, "stream")):
|
|
130
146
|
yield from self._agent.stream(
|
|
@@ -142,6 +158,7 @@ class HexgateLangchainAgent:
|
|
|
142
158
|
) -> AsyncIterator[dict[str, Any]]:
|
|
143
159
|
"""Stream the agent events asynchronously inside a User scope."""
|
|
144
160
|
await self._refresh_async()
|
|
161
|
+
await self._check_ban_async(user)
|
|
145
162
|
async with user:
|
|
146
163
|
with propagate_attributes(**self._propagate_kwargs(user, "astream_events")):
|
|
147
164
|
async for event in self._agent.astream_events(
|
|
@@ -153,5 +170,10 @@ class HexgateLangchainAgent:
|
|
|
153
170
|
yield event
|
|
154
171
|
|
|
155
172
|
def __getattr__(self, name: str) -> Any:
|
|
156
|
-
"""Delegate unknown attributes to the wrapped agent.
|
|
173
|
+
"""Delegate unknown attributes to the wrapped agent.
|
|
174
|
+
|
|
175
|
+
Only the wrapped run methods (invoke/ainvoke/stream/astream/
|
|
176
|
+
astream_events) enforce the ban gate + User scope; methods reached
|
|
177
|
+
here (batch, abatch, astream_log, …) bypass them.
|
|
178
|
+
"""
|
|
157
179
|
return getattr(self._agent, name)
|
|
@@ -16,7 +16,9 @@ from langgraph.graph.state import CompiledStateGraph
|
|
|
16
16
|
|
|
17
17
|
from hexgate.adapters.langchain.agent import HexgateLangchainAgent
|
|
18
18
|
from hexgate.adapters.langchain.tools import install_enforcer_on_tools
|
|
19
|
+
from hexgate.cloud.client import HexgateClient, HexgateConfig
|
|
19
20
|
from hexgate.config.env import resolve_api_key
|
|
21
|
+
from hexgate.security.bans import resolve_ban_gate
|
|
20
22
|
from hexgate.security.binding import PolicyBinding, resolve_policy
|
|
21
23
|
from hexgate.security.enforcer import build_enforcer
|
|
22
24
|
|
|
@@ -45,7 +47,10 @@ def wrap_langchain_agent(
|
|
|
45
47
|
agent_name = getattr(agent, "name", "default")
|
|
46
48
|
tool_names = [tool.name for tool in tools]
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
# One client shared by the policy and ban resolvers — avoids a second
|
|
51
|
+
# biscuit verify + JWKS round-trip per wrapped agent.
|
|
52
|
+
client = HexgateClient(HexgateConfig.from_env(api_key=resolved_key))
|
|
53
|
+
resolved = resolve_policy(agent_name, api_key=resolved_key, client=client)
|
|
49
54
|
enforcer = build_enforcer(
|
|
50
55
|
resolved.engine, agent_name=agent_name, api_key=resolved_key
|
|
51
56
|
)
|
|
@@ -56,4 +61,5 @@ def wrap_langchain_agent(
|
|
|
56
61
|
api_key=resolved_key,
|
|
57
62
|
tool_names=tool_names,
|
|
58
63
|
binding=PolicyBinding(enforcer, resolved.source),
|
|
64
|
+
ban_gate=resolve_ban_gate(agent_name, api_key=resolved_key, client=client),
|
|
59
65
|
)
|
|
@@ -26,8 +26,10 @@ from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor
|
|
|
26
26
|
|
|
27
27
|
from hexgate.adapters.openai.wrapper import wrap_openai_agent
|
|
28
28
|
from hexgate.agents.factory import ApprovalHandler
|
|
29
|
+
from hexgate.cloud.client import HexgateClient, HexgateConfig
|
|
29
30
|
from hexgate.config.env import resolve_api_key
|
|
30
31
|
from hexgate.runtime import User
|
|
32
|
+
from hexgate.security.bans import BanGate, resolve_ban_gate
|
|
31
33
|
from hexgate.security.binding import PolicyBinding, resolve_policy
|
|
32
34
|
from hexgate.security.enforcer import build_enforcer
|
|
33
35
|
|
|
@@ -46,8 +48,12 @@ class HexgateRunner:
|
|
|
46
48
|
raise ValueError(
|
|
47
49
|
"HEXGATE_API_KEY is not set. Pass api_key= explicitly or set the HEXGATE_API_KEY environment variable."
|
|
48
50
|
)
|
|
51
|
+
# One client shared by the policy and ban resolvers below.
|
|
52
|
+
self._client = HexgateClient(HexgateConfig.from_env(api_key=self.api_key))
|
|
49
53
|
# Cached per agent name — keeps the ETag memory alive across runs.
|
|
50
54
|
self._bindings: dict[str, PolicyBinding] = {}
|
|
55
|
+
# Ban gates cached per agent name too (None cached to avoid re-resolving).
|
|
56
|
+
self._ban_gates: dict[str, BanGate | None] = {}
|
|
51
57
|
self._approval_handler = approval_handler
|
|
52
58
|
|
|
53
59
|
def _binding_for(self, agent: Agent) -> PolicyBinding:
|
|
@@ -64,7 +70,7 @@ class HexgateRunner:
|
|
|
64
70
|
name = getattr(agent, "name", None) or "default"
|
|
65
71
|
binding = self._bindings.get(name)
|
|
66
72
|
if binding is None:
|
|
67
|
-
resolved = resolve_policy(name, api_key=self.api_key)
|
|
73
|
+
resolved = resolve_policy(name, api_key=self.api_key, client=self._client)
|
|
68
74
|
enforcer = build_enforcer(
|
|
69
75
|
resolved.engine, agent_name=name, api_key=self.api_key
|
|
70
76
|
)
|
|
@@ -72,6 +78,16 @@ class HexgateRunner:
|
|
|
72
78
|
self._bindings[name] = binding
|
|
73
79
|
return binding
|
|
74
80
|
|
|
81
|
+
def _ban_gate_for(self, agent: Agent) -> BanGate | None:
|
|
82
|
+
"""Get-or-resolve the cached ban gate for ``agent``'s name (``None`` in
|
|
83
|
+
local mode / no key). ``None`` is cached too, so we resolve once."""
|
|
84
|
+
name = getattr(agent, "name", None) or "default"
|
|
85
|
+
if name not in self._ban_gates:
|
|
86
|
+
self._ban_gates[name] = resolve_ban_gate(
|
|
87
|
+
name, api_key=self.api_key, client=self._client
|
|
88
|
+
)
|
|
89
|
+
return self._ban_gates[name]
|
|
90
|
+
|
|
75
91
|
def _setup_observability(self):
|
|
76
92
|
"""Install Langfuse + OpenAIAgentsInstrumentor (idempotent)."""
|
|
77
93
|
try:
|
|
@@ -105,6 +121,9 @@ class HexgateRunner:
|
|
|
105
121
|
self._setup_observability()
|
|
106
122
|
binding = self._binding_for(agent)
|
|
107
123
|
await binding.refresh_async() # per-run policy pull; 304 when unchanged
|
|
124
|
+
ban_gate = self._ban_gate_for(agent)
|
|
125
|
+
if ban_gate is not None:
|
|
126
|
+
await ban_gate.check_async(user)
|
|
108
127
|
wrapped_agent = wrap_openai_agent(
|
|
109
128
|
agent,
|
|
110
129
|
enforcer=binding.enforcer,
|
|
@@ -128,6 +147,9 @@ class HexgateRunner:
|
|
|
128
147
|
self._setup_observability()
|
|
129
148
|
binding = self._binding_for(agent)
|
|
130
149
|
binding.refresh() # per-run policy pull; 304 when unchanged
|
|
150
|
+
ban_gate = self._ban_gate_for(agent)
|
|
151
|
+
if ban_gate is not None:
|
|
152
|
+
ban_gate.check(user)
|
|
131
153
|
wrapped_agent = wrap_openai_agent(
|
|
132
154
|
agent,
|
|
133
155
|
enforcer=binding.enforcer,
|
|
@@ -158,6 +180,10 @@ class HexgateRunner:
|
|
|
158
180
|
self._setup_observability()
|
|
159
181
|
binding = self._binding_for(agent)
|
|
160
182
|
binding.refresh() # must precede the wrap + setup
|
|
183
|
+
ban_gate = self._ban_gate_for(agent)
|
|
184
|
+
if ban_gate is not None:
|
|
185
|
+
# Before run_streamed spawns its task, so a banned run yields nothing.
|
|
186
|
+
ban_gate.check(user)
|
|
161
187
|
wrapped_agent = wrap_openai_agent(
|
|
162
188
|
agent,
|
|
163
189
|
enforcer=binding.enforcer,
|