hexgate 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hexgate-0.2.2/hexgate.egg-info → hexgate-0.2.4}/PKG-INFO +56 -18
- hexgate-0.2.2/PKG-INFO → hexgate-0.2.4/README.md +54 -55
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/google/runner.py +1 -1
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/langchain/agent.py +8 -5
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/langchain/wrapper.py +2 -2
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/openai/runner.py +1 -1
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/pydantic_ai/agent.py +1 -1
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/pydantic_ai/wrapper.py +1 -1
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/agents/factory.py +23 -8
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/agents/loader.py +4 -4
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/audit.py +30 -4
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/_common.py +18 -8
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/register/hexgate.py +3 -3
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/register/main.py +2 -2
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/register/models.py +1 -1
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/serve.py +3 -3
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cloud/__init__.py +1 -1
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cloud/attenuate.py +1 -1
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cloud/biscuit.py +1 -1
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cloud/client.py +7 -7
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/runtime/context.py +12 -9
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/binding.py +16 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/rego.py +1 -1
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/wasm_engine.py +3 -3
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/streaming/__init__.py +1 -1
- hexgate-0.2.2/README.md → hexgate-0.2.4/hexgate.egg-info/PKG-INFO +93 -16
- {hexgate-0.2.2 → hexgate-0.2.4}/pyproject.toml +2 -2
- {hexgate-0.2.2 → hexgate-0.2.4}/LICENSE +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/google/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/google/tools.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/google/wrapper.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/langchain/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/langchain/tools.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/openai/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/openai/tools.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/openai/wrapper.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/pydantic_ai/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/adapters/pydantic_ai/tools.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/agents/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/agents/builtin/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/agents/builtin/researcher/agent.yaml +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/agents/builtin/researcher/policy.yaml +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/agents/builtin/researcher/system.md +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/agents/models.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/agents/prompts/agent_system.md +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/bootstrap.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/chat.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/policy/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/policy/main.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/register/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/register/google.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/register/langchain.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/register/manifest.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/register/openai.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/register/pydantic_ai.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/register/register.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/cli/state.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/config/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/config/settings.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/runtime/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/runtime/command_policy.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/runtime/sandbox_runtime.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/runtime/srt.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/runtime/workspace.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/bundle.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/constraints.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/decision.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/enforcer.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/errors.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/file_scope.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/models.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/policy.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/policy_set.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/rego_wasm.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/signing.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/security/source.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/streaming/events.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/streaming/normalize.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tools/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tools/bash.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tools/decorators.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tools/fetch.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tools/files/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tools/files/_common.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tools/files/edit_file.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tools/files/glob.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tools/files/grep.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tools/files/read_file.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tools/files/write_file.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tools/refund.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tools/websearch.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tracing/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/tracing/langfuse.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/utils/__init__.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate/utils/retry.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate.egg-info/SOURCES.txt +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate.egg-info/dependency_links.txt +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate.egg-info/entry_points.txt +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate.egg-info/requires.txt +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/hexgate.egg-info/top_level.txt +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/setup.cfg +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/tests/test_bootstrap.py +0 -0
- {hexgate-0.2.2 → hexgate-0.2.4}/tests/test_demo.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexgate
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: Hexgate — authorization infrastructure for AI agents (agent runtime + cloud client).
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.13
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
@@ -39,29 +39,38 @@ Dynamic: license-file
|
|
|
39
39
|
|
|
40
40
|
<div align="center">
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
<img src="./icon.svg" alt="Hexgate" width="96" height="96" />
|
|
43
|
+
|
|
44
|
+
# Hexgate
|
|
43
45
|
|
|
44
46
|
**Authorization infrastructure for AI agents.**
|
|
45
47
|
Policy enforcement, signed policy bundles, per-request user scope, audit trail — for OpenAI Agents, LangChain, Google ADK, Pydantic AI, or a native runtime.
|
|
46
48
|
|
|
49
|
+
[**Website**](https://hexgate.ai) · [Docs](https://docs.hexgate.ai) · [PyPI](https://pypi.org/project/hexgate/) · [Discussions](https://github.com/HexamindOrganisation/hexgate/discussions)
|
|
50
|
+
|
|
47
51
|
[](https://pypi.org/project/hexgate/)
|
|
48
|
-
[](https://pypi.org/project/hexgate/)
|
|
49
52
|
[](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml)
|
|
50
53
|
[](https://pypi.org/project/hexgate/)
|
|
51
54
|
[](LICENSE)
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
<br />
|
|
57
|
+
|
|
58
|
+
<img src="./assets/hero.png" alt="Control what your agents do — not just what they say. Policy decisions streaming live from the PolicyEnforcer." />
|
|
59
|
+
|
|
60
|
+
<br />
|
|
61
|
+
|
|
62
|
+
[Quick Start](#-quick-start--local-cli) · [Two paths](#-which-path-do-i-pick) · [Framework adapters](#-framework-agent-wrapping) · [Policy bundles](#-policy-bundles--compile-sign-enforce-wasm) · [User scope](#-user-scope--roles) · [Platform](#-hexgate-platform)
|
|
54
63
|
|
|
55
64
|
</div>
|
|
56
65
|
|
|
57
66
|
---
|
|
58
67
|
|
|
59
|
-
## What is
|
|
68
|
+
## What is Hexgate?
|
|
60
69
|
|
|
61
|
-
|
|
70
|
+
Hexgate is two things that move together:
|
|
62
71
|
|
|
63
72
|
- **`hexgate` — the SDK.** A Python runtime that gates every tool call through a typed `Decision` (allow / deny / approval-required), wraps your existing OpenAI / LangChain / Google ADK / Pydantic AI agent without rewriting it, and threads per-request user identity through tracing + audit.
|
|
64
|
-
- **The
|
|
73
|
+
- **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.
|
|
65
74
|
|
|
66
75
|
You can use the SDK with nothing else (single-process REPL, YAML on disk). Or plug in the platform when you want auditable decisions in ClickHouse, a shared Playground UI, and live policy edits.
|
|
67
76
|
|
|
@@ -79,7 +88,7 @@ You can use the SDK with nothing else (single-process REPL, YAML on disk). Or pl
|
|
|
79
88
|
┌────────────────┐ ┌──────────────────┐ ┌────────────────┐
|
|
80
89
|
│ Local policy │ │ Signed WASM │ │ Audit log │
|
|
81
90
|
│ (YAML / dir, │ │ bundle from │ │ (ClickHouse │
|
|
82
|
-
│ hot reload) │ │
|
|
91
|
+
│ hot reload) │ │ Hexgate cloud │ │ via REST) │
|
|
83
92
|
└────────────────┘ └──────────────────┘ └────────────────┘
|
|
84
93
|
```
|
|
85
94
|
|
|
@@ -103,7 +112,7 @@ You can use the SDK with nothing else (single-process REPL, YAML on disk). Or pl
|
|
|
103
112
|
- [Environment](#-environment)
|
|
104
113
|
- [Tests & dev tooling](#-tests--dev-tooling)
|
|
105
114
|
- [CLI reference](#-cli-reference)
|
|
106
|
-
- [
|
|
115
|
+
- [Hexgate platform](#-hexgate-platform)
|
|
107
116
|
- [User scope + roles](#-user-scope--roles)
|
|
108
117
|
- [Stream results](#-stream-results)
|
|
109
118
|
|
|
@@ -177,7 +186,7 @@ Both commands accept either a plain agent id (`--agent researcher`) or a uvicorn
|
|
|
177
186
|
|
|
178
187
|
## 🚀 Quick Start — Platform
|
|
179
188
|
|
|
180
|
-
To run the full
|
|
189
|
+
To run the full Hexgate control plane locally (FastAPI backend + dashboard + your local agent serving over WebSocket), you need **three terminals**. The Makefile has a target that prints the recipe:
|
|
181
190
|
|
|
182
191
|
```bash
|
|
183
192
|
make demo-platform # prints the 3-terminal recipe below
|
|
@@ -214,6 +223,31 @@ make dashboard-install # pnpm install inside platform/dashboard/
|
|
|
214
223
|
|
|
215
224
|
Then open http://localhost:5173/playground — type a message, watch the live stream of tool calls and policy decisions from your local agent.
|
|
216
225
|
|
|
226
|
+
### Control-plane database (SQLite / Postgres)
|
|
227
|
+
|
|
228
|
+
`make platform-api` uses a local **SQLite** file — zero setup, fine for
|
|
229
|
+
dev and the test suite. Set `DATABASE_URL` to run on **Postgres** instead
|
|
230
|
+
(what deployments use); bare `postgres://` URLs are normalized to the
|
|
231
|
+
`asyncpg` driver. A local Postgres is in the dev compose:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
make platform-api-pg # starts Postgres (Docker) + runs the API against it
|
|
235
|
+
# equivalent to:
|
|
236
|
+
make postgres-up # start Postgres, wait until healthy
|
|
237
|
+
DATABASE_URL=postgresql+asyncpg://hexgate:hexgate-dev-password@localhost:5433/hexgate make platform-api
|
|
238
|
+
make postgres-reset # wipe ONLY the Postgres data volume
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
No migration system: the schema is created with `create_all`, so a model
|
|
242
|
+
change means resetting the volume (`make postgres-reset`), not migrating —
|
|
243
|
+
data is treated as disposable. A managed Postgres (e.g. Scaleway) needs SSL
|
|
244
|
+
in the DSN, e.g. `…/hexgate?ssl=require`.
|
|
245
|
+
|
|
246
|
+
`DATABASE_URL` is read from the real environment or `platform/api/.env`
|
|
247
|
+
(see `.env.sample`). The opt-in `tests/test_postgres_smoke.py` is the only
|
|
248
|
+
test that exercises the Postgres path (the rest run on SQLite); it runs
|
|
249
|
+
when `DATABASE_URL` points at Postgres.
|
|
250
|
+
|
|
217
251
|
### Audit log (ClickHouse)
|
|
218
252
|
|
|
219
253
|
Policy decisions are written to a local ClickHouse instance for the audit dashboard. Requires Docker.
|
|
@@ -660,7 +694,7 @@ What happens under the hood:
|
|
|
660
694
|
|
|
661
695
|
Working scripts in `examples/`:
|
|
662
696
|
|
|
663
|
-
- `examples/customer_bot.py` — canonical
|
|
697
|
+
- `examples/customer_bot.py` — canonical Hexgate path: `create_agent(...)` + the dashboard register/serve loop end-to-end.
|
|
664
698
|
- `examples/openai_demo.py` — `HexgateRunner` (OpenAI Agents SDK) end-to-end.
|
|
665
699
|
- `examples/google_demo.py` — `HexgateRunner` (Google ADK) end-to-end with `InMemorySessionService`.
|
|
666
700
|
- `examples/pydantic_ai_demo.py` — `wrap_pydantic_agent` (Pydantic AI) end-to-end.
|
|
@@ -795,7 +829,7 @@ That means the same agent code can stay simple in development, while deployment
|
|
|
795
829
|
|
|
796
830
|
## 🧩 Policy Bundles — Compile, Sign, Enforce (WASM)
|
|
797
831
|
|
|
798
|
-
|
|
832
|
+
Hexgate has **two policy enforcement engines** that return identical decisions (there's a parity test suite that proves it):
|
|
799
833
|
|
|
800
834
|
- **pydantic** (default) — evaluates constraints in-process. Zero setup; this is what every example above uses.
|
|
801
835
|
- **WASM** — compiles `policy.yaml` → Rego → a WebAssembly module evaluated via `wasmtime`. This is the path production ships: one compiled artifact, byte-for-byte reproducible, cryptographically signed by the platform.
|
|
@@ -1131,7 +1165,7 @@ hexgate chat --use examples/research_agents.py --agent update_researcher --appro
|
|
|
1131
1165
|
|
|
1132
1166
|
### `hexgate register` — push a manifest to the platform
|
|
1133
1167
|
|
|
1134
|
-
Register a code-defined agent's manifest with the
|
|
1168
|
+
Register a code-defined agent's manifest with the Hexgate platform. `--agent`
|
|
1135
1169
|
takes a Python import path of the form `module.path:attribute`, the same shape
|
|
1136
1170
|
as ASGI/WSGI entrypoints. The CLI imports the module, grabs the agent object,
|
|
1137
1171
|
and POSTs its manifest to `${HEXGATE_API_URL}/v1/agents` using
|
|
@@ -1170,7 +1204,7 @@ system prompt directly off the object. No flags needed.
|
|
|
1170
1204
|
`--system-prompt` accepts either a literal string or a path to a `.md` /
|
|
1171
1205
|
`.txt` / `.jinja` file (read as text at register time).
|
|
1172
1206
|
|
|
1173
|
-
Supported frameworks: OpenAI Agents SDK, Google ADK, Pydantic AI, LangChain/LangGraph,
|
|
1207
|
+
Supported frameworks: OpenAI Agents SDK, Google ADK, Pydantic AI, LangChain/LangGraph, Hexgate agents.
|
|
1174
1208
|
|
|
1175
1209
|
### `hexgate serve` — bridge a local agent to the platform's relay
|
|
1176
1210
|
|
|
@@ -1207,7 +1241,7 @@ print(manifest.model_dump())
|
|
|
1207
1241
|
```
|
|
1208
1242
|
|
|
1209
1243
|
`create_manifest` dispatches on the framework of `agent`. The supported
|
|
1210
|
-
types are the same set `hexgate register` accepts:
|
|
1244
|
+
types are the same set `hexgate register` accepts: Hexgate, OpenAI Agents
|
|
1211
1245
|
SDK, Google ADK, Pydantic AI, and LangChain/LangGraph compiled graphs.
|
|
1212
1246
|
For LangGraph you must pass `tools=` explicitly, and may pass `model=` /
|
|
1213
1247
|
`system_prompt=`, since compiled graphs don't expose those fields after
|
|
@@ -1217,7 +1251,7 @@ The return value is an `AgentManifest` (a Pydantic model, also re-exported
|
|
|
1217
1251
|
from `hexgate` for type annotations) — the same schema the platform
|
|
1218
1252
|
stores and the dashboard renders.
|
|
1219
1253
|
|
|
1220
|
-
## 🌐
|
|
1254
|
+
## 🌐 Hexgate Platform
|
|
1221
1255
|
|
|
1222
1256
|
The `platform/` directory contains an optional control plane that hosts agent definitions, dev tokens, and a live debug surface. The SDK works fully without it (`load_local_agent`, `load_builtin_agent` keep their existing semantics) — but with it you get:
|
|
1223
1257
|
|
|
@@ -1336,7 +1370,7 @@ the name from the loaded agent's `.name` attribute — no env var needed.
|
|
|
1336
1370
|
|
|
1337
1371
|
## 👤 User Scope + Roles
|
|
1338
1372
|
|
|
1339
|
-
Real backends serve many users, and different users get different capabilities.
|
|
1373
|
+
Real backends serve many users, and different users get different capabilities. Hexgate splits that into two pieces:
|
|
1340
1374
|
|
|
1341
1375
|
- **`User`** — the per-request scope. Marks "this invocation acts on behalf of alice, in role X." Async context manager; pushes a fact-bearing Biscuit through the agent runtime.
|
|
1342
1376
|
- **Role policies** — one `policy.yaml` per role, optionally inheriting from a base mixin. The runtime picks the right one at call time based on the active `User.role`.
|
|
@@ -1486,3 +1520,7 @@ async for event in stream_agent(agent, handler, "latest AI breakthroughs"):
|
|
|
1486
1520
|
- assistant text deltas
|
|
1487
1521
|
- tool lifecycle
|
|
1488
1522
|
- final run completion
|
|
1523
|
+
|
|
1524
|
+
---
|
|
1525
|
+
|
|
1526
|
+
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).
|
|
@@ -1,67 +1,37 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: hexgate
|
|
3
|
-
Version: 0.2.2
|
|
4
|
-
Summary: HexaGate — 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
|
-
Provides-Extra: dev
|
|
33
|
-
Requires-Dist: ipykernel; extra == "dev"
|
|
34
|
-
Requires-Dist: jupyter; extra == "dev"
|
|
35
|
-
Requires-Dist: pytest>=8.4.1; extra == "dev"
|
|
36
|
-
Requires-Dist: pytest-asyncio>=1.0.0; extra == "dev"
|
|
37
|
-
Requires-Dist: ruff>=0.12.2; extra == "dev"
|
|
38
|
-
Dynamic: license-file
|
|
39
|
-
|
|
40
1
|
<div align="center">
|
|
41
2
|
|
|
42
|
-
|
|
3
|
+
<img src="./icon.svg" alt="Hexgate" width="96" height="96" />
|
|
4
|
+
|
|
5
|
+
# Hexgate
|
|
43
6
|
|
|
44
7
|
**Authorization infrastructure for AI agents.**
|
|
45
8
|
Policy enforcement, signed policy bundles, per-request user scope, audit trail — for OpenAI Agents, LangChain, Google ADK, Pydantic AI, or a native runtime.
|
|
46
9
|
|
|
10
|
+
[**Website**](https://hexgate.ai) · [Docs](https://docs.hexgate.ai) · [PyPI](https://pypi.org/project/hexgate/) · [Discussions](https://github.com/HexamindOrganisation/hexgate/discussions)
|
|
11
|
+
|
|
47
12
|
[](https://pypi.org/project/hexgate/)
|
|
48
|
-
[](https://pypi.org/project/hexgate/)
|
|
49
13
|
[](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml)
|
|
50
14
|
[](https://pypi.org/project/hexgate/)
|
|
51
15
|
[](LICENSE)
|
|
52
16
|
|
|
53
|
-
|
|
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
|
+
<br />
|
|
22
|
+
|
|
23
|
+
[Quick Start](#-quick-start--local-cli) · [Two paths](#-which-path-do-i-pick) · [Framework adapters](#-framework-agent-wrapping) · [Policy bundles](#-policy-bundles--compile-sign-enforce-wasm) · [User scope](#-user-scope--roles) · [Platform](#-hexgate-platform)
|
|
54
24
|
|
|
55
25
|
</div>
|
|
56
26
|
|
|
57
27
|
---
|
|
58
28
|
|
|
59
|
-
## What is
|
|
29
|
+
## What is Hexgate?
|
|
60
30
|
|
|
61
|
-
|
|
31
|
+
Hexgate is two things that move together:
|
|
62
32
|
|
|
63
33
|
- **`hexgate` — the SDK.** A Python runtime that gates every tool call through a typed `Decision` (allow / deny / approval-required), wraps your existing OpenAI / LangChain / Google ADK / Pydantic AI agent without rewriting it, and threads per-request user identity through tracing + audit.
|
|
64
|
-
- **The
|
|
34
|
+
- **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.
|
|
65
35
|
|
|
66
36
|
You can use the SDK with nothing else (single-process REPL, YAML on disk). Or plug in the platform when you want auditable decisions in ClickHouse, a shared Playground UI, and live policy edits.
|
|
67
37
|
|
|
@@ -79,7 +49,7 @@ You can use the SDK with nothing else (single-process REPL, YAML on disk). Or pl
|
|
|
79
49
|
┌────────────────┐ ┌──────────────────┐ ┌────────────────┐
|
|
80
50
|
│ Local policy │ │ Signed WASM │ │ Audit log │
|
|
81
51
|
│ (YAML / dir, │ │ bundle from │ │ (ClickHouse │
|
|
82
|
-
│ hot reload) │ │
|
|
52
|
+
│ hot reload) │ │ Hexgate cloud │ │ via REST) │
|
|
83
53
|
└────────────────┘ └──────────────────┘ └────────────────┘
|
|
84
54
|
```
|
|
85
55
|
|
|
@@ -103,7 +73,7 @@ You can use the SDK with nothing else (single-process REPL, YAML on disk). Or pl
|
|
|
103
73
|
- [Environment](#-environment)
|
|
104
74
|
- [Tests & dev tooling](#-tests--dev-tooling)
|
|
105
75
|
- [CLI reference](#-cli-reference)
|
|
106
|
-
- [
|
|
76
|
+
- [Hexgate platform](#-hexgate-platform)
|
|
107
77
|
- [User scope + roles](#-user-scope--roles)
|
|
108
78
|
- [Stream results](#-stream-results)
|
|
109
79
|
|
|
@@ -177,7 +147,7 @@ Both commands accept either a plain agent id (`--agent researcher`) or a uvicorn
|
|
|
177
147
|
|
|
178
148
|
## 🚀 Quick Start — Platform
|
|
179
149
|
|
|
180
|
-
To run the full
|
|
150
|
+
To run the full Hexgate control plane locally (FastAPI backend + dashboard + your local agent serving over WebSocket), you need **three terminals**. The Makefile has a target that prints the recipe:
|
|
181
151
|
|
|
182
152
|
```bash
|
|
183
153
|
make demo-platform # prints the 3-terminal recipe below
|
|
@@ -214,6 +184,31 @@ make dashboard-install # pnpm install inside platform/dashboard/
|
|
|
214
184
|
|
|
215
185
|
Then open http://localhost:5173/playground — type a message, watch the live stream of tool calls and policy decisions from your local agent.
|
|
216
186
|
|
|
187
|
+
### Control-plane database (SQLite / Postgres)
|
|
188
|
+
|
|
189
|
+
`make platform-api` uses a local **SQLite** file — zero setup, fine for
|
|
190
|
+
dev and the test suite. Set `DATABASE_URL` to run on **Postgres** instead
|
|
191
|
+
(what deployments use); bare `postgres://` URLs are normalized to the
|
|
192
|
+
`asyncpg` driver. A local Postgres is in the dev compose:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
make platform-api-pg # starts Postgres (Docker) + runs the API against it
|
|
196
|
+
# equivalent to:
|
|
197
|
+
make postgres-up # start Postgres, wait until healthy
|
|
198
|
+
DATABASE_URL=postgresql+asyncpg://hexgate:hexgate-dev-password@localhost:5433/hexgate make platform-api
|
|
199
|
+
make postgres-reset # wipe ONLY the Postgres data volume
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
No migration system: the schema is created with `create_all`, so a model
|
|
203
|
+
change means resetting the volume (`make postgres-reset`), not migrating —
|
|
204
|
+
data is treated as disposable. A managed Postgres (e.g. Scaleway) needs SSL
|
|
205
|
+
in the DSN, e.g. `…/hexgate?ssl=require`.
|
|
206
|
+
|
|
207
|
+
`DATABASE_URL` is read from the real environment or `platform/api/.env`
|
|
208
|
+
(see `.env.sample`). The opt-in `tests/test_postgres_smoke.py` is the only
|
|
209
|
+
test that exercises the Postgres path (the rest run on SQLite); it runs
|
|
210
|
+
when `DATABASE_URL` points at Postgres.
|
|
211
|
+
|
|
217
212
|
### Audit log (ClickHouse)
|
|
218
213
|
|
|
219
214
|
Policy decisions are written to a local ClickHouse instance for the audit dashboard. Requires Docker.
|
|
@@ -660,7 +655,7 @@ What happens under the hood:
|
|
|
660
655
|
|
|
661
656
|
Working scripts in `examples/`:
|
|
662
657
|
|
|
663
|
-
- `examples/customer_bot.py` — canonical
|
|
658
|
+
- `examples/customer_bot.py` — canonical Hexgate path: `create_agent(...)` + the dashboard register/serve loop end-to-end.
|
|
664
659
|
- `examples/openai_demo.py` — `HexgateRunner` (OpenAI Agents SDK) end-to-end.
|
|
665
660
|
- `examples/google_demo.py` — `HexgateRunner` (Google ADK) end-to-end with `InMemorySessionService`.
|
|
666
661
|
- `examples/pydantic_ai_demo.py` — `wrap_pydantic_agent` (Pydantic AI) end-to-end.
|
|
@@ -795,7 +790,7 @@ That means the same agent code can stay simple in development, while deployment
|
|
|
795
790
|
|
|
796
791
|
## 🧩 Policy Bundles — Compile, Sign, Enforce (WASM)
|
|
797
792
|
|
|
798
|
-
|
|
793
|
+
Hexgate has **two policy enforcement engines** that return identical decisions (there's a parity test suite that proves it):
|
|
799
794
|
|
|
800
795
|
- **pydantic** (default) — evaluates constraints in-process. Zero setup; this is what every example above uses.
|
|
801
796
|
- **WASM** — compiles `policy.yaml` → Rego → a WebAssembly module evaluated via `wasmtime`. This is the path production ships: one compiled artifact, byte-for-byte reproducible, cryptographically signed by the platform.
|
|
@@ -1131,7 +1126,7 @@ hexgate chat --use examples/research_agents.py --agent update_researcher --appro
|
|
|
1131
1126
|
|
|
1132
1127
|
### `hexgate register` — push a manifest to the platform
|
|
1133
1128
|
|
|
1134
|
-
Register a code-defined agent's manifest with the
|
|
1129
|
+
Register a code-defined agent's manifest with the Hexgate platform. `--agent`
|
|
1135
1130
|
takes a Python import path of the form `module.path:attribute`, the same shape
|
|
1136
1131
|
as ASGI/WSGI entrypoints. The CLI imports the module, grabs the agent object,
|
|
1137
1132
|
and POSTs its manifest to `${HEXGATE_API_URL}/v1/agents` using
|
|
@@ -1170,7 +1165,7 @@ system prompt directly off the object. No flags needed.
|
|
|
1170
1165
|
`--system-prompt` accepts either a literal string or a path to a `.md` /
|
|
1171
1166
|
`.txt` / `.jinja` file (read as text at register time).
|
|
1172
1167
|
|
|
1173
|
-
Supported frameworks: OpenAI Agents SDK, Google ADK, Pydantic AI, LangChain/LangGraph,
|
|
1168
|
+
Supported frameworks: OpenAI Agents SDK, Google ADK, Pydantic AI, LangChain/LangGraph, Hexgate agents.
|
|
1174
1169
|
|
|
1175
1170
|
### `hexgate serve` — bridge a local agent to the platform's relay
|
|
1176
1171
|
|
|
@@ -1207,7 +1202,7 @@ print(manifest.model_dump())
|
|
|
1207
1202
|
```
|
|
1208
1203
|
|
|
1209
1204
|
`create_manifest` dispatches on the framework of `agent`. The supported
|
|
1210
|
-
types are the same set `hexgate register` accepts:
|
|
1205
|
+
types are the same set `hexgate register` accepts: Hexgate, OpenAI Agents
|
|
1211
1206
|
SDK, Google ADK, Pydantic AI, and LangChain/LangGraph compiled graphs.
|
|
1212
1207
|
For LangGraph you must pass `tools=` explicitly, and may pass `model=` /
|
|
1213
1208
|
`system_prompt=`, since compiled graphs don't expose those fields after
|
|
@@ -1217,7 +1212,7 @@ The return value is an `AgentManifest` (a Pydantic model, also re-exported
|
|
|
1217
1212
|
from `hexgate` for type annotations) — the same schema the platform
|
|
1218
1213
|
stores and the dashboard renders.
|
|
1219
1214
|
|
|
1220
|
-
## 🌐
|
|
1215
|
+
## 🌐 Hexgate Platform
|
|
1221
1216
|
|
|
1222
1217
|
The `platform/` directory contains an optional control plane that hosts agent definitions, dev tokens, and a live debug surface. The SDK works fully without it (`load_local_agent`, `load_builtin_agent` keep their existing semantics) — but with it you get:
|
|
1223
1218
|
|
|
@@ -1336,7 +1331,7 @@ the name from the loaded agent's `.name` attribute — no env var needed.
|
|
|
1336
1331
|
|
|
1337
1332
|
## 👤 User Scope + Roles
|
|
1338
1333
|
|
|
1339
|
-
Real backends serve many users, and different users get different capabilities.
|
|
1334
|
+
Real backends serve many users, and different users get different capabilities. Hexgate splits that into two pieces:
|
|
1340
1335
|
|
|
1341
1336
|
- **`User`** — the per-request scope. Marks "this invocation acts on behalf of alice, in role X." Async context manager; pushes a fact-bearing Biscuit through the agent runtime.
|
|
1342
1337
|
- **Role policies** — one `policy.yaml` per role, optionally inheriting from a base mixin. The runtime picks the right one at call time based on the active `User.role`.
|
|
@@ -1486,3 +1481,7 @@ async for event in stream_agent(agent, handler, "latest AI breakthroughs"):
|
|
|
1486
1481
|
- assistant text deltas
|
|
1487
1482
|
- tool lifecycle
|
|
1488
1483
|
- final run completion
|
|
1484
|
+
|
|
1485
|
+
---
|
|
1486
|
+
|
|
1487
|
+
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).
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""Proxy around a pre-built ``CompiledStateGraph`` for
|
|
1
|
+
"""Proxy around a pre-built ``CompiledStateGraph`` for Hexgate-aware calls."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator
|
|
5
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator, Literal
|
|
6
6
|
|
|
7
7
|
from langchain_core.runnables import RunnableConfig
|
|
8
8
|
from langfuse import get_client, propagate_attributes
|
|
@@ -60,7 +60,7 @@ class HexgateLangchainAgent:
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
def _with_callbacks(self, config: RunnableConfig | None) -> RunnableConfig:
|
|
63
|
-
"""Append the
|
|
63
|
+
"""Append the Hexgate callback handler to ``config['callbacks']``."""
|
|
64
64
|
merged: RunnableConfig = dict(config) if config else {}
|
|
65
65
|
callbacks = list(merged.get("callbacks") or [])
|
|
66
66
|
if self._callback_handler not in callbacks:
|
|
@@ -134,10 +134,10 @@ class HexgateLangchainAgent:
|
|
|
134
134
|
async def astream_events(
|
|
135
135
|
self,
|
|
136
136
|
input: dict[str, Any],
|
|
137
|
-
version: str,
|
|
138
137
|
*,
|
|
139
138
|
user: User,
|
|
140
139
|
config: RunnableConfig | None = None,
|
|
140
|
+
version: Literal["v1", "v2"] = "v2",
|
|
141
141
|
**kwargs: Any,
|
|
142
142
|
) -> AsyncIterator[dict[str, Any]]:
|
|
143
143
|
"""Stream the agent events asynchronously inside a User scope."""
|
|
@@ -145,7 +145,10 @@ class HexgateLangchainAgent:
|
|
|
145
145
|
async with user:
|
|
146
146
|
with propagate_attributes(**self._propagate_kwargs(user, "astream_events")):
|
|
147
147
|
async for event in self._agent.astream_events(
|
|
148
|
-
input,
|
|
148
|
+
input,
|
|
149
|
+
config=self._with_callbacks(config),
|
|
150
|
+
version=version,
|
|
151
|
+
**kwargs,
|
|
149
152
|
):
|
|
150
153
|
yield event
|
|
151
154
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""BYO-graph entry point: retrofit a pre-built ``CompiledStateGraph`` with
|
|
2
|
-
|
|
2
|
+
Hexgate policy. Tools are mutated in place so the graph keeps its
|
|
3
3
|
references; the returned :class:`HexgateLangchainAgent` opens a User
|
|
4
4
|
scope + Langfuse propagation per call. For the manifest-driven path,
|
|
5
5
|
use :func:`hexgate.enforce_policy` instead.
|
|
@@ -28,7 +28,7 @@ def wrap_langchain_agent(
|
|
|
28
28
|
tools: list[BaseTool],
|
|
29
29
|
api_key: str | None = None,
|
|
30
30
|
) -> HexgateLangchainAgent:
|
|
31
|
-
"""Wrap a pre-built LangGraph agent with
|
|
31
|
+
"""Wrap a pre-built LangGraph agent with Hexgate policy enforcement.
|
|
32
32
|
|
|
33
33
|
Mutates ``tools`` in place so the graph keeps its references.
|
|
34
34
|
The returned proxy takes ``user`` per invocation; role resolves at
|
|
@@ -32,7 +32,7 @@ from hexgate.security.enforcer import build_enforcer
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
class HexgateRunner:
|
|
35
|
-
"""Runner for OpenAI agents with
|
|
35
|
+
"""Runner for OpenAI agents with Hexgate tool policy and observability."""
|
|
36
36
|
|
|
37
37
|
def __init__(self, api_key: str | None = None):
|
|
38
38
|
self.api_key = api_key or os.getenv("HEXGATE_KEY")
|
|
@@ -49,7 +49,7 @@ def wrap_pydantic_agent(
|
|
|
49
49
|
agent: Agent,
|
|
50
50
|
api_key: str | None = None,
|
|
51
51
|
) -> HexgatePydanticAgent:
|
|
52
|
-
"""Wrap a pydantic_ai agent with
|
|
52
|
+
"""Wrap a pydantic_ai agent with Hexgate policy + observability.
|
|
53
53
|
|
|
54
54
|
Returns a :class:`HexgatePydanticAgent` backed by a clone of the
|
|
55
55
|
caller's ``agent``; the original is not mutated. The proxy takes
|
|
@@ -190,6 +190,13 @@ def extract_input_text(input: AgentInput) -> str:
|
|
|
190
190
|
return _extract_query_from_messages(input)
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
# One-shot dedupe for the "local agent with active User scope" warning
|
|
194
|
+
# inside `_resolve_user_facts`. The dashboard playground hits this once
|
|
195
|
+
# per turn (and the platform-served sessions never hit it at all), so
|
|
196
|
+
# without a flag the warning floods the log on every chat message.
|
|
197
|
+
_warned_local_agent_user_scope: bool = False
|
|
198
|
+
|
|
199
|
+
|
|
193
200
|
def _resolve_user_facts(agent: HexgateAgent) -> dict[str, list[str | int]] | None:
|
|
194
201
|
"""Lazily attenuate when a :class:`User` scope is active.
|
|
195
202
|
|
|
@@ -206,14 +213,22 @@ def _resolve_user_facts(agent: HexgateAgent) -> dict[str, list[str | int]] | Non
|
|
|
206
213
|
client = agent.hexgate_client
|
|
207
214
|
if client is None:
|
|
208
215
|
# Local agent or test stub — User scope is set but there's nothing to
|
|
209
|
-
# attenuate against. Surface a single warning so devs see why their
|
|
210
|
-
# `requires_user` predicate isn't firing on a local-loaded agent
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
216
|
+
# attenuate against. Surface a *single* warning so devs see why their
|
|
217
|
+
# `requires_user` predicate isn't firing on a local-loaded agent, then
|
|
218
|
+
# stay quiet: every subsequent turn would re-fire the same message
|
|
219
|
+
# otherwise (3-5x per chat session in the dashboard playground was
|
|
220
|
+
# the symptom).
|
|
221
|
+
global _warned_local_agent_user_scope
|
|
222
|
+
if not _warned_local_agent_user_scope:
|
|
223
|
+
import logging
|
|
224
|
+
|
|
225
|
+
logging.getLogger(__name__).warning(
|
|
226
|
+
"User scope active but agent has no hexgate_client; "
|
|
227
|
+
"biscuit_facts will be empty (use load_hexgate_agent for "
|
|
228
|
+
"attenuation). Subsequent occurrences in this process are "
|
|
229
|
+
"suppressed."
|
|
230
|
+
)
|
|
231
|
+
_warned_local_agent_user_scope = True
|
|
217
232
|
return None
|
|
218
233
|
from hexgate.cloud.attenuate import attenuate_for_user
|
|
219
234
|
from hexgate.cloud.biscuit import (
|
|
@@ -462,10 +462,10 @@ def load_hexgate_agent(
|
|
|
462
462
|
approval_handler: ApprovalHandler | None = None,
|
|
463
463
|
decision_observer: "DecisionObserver | None" = None,
|
|
464
464
|
) -> tuple[AgentGraph, CallbackHandler]:
|
|
465
|
-
"""Fetch an agent from
|
|
465
|
+
"""Fetch an agent from Hexgate and return it with policy enforcement applied.
|
|
466
466
|
|
|
467
467
|
Mirrors `load_local_agent` but sources the three YAMLs (agent, policy, system)
|
|
468
|
-
from the
|
|
468
|
+
from the Hexgate API instead of disk. Tool resolution and enforcement are
|
|
469
469
|
identical — only the bytes' origin differs.
|
|
470
470
|
|
|
471
471
|
``name`` is required. The Phase-7 env-var fallback chain
|
|
@@ -555,7 +555,7 @@ def load_agent(
|
|
|
555
555
|
approval_handler: ApprovalHandler | None = None,
|
|
556
556
|
decision_observer: "DecisionObserver | None" = None,
|
|
557
557
|
) -> tuple[AgentGraph, CallbackHandler]:
|
|
558
|
-
"""Load an agent from
|
|
558
|
+
"""Load an agent from Hexgate (when HEXGATE_KEY is set), local, or builtin.
|
|
559
559
|
|
|
560
560
|
``name`` is required for every path post-Phase 7 — the
|
|
561
561
|
HEXGATE_AGENT_NAME env-var fallback was removed when ``hexgate
|
|
@@ -584,7 +584,7 @@ def load_agent(
|
|
|
584
584
|
decision_observer=decision_observer,
|
|
585
585
|
)
|
|
586
586
|
if name is None:
|
|
587
|
-
raise ValueError("load_agent() requires a name when not using
|
|
587
|
+
raise ValueError("load_agent() requires a name when not using Hexgate Cloud")
|
|
588
588
|
source = resolve_agent_source(name, base_dir)
|
|
589
589
|
if source == "local":
|
|
590
590
|
return load_local_agent(
|
|
@@ -149,7 +149,13 @@ class AuditSender:
|
|
|
149
149
|
# latch onto the first loop that drives them and reject any other
|
|
150
150
|
# (e.g. a second asyncio.run()). Build them eagerly so configure()
|
|
151
151
|
# stays sync, but track the loop and rebuild if it rotates.
|
|
152
|
-
|
|
152
|
+
#
|
|
153
|
+
# Capture the build-time loop so emit() can reach it from an executor
|
|
154
|
+
# thread (a sync tool under run_in_executor has no loop of its own).
|
|
155
|
+
try:
|
|
156
|
+
self._loop: asyncio.AbstractEventLoop | None = asyncio.get_running_loop()
|
|
157
|
+
except RuntimeError:
|
|
158
|
+
self._loop = None
|
|
153
159
|
self._semaphore = asyncio.Semaphore(max_in_flight)
|
|
154
160
|
self._client: httpx.AsyncClient | None = self._new_client()
|
|
155
161
|
self._tasks: set[asyncio.Task[None]] = set()
|
|
@@ -182,11 +188,31 @@ class AuditSender:
|
|
|
182
188
|
try:
|
|
183
189
|
loop = asyncio.get_running_loop()
|
|
184
190
|
except RuntimeError:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
191
|
+
# Called off-loop (sync tool on a run_in_executor thread): route
|
|
192
|
+
# to the build-time loop instead of dropping the event.
|
|
193
|
+
loop = self._loop
|
|
194
|
+
if loop is None or loop.is_closed():
|
|
195
|
+
if not self._warned_no_loop:
|
|
196
|
+
_log.warning(
|
|
197
|
+
"audit emit called with no running loop and no live "
|
|
198
|
+
"bound loop; skipping"
|
|
199
|
+
)
|
|
200
|
+
self._warned_no_loop = True
|
|
201
|
+
return
|
|
202
|
+
try:
|
|
203
|
+
loop.call_soon_threadsafe(self._spawn_send, event)
|
|
204
|
+
except RuntimeError:
|
|
205
|
+
pass # loop torn down between the is_closed() check and the call
|
|
188
206
|
return
|
|
189
207
|
self._ensure_loop_state(loop)
|
|
208
|
+
self._spawn_send(event)
|
|
209
|
+
|
|
210
|
+
def _spawn_send(self, event: AuditEvent) -> None:
|
|
211
|
+
"""Create the send task. MUST run on the bound loop's thread —
|
|
212
|
+
``create_task`` and the loop-bound semaphore require the running loop.
|
|
213
|
+
Reached on-loop from :meth:`emit`, or via ``call_soon_threadsafe``."""
|
|
214
|
+
if self._closing or self._client is None:
|
|
215
|
+
return
|
|
190
216
|
if self._semaphore.locked():
|
|
191
217
|
self._dropped += 1
|
|
192
218
|
if self._dropped % 100 == 1:
|