hexgate 0.2.6__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. hexgate-0.2.8/PKG-INFO +168 -0
  2. hexgate-0.2.8/README.md +127 -0
  3. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/__init__.py +28 -16
  4. hexgate-0.2.8/hexgate/adapters/google/mcp.py +95 -0
  5. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/google/runner.py +17 -2
  6. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/google/tools.py +27 -3
  7. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/google/wrapper.py +20 -7
  8. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/langchain/agent.py +23 -1
  9. hexgate-0.2.8/hexgate/adapters/langchain/mcp.py +47 -0
  10. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/langchain/tools.py +7 -25
  11. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/langchain/wrapper.py +7 -1
  12. hexgate-0.2.8/hexgate/adapters/openai/mcp.py +97 -0
  13. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/openai/runner.py +50 -5
  14. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/openai/tools.py +28 -3
  15. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/openai/wrapper.py +12 -3
  16. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/agent.py +22 -1
  17. hexgate-0.2.8/hexgate/adapters/pydantic_ai/mcp.py +60 -0
  18. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/tools.py +27 -3
  19. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/wrapper.py +18 -6
  20. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/agents/__init__.py +6 -14
  21. hexgate-0.2.8/hexgate/agents/approvals.py +53 -0
  22. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/agents/factory.py +36 -6
  23. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/agents/loader.py +53 -111
  24. hexgate-0.2.8/hexgate/audit.py +168 -0
  25. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/bootstrap.py +2 -1
  26. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/_common.py +7 -41
  27. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/chat.py +2 -3
  28. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/policy/main.py +68 -18
  29. hexgate-0.2.8/hexgate/cli/register/__init__.py +11 -0
  30. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/register/register.py +2 -2
  31. hexgate-0.2.8/hexgate/cli/serve.py +692 -0
  32. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cloud/__init__.py +2 -2
  33. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cloud/client.py +18 -1
  34. hexgate-0.2.8/hexgate/manifest/__init__.py +23 -0
  35. hexgate-0.2.6/hexgate/cli/register/manifest.py → hexgate-0.2.8/hexgate/manifest/builder.py +6 -6
  36. {hexgate-0.2.6/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/google.py +64 -11
  37. {hexgate-0.2.6/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/langchain.py +1 -1
  38. hexgate-0.2.6/hexgate/cli/register/hexgate.py → hexgate-0.2.8/hexgate/manifest/native.py +1 -1
  39. {hexgate-0.2.6/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/openai.py +1 -1
  40. {hexgate-0.2.6/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/pydantic_ai.py +2 -2
  41. hexgate-0.2.8/hexgate/mcp/__init__.py +61 -0
  42. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/mcp/proxy.py +162 -52
  43. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/__init__.py +44 -1
  44. hexgate-0.2.8/hexgate/security/bans.py +330 -0
  45. hexgate-0.2.8/hexgate/security/builder.py +203 -0
  46. hexgate-0.2.8/hexgate/security/constraints.py +862 -0
  47. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/enforcer.py +2 -1
  48. hexgate-0.2.8/hexgate/security/errors.py +39 -0
  49. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/models.py +20 -2
  50. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/policy.py +15 -2
  51. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/policy_set.py +28 -1
  52. hexgate-0.2.8/hexgate/security/rego.py +710 -0
  53. hexgate-0.2.8/hexgate/security/testing.py +82 -0
  54. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/__init__.py +8 -6
  55. hexgate-0.2.8/hexgate/tools/_relocated.py +22 -0
  56. hexgate-0.2.6/hexgate/audit.py → hexgate-0.2.8/hexgate/tracing/_senders.py +74 -166
  57. hexgate-0.2.8/hexgate/tracing/usage.py +70 -0
  58. hexgate-0.2.8/hexgate.egg-info/PKG-INFO +168 -0
  59. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate.egg-info/SOURCES.txt +19 -15
  60. {hexgate-0.2.6 → hexgate-0.2.8}/pyproject.toml +1 -9
  61. {hexgate-0.2.6 → hexgate-0.2.8}/tests/test_bootstrap.py +9 -8
  62. hexgate-0.2.6/PKG-INFO +0 -1589
  63. hexgate-0.2.6/README.md +0 -1548
  64. hexgate-0.2.6/hexgate/agents/builtin/__init__.py +0 -1
  65. hexgate-0.2.6/hexgate/agents/builtin/researcher/agent.yaml +0 -7
  66. hexgate-0.2.6/hexgate/agents/builtin/researcher/policy.yaml +0 -10
  67. hexgate-0.2.6/hexgate/agents/builtin/researcher/system.md +0 -5
  68. hexgate-0.2.6/hexgate/agents/prompts/agent_system.md +0 -14
  69. hexgate-0.2.6/hexgate/cli/register/__init__.py +0 -8
  70. hexgate-0.2.6/hexgate/cli/serve.py +0 -337
  71. hexgate-0.2.6/hexgate/mcp/__init__.py +0 -47
  72. hexgate-0.2.6/hexgate/security/constraints.py +0 -252
  73. hexgate-0.2.6/hexgate/security/errors.py +0 -11
  74. hexgate-0.2.6/hexgate/security/rego.py +0 -334
  75. hexgate-0.2.6/hexgate/tools/fetch.py +0 -72
  76. hexgate-0.2.6/hexgate/tools/refund.py +0 -53
  77. hexgate-0.2.6/hexgate/tools/websearch.py +0 -72
  78. hexgate-0.2.6/hexgate.egg-info/PKG-INFO +0 -1589
  79. {hexgate-0.2.6 → hexgate-0.2.8}/LICENSE +0 -0
  80. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/__init__.py +0 -0
  81. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/google/__init__.py +0 -0
  82. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/langchain/__init__.py +0 -0
  83. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/openai/__init__.py +0 -0
  84. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/adapters/pydantic_ai/__init__.py +0 -0
  85. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/agents/models.py +0 -0
  86. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/__init__.py +0 -0
  87. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/policy/__init__.py +0 -0
  88. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/register/main.py +0 -0
  89. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cli/state.py +0 -0
  90. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cloud/attenuate.py +0 -0
  91. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/cloud/biscuit.py +0 -0
  92. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/config/__init__.py +0 -0
  93. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/config/env.py +0 -0
  94. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/config/settings.py +0 -0
  95. {hexgate-0.2.6/hexgate/cli/register → hexgate-0.2.8/hexgate/manifest}/models.py +0 -0
  96. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/mcp/client.py +0 -0
  97. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/mcp/config.py +0 -0
  98. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/runtime/__init__.py +0 -0
  99. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/runtime/command_policy.py +0 -0
  100. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/runtime/context.py +0 -0
  101. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/runtime/sandbox_runtime.py +0 -0
  102. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/runtime/srt.py +0 -0
  103. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/runtime/workspace.py +0 -0
  104. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/binding.py +0 -0
  105. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/bundle.py +0 -0
  106. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/decision.py +0 -0
  107. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/file_scope.py +0 -0
  108. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/rego_wasm.py +0 -0
  109. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/signing.py +0 -0
  110. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/source.py +0 -0
  111. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/security/wasm_engine.py +0 -0
  112. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/streaming/__init__.py +0 -0
  113. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/streaming/events.py +0 -0
  114. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/streaming/normalize.py +0 -0
  115. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/bash.py +0 -0
  116. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/decorators.py +0 -0
  117. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/__init__.py +0 -0
  118. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/_common.py +0 -0
  119. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/edit_file.py +0 -0
  120. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/glob.py +0 -0
  121. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/grep.py +0 -0
  122. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/read_file.py +0 -0
  123. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tools/files/write_file.py +0 -0
  124. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tracing/__init__.py +0 -0
  125. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/tracing/langfuse.py +0 -0
  126. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/utils/__init__.py +0 -0
  127. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate/utils/retry.py +0 -0
  128. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate.egg-info/dependency_links.txt +0 -0
  129. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate.egg-info/entry_points.txt +0 -0
  130. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate.egg-info/requires.txt +0 -0
  131. {hexgate-0.2.6 → hexgate-0.2.8}/hexgate.egg-info/top_level.txt +0 -0
  132. {hexgate-0.2.6 → hexgate-0.2.8}/setup.cfg +0 -0
  133. {hexgate-0.2.6 → hexgate-0.2.8}/tests/test_demo.py +0 -0
hexgate-0.2.8/PKG-INFO ADDED
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: hexgate
3
+ Version: 0.2.8
4
+ Summary: Hexgate — authorization infrastructure for AI agents (agent runtime + cloud client).
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.13
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: bashlex>=0.18
10
+ Requires-Dist: biscuit-python>=0.4
11
+ Requires-Dist: cryptography>=42
12
+ Requires-Dist: httpx>=0.28.1
13
+ Requires-Dist: langchain
14
+ Requires-Dist: langchain-openai
15
+ Requires-Dist: langchain-core
16
+ Requires-Dist: langfuse
17
+ Requires-Dist: pydantic>=2.12.4
18
+ Requires-Dist: python-dotenv>=1.1.1
19
+ Requires-Dist: pyyaml>=6.0.2
20
+ Requires-Dist: rich>=13.9.4
21
+ Requires-Dist: websockets>=13.0
22
+ Requires-Dist: openai-agents>=0.0.10
23
+ Requires-Dist: langgraph>=0.2
24
+ Requires-Dist: nest_asyncio>=1.6
25
+ Requires-Dist: openinference-instrumentation-openai-agents>=0.1
26
+ Requires-Dist: google-adk>=1.0
27
+ Requires-Dist: google-genai>=1.0
28
+ Requires-Dist: litellm>=1.50
29
+ Requires-Dist: openinference-instrumentation-google-adk>=0.1.11
30
+ Requires-Dist: pydantic-ai-slim>=1.88.0
31
+ Requires-Dist: wasmtime>=20.0
32
+ Requires-Dist: mcp>=1.0
33
+ Provides-Extra: dev
34
+ Requires-Dist: ipykernel; extra == "dev"
35
+ Requires-Dist: jupyter; extra == "dev"
36
+ Requires-Dist: pytest>=8.4.1; extra == "dev"
37
+ Requires-Dist: pytest-asyncio>=1.0.0; extra == "dev"
38
+ Requires-Dist: pytest-cov>=6.0.0; extra == "dev"
39
+ Requires-Dist: ruff>=0.12.2; extra == "dev"
40
+ Dynamic: license-file
41
+
42
+ <div align="center">
43
+
44
+ <img src="./icon.svg" alt="Hexgate" width="96" height="96" />
45
+
46
+ # Hexgate
47
+
48
+ **Runtime authorization for AI agents.**
49
+ On every tool call, Hexgate decides whether *this user*, in *this role*, may run *this tool* with *these arguments* — allow, deny, or require approval. For OpenAI Agents, LangChain, Google ADK, Pydantic AI, or a native runtime.
50
+
51
+ [**Website**](https://hexgate.ai) · [**Docs**](https://docs.hexgate.ai) · [PyPI](https://pypi.org/project/hexgate/)
52
+ [![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}")
@@ -0,0 +1,95 @@
1
+ """Google ADK adapter for :class:`~hexgate.mcp.MCPToolset`.
2
+
3
+ Every :class:`~hexgate.mcp.MCPToolProxy` produced by the toolset becomes
4
+ a :class:`google.adk.tools.BaseTool` subclass whose ``_get_declaration``
5
+ returns a ``FunctionDeclaration`` carrying the MCP tool's raw
6
+ JSON Schema (via ``parametersJsonSchema``) and whose ``run_async``
7
+ forwards to the proxy's ``call``. Once wrapped, the resulting tools
8
+ are indistinguishable from ADK-native :class:`FunctionTool` instances
9
+ to the rest of the Google ADK path — attach them to an ``Agent``, then
10
+ wrap via :func:`~hexgate.adapters.google.wrap_google_agent` so the
11
+ existing per-tool policy gate covers MCP invocations too.
12
+
13
+ Usage::
14
+
15
+ from google.adk.agents import Agent
16
+ from hexgate.adapters.google import wrap_google_agent
17
+ from hexgate.adapters.google.mcp import wrap_mcp_toolset
18
+ from hexgate.mcp import MCPServerConfig, MCPToolset
19
+
20
+ slack = MCPServerConfig(name="slack", transport="stdio", command="slack-mcp")
21
+ async with MCPToolset(slack) as mcp:
22
+ agent = Agent(
23
+ name="bot",
24
+ tools=[*wrap_mcp_toolset(mcp), *native],
25
+ )
26
+ wrapped, binding = wrap_google_agent(agent, api_key=api_key)
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from typing import Any
32
+
33
+ from google.adk.tools import BaseTool
34
+ from google.adk.tools.tool_context import ToolContext
35
+ from google.genai import types as genai_types
36
+
37
+ from hexgate.mcp.proxy import MCPToolProxy, MCPToolset
38
+
39
+
40
+ class _MCPProxyTool(BaseTool):
41
+ """One :class:`BaseTool` that forwards to an :class:`MCPToolProxy`.
42
+
43
+ ADK's :class:`FunctionTool` derives its schema from a Python
44
+ callable's signature via reflection, which can't express the shapes
45
+ MCP servers advertise (partial ``required``, ``anyOf`` unions,
46
+ nested objects with dynamic keys). Subclassing :class:`BaseTool`
47
+ and returning a hand-built :class:`FunctionDeclaration` from
48
+ ``_get_declaration`` bypasses the reflection path and hands the
49
+ server's raw JSON Schema straight to the model — via ADK's
50
+ ``parametersJsonSchema`` alias which accepts JSON Schema dicts.
51
+ """
52
+
53
+ def __init__(self, proxy: MCPToolProxy) -> None:
54
+ super().__init__(name=proxy.qualified_name, description=proxy.description)
55
+ # Store the schema + call as private attrs — ADK's BaseTool has
56
+ # no field for them, so we ride on the object dict.
57
+ self._input_schema = proxy.input_schema
58
+ self._call = proxy.call
59
+
60
+ def _get_declaration(self) -> genai_types.FunctionDeclaration:
61
+ # Gemini's FunctionDeclaration validator rejects any
62
+ # parametersJsonSchema missing a top-level `type` — literal `{}`,
63
+ # or a partial like `{"properties": {...}}`, or a bare
64
+ # `{"anyOf": [...]}`. All three shapes cause the whole tool
65
+ # list to fail at declaration time, so the agent never gets to
66
+ # run. Fill in `type: "object"` (the only shape Gemini accepts
67
+ # for a function-args container) whenever it's absent, and
68
+ # ensure a `properties` map so the LLM sees an argument surface
69
+ # rather than an opaque object. LangChain and OpenAI Agents
70
+ # both tolerate the missing `type` unchanged.
71
+ schema = self._input_schema if isinstance(self._input_schema, dict) else {}
72
+ if "type" not in schema:
73
+ schema = {"type": "object", "properties": {}, **schema}
74
+ return genai_types.FunctionDeclaration(
75
+ name=self.name,
76
+ description=self.description,
77
+ parametersJsonSchema=schema,
78
+ )
79
+
80
+ async def run_async(
81
+ self, *, args: dict[str, Any], tool_context: ToolContext
82
+ ) -> Any:
83
+ return await self._call(**(args or {}))
84
+
85
+
86
+ def wrap_mcp_toolset(toolset: MCPToolset) -> list[BaseTool]:
87
+ """Wrap every proxy in ``toolset`` as a Google ADK :class:`BaseTool`.
88
+
89
+ The returned tools share the toolset's connection lifecycle — they
90
+ stop working (returning a ``use_after_close`` envelope) once the
91
+ ``async with MCPToolset(...)`` block exits. Combine with
92
+ :func:`~hexgate.adapters.google.wrap_google_agent` to gate every
93
+ invocation through :class:`~hexgate.security.PolicyEnforcer`.
94
+ """
95
+ return [_MCPProxyTool(p) for p in toolset.proxies]
@@ -16,8 +16,11 @@ from langfuse import get_client, propagate_attributes
16
16
  from openinference.instrumentation.google_adk import GoogleADKInstrumentor
17
17
 
18
18
  from hexgate.adapters.google.wrapper import wrap_google_agent
19
+ from hexgate.agents.factory import ApprovalHandler
20
+ from hexgate.cloud.client import HexgateClient, HexgateConfig
19
21
  from hexgate.config.env import resolve_api_key
20
22
  from hexgate.runtime import User
23
+ from hexgate.security.bans import resolve_ban_gate
21
24
 
22
25
 
23
26
  class HexgateRunner:
@@ -30,6 +33,7 @@ class HexgateRunner:
30
33
  app_name: str,
31
34
  session_service: BaseSessionService,
32
35
  api_key: str | None = None,
36
+ approval_handler: ApprovalHandler | None = None,
33
37
  **runner_kwargs: Any,
34
38
  ):
35
39
  self.api_key = resolve_api_key(api_key)
@@ -39,9 +43,13 @@ class HexgateRunner:
39
43
  )
40
44
  # Policy resolves at construction (the loud-failure point); the
41
45
  # Runner is built once — refresh swaps the enforcer's policy
42
- # without touching it.
46
+ # without touching it. One client is shared with the ban resolver.
47
+ client = HexgateClient(HexgateConfig.from_env(api_key=self.api_key))
43
48
  self._wrapped_agent, self._binding = wrap_google_agent(
44
- agent, api_key=self.api_key
49
+ agent,
50
+ api_key=self.api_key,
51
+ approval_handler=approval_handler,
52
+ client=client,
45
53
  )
46
54
  self._runner = Runner(
47
55
  agent=self._wrapped_agent,
@@ -50,6 +58,9 @@ class HexgateRunner:
50
58
  **runner_kwargs,
51
59
  )
52
60
  self._agent_name = getattr(agent, "name", "default")
61
+ self._ban_gate = resolve_ban_gate(
62
+ self._agent_name, api_key=self.api_key, client=client
63
+ )
53
64
 
54
65
  def _setup_observability(self):
55
66
  """Install Langfuse + GoogleADKInstrumentor (idempotent)."""
@@ -88,6 +99,8 @@ class HexgateRunner:
88
99
  """
89
100
  self._setup_observability()
90
101
  self._binding.refresh() # per-run policy pull; 304 when unchanged
102
+ if self._ban_gate is not None:
103
+ self._ban_gate.check(user)
91
104
  with user.sync_scope(), self._propagate(user):
92
105
  agen = self._runner.run_async(
93
106
  user_id=user.user_id,
@@ -116,6 +129,8 @@ class HexgateRunner:
116
129
  """Run the Google ADK agent asynchronously, yielding events."""
117
130
  self._setup_observability()
118
131
  await self._binding.refresh_async() # per-run policy pull; 304 when unchanged
132
+ if self._ban_gate is not None:
133
+ await self._ban_gate.check_async(user)
119
134
  async with user:
120
135
  with self._propagate(user):
121
136
  async for event in self._runner.run_async(
@@ -1,6 +1,11 @@
1
1
  """Google ADK adapter: wrap ``BaseTool`` so ``run_async`` consults a
2
2
  :class:`PolicyEnforcer` first. Non-allow outcomes render as markered
3
3
  strings the model sees as tool output.
4
+
5
+ When a caller supplies ``approval_handler``, a ``NEEDS_APPROVAL``
6
+ decision fires the callback and runs the original tool on truthy return;
7
+ falsy return (or a missing handler) keeps today's behavior of surfacing
8
+ the ``[approval_required]`` marker to the model.
4
9
  """
5
10
 
6
11
  from __future__ import annotations
@@ -14,6 +19,9 @@ from google.adk.tools.base_tool import BaseTool
14
19
  from google.adk.tools.function_tool import FunctionTool
15
20
  from google.adk.tools.tool_context import ToolContext
16
21
 
22
+ from hexgate.agents.approvals import resolve_approval_async
23
+ from hexgate.agents.factory import ApprovalHandler
24
+ from hexgate.security.decision import DecisionOutcome
17
25
  from hexgate.security.enforcer import PolicyEnforcer
18
26
 
19
27
 
@@ -32,7 +40,12 @@ def _normalize(tool: ToolEntry) -> BaseTool:
32
40
  )
33
41
 
34
42
 
35
- def wrap_tool(tool: ToolEntry, enforcer: PolicyEnforcer) -> BaseTool:
43
+ def wrap_tool(
44
+ tool: ToolEntry,
45
+ enforcer: PolicyEnforcer,
46
+ *,
47
+ approval_handler: ApprovalHandler | None = None,
48
+ ) -> BaseTool:
36
49
  """Return a copy of ``tool`` with ``run_async`` gated by ``enforcer``."""
37
50
  base = _normalize(tool)
38
51
  name = base.name
@@ -45,6 +58,12 @@ def wrap_tool(tool: ToolEntry, enforcer: PolicyEnforcer) -> BaseTool:
45
58
  decision = enforcer.decide(name, args or {})
46
59
  if decision.allowed:
47
60
  return await original_run_async(args=args, tool_context=tool_context)
61
+ if (
62
+ decision.outcome is DecisionOutcome.NEEDS_APPROVAL
63
+ and approval_handler is not None
64
+ and await resolve_approval_async(approval_handler, decision)
65
+ ):
66
+ return await original_run_async(args=args, tool_context=tool_context)
48
67
  return decision.as_error_message()
49
68
 
50
69
  wrapped = copy.copy(base)
@@ -52,6 +71,11 @@ def wrap_tool(tool: ToolEntry, enforcer: PolicyEnforcer) -> BaseTool:
52
71
  return wrapped
53
72
 
54
73
 
55
- def wrap_tools(tools: list[ToolEntry], enforcer: PolicyEnforcer) -> list[BaseTool]:
74
+ def wrap_tools(
75
+ tools: list[ToolEntry],
76
+ enforcer: PolicyEnforcer,
77
+ *,
78
+ approval_handler: ApprovalHandler | None = None,
79
+ ) -> list[BaseTool]:
56
80
  """Return a fresh list of policy-gated copies."""
57
- return [wrap_tool(t, enforcer) for t in tools]
81
+ return [wrap_tool(t, enforcer, approval_handler=approval_handler) for t in tools]
@@ -10,31 +10,44 @@ is what the runner refreshes per run.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ from typing import TYPE_CHECKING
14
+
13
15
  from google.adk.agents import BaseAgent
14
16
 
15
17
  from hexgate.adapters.google.tools import wrap_tools
18
+ from hexgate.agents.factory import ApprovalHandler
16
19
  from hexgate.security.binding import PolicyBinding, resolve_policy
17
20
  from hexgate.security.enforcer import build_enforcer
18
21
 
22
+ if TYPE_CHECKING:
23
+ from hexgate.cloud.client import HexgateClient
24
+
19
25
 
20
26
  def wrap_google_agent(
21
- agent: BaseAgent, *, api_key: str
27
+ agent: BaseAgent,
28
+ *,
29
+ api_key: str,
30
+ approval_handler: ApprovalHandler | None = None,
31
+ client: HexgateClient | None = None,
22
32
  ) -> tuple[BaseAgent, PolicyBinding]:
23
33
  """Return a policy-gated clone of ``agent`` plus its refresh binding.
24
34
 
25
35
  Caller must open a :class:`User` scope around the run.
26
- ``NEEDS_APPROVAL`` outcomes surface as ``[approval_required]``-prefixed
27
- strings in tool results; ``[policy_denied]`` for denials. Refresh the
28
- returned binding at run boundaries (``HexgateRunner`` does). Fail-loud:
29
- an unregistered agent (platform 404) raises — register it first with
36
+ ``NEEDS_APPROVAL`` outcomes fire ``approval_handler`` (async
37
+ ``fn(decision) -> bool`` or ``bool`` shorthand); a truthy return
38
+ runs the tool, falsy or missing handler surfaces the
39
+ ``[approval_required]``-prefixed string as tool result.
40
+ ``[policy_denied]`` marks plain denials. Refresh the returned
41
+ binding at run boundaries (``HexgateRunner`` does). Fail-loud: an
42
+ unregistered agent (platform 404) raises — register it first with
30
43
  ``hexgate register``.
31
44
  """
32
45
  agent_name = getattr(agent, "name", "default")
33
46
  tools = list(getattr(agent, "tools", []) or [])
34
47
 
35
- resolved = resolve_policy(agent_name, api_key=api_key)
48
+ resolved = resolve_policy(agent_name, api_key=api_key, client=client)
36
49
  enforcer = build_enforcer(resolved.engine, agent_name=agent_name, api_key=api_key)
37
- guarded_tools = wrap_tools(tools, enforcer)
50
+ guarded_tools = wrap_tools(tools, enforcer, approval_handler=approval_handler)
38
51
  return (
39
52
  agent.model_copy(update={"tools": guarded_tools}),
40
53
  PolicyBinding(enforcer, resolved.source),