hexgate 0.2.4__tar.gz → 0.2.6__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.4 → hexgate-0.2.6}/PKG-INFO +92 -29
- {hexgate-0.2.4 → hexgate-0.2.6}/README.md +89 -28
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/google/runner.py +3 -3
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/langchain/wrapper.py +4 -5
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/openai/runner.py +3 -3
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/pydantic_ai/wrapper.py +4 -4
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/factory.py +6 -7
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/loader.py +4 -4
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/audit.py +10 -9
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/bootstrap.py +5 -6
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/_common.py +37 -8
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/chat.py +1 -1
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/register.py +4 -5
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cloud/attenuate.py +1 -1
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cloud/biscuit.py +1 -1
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cloud/client.py +7 -9
- hexgate-0.2.6/hexgate/config/env.py +38 -0
- hexgate-0.2.6/hexgate/config/settings.py +34 -0
- hexgate-0.2.6/hexgate/mcp/__init__.py +47 -0
- hexgate-0.2.6/hexgate/mcp/client.py +170 -0
- hexgate-0.2.6/hexgate/mcp/config.py +93 -0
- hexgate-0.2.6/hexgate/mcp/proxy.py +347 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/__init__.py +2 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/binding.py +35 -5
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/enforcer.py +1 -1
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/source.py +135 -40
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate.egg-info/PKG-INFO +92 -29
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate.egg-info/SOURCES.txt +5 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate.egg-info/requires.txt +2 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/pyproject.toml +32 -1
- {hexgate-0.2.4 → hexgate-0.2.6}/tests/test_bootstrap.py +9 -12
- hexgate-0.2.4/hexgate/config/settings.py +0 -50
- {hexgate-0.2.4 → hexgate-0.2.6}/LICENSE +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/google/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/google/tools.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/google/wrapper.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/langchain/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/langchain/agent.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/langchain/tools.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/openai/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/openai/tools.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/openai/wrapper.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/pydantic_ai/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/pydantic_ai/agent.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/pydantic_ai/tools.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/builtin/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/builtin/researcher/agent.yaml +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/builtin/researcher/policy.yaml +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/builtin/researcher/system.md +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/models.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/prompts/agent_system.md +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/policy/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/policy/main.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/google.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/hexgate.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/langchain.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/main.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/manifest.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/models.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/openai.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/pydantic_ai.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/serve.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/state.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cloud/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/config/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/runtime/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/runtime/command_policy.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/runtime/context.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/runtime/sandbox_runtime.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/runtime/srt.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/runtime/workspace.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/bundle.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/constraints.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/decision.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/errors.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/file_scope.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/models.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/policy.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/policy_set.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/rego.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/rego_wasm.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/signing.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/wasm_engine.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/streaming/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/streaming/events.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/streaming/normalize.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/bash.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/decorators.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/fetch.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/_common.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/edit_file.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/glob.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/grep.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/read_file.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/write_file.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/refund.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/websearch.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tracing/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tracing/langfuse.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/utils/__init__.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/utils/retry.py +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate.egg-info/dependency_links.txt +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate.egg-info/entry_points.txt +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/hexgate.egg-info/top_level.txt +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/setup.cfg +0 -0
- {hexgate-0.2.4 → hexgate-0.2.6}/tests/test_demo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexgate
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Hexgate — authorization infrastructure for AI agents (agent runtime + cloud client).
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.13
|
|
@@ -29,11 +29,13 @@ Requires-Dist: litellm>=1.50
|
|
|
29
29
|
Requires-Dist: openinference-instrumentation-google-adk>=0.1.11
|
|
30
30
|
Requires-Dist: pydantic-ai-slim>=1.88.0
|
|
31
31
|
Requires-Dist: wasmtime>=20.0
|
|
32
|
+
Requires-Dist: mcp>=1.0
|
|
32
33
|
Provides-Extra: dev
|
|
33
34
|
Requires-Dist: ipykernel; extra == "dev"
|
|
34
35
|
Requires-Dist: jupyter; extra == "dev"
|
|
35
36
|
Requires-Dist: pytest>=8.4.1; extra == "dev"
|
|
36
37
|
Requires-Dist: pytest-asyncio>=1.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-cov>=6.0.0; extra == "dev"
|
|
37
39
|
Requires-Dist: ruff>=0.12.2; extra == "dev"
|
|
38
40
|
Dynamic: license-file
|
|
39
41
|
|
|
@@ -50,6 +52,7 @@ Policy enforcement, signed policy bundles, per-request user scope, audit trail
|
|
|
50
52
|
|
|
51
53
|
[](https://pypi.org/project/hexgate/)
|
|
52
54
|
[](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml)
|
|
55
|
+
[](https://codecov.io/gh/HexamindOrganisation/hexgate)
|
|
53
56
|
[](https://pypi.org/project/hexgate/)
|
|
54
57
|
[](LICENSE)
|
|
55
58
|
|
|
@@ -145,11 +148,11 @@ cp .env.sample .env
|
|
|
145
148
|
hexgate chat --agent example_agent
|
|
146
149
|
```
|
|
147
150
|
|
|
148
|
-
|
|
151
|
+
Keys the built-in example agent uses (bootstrap no longer hard-requires them — each is only needed if you invoke the piece that reads it):
|
|
149
152
|
|
|
150
|
-
- `OPENAI_API_KEY`
|
|
151
|
-
- `LINKUP_API_KEY`
|
|
152
|
-
- `TAVILY_API_KEY`
|
|
153
|
+
- `OPENAI_API_KEY` — the default `openai:gpt-5.4` model. Skip it if you pass your own model; if it's missing, the model provider raises a key-named error when the agent is built.
|
|
154
|
+
- `LINKUP_API_KEY` — the built-in `web_search` tool, which raises a clear error the first time it runs without a key.
|
|
155
|
+
- `TAVILY_API_KEY` — the built-in `fetch` tool, same as above.
|
|
153
156
|
|
|
154
157
|
Run `hexgate --help` to see all subcommands (`chat`, `serve`, `register`), and `hexgate <subcommand> --help` for the flags each one accepts.
|
|
155
158
|
|
|
@@ -173,7 +176,7 @@ The included local agent lives in `examples/example_agent/`, and the CLI can als
|
|
|
173
176
|
|
|
174
177
|
The two Quick Starts above aren't competing — they answer different questions.
|
|
175
178
|
|
|
176
|
-
**Inner loop — `hexgate chat`.** A single-process REPL against a local or builtin agent. No platform, no Docker, no browser. The chat command sets `HEXGATE_LOCAL_MODE=1` automatically so audit stays on your machine even if `
|
|
179
|
+
**Inner loop — `hexgate chat`.** A single-process REPL against a local or builtin agent. No platform, no Docker, no browser. The chat command sets `HEXGATE_LOCAL_MODE=1` automatically so audit stays on your machine even if `HEXGATE_API_KEY` lives in your `.env` from an earlier session. Denies and approval-required calls render as inline panels in the terminal — same `Decision` data the platform would log, surfaced where you're iterating. Reach for `chat` when you're authoring a policy YAML, tweaking a tool, or shaping a system prompt.
|
|
177
180
|
|
|
178
181
|
**Team loop — `hexgate serve` + dashboard Playground.** Same agent code, but the policy + decisions round-trip through the platform. You get auditable decisions in ClickHouse, the shared Playground UI, and live policy edits via the dashboard. Reach for `serve` when you're collaborating on an agent's behaviour, debugging a production-like trace, or demoing.
|
|
179
182
|
|
|
@@ -201,7 +204,7 @@ make dashboard
|
|
|
201
204
|
|
|
202
205
|
# Terminal 3 — mint a token, then serve your local agent
|
|
203
206
|
# 1. Open http://localhost:5173/tokens, click "Mint new token", copy the value.
|
|
204
|
-
# 2. Add to asianf/.env:
|
|
207
|
+
# 2. Add to asianf/.env: HEXGATE_API_KEY=fty_live_...
|
|
205
208
|
# 3. Pick the agent's Python entrypoint (module:attr — uvicorn-style)
|
|
206
209
|
# and let `hexgate serve` take over:
|
|
207
210
|
make serve # default — examples.customer_bot:agent
|
|
@@ -281,6 +284,22 @@ Integration tests (`pytest -m integration`) round-trip rows through the live Cli
|
|
|
281
284
|
|
|
282
285
|
The dashboard's `/policies` page lets you edit each agent's policy. `hexgate serve` re-fetches at every turn boundary, so your edits take effect on the next chat message without a restart.
|
|
283
286
|
|
|
287
|
+
### Outbound email (Resend)
|
|
288
|
+
|
|
289
|
+
Verification emails and password-reset links go through [Resend](https://resend.com). The platform API has two senders:
|
|
290
|
+
|
|
291
|
+
- **Dev (default)** — `StderrEmailSender` prints the mail body (including the magic link) to stderr. No keys needed; click the link out of the terminal to exercise the flow end-to-end.
|
|
292
|
+
- **Production** — `ResendEmailSender`, picked up automatically when both `RESEND_API_KEY` and `HEXGATE_EMAIL_FROM` are set. The from-address must be on a verified Resend domain.
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
# platform/api/.env
|
|
296
|
+
RESEND_API_KEY=re_…
|
|
297
|
+
HEXGATE_EMAIL_FROM="Hexgate <noreply@yourdomain.com>"
|
|
298
|
+
HEXGATE_DASHBOARD_URL=https://app.yourdomain.com # ← link host inside the email
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Partial config (only one var set) keeps the dev stderr sender so an operator notices the misconfig in the startup log line instead of silently failing every send at delivery. Resend API failures are logged at error level but never 5xx the calling endpoint — the user can request a new link.
|
|
302
|
+
|
|
284
303
|
## ✨ Core Primitives
|
|
285
304
|
|
|
286
305
|
The two main primitives are:
|
|
@@ -318,7 +337,7 @@ Dev wrote an OpenAI Agents / LangChain / Google ADK / Pydantic AI agent. They wr
|
|
|
318
337
|
from hexgate.adapters.openai import HexgateRunner # or .langchain.wrap_langchain_agent, .google.HexgateRunner, .pydantic_ai.wrap_pydantic_agent
|
|
319
338
|
from hexgate.runtime import User
|
|
320
339
|
|
|
321
|
-
runner = HexgateRunner() # picks up
|
|
340
|
+
runner = HexgateRunner() # picks up HEXGATE_API_KEY from env
|
|
322
341
|
await runner.run(
|
|
323
342
|
my_agent,
|
|
324
343
|
"refund 30",
|
|
@@ -353,8 +372,8 @@ Same enforcement seam, same `User` scope. The difference is whose system of reco
|
|
|
353
372
|
|
|
354
373
|
| What dev sets | What changes |
|
|
355
374
|
|---|---|
|
|
356
|
-
| `
|
|
357
|
-
| `HEXGATE_API_URL=
|
|
375
|
+
| `HEXGATE_API_KEY=fty_live_<project>_…` | Wakes up the platform path. Without it, adapters / `load_agent` fall back to local / builtin. (Renamed from `HEXGATE_KEY`, which is no longer read.) |
|
|
376
|
+
| `HEXGATE_API_URL=https://app.hexgate.ai` *(optional)* | Platform endpoint. Defaults to Hexgate Cloud (`https://app.hexgate.ai`). Set to `http://localhost:8000` only when self-hosting the platform locally — your key must be minted by whichever platform this points at. |
|
|
358
377
|
| `HEXGATE_LOCAL_POLICY=./policy.yaml` *or* `./bundle/` | Dev escape hatch: enforce a policy from disk, hot-reload on save. Wins over the platform's bundle. |
|
|
359
378
|
| `HEXGATE_BUNDLE_SIGN_KEY_PATH=./keys/dev.private` *(optional)* | Sign locally-recompiled yaml so `bundle.is_signed` reads True. |
|
|
360
379
|
| `HEXGATE_BUNDLE_PUBKEY_PATH=./keys/prod.public` *(optional)* | Verify a pre-built bundle dir against this pubkey on every reload. |
|
|
@@ -362,6 +381,11 @@ Same enforcement seam, same `User` scope. The difference is whose system of reco
|
|
|
362
381
|
|
|
363
382
|
No config object to instantiate, no `enforce_policy(...)` call to remember on the platform path. The adapter / loader threads it all through.
|
|
364
383
|
|
|
384
|
+
**Connecting to Hexgate.** The key and the URL are coupled: a `fty_live_…` key only verifies against the platform instance that minted it.
|
|
385
|
+
|
|
386
|
+
- **Hosted (default):** set `HEXGATE_API_KEY` to the key from [app.hexgate.ai](https://app.hexgate.ai). Leave `HEXGATE_API_URL` unset — it defaults to `https://app.hexgate.ai`.
|
|
387
|
+
- **Self-hosted / local platform:** additionally set `HEXGATE_API_URL=http://localhost:8000` (or your host), and use a key minted by *that* platform.
|
|
388
|
+
|
|
365
389
|
### Where enforcement actually happens
|
|
366
390
|
|
|
367
391
|
Walk through one tool call:
|
|
@@ -375,7 +399,7 @@ Walk through one tool call:
|
|
|
375
399
|
`_policy_source` is set automatically by the loader based on env:
|
|
376
400
|
|
|
377
401
|
- `HEXGATE_LOCAL_POLICY` set → `YamlPolicySource` or `BundleDirPolicySource` (mtime-driven refresh)
|
|
378
|
-
- `
|
|
402
|
+
- `HEXGATE_API_KEY` set, no local override → `PlatformPolicySource` (ETag / `304 Not Modified` refresh)
|
|
379
403
|
- Neither → no source attached; enforcement uses whatever was loaded once
|
|
380
404
|
|
|
381
405
|
**Scope of the per-turn refresh:** only the policy bundle. `system_prompt`, the manifest's tool list, and the model id are read once at agent construction and stay fixed for the lifetime of the process. Edit those on the dashboard and the change lands at the next `hexgate serve` restart — not at the next turn. The split is deliberate: policy is the operator's primary lever (and the one that needs to be auditable per-decision), while the manifest is an author-time concept.
|
|
@@ -385,7 +409,7 @@ Walk through one tool call:
|
|
|
385
409
|
1. **Per-call identity stays explicit.** `User` is the one piece the adapter can't infer from env, because it's per-request, not per-process. One line wrapping each call (`user=User(...)` kwarg on adapters, `async with User(...)` for native).
|
|
386
410
|
2. **`approval_required` tools.** If the policy uses that mode, dev decides what happens — pass `approval_handler=` (True / False / callable) when wrapping. Default for `hexgate serve` is auto-approve; for `hexgate chat` it prompts the TTY. Native code gets whatever the dev wires.
|
|
387
411
|
|
|
388
|
-
Everything else — fetch, verify, hot-reload, role selection, signature check, decision rendering, tracing — the runtime handles. Set `
|
|
412
|
+
Everything else — fetch, verify, hot-reload, role selection, signature check, decision rendering, tracing — the runtime handles. Set `HEXGATE_API_KEY` and wrap, or set `HEXGATE_LOCAL_POLICY` and wrap. That's the surface.
|
|
389
413
|
|
|
390
414
|
## 📦 What You Can Import
|
|
391
415
|
|
|
@@ -428,6 +452,46 @@ from hexgate import (
|
|
|
428
452
|
)
|
|
429
453
|
```
|
|
430
454
|
|
|
455
|
+
## 🔌 MCP servers (proxy)
|
|
456
|
+
|
|
457
|
+
`hexgate.mcp` wraps any [Model Context Protocol](https://modelcontextprotocol.io) server as a set of LangChain tools that flow through the same policy enforcement, audit, and approval pipeline as native `@agent_tool` functions. Zero glue code — `tools/list` runs at connect time and each exposed tool auto-registers under a `mcp-<server>-<tool>` namespace.
|
|
458
|
+
|
|
459
|
+
```python
|
|
460
|
+
from hexgate import create_agent, enforce_policy
|
|
461
|
+
from hexgate.mcp import MCPServerConfig, MCPToolset
|
|
462
|
+
|
|
463
|
+
slack = MCPServerConfig(
|
|
464
|
+
name="slack", transport="stdio",
|
|
465
|
+
command="slack-mcp-server",
|
|
466
|
+
env={"SLACK_TOKEN": "..."},
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
async with MCPToolset(slack) as mcp:
|
|
470
|
+
agent, handler = create_agent(model="gpt-5.4", tools=mcp.tools)
|
|
471
|
+
agent = enforce_policy(agent, "policy.yaml")
|
|
472
|
+
await agent.ainvoke({"messages": [...]}, config={"configurable": {}})
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Then in `policy.yaml` reference the MCP tools by qualified name:
|
|
476
|
+
|
|
477
|
+
```yaml
|
|
478
|
+
roles:
|
|
479
|
+
default:
|
|
480
|
+
tools:
|
|
481
|
+
"mcp-slack-list_channels": { mode: allow }
|
|
482
|
+
"mcp-slack-send_message":
|
|
483
|
+
mode: approval_required
|
|
484
|
+
"mcp-github-create_issue":
|
|
485
|
+
mode: allow
|
|
486
|
+
constraints: ["args.repo == 'hexgate'"]
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Hyphens (not colons or dots) because OpenAI Function Calling rejects other separators. Server names must match `^[a-z0-9-]{1,32}$` so qualified names stay under OpenAI's 64-char tool-name limit.
|
|
490
|
+
|
|
491
|
+
Both transports are supported: **stdio** (`command` + `args` + `env` for subprocess MCP servers) and **streamable HTTP** (`url` + `headers` for remote endpoints). The toolset is an async context manager — opening connects + lists every server's catalog; closing tears down transports symmetrically (with cleanup on partial-open failures).
|
|
492
|
+
|
|
493
|
+
**Try it:** `make demo-mcp` runs `examples/mcp_demo.py` — one self-contained file that spawns a tiny FastMCP server, attaches it, and walks through one tool call per policy outcome (allow / deny / approval-required). No external services, no LLM key.
|
|
494
|
+
|
|
431
495
|
## 🤝 Framework Agent Wrapping
|
|
432
496
|
|
|
433
497
|
In addition to its native `create_agent(...)` runtime, `hexgate` ships adapters that wrap agents built with **OpenAI Agents SDK**, **LangChain / LangGraph**, **Google ADK**, or **Pydantic AI** to add two things without touching the agent's logic:
|
|
@@ -447,7 +511,7 @@ The four integrations differ in shape because the underlying SDKs do:
|
|
|
447
511
|
|
|
448
512
|
Role resolution happens **at call time** from the active `User` contextvar — one wrapped agent serves many users concurrently because the scope is per-call. The original agent object is left intact (or, for LangChain BYO-graph tools, mutated by design so the same `tools` list flows through `create_react_agent`); the wrapper holds the policy.
|
|
449
513
|
|
|
450
|
-
All adapters resolve the API key the same way: from the explicit `api_key=` argument, falling back to the `
|
|
514
|
+
All adapters resolve the API key the same way: from the explicit `api_key=` argument, falling back to the `HEXGATE_API_KEY` environment variable.
|
|
451
515
|
|
|
452
516
|
### OpenAI Agents SDK — `HexgateRunner`
|
|
453
517
|
|
|
@@ -477,7 +541,7 @@ async def main():
|
|
|
477
541
|
model="gpt-4o-mini",
|
|
478
542
|
)
|
|
479
543
|
|
|
480
|
-
runner = HexgateRunner() # picks up
|
|
544
|
+
runner = HexgateRunner() # picks up HEXGATE_API_KEY from env
|
|
481
545
|
result = await runner.run(
|
|
482
546
|
agent,
|
|
483
547
|
"What's the weather in Cherbourg?",
|
|
@@ -537,7 +601,7 @@ async def main():
|
|
|
537
601
|
agent = wrap_langchain_agent(
|
|
538
602
|
agent=graph,
|
|
539
603
|
tools=TOOLS, # same list passed to create_react_agent — wrapped in place
|
|
540
|
-
api_key="sk-...", # or rely on
|
|
604
|
+
api_key="sk-...", # or rely on HEXGATE_API_KEY
|
|
541
605
|
)
|
|
542
606
|
|
|
543
607
|
result = await agent.ainvoke(
|
|
@@ -613,7 +677,7 @@ async def main():
|
|
|
613
677
|
agent=agent,
|
|
614
678
|
app_name="google_runner_example",
|
|
615
679
|
session_service=session_service,
|
|
616
|
-
) # picks up
|
|
680
|
+
) # picks up HEXGATE_API_KEY from env
|
|
617
681
|
|
|
618
682
|
user_msg = types.Content(
|
|
619
683
|
role="user", parts=[types.Part(text="What is the weather in New Delhi?")]
|
|
@@ -665,7 +729,7 @@ async def main():
|
|
|
665
729
|
|
|
666
730
|
agent = wrap_pydantic_agent(
|
|
667
731
|
agent=agent,
|
|
668
|
-
api_key="sk-...", # or rely on
|
|
732
|
+
api_key="sk-...", # or rely on HEXGATE_API_KEY
|
|
669
733
|
)
|
|
670
734
|
|
|
671
735
|
result = await agent.run(
|
|
@@ -1083,13 +1147,12 @@ Worth being explicit about the gaps so operators know where to layer their own c
|
|
|
1083
1147
|
|
|
1084
1148
|
## 🔧 Environment
|
|
1085
1149
|
|
|
1086
|
-
Copy `.env.sample` to `.env
|
|
1150
|
+
Copy `.env.sample` to `.env`. None of these are required to boot — set the ones whose feature you use. A missing key surfaces when that feature runs: the built-in tools raise their own clear error at call time, and the model provider raises a key-named error when the agent is built.
|
|
1087
1151
|
|
|
1088
|
-
- `OPENAI_API_KEY`
|
|
1089
|
-
- `LINKUP_API_KEY`
|
|
1090
|
-
- `TAVILY_API_KEY`
|
|
1091
|
-
- `LANGFUSE_SECRET_KEY`
|
|
1092
|
-
- `LANGFUSE_PUBLIC_KEY`
|
|
1152
|
+
- `OPENAI_API_KEY` — default model
|
|
1153
|
+
- `LINKUP_API_KEY` — built-in `web_search` tool
|
|
1154
|
+
- `TAVILY_API_KEY` — built-in `fetch` tool
|
|
1155
|
+
- `LANGFUSE_SECRET_KEY` / `LANGFUSE_PUBLIC_KEY` — tracing (optional)
|
|
1093
1156
|
- optional `LANGFUSE_HOST`
|
|
1094
1157
|
|
|
1095
1158
|
Policy-bundle enforcement (see [Policy Bundles](#-policy-bundles--compile-sign-enforce-wasm)) reads a few more, all optional:
|
|
@@ -1169,7 +1232,7 @@ Register a code-defined agent's manifest with the Hexgate platform. `--agent`
|
|
|
1169
1232
|
takes a Python import path of the form `module.path:attribute`, the same shape
|
|
1170
1233
|
as ASGI/WSGI entrypoints. The CLI imports the module, grabs the agent object,
|
|
1171
1234
|
and POSTs its manifest to `${HEXGATE_API_URL}/v1/agents` using
|
|
1172
|
-
`${
|
|
1235
|
+
`${HEXGATE_API_KEY}` as the bearer token:
|
|
1173
1236
|
|
|
1174
1237
|
```bash
|
|
1175
1238
|
hexgate register --agent examples.customer_bot:agent
|
|
@@ -1311,8 +1374,8 @@ Bridges your local agent runtime to the dashboard via the platform's WebSocket r
|
|
|
1311
1374
|
|
|
1312
1375
|
```bash
|
|
1313
1376
|
# in asianf/.env
|
|
1314
|
-
|
|
1315
|
-
HEXGATE_API_URL=http://localhost:8000 # optional
|
|
1377
|
+
HEXGATE_API_KEY=fty_live_<project>_<biscuit>
|
|
1378
|
+
HEXGATE_API_URL=http://localhost:8000 # optional; only when self-hosting the platform locally
|
|
1316
1379
|
|
|
1317
1380
|
# pick an agent module:attr — uvicorn-style spec
|
|
1318
1381
|
uv run hexgate serve examples.customer_bot:agent
|
|
@@ -1351,7 +1414,7 @@ There's no longer a `HEXGATE_AGENT_NAME` env var, `--agent` flag, or
|
|
|
1351
1414
|
`--use` flag — the spec carries everything. If you've been setting
|
|
1352
1415
|
`HEXGATE_AGENT_NAME` in `.env`, drop it.
|
|
1353
1416
|
|
|
1354
|
-
### How `load_agent()` resolves with `
|
|
1417
|
+
### How `load_agent()` resolves with `HEXGATE_API_KEY`
|
|
1355
1418
|
|
|
1356
1419
|
```python
|
|
1357
1420
|
from hexgate import load_agent
|
|
@@ -1359,8 +1422,8 @@ from hexgate import load_agent
|
|
|
1359
1422
|
agent, handler = load_agent("read_only") # explicit name required
|
|
1360
1423
|
```
|
|
1361
1424
|
|
|
1362
|
-
When `
|
|
1363
|
-
the platform (via `load_hexgate_agent`). When `
|
|
1425
|
+
When `HEXGATE_API_KEY` is set, `load_agent(name)` fetches the named agent from
|
|
1426
|
+
the platform (via `load_hexgate_agent`). When `HEXGATE_API_KEY` is not set, it
|
|
1364
1427
|
falls back to local / registered / builtin resolution — no platform call.
|
|
1365
1428
|
|
|
1366
1429
|
The legacy `HEXGATE_AGENT_NAME` env-var fallback was removed in Phase 7;
|
|
@@ -11,6 +11,7 @@ Policy enforcement, signed policy bundles, per-request user scope, audit trail
|
|
|
11
11
|
|
|
12
12
|
[](https://pypi.org/project/hexgate/)
|
|
13
13
|
[](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml)
|
|
14
|
+
[](https://codecov.io/gh/HexamindOrganisation/hexgate)
|
|
14
15
|
[](https://pypi.org/project/hexgate/)
|
|
15
16
|
[](LICENSE)
|
|
16
17
|
|
|
@@ -106,11 +107,11 @@ cp .env.sample .env
|
|
|
106
107
|
hexgate chat --agent example_agent
|
|
107
108
|
```
|
|
108
109
|
|
|
109
|
-
|
|
110
|
+
Keys the built-in example agent uses (bootstrap no longer hard-requires them — each is only needed if you invoke the piece that reads it):
|
|
110
111
|
|
|
111
|
-
- `OPENAI_API_KEY`
|
|
112
|
-
- `LINKUP_API_KEY`
|
|
113
|
-
- `TAVILY_API_KEY`
|
|
112
|
+
- `OPENAI_API_KEY` — the default `openai:gpt-5.4` model. Skip it if you pass your own model; if it's missing, the model provider raises a key-named error when the agent is built.
|
|
113
|
+
- `LINKUP_API_KEY` — the built-in `web_search` tool, which raises a clear error the first time it runs without a key.
|
|
114
|
+
- `TAVILY_API_KEY` — the built-in `fetch` tool, same as above.
|
|
114
115
|
|
|
115
116
|
Run `hexgate --help` to see all subcommands (`chat`, `serve`, `register`), and `hexgate <subcommand> --help` for the flags each one accepts.
|
|
116
117
|
|
|
@@ -134,7 +135,7 @@ The included local agent lives in `examples/example_agent/`, and the CLI can als
|
|
|
134
135
|
|
|
135
136
|
The two Quick Starts above aren't competing — they answer different questions.
|
|
136
137
|
|
|
137
|
-
**Inner loop — `hexgate chat`.** A single-process REPL against a local or builtin agent. No platform, no Docker, no browser. The chat command sets `HEXGATE_LOCAL_MODE=1` automatically so audit stays on your machine even if `
|
|
138
|
+
**Inner loop — `hexgate chat`.** A single-process REPL against a local or builtin agent. No platform, no Docker, no browser. The chat command sets `HEXGATE_LOCAL_MODE=1` automatically so audit stays on your machine even if `HEXGATE_API_KEY` lives in your `.env` from an earlier session. Denies and approval-required calls render as inline panels in the terminal — same `Decision` data the platform would log, surfaced where you're iterating. Reach for `chat` when you're authoring a policy YAML, tweaking a tool, or shaping a system prompt.
|
|
138
139
|
|
|
139
140
|
**Team loop — `hexgate serve` + dashboard Playground.** Same agent code, but the policy + decisions round-trip through the platform. You get auditable decisions in ClickHouse, the shared Playground UI, and live policy edits via the dashboard. Reach for `serve` when you're collaborating on an agent's behaviour, debugging a production-like trace, or demoing.
|
|
140
141
|
|
|
@@ -162,7 +163,7 @@ make dashboard
|
|
|
162
163
|
|
|
163
164
|
# Terminal 3 — mint a token, then serve your local agent
|
|
164
165
|
# 1. Open http://localhost:5173/tokens, click "Mint new token", copy the value.
|
|
165
|
-
# 2. Add to asianf/.env:
|
|
166
|
+
# 2. Add to asianf/.env: HEXGATE_API_KEY=fty_live_...
|
|
166
167
|
# 3. Pick the agent's Python entrypoint (module:attr — uvicorn-style)
|
|
167
168
|
# and let `hexgate serve` take over:
|
|
168
169
|
make serve # default — examples.customer_bot:agent
|
|
@@ -242,6 +243,22 @@ Integration tests (`pytest -m integration`) round-trip rows through the live Cli
|
|
|
242
243
|
|
|
243
244
|
The dashboard's `/policies` page lets you edit each agent's policy. `hexgate serve` re-fetches at every turn boundary, so your edits take effect on the next chat message without a restart.
|
|
244
245
|
|
|
246
|
+
### Outbound email (Resend)
|
|
247
|
+
|
|
248
|
+
Verification emails and password-reset links go through [Resend](https://resend.com). The platform API has two senders:
|
|
249
|
+
|
|
250
|
+
- **Dev (default)** — `StderrEmailSender` prints the mail body (including the magic link) to stderr. No keys needed; click the link out of the terminal to exercise the flow end-to-end.
|
|
251
|
+
- **Production** — `ResendEmailSender`, picked up automatically when both `RESEND_API_KEY` and `HEXGATE_EMAIL_FROM` are set. The from-address must be on a verified Resend domain.
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# platform/api/.env
|
|
255
|
+
RESEND_API_KEY=re_…
|
|
256
|
+
HEXGATE_EMAIL_FROM="Hexgate <noreply@yourdomain.com>"
|
|
257
|
+
HEXGATE_DASHBOARD_URL=https://app.yourdomain.com # ← link host inside the email
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Partial config (only one var set) keeps the dev stderr sender so an operator notices the misconfig in the startup log line instead of silently failing every send at delivery. Resend API failures are logged at error level but never 5xx the calling endpoint — the user can request a new link.
|
|
261
|
+
|
|
245
262
|
## ✨ Core Primitives
|
|
246
263
|
|
|
247
264
|
The two main primitives are:
|
|
@@ -279,7 +296,7 @@ Dev wrote an OpenAI Agents / LangChain / Google ADK / Pydantic AI agent. They wr
|
|
|
279
296
|
from hexgate.adapters.openai import HexgateRunner # or .langchain.wrap_langchain_agent, .google.HexgateRunner, .pydantic_ai.wrap_pydantic_agent
|
|
280
297
|
from hexgate.runtime import User
|
|
281
298
|
|
|
282
|
-
runner = HexgateRunner() # picks up
|
|
299
|
+
runner = HexgateRunner() # picks up HEXGATE_API_KEY from env
|
|
283
300
|
await runner.run(
|
|
284
301
|
my_agent,
|
|
285
302
|
"refund 30",
|
|
@@ -314,8 +331,8 @@ Same enforcement seam, same `User` scope. The difference is whose system of reco
|
|
|
314
331
|
|
|
315
332
|
| What dev sets | What changes |
|
|
316
333
|
|---|---|
|
|
317
|
-
| `
|
|
318
|
-
| `HEXGATE_API_URL=
|
|
334
|
+
| `HEXGATE_API_KEY=fty_live_<project>_…` | Wakes up the platform path. Without it, adapters / `load_agent` fall back to local / builtin. (Renamed from `HEXGATE_KEY`, which is no longer read.) |
|
|
335
|
+
| `HEXGATE_API_URL=https://app.hexgate.ai` *(optional)* | Platform endpoint. Defaults to Hexgate Cloud (`https://app.hexgate.ai`). Set to `http://localhost:8000` only when self-hosting the platform locally — your key must be minted by whichever platform this points at. |
|
|
319
336
|
| `HEXGATE_LOCAL_POLICY=./policy.yaml` *or* `./bundle/` | Dev escape hatch: enforce a policy from disk, hot-reload on save. Wins over the platform's bundle. |
|
|
320
337
|
| `HEXGATE_BUNDLE_SIGN_KEY_PATH=./keys/dev.private` *(optional)* | Sign locally-recompiled yaml so `bundle.is_signed` reads True. |
|
|
321
338
|
| `HEXGATE_BUNDLE_PUBKEY_PATH=./keys/prod.public` *(optional)* | Verify a pre-built bundle dir against this pubkey on every reload. |
|
|
@@ -323,6 +340,11 @@ Same enforcement seam, same `User` scope. The difference is whose system of reco
|
|
|
323
340
|
|
|
324
341
|
No config object to instantiate, no `enforce_policy(...)` call to remember on the platform path. The adapter / loader threads it all through.
|
|
325
342
|
|
|
343
|
+
**Connecting to Hexgate.** The key and the URL are coupled: a `fty_live_…` key only verifies against the platform instance that minted it.
|
|
344
|
+
|
|
345
|
+
- **Hosted (default):** set `HEXGATE_API_KEY` to the key from [app.hexgate.ai](https://app.hexgate.ai). Leave `HEXGATE_API_URL` unset — it defaults to `https://app.hexgate.ai`.
|
|
346
|
+
- **Self-hosted / local platform:** additionally set `HEXGATE_API_URL=http://localhost:8000` (or your host), and use a key minted by *that* platform.
|
|
347
|
+
|
|
326
348
|
### Where enforcement actually happens
|
|
327
349
|
|
|
328
350
|
Walk through one tool call:
|
|
@@ -336,7 +358,7 @@ Walk through one tool call:
|
|
|
336
358
|
`_policy_source` is set automatically by the loader based on env:
|
|
337
359
|
|
|
338
360
|
- `HEXGATE_LOCAL_POLICY` set → `YamlPolicySource` or `BundleDirPolicySource` (mtime-driven refresh)
|
|
339
|
-
- `
|
|
361
|
+
- `HEXGATE_API_KEY` set, no local override → `PlatformPolicySource` (ETag / `304 Not Modified` refresh)
|
|
340
362
|
- Neither → no source attached; enforcement uses whatever was loaded once
|
|
341
363
|
|
|
342
364
|
**Scope of the per-turn refresh:** only the policy bundle. `system_prompt`, the manifest's tool list, and the model id are read once at agent construction and stay fixed for the lifetime of the process. Edit those on the dashboard and the change lands at the next `hexgate serve` restart — not at the next turn. The split is deliberate: policy is the operator's primary lever (and the one that needs to be auditable per-decision), while the manifest is an author-time concept.
|
|
@@ -346,7 +368,7 @@ Walk through one tool call:
|
|
|
346
368
|
1. **Per-call identity stays explicit.** `User` is the one piece the adapter can't infer from env, because it's per-request, not per-process. One line wrapping each call (`user=User(...)` kwarg on adapters, `async with User(...)` for native).
|
|
347
369
|
2. **`approval_required` tools.** If the policy uses that mode, dev decides what happens — pass `approval_handler=` (True / False / callable) when wrapping. Default for `hexgate serve` is auto-approve; for `hexgate chat` it prompts the TTY. Native code gets whatever the dev wires.
|
|
348
370
|
|
|
349
|
-
Everything else — fetch, verify, hot-reload, role selection, signature check, decision rendering, tracing — the runtime handles. Set `
|
|
371
|
+
Everything else — fetch, verify, hot-reload, role selection, signature check, decision rendering, tracing — the runtime handles. Set `HEXGATE_API_KEY` and wrap, or set `HEXGATE_LOCAL_POLICY` and wrap. That's the surface.
|
|
350
372
|
|
|
351
373
|
## 📦 What You Can Import
|
|
352
374
|
|
|
@@ -389,6 +411,46 @@ from hexgate import (
|
|
|
389
411
|
)
|
|
390
412
|
```
|
|
391
413
|
|
|
414
|
+
## 🔌 MCP servers (proxy)
|
|
415
|
+
|
|
416
|
+
`hexgate.mcp` wraps any [Model Context Protocol](https://modelcontextprotocol.io) server as a set of LangChain tools that flow through the same policy enforcement, audit, and approval pipeline as native `@agent_tool` functions. Zero glue code — `tools/list` runs at connect time and each exposed tool auto-registers under a `mcp-<server>-<tool>` namespace.
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
from hexgate import create_agent, enforce_policy
|
|
420
|
+
from hexgate.mcp import MCPServerConfig, MCPToolset
|
|
421
|
+
|
|
422
|
+
slack = MCPServerConfig(
|
|
423
|
+
name="slack", transport="stdio",
|
|
424
|
+
command="slack-mcp-server",
|
|
425
|
+
env={"SLACK_TOKEN": "..."},
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
async with MCPToolset(slack) as mcp:
|
|
429
|
+
agent, handler = create_agent(model="gpt-5.4", tools=mcp.tools)
|
|
430
|
+
agent = enforce_policy(agent, "policy.yaml")
|
|
431
|
+
await agent.ainvoke({"messages": [...]}, config={"configurable": {}})
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Then in `policy.yaml` reference the MCP tools by qualified name:
|
|
435
|
+
|
|
436
|
+
```yaml
|
|
437
|
+
roles:
|
|
438
|
+
default:
|
|
439
|
+
tools:
|
|
440
|
+
"mcp-slack-list_channels": { mode: allow }
|
|
441
|
+
"mcp-slack-send_message":
|
|
442
|
+
mode: approval_required
|
|
443
|
+
"mcp-github-create_issue":
|
|
444
|
+
mode: allow
|
|
445
|
+
constraints: ["args.repo == 'hexgate'"]
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Hyphens (not colons or dots) because OpenAI Function Calling rejects other separators. Server names must match `^[a-z0-9-]{1,32}$` so qualified names stay under OpenAI's 64-char tool-name limit.
|
|
449
|
+
|
|
450
|
+
Both transports are supported: **stdio** (`command` + `args` + `env` for subprocess MCP servers) and **streamable HTTP** (`url` + `headers` for remote endpoints). The toolset is an async context manager — opening connects + lists every server's catalog; closing tears down transports symmetrically (with cleanup on partial-open failures).
|
|
451
|
+
|
|
452
|
+
**Try it:** `make demo-mcp` runs `examples/mcp_demo.py` — one self-contained file that spawns a tiny FastMCP server, attaches it, and walks through one tool call per policy outcome (allow / deny / approval-required). No external services, no LLM key.
|
|
453
|
+
|
|
392
454
|
## 🤝 Framework Agent Wrapping
|
|
393
455
|
|
|
394
456
|
In addition to its native `create_agent(...)` runtime, `hexgate` ships adapters that wrap agents built with **OpenAI Agents SDK**, **LangChain / LangGraph**, **Google ADK**, or **Pydantic AI** to add two things without touching the agent's logic:
|
|
@@ -408,7 +470,7 @@ The four integrations differ in shape because the underlying SDKs do:
|
|
|
408
470
|
|
|
409
471
|
Role resolution happens **at call time** from the active `User` contextvar — one wrapped agent serves many users concurrently because the scope is per-call. The original agent object is left intact (or, for LangChain BYO-graph tools, mutated by design so the same `tools` list flows through `create_react_agent`); the wrapper holds the policy.
|
|
410
472
|
|
|
411
|
-
All adapters resolve the API key the same way: from the explicit `api_key=` argument, falling back to the `
|
|
473
|
+
All adapters resolve the API key the same way: from the explicit `api_key=` argument, falling back to the `HEXGATE_API_KEY` environment variable.
|
|
412
474
|
|
|
413
475
|
### OpenAI Agents SDK — `HexgateRunner`
|
|
414
476
|
|
|
@@ -438,7 +500,7 @@ async def main():
|
|
|
438
500
|
model="gpt-4o-mini",
|
|
439
501
|
)
|
|
440
502
|
|
|
441
|
-
runner = HexgateRunner() # picks up
|
|
503
|
+
runner = HexgateRunner() # picks up HEXGATE_API_KEY from env
|
|
442
504
|
result = await runner.run(
|
|
443
505
|
agent,
|
|
444
506
|
"What's the weather in Cherbourg?",
|
|
@@ -498,7 +560,7 @@ async def main():
|
|
|
498
560
|
agent = wrap_langchain_agent(
|
|
499
561
|
agent=graph,
|
|
500
562
|
tools=TOOLS, # same list passed to create_react_agent — wrapped in place
|
|
501
|
-
api_key="sk-...", # or rely on
|
|
563
|
+
api_key="sk-...", # or rely on HEXGATE_API_KEY
|
|
502
564
|
)
|
|
503
565
|
|
|
504
566
|
result = await agent.ainvoke(
|
|
@@ -574,7 +636,7 @@ async def main():
|
|
|
574
636
|
agent=agent,
|
|
575
637
|
app_name="google_runner_example",
|
|
576
638
|
session_service=session_service,
|
|
577
|
-
) # picks up
|
|
639
|
+
) # picks up HEXGATE_API_KEY from env
|
|
578
640
|
|
|
579
641
|
user_msg = types.Content(
|
|
580
642
|
role="user", parts=[types.Part(text="What is the weather in New Delhi?")]
|
|
@@ -626,7 +688,7 @@ async def main():
|
|
|
626
688
|
|
|
627
689
|
agent = wrap_pydantic_agent(
|
|
628
690
|
agent=agent,
|
|
629
|
-
api_key="sk-...", # or rely on
|
|
691
|
+
api_key="sk-...", # or rely on HEXGATE_API_KEY
|
|
630
692
|
)
|
|
631
693
|
|
|
632
694
|
result = await agent.run(
|
|
@@ -1044,13 +1106,12 @@ Worth being explicit about the gaps so operators know where to layer their own c
|
|
|
1044
1106
|
|
|
1045
1107
|
## 🔧 Environment
|
|
1046
1108
|
|
|
1047
|
-
Copy `.env.sample` to `.env
|
|
1109
|
+
Copy `.env.sample` to `.env`. None of these are required to boot — set the ones whose feature you use. A missing key surfaces when that feature runs: the built-in tools raise their own clear error at call time, and the model provider raises a key-named error when the agent is built.
|
|
1048
1110
|
|
|
1049
|
-
- `OPENAI_API_KEY`
|
|
1050
|
-
- `LINKUP_API_KEY`
|
|
1051
|
-
- `TAVILY_API_KEY`
|
|
1052
|
-
- `LANGFUSE_SECRET_KEY`
|
|
1053
|
-
- `LANGFUSE_PUBLIC_KEY`
|
|
1111
|
+
- `OPENAI_API_KEY` — default model
|
|
1112
|
+
- `LINKUP_API_KEY` — built-in `web_search` tool
|
|
1113
|
+
- `TAVILY_API_KEY` — built-in `fetch` tool
|
|
1114
|
+
- `LANGFUSE_SECRET_KEY` / `LANGFUSE_PUBLIC_KEY` — tracing (optional)
|
|
1054
1115
|
- optional `LANGFUSE_HOST`
|
|
1055
1116
|
|
|
1056
1117
|
Policy-bundle enforcement (see [Policy Bundles](#-policy-bundles--compile-sign-enforce-wasm)) reads a few more, all optional:
|
|
@@ -1130,7 +1191,7 @@ Register a code-defined agent's manifest with the Hexgate platform. `--agent`
|
|
|
1130
1191
|
takes a Python import path of the form `module.path:attribute`, the same shape
|
|
1131
1192
|
as ASGI/WSGI entrypoints. The CLI imports the module, grabs the agent object,
|
|
1132
1193
|
and POSTs its manifest to `${HEXGATE_API_URL}/v1/agents` using
|
|
1133
|
-
`${
|
|
1194
|
+
`${HEXGATE_API_KEY}` as the bearer token:
|
|
1134
1195
|
|
|
1135
1196
|
```bash
|
|
1136
1197
|
hexgate register --agent examples.customer_bot:agent
|
|
@@ -1272,8 +1333,8 @@ Bridges your local agent runtime to the dashboard via the platform's WebSocket r
|
|
|
1272
1333
|
|
|
1273
1334
|
```bash
|
|
1274
1335
|
# in asianf/.env
|
|
1275
|
-
|
|
1276
|
-
HEXGATE_API_URL=http://localhost:8000 # optional
|
|
1336
|
+
HEXGATE_API_KEY=fty_live_<project>_<biscuit>
|
|
1337
|
+
HEXGATE_API_URL=http://localhost:8000 # optional; only when self-hosting the platform locally
|
|
1277
1338
|
|
|
1278
1339
|
# pick an agent module:attr — uvicorn-style spec
|
|
1279
1340
|
uv run hexgate serve examples.customer_bot:agent
|
|
@@ -1312,7 +1373,7 @@ There's no longer a `HEXGATE_AGENT_NAME` env var, `--agent` flag, or
|
|
|
1312
1373
|
`--use` flag — the spec carries everything. If you've been setting
|
|
1313
1374
|
`HEXGATE_AGENT_NAME` in `.env`, drop it.
|
|
1314
1375
|
|
|
1315
|
-
### How `load_agent()` resolves with `
|
|
1376
|
+
### How `load_agent()` resolves with `HEXGATE_API_KEY`
|
|
1316
1377
|
|
|
1317
1378
|
```python
|
|
1318
1379
|
from hexgate import load_agent
|
|
@@ -1320,8 +1381,8 @@ from hexgate import load_agent
|
|
|
1320
1381
|
agent, handler = load_agent("read_only") # explicit name required
|
|
1321
1382
|
```
|
|
1322
1383
|
|
|
1323
|
-
When `
|
|
1324
|
-
the platform (via `load_hexgate_agent`). When `
|
|
1384
|
+
When `HEXGATE_API_KEY` is set, `load_agent(name)` fetches the named agent from
|
|
1385
|
+
the platform (via `load_hexgate_agent`). When `HEXGATE_API_KEY` is not set, it
|
|
1325
1386
|
falls back to local / registered / builtin resolution — no platform call.
|
|
1326
1387
|
|
|
1327
1388
|
The legacy `HEXGATE_AGENT_NAME` env-var fallback was removed in Phase 7;
|
|
@@ -4,7 +4,6 @@ active role. Langfuse propagation mirrors User identity into spans.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
7
|
-
import os
|
|
8
7
|
from contextlib import contextmanager
|
|
9
8
|
from typing import Any, AsyncGenerator, Generator
|
|
10
9
|
|
|
@@ -17,6 +16,7 @@ from langfuse import get_client, propagate_attributes
|
|
|
17
16
|
from openinference.instrumentation.google_adk import GoogleADKInstrumentor
|
|
18
17
|
|
|
19
18
|
from hexgate.adapters.google.wrapper import wrap_google_agent
|
|
19
|
+
from hexgate.config.env import resolve_api_key
|
|
20
20
|
from hexgate.runtime import User
|
|
21
21
|
|
|
22
22
|
|
|
@@ -32,10 +32,10 @@ class HexgateRunner:
|
|
|
32
32
|
api_key: str | None = None,
|
|
33
33
|
**runner_kwargs: Any,
|
|
34
34
|
):
|
|
35
|
-
self.api_key = api_key
|
|
35
|
+
self.api_key = resolve_api_key(api_key)
|
|
36
36
|
if self.api_key is None:
|
|
37
37
|
raise ValueError(
|
|
38
|
-
"
|
|
38
|
+
"HEXGATE_API_KEY is not set. Pass api_key= explicitly or set the HEXGATE_API_KEY environment variable."
|
|
39
39
|
)
|
|
40
40
|
# Policy resolves at construction (the loud-failure point); the
|
|
41
41
|
# Runner is built once — refresh swaps the enforcer's policy
|
|
@@ -11,13 +11,12 @@ proxy at the top of every call.
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
import os
|
|
15
|
-
|
|
16
14
|
from langchain_core.tools import BaseTool
|
|
17
15
|
from langgraph.graph.state import CompiledStateGraph
|
|
18
16
|
|
|
19
17
|
from hexgate.adapters.langchain.agent import HexgateLangchainAgent
|
|
20
18
|
from hexgate.adapters.langchain.tools import install_enforcer_on_tools
|
|
19
|
+
from hexgate.config.env import resolve_api_key
|
|
21
20
|
from hexgate.security.binding import PolicyBinding, resolve_policy
|
|
22
21
|
from hexgate.security.enforcer import build_enforcer
|
|
23
22
|
|
|
@@ -33,14 +32,14 @@ def wrap_langchain_agent(
|
|
|
33
32
|
Mutates ``tools`` in place so the graph keeps its references.
|
|
34
33
|
The returned proxy takes ``user`` per invocation; role resolves at
|
|
35
34
|
call time from the active :class:`User`. ``api_key`` falls back to
|
|
36
|
-
``
|
|
35
|
+
``HEXGATE_API_KEY``. ``NEEDS_APPROVAL`` outcomes render as structured
|
|
37
36
|
errors — wire any host-side approval flow outside the SDK. The
|
|
38
37
|
enforced policy is the platform's; unlisted tools are denied.
|
|
39
38
|
"""
|
|
40
|
-
resolved_key = api_key
|
|
39
|
+
resolved_key = resolve_api_key(api_key)
|
|
41
40
|
if not resolved_key:
|
|
42
41
|
raise ValueError(
|
|
43
|
-
"No API key provided. Pass api_key= explicitly or set
|
|
42
|
+
"No API key provided. Pass api_key= explicitly or set the HEXGATE_API_KEY environment variable."
|
|
44
43
|
)
|
|
45
44
|
|
|
46
45
|
agent_name = getattr(agent, "name", "default")
|
|
@@ -8,7 +8,6 @@ enforcer, so a refresh swap reaches every clone.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
|
-
import os
|
|
12
11
|
from contextlib import contextmanager
|
|
13
12
|
|
|
14
13
|
import nest_asyncio
|
|
@@ -26,6 +25,7 @@ from langfuse import get_client, propagate_attributes
|
|
|
26
25
|
from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor
|
|
27
26
|
|
|
28
27
|
from hexgate.adapters.openai.wrapper import wrap_openai_agent
|
|
28
|
+
from hexgate.config.env import resolve_api_key
|
|
29
29
|
from hexgate.runtime import User
|
|
30
30
|
from hexgate.security.binding import PolicyBinding, resolve_policy
|
|
31
31
|
from hexgate.security.enforcer import build_enforcer
|
|
@@ -35,10 +35,10 @@ class HexgateRunner:
|
|
|
35
35
|
"""Runner for OpenAI agents with Hexgate tool policy and observability."""
|
|
36
36
|
|
|
37
37
|
def __init__(self, api_key: str | None = None):
|
|
38
|
-
self.api_key = api_key
|
|
38
|
+
self.api_key = resolve_api_key(api_key)
|
|
39
39
|
if self.api_key is None:
|
|
40
40
|
raise ValueError(
|
|
41
|
-
"
|
|
41
|
+
"HEXGATE_API_KEY is not set. Pass api_key= explicitly or set the HEXGATE_API_KEY environment variable."
|
|
42
42
|
)
|
|
43
43
|
# Cached per agent name — keeps the ETag memory alive across runs.
|
|
44
44
|
self._bindings: dict[str, PolicyBinding] = {}
|