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.
Files changed (131) hide show
  1. hexgate-0.2.8/PKG-INFO +168 -0
  2. hexgate-0.2.8/README.md +127 -0
  3. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/__init__.py +28 -16
  4. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/google/runner.py +15 -2
  5. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/google/wrapper.py +7 -1
  6. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/langchain/agent.py +23 -1
  7. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/langchain/wrapper.py +7 -1
  8. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/openai/runner.py +27 -1
  9. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/agent.py +22 -1
  10. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/wrapper.py +7 -1
  11. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/agents/__init__.py +6 -14
  12. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/agents/factory.py +36 -6
  13. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/agents/loader.py +53 -111
  14. hexgate-0.2.8/hexgate/audit.py +168 -0
  15. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/bootstrap.py +2 -1
  16. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/_common.py +7 -41
  17. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/chat.py +2 -3
  18. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/policy/main.py +68 -18
  19. hexgate-0.2.8/hexgate/cli/register/__init__.py +11 -0
  20. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/register/register.py +2 -2
  21. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cloud/__init__.py +2 -2
  22. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cloud/client.py +18 -1
  23. hexgate-0.2.8/hexgate/manifest/__init__.py +23 -0
  24. hexgate-0.2.7/hexgate/cli/register/manifest.py → hexgate-0.2.8/hexgate/manifest/builder.py +6 -6
  25. {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/google.py +1 -1
  26. {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/langchain.py +1 -1
  27. hexgate-0.2.7/hexgate/cli/register/hexgate.py → hexgate-0.2.8/hexgate/manifest/native.py +1 -1
  28. {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/openai.py +1 -1
  29. {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/pydantic_ai.py +2 -2
  30. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/__init__.py +44 -1
  31. hexgate-0.2.8/hexgate/security/bans.py +330 -0
  32. hexgate-0.2.8/hexgate/security/builder.py +203 -0
  33. hexgate-0.2.8/hexgate/security/constraints.py +862 -0
  34. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/enforcer.py +2 -1
  35. hexgate-0.2.8/hexgate/security/errors.py +39 -0
  36. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/models.py +20 -2
  37. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/policy.py +15 -2
  38. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/policy_set.py +28 -1
  39. hexgate-0.2.8/hexgate/security/rego.py +710 -0
  40. hexgate-0.2.8/hexgate/security/testing.py +82 -0
  41. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/__init__.py +8 -6
  42. hexgate-0.2.8/hexgate/tools/_relocated.py +22 -0
  43. hexgate-0.2.7/hexgate/audit.py → hexgate-0.2.8/hexgate/tracing/_senders.py +74 -166
  44. hexgate-0.2.8/hexgate/tracing/usage.py +70 -0
  45. hexgate-0.2.8/hexgate.egg-info/PKG-INFO +168 -0
  46. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate.egg-info/SOURCES.txt +14 -15
  47. {hexgate-0.2.7 → hexgate-0.2.8}/pyproject.toml +1 -9
  48. {hexgate-0.2.7 → hexgate-0.2.8}/tests/test_bootstrap.py +9 -8
  49. hexgate-0.2.7/PKG-INFO +0 -1589
  50. hexgate-0.2.7/README.md +0 -1548
  51. hexgate-0.2.7/hexgate/agents/builtin/__init__.py +0 -1
  52. hexgate-0.2.7/hexgate/agents/builtin/researcher/agent.yaml +0 -7
  53. hexgate-0.2.7/hexgate/agents/builtin/researcher/policy.yaml +0 -10
  54. hexgate-0.2.7/hexgate/agents/builtin/researcher/system.md +0 -5
  55. hexgate-0.2.7/hexgate/agents/prompts/agent_system.md +0 -14
  56. hexgate-0.2.7/hexgate/cli/register/__init__.py +0 -8
  57. hexgate-0.2.7/hexgate/security/constraints.py +0 -252
  58. hexgate-0.2.7/hexgate/security/errors.py +0 -11
  59. hexgate-0.2.7/hexgate/security/rego.py +0 -334
  60. hexgate-0.2.7/hexgate/tools/fetch.py +0 -72
  61. hexgate-0.2.7/hexgate/tools/refund.py +0 -53
  62. hexgate-0.2.7/hexgate/tools/websearch.py +0 -72
  63. hexgate-0.2.7/hexgate.egg-info/PKG-INFO +0 -1589
  64. {hexgate-0.2.7 → hexgate-0.2.8}/LICENSE +0 -0
  65. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/__init__.py +0 -0
  66. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/google/__init__.py +0 -0
  67. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/google/mcp.py +0 -0
  68. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/google/tools.py +0 -0
  69. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/langchain/__init__.py +0 -0
  70. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/langchain/mcp.py +0 -0
  71. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/langchain/tools.py +0 -0
  72. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/openai/__init__.py +0 -0
  73. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/openai/mcp.py +0 -0
  74. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/openai/tools.py +0 -0
  75. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/openai/wrapper.py +0 -0
  76. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/__init__.py +0 -0
  77. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/mcp.py +0 -0
  78. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/tools.py +0 -0
  79. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/agents/approvals.py +0 -0
  80. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/agents/models.py +0 -0
  81. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/__init__.py +0 -0
  82. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/policy/__init__.py +0 -0
  83. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/register/main.py +0 -0
  84. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/serve.py +0 -0
  85. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cli/state.py +0 -0
  86. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cloud/attenuate.py +0 -0
  87. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/cloud/biscuit.py +0 -0
  88. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/config/__init__.py +0 -0
  89. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/config/env.py +0 -0
  90. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/config/settings.py +0 -0
  91. {hexgate-0.2.7/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/models.py +0 -0
  92. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/mcp/__init__.py +0 -0
  93. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/mcp/client.py +0 -0
  94. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/mcp/config.py +0 -0
  95. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/mcp/proxy.py +0 -0
  96. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/runtime/__init__.py +0 -0
  97. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/runtime/command_policy.py +0 -0
  98. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/runtime/context.py +0 -0
  99. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/runtime/sandbox_runtime.py +0 -0
  100. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/runtime/srt.py +0 -0
  101. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/runtime/workspace.py +0 -0
  102. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/binding.py +0 -0
  103. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/bundle.py +0 -0
  104. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/decision.py +0 -0
  105. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/file_scope.py +0 -0
  106. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/rego_wasm.py +0 -0
  107. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/signing.py +0 -0
  108. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/source.py +0 -0
  109. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/security/wasm_engine.py +0 -0
  110. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/streaming/__init__.py +0 -0
  111. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/streaming/events.py +0 -0
  112. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/streaming/normalize.py +0 -0
  113. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/bash.py +0 -0
  114. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/decorators.py +0 -0
  115. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/__init__.py +0 -0
  116. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/_common.py +0 -0
  117. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/edit_file.py +0 -0
  118. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/glob.py +0 -0
  119. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/grep.py +0 -0
  120. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/read_file.py +0 -0
  121. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tools/files/write_file.py +0 -0
  122. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tracing/__init__.py +0 -0
  123. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/tracing/langfuse.py +0 -0
  124. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/utils/__init__.py +0 -0
  125. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate/utils/retry.py +0 -0
  126. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate.egg-info/dependency_links.txt +0 -0
  127. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate.egg-info/entry_points.txt +0 -0
  128. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate.egg-info/requires.txt +0 -0
  129. {hexgate-0.2.7 → hexgate-0.2.8}/hexgate.egg-info/top_level.txt +0 -0
  130. {hexgate-0.2.7 → hexgate-0.2.8}/setup.cfg +0 -0
  131. {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
+ [![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}")
@@ -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, api_key=self.api_key, approval_handler=approval_handler
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
- resolved = resolve_policy(agent_name, api_key=resolved_key)
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,