hexgate 0.2.7__tar.gz → 0.2.9__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.9/PKG-INFO +168 -0
- hexgate-0.2.9/README.md +127 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/__init__.py +28 -16
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/google/runner.py +22 -5
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/google/tools.py +1 -1
- hexgate-0.2.9/hexgate/adapters/google/usage.py +44 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/google/wrapper.py +8 -2
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/langchain/agent.py +32 -4
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/langchain/tools.py +1 -1
- hexgate-0.2.9/hexgate/adapters/langchain/usage.py +84 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/langchain/wrapper.py +8 -1
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/openai/runner.py +98 -5
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/openai/tools.py +1 -1
- hexgate-0.2.9/hexgate/adapters/openai/usage.py +39 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/openai/wrapper.py +1 -1
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/pydantic_ai/agent.py +42 -3
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/pydantic_ai/tools.py +1 -1
- hexgate-0.2.9/hexgate/adapters/pydantic_ai/usage.py +38 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/pydantic_ai/wrapper.py +8 -2
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/agents/__init__.py +6 -14
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/agents/approvals.py +1 -1
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/agents/factory.py +62 -10
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/agents/loader.py +53 -111
- hexgate-0.2.9/hexgate/approvals.py +24 -0
- hexgate-0.2.9/hexgate/audit.py +168 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/bootstrap.py +2 -1
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/_common.py +7 -41
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/chat.py +3 -4
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/policy/main.py +68 -18
- hexgate-0.2.9/hexgate/cli/register/__init__.py +11 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/register/register.py +2 -2
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cloud/__init__.py +2 -2
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cloud/client.py +18 -1
- hexgate-0.2.9/hexgate/manifest/__init__.py +23 -0
- hexgate-0.2.7/hexgate/cli/register/manifest.py → hexgate-0.2.9/hexgate/manifest/builder.py +6 -6
- {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.9/hexgate/manifest}/google.py +1 -1
- {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.9/hexgate/manifest}/langchain.py +1 -1
- hexgate-0.2.7/hexgate/cli/register/hexgate.py → hexgate-0.2.9/hexgate/manifest/native.py +1 -1
- {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.9/hexgate/manifest}/openai.py +1 -1
- {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.9/hexgate/manifest}/pydantic_ai.py +4 -4
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/__init__.py +44 -1
- hexgate-0.2.9/hexgate/security/bans.py +330 -0
- hexgate-0.2.9/hexgate/security/builder.py +203 -0
- hexgate-0.2.9/hexgate/security/constraints.py +862 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/enforcer.py +2 -1
- hexgate-0.2.9/hexgate/security/errors.py +39 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/models.py +20 -2
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/policy.py +15 -2
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/policy_set.py +28 -1
- hexgate-0.2.9/hexgate/security/rego.py +710 -0
- hexgate-0.2.9/hexgate/security/testing.py +82 -0
- hexgate-0.2.9/hexgate/tools/__init__.py +42 -0
- hexgate-0.2.9/hexgate/tools/_relocated.py +22 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/decorators.py +10 -3
- hexgate-0.2.7/hexgate/audit.py → hexgate-0.2.9/hexgate/tracing/_senders.py +74 -166
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tracing/langfuse.py +19 -25
- hexgate-0.2.9/hexgate/tracing/langfuse_core.py +44 -0
- hexgate-0.2.9/hexgate/tracing/usage.py +111 -0
- hexgate-0.2.9/hexgate.egg-info/PKG-INFO +168 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate.egg-info/SOURCES.txt +20 -15
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate.egg-info/requires.txt +1 -1
- {hexgate-0.2.7 → hexgate-0.2.9}/pyproject.toml +2 -10
- {hexgate-0.2.7 → hexgate-0.2.9}/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/__init__.py +0 -21
- 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.9}/LICENSE +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/google/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/google/mcp.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/langchain/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/langchain/mcp.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/openai/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/openai/mcp.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/pydantic_ai/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/pydantic_ai/mcp.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/agents/models.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/policy/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/register/main.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/serve.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/state.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cloud/attenuate.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cloud/biscuit.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/config/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/config/env.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/config/settings.py +0 -0
- {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.9/hexgate/manifest}/models.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/mcp/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/mcp/client.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/mcp/config.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/mcp/proxy.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/runtime/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/runtime/command_policy.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/runtime/context.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/runtime/sandbox_runtime.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/runtime/srt.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/runtime/workspace.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/binding.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/bundle.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/decision.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/file_scope.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/rego_wasm.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/signing.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/source.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/wasm_engine.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/streaming/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/streaming/events.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/streaming/normalize.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/bash.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/_common.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/edit_file.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/glob.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/grep.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/read_file.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/write_file.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tracing/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/utils/__init__.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/utils/retry.py +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate.egg-info/dependency_links.txt +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate.egg-info/entry_points.txt +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/hexgate.egg-info/top_level.txt +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/setup.cfg +0 -0
- {hexgate-0.2.7 → hexgate-0.2.9}/tests/test_demo.py +0 -0
hexgate-0.2.9/PKG-INFO
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hexgate
|
|
3
|
+
Version: 0.2.9
|
|
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.14
|
|
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.9/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}")
|
|
@@ -9,16 +9,20 @@ from typing import Any, AsyncGenerator, Generator
|
|
|
9
9
|
|
|
10
10
|
import nest_asyncio
|
|
11
11
|
from google.adk.agents import BaseAgent
|
|
12
|
+
from google.adk.apps import App
|
|
12
13
|
from google.adk.runners import Runner
|
|
13
14
|
from google.adk.sessions import BaseSessionService
|
|
14
15
|
from google.genai import types
|
|
15
16
|
from langfuse import get_client, propagate_attributes
|
|
16
17
|
from openinference.instrumentation.google_adk import GoogleADKInstrumentor
|
|
17
18
|
|
|
19
|
+
from hexgate.adapters.google.usage import HexgateUsagePlugin
|
|
18
20
|
from hexgate.adapters.google.wrapper import wrap_google_agent
|
|
19
|
-
from hexgate.
|
|
21
|
+
from hexgate.approvals import ApprovalHandler
|
|
22
|
+
from hexgate.cloud.client import HexgateClient, HexgateConfig
|
|
20
23
|
from hexgate.config.env import resolve_api_key
|
|
21
24
|
from hexgate.runtime import User
|
|
25
|
+
from hexgate.security.bans import resolve_ban_gate
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
class HexgateRunner:
|
|
@@ -41,17 +45,26 @@ class HexgateRunner:
|
|
|
41
45
|
)
|
|
42
46
|
# Policy resolves at construction (the loud-failure point); the
|
|
43
47
|
# Runner is built once — refresh swaps the enforcer's policy
|
|
44
|
-
# without touching it.
|
|
48
|
+
# without touching it. One client is shared with the ban resolver.
|
|
49
|
+
client = HexgateClient(HexgateConfig.from_env(api_key=self.api_key))
|
|
45
50
|
self._wrapped_agent, self._binding = wrap_google_agent(
|
|
46
|
-
agent,
|
|
51
|
+
agent,
|
|
52
|
+
api_key=self.api_key,
|
|
53
|
+
approval_handler=approval_handler,
|
|
54
|
+
client=client,
|
|
47
55
|
)
|
|
56
|
+
plugins = list(runner_kwargs.pop("plugins", None) or [])
|
|
57
|
+
plugins.append(HexgateUsagePlugin(api_key=self.api_key))
|
|
58
|
+
app = App(name=app_name, root_agent=self._wrapped_agent, plugins=plugins)
|
|
48
59
|
self._runner = Runner(
|
|
49
|
-
|
|
50
|
-
app_name=app_name,
|
|
60
|
+
app=app,
|
|
51
61
|
session_service=session_service,
|
|
52
62
|
**runner_kwargs,
|
|
53
63
|
)
|
|
54
64
|
self._agent_name = getattr(agent, "name", "default")
|
|
65
|
+
self._ban_gate = resolve_ban_gate(
|
|
66
|
+
self._agent_name, api_key=self.api_key, client=client
|
|
67
|
+
)
|
|
55
68
|
|
|
56
69
|
def _setup_observability(self):
|
|
57
70
|
"""Install Langfuse + GoogleADKInstrumentor (idempotent)."""
|
|
@@ -90,6 +103,8 @@ class HexgateRunner:
|
|
|
90
103
|
"""
|
|
91
104
|
self._setup_observability()
|
|
92
105
|
self._binding.refresh() # per-run policy pull; 304 when unchanged
|
|
106
|
+
if self._ban_gate is not None:
|
|
107
|
+
self._ban_gate.check(user)
|
|
93
108
|
with user.sync_scope(), self._propagate(user):
|
|
94
109
|
agen = self._runner.run_async(
|
|
95
110
|
user_id=user.user_id,
|
|
@@ -118,6 +133,8 @@ class HexgateRunner:
|
|
|
118
133
|
"""Run the Google ADK agent asynchronously, yielding events."""
|
|
119
134
|
self._setup_observability()
|
|
120
135
|
await self._binding.refresh_async() # per-run policy pull; 304 when unchanged
|
|
136
|
+
if self._ban_gate is not None:
|
|
137
|
+
await self._ban_gate.check_async(user)
|
|
121
138
|
async with user:
|
|
122
139
|
with self._propagate(user):
|
|
123
140
|
async for event in self._runner.run_async(
|
|
@@ -20,7 +20,7 @@ from google.adk.tools.function_tool import FunctionTool
|
|
|
20
20
|
from google.adk.tools.tool_context import ToolContext
|
|
21
21
|
|
|
22
22
|
from hexgate.agents.approvals import resolve_approval_async
|
|
23
|
-
from hexgate.
|
|
23
|
+
from hexgate.approvals import ApprovalHandler
|
|
24
24
|
from hexgate.security.decision import DecisionOutcome
|
|
25
25
|
from hexgate.security.enforcer import PolicyEnforcer
|
|
26
26
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Google ADK per-call token usage capture via a ``BasePlugin``.
|
|
2
|
+
|
|
3
|
+
``after_model_callback`` fires once per underlying model call, so a single
|
|
4
|
+
run with several turns (tool-calling loops, sub-agent handoffs) can emit
|
|
5
|
+
more than one usage event. ``callback_context.agent_name`` is read per-call
|
|
6
|
+
rather than fixed at construction, since one ``Runner`` can drive several
|
|
7
|
+
named sub-agents.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from google.adk.agents.callback_context import CallbackContext
|
|
13
|
+
from google.adk.models.llm_response import LlmResponse
|
|
14
|
+
from google.adk.plugins.base_plugin import BasePlugin
|
|
15
|
+
|
|
16
|
+
from hexgate.tracing.usage import emit_llm_usage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HexgateUsagePlugin(BasePlugin):
|
|
20
|
+
"""Emits one :class:`~hexgate.tracing.usage.LlmUsageEvent` per
|
|
21
|
+
``after_model_callback`` callback. Never rewrites the response — always
|
|
22
|
+
returns ``None`` so the real model output reaches the agent unchanged."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, *, api_key: str) -> None:
|
|
25
|
+
super().__init__(name="hexgate_usage")
|
|
26
|
+
self._api_key = api_key
|
|
27
|
+
|
|
28
|
+
async def after_model_callback(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
callback_context: CallbackContext,
|
|
32
|
+
llm_response: LlmResponse,
|
|
33
|
+
) -> LlmResponse | None:
|
|
34
|
+
usage = llm_response.usage_metadata
|
|
35
|
+
if usage is None:
|
|
36
|
+
return None
|
|
37
|
+
emit_llm_usage(
|
|
38
|
+
callback_context.agent_name,
|
|
39
|
+
llm_response.model_version or "",
|
|
40
|
+
usage.prompt_token_count or 0,
|
|
41
|
+
usage.candidates_token_count or 0,
|
|
42
|
+
api_key=self._api_key,
|
|
43
|
+
)
|
|
44
|
+
return None
|
|
@@ -10,19 +10,25 @@ 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
|
|
16
|
-
from hexgate.
|
|
18
|
+
from hexgate.approvals 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 (
|
|
@@ -9,9 +9,11 @@ from langfuse import get_client, propagate_attributes
|
|
|
9
9
|
from langfuse.langchain import CallbackHandler
|
|
10
10
|
from langgraph.graph.state import CompiledStateGraph
|
|
11
11
|
|
|
12
|
+
from hexgate.adapters.langchain.usage import HexgateUsageCallbackHandler
|
|
12
13
|
from hexgate.runtime import User
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
16
|
+
from hexgate.security.bans import BanGate
|
|
15
17
|
from hexgate.security.binding import PolicyBinding
|
|
16
18
|
|
|
17
19
|
|
|
@@ -32,14 +34,20 @@ class HexgateLangchainAgent:
|
|
|
32
34
|
agent: CompiledStateGraph,
|
|
33
35
|
api_key: str,
|
|
34
36
|
tool_names: list[str],
|
|
37
|
+
agent_name: str = "default",
|
|
35
38
|
binding: PolicyBinding | None = None,
|
|
39
|
+
ban_gate: BanGate | None = None,
|
|
36
40
|
) -> None:
|
|
37
41
|
self._agent = agent
|
|
38
42
|
self._binding = binding
|
|
43
|
+
self._ban_gate = ban_gate
|
|
39
44
|
self._api_key = api_key
|
|
40
45
|
self._tool_names = tool_names
|
|
41
46
|
self._langfuse = get_client()
|
|
42
47
|
self._callback_handler = CallbackHandler()
|
|
48
|
+
self._usage_handler = HexgateUsageCallbackHandler(
|
|
49
|
+
agent_name=agent_name, api_key=api_key
|
|
50
|
+
)
|
|
43
51
|
|
|
44
52
|
async def _refresh_async(self) -> None:
|
|
45
53
|
"""Refresh the policy binding, if attached (async entry points)."""
|
|
@@ -51,6 +59,15 @@ class HexgateLangchainAgent:
|
|
|
51
59
|
if self._binding is not None:
|
|
52
60
|
self._binding.refresh()
|
|
53
61
|
|
|
62
|
+
async def _check_ban_async(self, user: User) -> None:
|
|
63
|
+
"""Refuse a banned agent/user before running, if a gate is attached."""
|
|
64
|
+
if self._ban_gate is not None:
|
|
65
|
+
await self._ban_gate.check_async(user)
|
|
66
|
+
|
|
67
|
+
def _check_ban(self, user: User) -> None:
|
|
68
|
+
if self._ban_gate is not None:
|
|
69
|
+
self._ban_gate.check(user)
|
|
70
|
+
|
|
54
71
|
def _propagate_kwargs(self, user: User, method: str) -> dict[str, Any]:
|
|
55
72
|
return {
|
|
56
73
|
"tags": [f"langchain.agent.{method}"],
|
|
@@ -60,11 +77,12 @@ class HexgateLangchainAgent:
|
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
def _with_callbacks(self, config: RunnableConfig | None) -> RunnableConfig:
|
|
63
|
-
"""Append the Hexgate callback
|
|
80
|
+
"""Append the Hexgate callback handlers to ``config['callbacks']``."""
|
|
64
81
|
merged: RunnableConfig = dict(config) if config else {}
|
|
65
82
|
callbacks = list(merged.get("callbacks") or [])
|
|
66
|
-
|
|
67
|
-
callbacks
|
|
83
|
+
for handler in (self._callback_handler, self._usage_handler):
|
|
84
|
+
if handler not in callbacks:
|
|
85
|
+
callbacks.append(handler)
|
|
68
86
|
merged["callbacks"] = callbacks
|
|
69
87
|
return merged
|
|
70
88
|
|
|
@@ -78,6 +96,7 @@ class HexgateLangchainAgent:
|
|
|
78
96
|
) -> dict[str, Any]:
|
|
79
97
|
"""Invoke the agent asynchronously inside a User scope."""
|
|
80
98
|
await self._refresh_async()
|
|
99
|
+
await self._check_ban_async(user)
|
|
81
100
|
async with user:
|
|
82
101
|
with propagate_attributes(**self._propagate_kwargs(user, "ainvoke")):
|
|
83
102
|
return await self._agent.ainvoke(
|
|
@@ -94,6 +113,7 @@ class HexgateLangchainAgent:
|
|
|
94
113
|
) -> dict[str, Any]:
|
|
95
114
|
"""Invoke the agent synchronously inside a User scope."""
|
|
96
115
|
self._refresh()
|
|
116
|
+
self._check_ban(user)
|
|
97
117
|
with user.sync_scope():
|
|
98
118
|
with propagate_attributes(**self._propagate_kwargs(user, "invoke")):
|
|
99
119
|
return self._agent.invoke(input, self._with_callbacks(config), **kwargs)
|
|
@@ -108,6 +128,7 @@ class HexgateLangchainAgent:
|
|
|
108
128
|
) -> AsyncIterator[dict[str, Any]]:
|
|
109
129
|
"""Stream the agent asynchronously inside a User scope."""
|
|
110
130
|
await self._refresh_async()
|
|
131
|
+
await self._check_ban_async(user)
|
|
111
132
|
async with user:
|
|
112
133
|
with propagate_attributes(**self._propagate_kwargs(user, "astream")):
|
|
113
134
|
async for chunk in self._agent.astream(
|
|
@@ -125,6 +146,7 @@ class HexgateLangchainAgent:
|
|
|
125
146
|
) -> Iterator[dict[str, Any]]:
|
|
126
147
|
"""Stream the agent synchronously inside a User scope."""
|
|
127
148
|
self._refresh()
|
|
149
|
+
self._check_ban(user)
|
|
128
150
|
with user.sync_scope():
|
|
129
151
|
with propagate_attributes(**self._propagate_kwargs(user, "stream")):
|
|
130
152
|
yield from self._agent.stream(
|
|
@@ -142,6 +164,7 @@ class HexgateLangchainAgent:
|
|
|
142
164
|
) -> AsyncIterator[dict[str, Any]]:
|
|
143
165
|
"""Stream the agent events asynchronously inside a User scope."""
|
|
144
166
|
await self._refresh_async()
|
|
167
|
+
await self._check_ban_async(user)
|
|
145
168
|
async with user:
|
|
146
169
|
with propagate_attributes(**self._propagate_kwargs(user, "astream_events")):
|
|
147
170
|
async for event in self._agent.astream_events(
|
|
@@ -153,5 +176,10 @@ class HexgateLangchainAgent:
|
|
|
153
176
|
yield event
|
|
154
177
|
|
|
155
178
|
def __getattr__(self, name: str) -> Any:
|
|
156
|
-
"""Delegate unknown attributes to the wrapped agent.
|
|
179
|
+
"""Delegate unknown attributes to the wrapped agent.
|
|
180
|
+
|
|
181
|
+
Only the wrapped run methods (invoke/ainvoke/stream/astream/
|
|
182
|
+
astream_events) enforce the ban gate + User scope; methods reached
|
|
183
|
+
here (batch, abatch, astream_log, …) bypass them.
|
|
184
|
+
"""
|
|
157
185
|
return getattr(self._agent, name)
|
|
@@ -26,7 +26,7 @@ from hexgate.agents.approvals import (
|
|
|
26
26
|
from hexgate.agents.approvals import (
|
|
27
27
|
resolve_approval_sync as _resolve_approval_sync,
|
|
28
28
|
)
|
|
29
|
-
from hexgate.
|
|
29
|
+
from hexgate.approvals import ApprovalHandler
|
|
30
30
|
from hexgate.security.decision import DecisionOutcome
|
|
31
31
|
from hexgate.security.enforcer import PolicyEnforcer
|
|
32
32
|
from hexgate.tools.decorators import TOOL_METADATA_ATTR
|