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.
Files changed (113) hide show
  1. {hexgate-0.2.4 → hexgate-0.2.6}/PKG-INFO +92 -29
  2. {hexgate-0.2.4 → hexgate-0.2.6}/README.md +89 -28
  3. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/google/runner.py +3 -3
  4. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/langchain/wrapper.py +4 -5
  5. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/openai/runner.py +3 -3
  6. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/pydantic_ai/wrapper.py +4 -4
  7. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/factory.py +6 -7
  8. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/loader.py +4 -4
  9. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/audit.py +10 -9
  10. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/bootstrap.py +5 -6
  11. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/_common.py +37 -8
  12. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/chat.py +1 -1
  13. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/register.py +4 -5
  14. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cloud/attenuate.py +1 -1
  15. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cloud/biscuit.py +1 -1
  16. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cloud/client.py +7 -9
  17. hexgate-0.2.6/hexgate/config/env.py +38 -0
  18. hexgate-0.2.6/hexgate/config/settings.py +34 -0
  19. hexgate-0.2.6/hexgate/mcp/__init__.py +47 -0
  20. hexgate-0.2.6/hexgate/mcp/client.py +170 -0
  21. hexgate-0.2.6/hexgate/mcp/config.py +93 -0
  22. hexgate-0.2.6/hexgate/mcp/proxy.py +347 -0
  23. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/__init__.py +2 -0
  24. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/binding.py +35 -5
  25. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/enforcer.py +1 -1
  26. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/source.py +135 -40
  27. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate.egg-info/PKG-INFO +92 -29
  28. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate.egg-info/SOURCES.txt +5 -0
  29. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate.egg-info/requires.txt +2 -0
  30. {hexgate-0.2.4 → hexgate-0.2.6}/pyproject.toml +32 -1
  31. {hexgate-0.2.4 → hexgate-0.2.6}/tests/test_bootstrap.py +9 -12
  32. hexgate-0.2.4/hexgate/config/settings.py +0 -50
  33. {hexgate-0.2.4 → hexgate-0.2.6}/LICENSE +0 -0
  34. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/__init__.py +0 -0
  35. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/__init__.py +0 -0
  36. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/google/__init__.py +0 -0
  37. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/google/tools.py +0 -0
  38. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/google/wrapper.py +0 -0
  39. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/langchain/__init__.py +0 -0
  40. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/langchain/agent.py +0 -0
  41. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/langchain/tools.py +0 -0
  42. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/openai/__init__.py +0 -0
  43. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/openai/tools.py +0 -0
  44. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/openai/wrapper.py +0 -0
  45. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/pydantic_ai/__init__.py +0 -0
  46. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/pydantic_ai/agent.py +0 -0
  47. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/adapters/pydantic_ai/tools.py +0 -0
  48. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/__init__.py +0 -0
  49. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/builtin/__init__.py +0 -0
  50. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/builtin/researcher/agent.yaml +0 -0
  51. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/builtin/researcher/policy.yaml +0 -0
  52. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/builtin/researcher/system.md +0 -0
  53. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/models.py +0 -0
  54. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/agents/prompts/agent_system.md +0 -0
  55. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/__init__.py +0 -0
  56. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/policy/__init__.py +0 -0
  57. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/policy/main.py +0 -0
  58. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/__init__.py +0 -0
  59. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/google.py +0 -0
  60. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/hexgate.py +0 -0
  61. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/langchain.py +0 -0
  62. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/main.py +0 -0
  63. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/manifest.py +0 -0
  64. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/models.py +0 -0
  65. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/openai.py +0 -0
  66. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/register/pydantic_ai.py +0 -0
  67. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/serve.py +0 -0
  68. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cli/state.py +0 -0
  69. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/cloud/__init__.py +0 -0
  70. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/config/__init__.py +0 -0
  71. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/runtime/__init__.py +0 -0
  72. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/runtime/command_policy.py +0 -0
  73. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/runtime/context.py +0 -0
  74. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/runtime/sandbox_runtime.py +0 -0
  75. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/runtime/srt.py +0 -0
  76. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/runtime/workspace.py +0 -0
  77. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/bundle.py +0 -0
  78. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/constraints.py +0 -0
  79. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/decision.py +0 -0
  80. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/errors.py +0 -0
  81. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/file_scope.py +0 -0
  82. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/models.py +0 -0
  83. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/policy.py +0 -0
  84. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/policy_set.py +0 -0
  85. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/rego.py +0 -0
  86. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/rego_wasm.py +0 -0
  87. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/signing.py +0 -0
  88. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/security/wasm_engine.py +0 -0
  89. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/streaming/__init__.py +0 -0
  90. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/streaming/events.py +0 -0
  91. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/streaming/normalize.py +0 -0
  92. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/__init__.py +0 -0
  93. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/bash.py +0 -0
  94. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/decorators.py +0 -0
  95. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/fetch.py +0 -0
  96. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/__init__.py +0 -0
  97. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/_common.py +0 -0
  98. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/edit_file.py +0 -0
  99. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/glob.py +0 -0
  100. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/grep.py +0 -0
  101. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/read_file.py +0 -0
  102. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/files/write_file.py +0 -0
  103. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/refund.py +0 -0
  104. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tools/websearch.py +0 -0
  105. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tracing/__init__.py +0 -0
  106. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/tracing/langfuse.py +0 -0
  107. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/utils/__init__.py +0 -0
  108. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate/utils/retry.py +0 -0
  109. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate.egg-info/dependency_links.txt +0 -0
  110. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate.egg-info/entry_points.txt +0 -0
  111. {hexgate-0.2.4 → hexgate-0.2.6}/hexgate.egg-info/top_level.txt +0 -0
  112. {hexgate-0.2.4 → hexgate-0.2.6}/setup.cfg +0 -0
  113. {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.4
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
  [![PyPI](https://img.shields.io/pypi/v/hexgate?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/hexgate/)
52
54
  [![CI](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml)
55
+ [![codecov](https://codecov.io/gh/HexamindOrganisation/hexgate/branch/main/graph/badge.svg?flag=sdk)](https://codecov.io/gh/HexamindOrganisation/hexgate)
53
56
  [![Downloads](https://img.shields.io/pypi/dm/hexgate?color=blueviolet)](https://pypi.org/project/hexgate/)
54
57
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
55
58
 
@@ -145,11 +148,11 @@ cp .env.sample .env
145
148
  hexgate chat --agent example_agent
146
149
  ```
147
150
 
148
- Required keys for the example CLI flow:
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 `HEXGATE_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.
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: HEXGATE_KEY=fty_live_...
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 HEXGATE_KEY from env
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
- | `HEXGATE_KEY=fty_live_<project>_…` | Wakes up the platform path. Without it, adapters / `load_agent` fall back to local / builtin. |
357
- | `HEXGATE_API_URL=http://localhost:8000` *(optional)* | Platform endpoint. Defaults to localhost. |
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
- - `HEXGATE_KEY` set, no local override → `PlatformPolicySource` (ETag / `304 Not Modified` refresh)
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 `HEXGATE_KEY` and wrap, or set `HEXGATE_LOCAL_POLICY` and wrap. That's the surface.
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 `HEXGATE_KEY` environment variable.
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 HEXGATE_KEY from env
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 HEXGATE_KEY
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 HEXGATE_KEY from env
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 HEXGATE_KEY
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` and set:
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
- `${HEXGATE_KEY}` as the bearer token:
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
- HEXGATE_KEY=fty_live_<project>_<biscuit>
1315
- HEXGATE_API_URL=http://localhost:8000 # optional, defaults to localhost:8000
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 `HEXGATE_KEY`
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 `HEXGATE_KEY` is set, `load_agent(name)` fetches the named agent from
1363
- the platform (via `load_hexgate_agent`). When `HEXGATE_KEY` is not set, it
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
  [![PyPI](https://img.shields.io/pypi/v/hexgate?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/hexgate/)
13
13
  [![CI](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/HexamindOrganisation/hexgate/actions/workflows/tests.yml)
14
+ [![codecov](https://codecov.io/gh/HexamindOrganisation/hexgate/branch/main/graph/badge.svg?flag=sdk)](https://codecov.io/gh/HexamindOrganisation/hexgate)
14
15
  [![Downloads](https://img.shields.io/pypi/dm/hexgate?color=blueviolet)](https://pypi.org/project/hexgate/)
15
16
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
16
17
 
@@ -106,11 +107,11 @@ cp .env.sample .env
106
107
  hexgate chat --agent example_agent
107
108
  ```
108
109
 
109
- Required keys for the example CLI flow:
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 `HEXGATE_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
+ **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: HEXGATE_KEY=fty_live_...
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 HEXGATE_KEY from env
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
- | `HEXGATE_KEY=fty_live_<project>_…` | Wakes up the platform path. Without it, adapters / `load_agent` fall back to local / builtin. |
318
- | `HEXGATE_API_URL=http://localhost:8000` *(optional)* | Platform endpoint. Defaults to localhost. |
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
- - `HEXGATE_KEY` set, no local override → `PlatformPolicySource` (ETag / `304 Not Modified` refresh)
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 `HEXGATE_KEY` and wrap, or set `HEXGATE_LOCAL_POLICY` and wrap. That's the surface.
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 `HEXGATE_KEY` environment variable.
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 HEXGATE_KEY from env
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 HEXGATE_KEY
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 HEXGATE_KEY from env
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 HEXGATE_KEY
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` and set:
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
- `${HEXGATE_KEY}` as the bearer token:
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
- HEXGATE_KEY=fty_live_<project>_<biscuit>
1276
- HEXGATE_API_URL=http://localhost:8000 # optional, defaults to localhost:8000
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 `HEXGATE_KEY`
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 `HEXGATE_KEY` is set, `load_agent(name)` fetches the named agent from
1324
- the platform (via `load_hexgate_agent`). When `HEXGATE_KEY` is not set, it
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 or os.getenv("HEXGATE_KEY")
35
+ self.api_key = resolve_api_key(api_key)
36
36
  if self.api_key is None:
37
37
  raise ValueError(
38
- "HEXGATE_KEY is not set. Pass api_key= explicitly or set HEXGATE_KEY environment variable."
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
- ``HEXGATE_KEY``. ``NEEDS_APPROVAL`` outcomes render as structured
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 if api_key else os.getenv("HEXGATE_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 HEXGATE_KEY environment variable."
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 or os.getenv("HEXGATE_KEY")
38
+ self.api_key = resolve_api_key(api_key)
39
39
  if self.api_key is None:
40
40
  raise ValueError(
41
- "HEXGATE_KEY is not set. Pass api_key= explicitly or set HEXGATE_KEY environment variable."
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] = {}