hexgate 0.2.5__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.5 → hexgate-0.2.6}/PKG-INFO +90 -29
  2. {hexgate-0.2.5 → hexgate-0.2.6}/README.md +88 -28
  3. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/google/runner.py +3 -3
  4. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/langchain/wrapper.py +4 -5
  5. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/openai/runner.py +3 -3
  6. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/pydantic_ai/wrapper.py +4 -4
  7. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/agents/factory.py +6 -7
  8. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/agents/loader.py +4 -4
  9. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/audit.py +10 -9
  10. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/bootstrap.py +5 -6
  11. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/_common.py +37 -8
  12. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/chat.py +1 -1
  13. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/register/register.py +4 -5
  14. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cloud/attenuate.py +1 -1
  15. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cloud/biscuit.py +1 -1
  16. {hexgate-0.2.5 → 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.5 → hexgate-0.2.6}/hexgate/security/binding.py +4 -3
  24. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/enforcer.py +1 -1
  25. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate.egg-info/PKG-INFO +90 -29
  26. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate.egg-info/SOURCES.txt +5 -0
  27. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate.egg-info/requires.txt +1 -0
  28. {hexgate-0.2.5 → hexgate-0.2.6}/pyproject.toml +6 -1
  29. {hexgate-0.2.5 → hexgate-0.2.6}/tests/test_bootstrap.py +9 -12
  30. hexgate-0.2.5/hexgate/config/settings.py +0 -50
  31. {hexgate-0.2.5 → hexgate-0.2.6}/LICENSE +0 -0
  32. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/__init__.py +0 -0
  33. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/__init__.py +0 -0
  34. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/google/__init__.py +0 -0
  35. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/google/tools.py +0 -0
  36. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/google/wrapper.py +0 -0
  37. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/langchain/__init__.py +0 -0
  38. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/langchain/agent.py +0 -0
  39. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/langchain/tools.py +0 -0
  40. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/openai/__init__.py +0 -0
  41. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/openai/tools.py +0 -0
  42. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/openai/wrapper.py +0 -0
  43. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/pydantic_ai/__init__.py +0 -0
  44. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/pydantic_ai/agent.py +0 -0
  45. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/adapters/pydantic_ai/tools.py +0 -0
  46. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/agents/__init__.py +0 -0
  47. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/agents/builtin/__init__.py +0 -0
  48. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/agents/builtin/researcher/agent.yaml +0 -0
  49. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/agents/builtin/researcher/policy.yaml +0 -0
  50. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/agents/builtin/researcher/system.md +0 -0
  51. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/agents/models.py +0 -0
  52. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/agents/prompts/agent_system.md +0 -0
  53. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/__init__.py +0 -0
  54. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/policy/__init__.py +0 -0
  55. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/policy/main.py +0 -0
  56. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/register/__init__.py +0 -0
  57. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/register/google.py +0 -0
  58. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/register/hexgate.py +0 -0
  59. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/register/langchain.py +0 -0
  60. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/register/main.py +0 -0
  61. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/register/manifest.py +0 -0
  62. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/register/models.py +0 -0
  63. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/register/openai.py +0 -0
  64. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/register/pydantic_ai.py +0 -0
  65. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/serve.py +0 -0
  66. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cli/state.py +0 -0
  67. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/cloud/__init__.py +0 -0
  68. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/config/__init__.py +0 -0
  69. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/runtime/__init__.py +0 -0
  70. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/runtime/command_policy.py +0 -0
  71. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/runtime/context.py +0 -0
  72. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/runtime/sandbox_runtime.py +0 -0
  73. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/runtime/srt.py +0 -0
  74. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/runtime/workspace.py +0 -0
  75. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/__init__.py +0 -0
  76. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/bundle.py +0 -0
  77. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/constraints.py +0 -0
  78. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/decision.py +0 -0
  79. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/errors.py +0 -0
  80. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/file_scope.py +0 -0
  81. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/models.py +0 -0
  82. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/policy.py +0 -0
  83. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/policy_set.py +0 -0
  84. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/rego.py +0 -0
  85. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/rego_wasm.py +0 -0
  86. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/signing.py +0 -0
  87. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/source.py +0 -0
  88. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/security/wasm_engine.py +0 -0
  89. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/streaming/__init__.py +0 -0
  90. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/streaming/events.py +0 -0
  91. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/streaming/normalize.py +0 -0
  92. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tools/__init__.py +0 -0
  93. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tools/bash.py +0 -0
  94. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tools/decorators.py +0 -0
  95. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tools/fetch.py +0 -0
  96. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tools/files/__init__.py +0 -0
  97. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tools/files/_common.py +0 -0
  98. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tools/files/edit_file.py +0 -0
  99. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tools/files/glob.py +0 -0
  100. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tools/files/grep.py +0 -0
  101. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tools/files/read_file.py +0 -0
  102. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tools/files/write_file.py +0 -0
  103. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tools/refund.py +0 -0
  104. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tools/websearch.py +0 -0
  105. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tracing/__init__.py +0 -0
  106. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/tracing/langfuse.py +0 -0
  107. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/utils/__init__.py +0 -0
  108. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate/utils/retry.py +0 -0
  109. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate.egg-info/dependency_links.txt +0 -0
  110. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate.egg-info/entry_points.txt +0 -0
  111. {hexgate-0.2.5 → hexgate-0.2.6}/hexgate.egg-info/top_level.txt +0 -0
  112. {hexgate-0.2.5 → hexgate-0.2.6}/setup.cfg +0 -0
  113. {hexgate-0.2.5 → 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.5
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,6 +29,7 @@ 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"
@@ -147,11 +148,11 @@ cp .env.sample .env
147
148
  hexgate chat --agent example_agent
148
149
  ```
149
150
 
150
- 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):
151
152
 
152
- - `OPENAI_API_KEY`
153
- - `LINKUP_API_KEY`
154
- - `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.
155
156
 
156
157
  Run `hexgate --help` to see all subcommands (`chat`, `serve`, `register`), and `hexgate <subcommand> --help` for the flags each one accepts.
157
158
 
@@ -175,7 +176,7 @@ The included local agent lives in `examples/example_agent/`, and the CLI can als
175
176
 
176
177
  The two Quick Starts above aren't competing — they answer different questions.
177
178
 
178
- **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.
179
180
 
180
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.
181
182
 
@@ -203,7 +204,7 @@ make dashboard
203
204
 
204
205
  # Terminal 3 — mint a token, then serve your local agent
205
206
  # 1. Open http://localhost:5173/tokens, click "Mint new token", copy the value.
206
- # 2. Add to asianf/.env: HEXGATE_KEY=fty_live_...
207
+ # 2. Add to asianf/.env: HEXGATE_API_KEY=fty_live_...
207
208
  # 3. Pick the agent's Python entrypoint (module:attr — uvicorn-style)
208
209
  # and let `hexgate serve` take over:
209
210
  make serve # default — examples.customer_bot:agent
@@ -283,6 +284,22 @@ Integration tests (`pytest -m integration`) round-trip rows through the live Cli
283
284
 
284
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.
285
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
+
286
303
  ## ✨ Core Primitives
287
304
 
288
305
  The two main primitives are:
@@ -320,7 +337,7 @@ Dev wrote an OpenAI Agents / LangChain / Google ADK / Pydantic AI agent. They wr
320
337
  from hexgate.adapters.openai import HexgateRunner # or .langchain.wrap_langchain_agent, .google.HexgateRunner, .pydantic_ai.wrap_pydantic_agent
321
338
  from hexgate.runtime import User
322
339
 
323
- runner = HexgateRunner() # picks up HEXGATE_KEY from env
340
+ runner = HexgateRunner() # picks up HEXGATE_API_KEY from env
324
341
  await runner.run(
325
342
  my_agent,
326
343
  "refund 30",
@@ -355,8 +372,8 @@ Same enforcement seam, same `User` scope. The difference is whose system of reco
355
372
 
356
373
  | What dev sets | What changes |
357
374
  |---|---|
358
- | `HEXGATE_KEY=fty_live_<project>_…` | Wakes up the platform path. Without it, adapters / `load_agent` fall back to local / builtin. |
359
- | `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. |
360
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. |
361
378
  | `HEXGATE_BUNDLE_SIGN_KEY_PATH=./keys/dev.private` *(optional)* | Sign locally-recompiled yaml so `bundle.is_signed` reads True. |
362
379
  | `HEXGATE_BUNDLE_PUBKEY_PATH=./keys/prod.public` *(optional)* | Verify a pre-built bundle dir against this pubkey on every reload. |
@@ -364,6 +381,11 @@ Same enforcement seam, same `User` scope. The difference is whose system of reco
364
381
 
365
382
  No config object to instantiate, no `enforce_policy(...)` call to remember on the platform path. The adapter / loader threads it all through.
366
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
+
367
389
  ### Where enforcement actually happens
368
390
 
369
391
  Walk through one tool call:
@@ -377,7 +399,7 @@ Walk through one tool call:
377
399
  `_policy_source` is set automatically by the loader based on env:
378
400
 
379
401
  - `HEXGATE_LOCAL_POLICY` set → `YamlPolicySource` or `BundleDirPolicySource` (mtime-driven refresh)
380
- - `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)
381
403
  - Neither → no source attached; enforcement uses whatever was loaded once
382
404
 
383
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.
@@ -387,7 +409,7 @@ Walk through one tool call:
387
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).
388
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.
389
411
 
390
- 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.
391
413
 
392
414
  ## 📦 What You Can Import
393
415
 
@@ -430,6 +452,46 @@ from hexgate import (
430
452
  )
431
453
  ```
432
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
+
433
495
  ## 🤝 Framework Agent Wrapping
434
496
 
435
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:
@@ -449,7 +511,7 @@ The four integrations differ in shape because the underlying SDKs do:
449
511
 
450
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.
451
513
 
452
- 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.
453
515
 
454
516
  ### OpenAI Agents SDK — `HexgateRunner`
455
517
 
@@ -479,7 +541,7 @@ async def main():
479
541
  model="gpt-4o-mini",
480
542
  )
481
543
 
482
- runner = HexgateRunner() # picks up HEXGATE_KEY from env
544
+ runner = HexgateRunner() # picks up HEXGATE_API_KEY from env
483
545
  result = await runner.run(
484
546
  agent,
485
547
  "What's the weather in Cherbourg?",
@@ -539,7 +601,7 @@ async def main():
539
601
  agent = wrap_langchain_agent(
540
602
  agent=graph,
541
603
  tools=TOOLS, # same list passed to create_react_agent — wrapped in place
542
- api_key="sk-...", # or rely on HEXGATE_KEY
604
+ api_key="sk-...", # or rely on HEXGATE_API_KEY
543
605
  )
544
606
 
545
607
  result = await agent.ainvoke(
@@ -615,7 +677,7 @@ async def main():
615
677
  agent=agent,
616
678
  app_name="google_runner_example",
617
679
  session_service=session_service,
618
- ) # picks up HEXGATE_KEY from env
680
+ ) # picks up HEXGATE_API_KEY from env
619
681
 
620
682
  user_msg = types.Content(
621
683
  role="user", parts=[types.Part(text="What is the weather in New Delhi?")]
@@ -667,7 +729,7 @@ async def main():
667
729
 
668
730
  agent = wrap_pydantic_agent(
669
731
  agent=agent,
670
- api_key="sk-...", # or rely on HEXGATE_KEY
732
+ api_key="sk-...", # or rely on HEXGATE_API_KEY
671
733
  )
672
734
 
673
735
  result = await agent.run(
@@ -1085,13 +1147,12 @@ Worth being explicit about the gaps so operators know where to layer their own c
1085
1147
 
1086
1148
  ## 🔧 Environment
1087
1149
 
1088
- 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.
1089
1151
 
1090
- - `OPENAI_API_KEY`
1091
- - `LINKUP_API_KEY`
1092
- - `TAVILY_API_KEY`
1093
- - `LANGFUSE_SECRET_KEY`
1094
- - `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)
1095
1156
  - optional `LANGFUSE_HOST`
1096
1157
 
1097
1158
  Policy-bundle enforcement (see [Policy Bundles](#-policy-bundles--compile-sign-enforce-wasm)) reads a few more, all optional:
@@ -1171,7 +1232,7 @@ Register a code-defined agent's manifest with the Hexgate platform. `--agent`
1171
1232
  takes a Python import path of the form `module.path:attribute`, the same shape
1172
1233
  as ASGI/WSGI entrypoints. The CLI imports the module, grabs the agent object,
1173
1234
  and POSTs its manifest to `${HEXGATE_API_URL}/v1/agents` using
1174
- `${HEXGATE_KEY}` as the bearer token:
1235
+ `${HEXGATE_API_KEY}` as the bearer token:
1175
1236
 
1176
1237
  ```bash
1177
1238
  hexgate register --agent examples.customer_bot:agent
@@ -1313,8 +1374,8 @@ Bridges your local agent runtime to the dashboard via the platform's WebSocket r
1313
1374
 
1314
1375
  ```bash
1315
1376
  # in asianf/.env
1316
- HEXGATE_KEY=fty_live_<project>_<biscuit>
1317
- 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
1318
1379
 
1319
1380
  # pick an agent module:attr — uvicorn-style spec
1320
1381
  uv run hexgate serve examples.customer_bot:agent
@@ -1353,7 +1414,7 @@ There's no longer a `HEXGATE_AGENT_NAME` env var, `--agent` flag, or
1353
1414
  `--use` flag — the spec carries everything. If you've been setting
1354
1415
  `HEXGATE_AGENT_NAME` in `.env`, drop it.
1355
1416
 
1356
- ### How `load_agent()` resolves with `HEXGATE_KEY`
1417
+ ### How `load_agent()` resolves with `HEXGATE_API_KEY`
1357
1418
 
1358
1419
  ```python
1359
1420
  from hexgate import load_agent
@@ -1361,8 +1422,8 @@ from hexgate import load_agent
1361
1422
  agent, handler = load_agent("read_only") # explicit name required
1362
1423
  ```
1363
1424
 
1364
- When `HEXGATE_KEY` is set, `load_agent(name)` fetches the named agent from
1365
- 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
1366
1427
  falls back to local / registered / builtin resolution — no platform call.
1367
1428
 
1368
1429
  The legacy `HEXGATE_AGENT_NAME` env-var fallback was removed in Phase 7;
@@ -107,11 +107,11 @@ cp .env.sample .env
107
107
  hexgate chat --agent example_agent
108
108
  ```
109
109
 
110
- 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):
111
111
 
112
- - `OPENAI_API_KEY`
113
- - `LINKUP_API_KEY`
114
- - `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.
115
115
 
116
116
  Run `hexgate --help` to see all subcommands (`chat`, `serve`, `register`), and `hexgate <subcommand> --help` for the flags each one accepts.
117
117
 
@@ -135,7 +135,7 @@ The included local agent lives in `examples/example_agent/`, and the CLI can als
135
135
 
136
136
  The two Quick Starts above aren't competing — they answer different questions.
137
137
 
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_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.
139
139
 
140
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.
141
141
 
@@ -163,7 +163,7 @@ make dashboard
163
163
 
164
164
  # Terminal 3 — mint a token, then serve your local agent
165
165
  # 1. Open http://localhost:5173/tokens, click "Mint new token", copy the value.
166
- # 2. Add to asianf/.env: HEXGATE_KEY=fty_live_...
166
+ # 2. Add to asianf/.env: HEXGATE_API_KEY=fty_live_...
167
167
  # 3. Pick the agent's Python entrypoint (module:attr — uvicorn-style)
168
168
  # and let `hexgate serve` take over:
169
169
  make serve # default — examples.customer_bot:agent
@@ -243,6 +243,22 @@ Integration tests (`pytest -m integration`) round-trip rows through the live Cli
243
243
 
244
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.
245
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
+
246
262
  ## ✨ Core Primitives
247
263
 
248
264
  The two main primitives are:
@@ -280,7 +296,7 @@ Dev wrote an OpenAI Agents / LangChain / Google ADK / Pydantic AI agent. They wr
280
296
  from hexgate.adapters.openai import HexgateRunner # or .langchain.wrap_langchain_agent, .google.HexgateRunner, .pydantic_ai.wrap_pydantic_agent
281
297
  from hexgate.runtime import User
282
298
 
283
- runner = HexgateRunner() # picks up HEXGATE_KEY from env
299
+ runner = HexgateRunner() # picks up HEXGATE_API_KEY from env
284
300
  await runner.run(
285
301
  my_agent,
286
302
  "refund 30",
@@ -315,8 +331,8 @@ Same enforcement seam, same `User` scope. The difference is whose system of reco
315
331
 
316
332
  | What dev sets | What changes |
317
333
  |---|---|
318
- | `HEXGATE_KEY=fty_live_<project>_…` | Wakes up the platform path. Without it, adapters / `load_agent` fall back to local / builtin. |
319
- | `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. |
320
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. |
321
337
  | `HEXGATE_BUNDLE_SIGN_KEY_PATH=./keys/dev.private` *(optional)* | Sign locally-recompiled yaml so `bundle.is_signed` reads True. |
322
338
  | `HEXGATE_BUNDLE_PUBKEY_PATH=./keys/prod.public` *(optional)* | Verify a pre-built bundle dir against this pubkey on every reload. |
@@ -324,6 +340,11 @@ Same enforcement seam, same `User` scope. The difference is whose system of reco
324
340
 
325
341
  No config object to instantiate, no `enforce_policy(...)` call to remember on the platform path. The adapter / loader threads it all through.
326
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
+
327
348
  ### Where enforcement actually happens
328
349
 
329
350
  Walk through one tool call:
@@ -337,7 +358,7 @@ Walk through one tool call:
337
358
  `_policy_source` is set automatically by the loader based on env:
338
359
 
339
360
  - `HEXGATE_LOCAL_POLICY` set → `YamlPolicySource` or `BundleDirPolicySource` (mtime-driven refresh)
340
- - `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)
341
362
  - Neither → no source attached; enforcement uses whatever was loaded once
342
363
 
343
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.
@@ -347,7 +368,7 @@ Walk through one tool call:
347
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).
348
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.
349
370
 
350
- 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.
351
372
 
352
373
  ## 📦 What You Can Import
353
374
 
@@ -390,6 +411,46 @@ from hexgate import (
390
411
  )
391
412
  ```
392
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
+
393
454
  ## 🤝 Framework Agent Wrapping
394
455
 
395
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:
@@ -409,7 +470,7 @@ The four integrations differ in shape because the underlying SDKs do:
409
470
 
410
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.
411
472
 
412
- 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.
413
474
 
414
475
  ### OpenAI Agents SDK — `HexgateRunner`
415
476
 
@@ -439,7 +500,7 @@ async def main():
439
500
  model="gpt-4o-mini",
440
501
  )
441
502
 
442
- runner = HexgateRunner() # picks up HEXGATE_KEY from env
503
+ runner = HexgateRunner() # picks up HEXGATE_API_KEY from env
443
504
  result = await runner.run(
444
505
  agent,
445
506
  "What's the weather in Cherbourg?",
@@ -499,7 +560,7 @@ async def main():
499
560
  agent = wrap_langchain_agent(
500
561
  agent=graph,
501
562
  tools=TOOLS, # same list passed to create_react_agent — wrapped in place
502
- api_key="sk-...", # or rely on HEXGATE_KEY
563
+ api_key="sk-...", # or rely on HEXGATE_API_KEY
503
564
  )
504
565
 
505
566
  result = await agent.ainvoke(
@@ -575,7 +636,7 @@ async def main():
575
636
  agent=agent,
576
637
  app_name="google_runner_example",
577
638
  session_service=session_service,
578
- ) # picks up HEXGATE_KEY from env
639
+ ) # picks up HEXGATE_API_KEY from env
579
640
 
580
641
  user_msg = types.Content(
581
642
  role="user", parts=[types.Part(text="What is the weather in New Delhi?")]
@@ -627,7 +688,7 @@ async def main():
627
688
 
628
689
  agent = wrap_pydantic_agent(
629
690
  agent=agent,
630
- api_key="sk-...", # or rely on HEXGATE_KEY
691
+ api_key="sk-...", # or rely on HEXGATE_API_KEY
631
692
  )
632
693
 
633
694
  result = await agent.run(
@@ -1045,13 +1106,12 @@ Worth being explicit about the gaps so operators know where to layer their own c
1045
1106
 
1046
1107
  ## 🔧 Environment
1047
1108
 
1048
- 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.
1049
1110
 
1050
- - `OPENAI_API_KEY`
1051
- - `LINKUP_API_KEY`
1052
- - `TAVILY_API_KEY`
1053
- - `LANGFUSE_SECRET_KEY`
1054
- - `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)
1055
1115
  - optional `LANGFUSE_HOST`
1056
1116
 
1057
1117
  Policy-bundle enforcement (see [Policy Bundles](#-policy-bundles--compile-sign-enforce-wasm)) reads a few more, all optional:
@@ -1131,7 +1191,7 @@ Register a code-defined agent's manifest with the Hexgate platform. `--agent`
1131
1191
  takes a Python import path of the form `module.path:attribute`, the same shape
1132
1192
  as ASGI/WSGI entrypoints. The CLI imports the module, grabs the agent object,
1133
1193
  and POSTs its manifest to `${HEXGATE_API_URL}/v1/agents` using
1134
- `${HEXGATE_KEY}` as the bearer token:
1194
+ `${HEXGATE_API_KEY}` as the bearer token:
1135
1195
 
1136
1196
  ```bash
1137
1197
  hexgate register --agent examples.customer_bot:agent
@@ -1273,8 +1333,8 @@ Bridges your local agent runtime to the dashboard via the platform's WebSocket r
1273
1333
 
1274
1334
  ```bash
1275
1335
  # in asianf/.env
1276
- HEXGATE_KEY=fty_live_<project>_<biscuit>
1277
- 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
1278
1338
 
1279
1339
  # pick an agent module:attr — uvicorn-style spec
1280
1340
  uv run hexgate serve examples.customer_bot:agent
@@ -1313,7 +1373,7 @@ There's no longer a `HEXGATE_AGENT_NAME` env var, `--agent` flag, or
1313
1373
  `--use` flag — the spec carries everything. If you've been setting
1314
1374
  `HEXGATE_AGENT_NAME` in `.env`, drop it.
1315
1375
 
1316
- ### How `load_agent()` resolves with `HEXGATE_KEY`
1376
+ ### How `load_agent()` resolves with `HEXGATE_API_KEY`
1317
1377
 
1318
1378
  ```python
1319
1379
  from hexgate import load_agent
@@ -1321,8 +1381,8 @@ from hexgate import load_agent
1321
1381
  agent, handler = load_agent("read_only") # explicit name required
1322
1382
  ```
1323
1383
 
1324
- When `HEXGATE_KEY` is set, `load_agent(name)` fetches the named agent from
1325
- 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
1326
1386
  falls back to local / registered / builtin resolution — no platform call.
1327
1387
 
1328
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] = {}
@@ -11,13 +11,13 @@ proxy at the top of every run.
11
11
  from __future__ import annotations
12
12
 
13
13
  import copy
14
- import os
15
14
 
16
15
  from pydantic_ai import Agent
17
16
  from pydantic_ai.tools import Tool
18
17
 
19
18
  from hexgate.adapters.pydantic_ai.agent import HexgatePydanticAgent
20
19
  from hexgate.adapters.pydantic_ai.tools import wrap_tools
20
+ from hexgate.config.env import resolve_api_key
21
21
  from hexgate.security.binding import PolicyBinding, resolve_policy
22
22
  from hexgate.security.enforcer import build_enforcer
23
23
 
@@ -56,13 +56,13 @@ def wrap_pydantic_agent(
56
56
  ``user`` per call; role resolves at call time from the active
57
57
  :class:`User`. ``NEEDS_APPROVAL`` raises :class:`ModelRetry` with
58
58
  an ``[approval_required]`` marker. ``api_key`` falls back to
59
- ``HEXGATE_KEY``. The enforced policy is the platform's; unlisted
59
+ ``HEXGATE_API_KEY``. The enforced policy is the platform's; unlisted
60
60
  tools are denied.
61
61
  """
62
- resolved_key = api_key or os.getenv("HEXGATE_KEY")
62
+ resolved_key = resolve_api_key(api_key)
63
63
  if not resolved_key:
64
64
  raise ValueError(
65
- "No API key provided. Pass api_key= explicitly or set HEXGATE_KEY environment variable."
65
+ "No API key provided. Pass api_key= explicitly or set the HEXGATE_API_KEY environment variable."
66
66
  )
67
67
 
68
68
  agent_name = getattr(agent, "name", None) or "default"