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.
Files changed (138) hide show
  1. hexgate-0.2.9/PKG-INFO +168 -0
  2. hexgate-0.2.9/README.md +127 -0
  3. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/__init__.py +28 -16
  4. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/google/runner.py +22 -5
  5. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/google/tools.py +1 -1
  6. hexgate-0.2.9/hexgate/adapters/google/usage.py +44 -0
  7. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/google/wrapper.py +8 -2
  8. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/langchain/agent.py +32 -4
  9. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/langchain/tools.py +1 -1
  10. hexgate-0.2.9/hexgate/adapters/langchain/usage.py +84 -0
  11. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/langchain/wrapper.py +8 -1
  12. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/openai/runner.py +98 -5
  13. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/openai/tools.py +1 -1
  14. hexgate-0.2.9/hexgate/adapters/openai/usage.py +39 -0
  15. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/openai/wrapper.py +1 -1
  16. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/pydantic_ai/agent.py +42 -3
  17. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/pydantic_ai/tools.py +1 -1
  18. hexgate-0.2.9/hexgate/adapters/pydantic_ai/usage.py +38 -0
  19. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/pydantic_ai/wrapper.py +8 -2
  20. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/agents/__init__.py +6 -14
  21. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/agents/approvals.py +1 -1
  22. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/agents/factory.py +62 -10
  23. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/agents/loader.py +53 -111
  24. hexgate-0.2.9/hexgate/approvals.py +24 -0
  25. hexgate-0.2.9/hexgate/audit.py +168 -0
  26. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/bootstrap.py +2 -1
  27. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/_common.py +7 -41
  28. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/chat.py +3 -4
  29. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/policy/main.py +68 -18
  30. hexgate-0.2.9/hexgate/cli/register/__init__.py +11 -0
  31. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/register/register.py +2 -2
  32. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cloud/__init__.py +2 -2
  33. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cloud/client.py +18 -1
  34. hexgate-0.2.9/hexgate/manifest/__init__.py +23 -0
  35. hexgate-0.2.7/hexgate/cli/register/manifest.py → hexgate-0.2.9/hexgate/manifest/builder.py +6 -6
  36. {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.9/hexgate/manifest}/google.py +1 -1
  37. {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.9/hexgate/manifest}/langchain.py +1 -1
  38. hexgate-0.2.7/hexgate/cli/register/hexgate.py → hexgate-0.2.9/hexgate/manifest/native.py +1 -1
  39. {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.9/hexgate/manifest}/openai.py +1 -1
  40. {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.9/hexgate/manifest}/pydantic_ai.py +4 -4
  41. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/__init__.py +44 -1
  42. hexgate-0.2.9/hexgate/security/bans.py +330 -0
  43. hexgate-0.2.9/hexgate/security/builder.py +203 -0
  44. hexgate-0.2.9/hexgate/security/constraints.py +862 -0
  45. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/enforcer.py +2 -1
  46. hexgate-0.2.9/hexgate/security/errors.py +39 -0
  47. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/models.py +20 -2
  48. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/policy.py +15 -2
  49. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/policy_set.py +28 -1
  50. hexgate-0.2.9/hexgate/security/rego.py +710 -0
  51. hexgate-0.2.9/hexgate/security/testing.py +82 -0
  52. hexgate-0.2.9/hexgate/tools/__init__.py +42 -0
  53. hexgate-0.2.9/hexgate/tools/_relocated.py +22 -0
  54. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/decorators.py +10 -3
  55. hexgate-0.2.7/hexgate/audit.py → hexgate-0.2.9/hexgate/tracing/_senders.py +74 -166
  56. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tracing/langfuse.py +19 -25
  57. hexgate-0.2.9/hexgate/tracing/langfuse_core.py +44 -0
  58. hexgate-0.2.9/hexgate/tracing/usage.py +111 -0
  59. hexgate-0.2.9/hexgate.egg-info/PKG-INFO +168 -0
  60. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate.egg-info/SOURCES.txt +20 -15
  61. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate.egg-info/requires.txt +1 -1
  62. {hexgate-0.2.7 → hexgate-0.2.9}/pyproject.toml +2 -10
  63. {hexgate-0.2.7 → hexgate-0.2.9}/tests/test_bootstrap.py +9 -8
  64. hexgate-0.2.7/PKG-INFO +0 -1589
  65. hexgate-0.2.7/README.md +0 -1548
  66. hexgate-0.2.7/hexgate/agents/builtin/__init__.py +0 -1
  67. hexgate-0.2.7/hexgate/agents/builtin/researcher/agent.yaml +0 -7
  68. hexgate-0.2.7/hexgate/agents/builtin/researcher/policy.yaml +0 -10
  69. hexgate-0.2.7/hexgate/agents/builtin/researcher/system.md +0 -5
  70. hexgate-0.2.7/hexgate/agents/prompts/agent_system.md +0 -14
  71. hexgate-0.2.7/hexgate/cli/register/__init__.py +0 -8
  72. hexgate-0.2.7/hexgate/security/constraints.py +0 -252
  73. hexgate-0.2.7/hexgate/security/errors.py +0 -11
  74. hexgate-0.2.7/hexgate/security/rego.py +0 -334
  75. hexgate-0.2.7/hexgate/tools/__init__.py +0 -21
  76. hexgate-0.2.7/hexgate/tools/fetch.py +0 -72
  77. hexgate-0.2.7/hexgate/tools/refund.py +0 -53
  78. hexgate-0.2.7/hexgate/tools/websearch.py +0 -72
  79. hexgate-0.2.7/hexgate.egg-info/PKG-INFO +0 -1589
  80. {hexgate-0.2.7 → hexgate-0.2.9}/LICENSE +0 -0
  81. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/__init__.py +0 -0
  82. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/google/__init__.py +0 -0
  83. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/google/mcp.py +0 -0
  84. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/langchain/__init__.py +0 -0
  85. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/langchain/mcp.py +0 -0
  86. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/openai/__init__.py +0 -0
  87. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/openai/mcp.py +0 -0
  88. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/pydantic_ai/__init__.py +0 -0
  89. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/adapters/pydantic_ai/mcp.py +0 -0
  90. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/agents/models.py +0 -0
  91. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/__init__.py +0 -0
  92. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/policy/__init__.py +0 -0
  93. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/register/main.py +0 -0
  94. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/serve.py +0 -0
  95. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cli/state.py +0 -0
  96. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cloud/attenuate.py +0 -0
  97. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/cloud/biscuit.py +0 -0
  98. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/config/__init__.py +0 -0
  99. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/config/env.py +0 -0
  100. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/config/settings.py +0 -0
  101. {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.9/hexgate/manifest}/models.py +0 -0
  102. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/mcp/__init__.py +0 -0
  103. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/mcp/client.py +0 -0
  104. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/mcp/config.py +0 -0
  105. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/mcp/proxy.py +0 -0
  106. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/runtime/__init__.py +0 -0
  107. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/runtime/command_policy.py +0 -0
  108. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/runtime/context.py +0 -0
  109. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/runtime/sandbox_runtime.py +0 -0
  110. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/runtime/srt.py +0 -0
  111. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/runtime/workspace.py +0 -0
  112. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/binding.py +0 -0
  113. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/bundle.py +0 -0
  114. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/decision.py +0 -0
  115. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/file_scope.py +0 -0
  116. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/rego_wasm.py +0 -0
  117. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/signing.py +0 -0
  118. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/source.py +0 -0
  119. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/security/wasm_engine.py +0 -0
  120. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/streaming/__init__.py +0 -0
  121. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/streaming/events.py +0 -0
  122. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/streaming/normalize.py +0 -0
  123. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/bash.py +0 -0
  124. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/__init__.py +0 -0
  125. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/_common.py +0 -0
  126. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/edit_file.py +0 -0
  127. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/glob.py +0 -0
  128. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/grep.py +0 -0
  129. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/read_file.py +0 -0
  130. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tools/files/write_file.py +0 -0
  131. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/tracing/__init__.py +0 -0
  132. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/utils/__init__.py +0 -0
  133. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate/utils/retry.py +0 -0
  134. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate.egg-info/dependency_links.txt +0 -0
  135. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate.egg-info/entry_points.txt +0 -0
  136. {hexgate-0.2.7 → hexgate-0.2.9}/hexgate.egg-info/top_level.txt +0 -0
  137. {hexgate-0.2.7 → hexgate-0.2.9}/setup.cfg +0 -0
  138. {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
+ [![PyPI](https://img.shields.io/pypi/v/hexgate?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/hexgate/)
53
+ [![CI](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml)
54
+ [![codecov](https://codecov.io/gh/HexamindOrganisation/hexgate/branch/main/graph/badge.svg?flag=sdk)](https://codecov.io/gh/HexamindOrganisation/hexgate)
55
+ [![Downloads](https://img.shields.io/pypi/dm/hexgate?color=blueviolet)](https://pypi.org/project/hexgate/)
56
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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).
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/hexgate?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/hexgate/)
12
+ [![CI](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml)
13
+ [![codecov](https://codecov.io/gh/HexamindOrganisation/hexgate/branch/main/graph/badge.svg?flag=sdk)](https://codecov.io/gh/HexamindOrganisation/hexgate)
14
+ [![Downloads](https://img.shields.io/pypi/dm/hexgate?color=blueviolet)](https://pypi.org/project/hexgate/)
15
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- register_agent,
22
- unregister_agent,
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 AgentPolicy
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
- "register_agent",
76
+ "register_agent_factory",
71
77
  "read_file",
72
- "refund_order",
73
78
  "stream_agent",
74
79
  "stream_agent_raw",
75
- "unregister_agent",
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.agents.factory import ApprovalHandler
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, api_key=self.api_key, approval_handler=approval_handler
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
- agent=self._wrapped_agent,
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.agents.factory import ApprovalHandler
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.agents.factory import ApprovalHandler
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 handler to ``config['callbacks']``."""
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
- if self._callback_handler not in callbacks:
67
- callbacks.append(self._callback_handler)
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.agents.factory import ApprovalHandler
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