hexgate 0.2.5__tar.gz → 0.2.7__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 (119) hide show
  1. {hexgate-0.2.5 → hexgate-0.2.7}/PKG-INFO +91 -30
  2. {hexgate-0.2.5 → hexgate-0.2.7}/README.md +89 -29
  3. hexgate-0.2.7/hexgate/adapters/google/mcp.py +95 -0
  4. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/google/runner.py +6 -4
  5. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/google/tools.py +27 -3
  6. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/google/wrapper.py +13 -6
  7. hexgate-0.2.7/hexgate/adapters/langchain/mcp.py +47 -0
  8. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/langchain/tools.py +7 -25
  9. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/langchain/wrapper.py +4 -5
  10. hexgate-0.2.7/hexgate/adapters/openai/mcp.py +97 -0
  11. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/openai/runner.py +26 -7
  12. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/openai/tools.py +28 -3
  13. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/openai/wrapper.py +12 -3
  14. hexgate-0.2.7/hexgate/adapters/pydantic_ai/mcp.py +60 -0
  15. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/pydantic_ai/tools.py +27 -3
  16. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/pydantic_ai/wrapper.py +14 -8
  17. hexgate-0.2.7/hexgate/agents/approvals.py +53 -0
  18. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/agents/factory.py +6 -7
  19. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/agents/loader.py +4 -4
  20. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/audit.py +10 -9
  21. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/bootstrap.py +5 -6
  22. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/_common.py +37 -8
  23. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/chat.py +1 -1
  24. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/register/google.py +63 -10
  25. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/register/register.py +4 -5
  26. hexgate-0.2.7/hexgate/cli/serve.py +692 -0
  27. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cloud/attenuate.py +1 -1
  28. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cloud/biscuit.py +1 -1
  29. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cloud/client.py +7 -9
  30. hexgate-0.2.7/hexgate/config/env.py +38 -0
  31. hexgate-0.2.7/hexgate/config/settings.py +34 -0
  32. hexgate-0.2.7/hexgate/mcp/__init__.py +61 -0
  33. hexgate-0.2.7/hexgate/mcp/client.py +170 -0
  34. hexgate-0.2.7/hexgate/mcp/config.py +93 -0
  35. hexgate-0.2.7/hexgate/mcp/proxy.py +457 -0
  36. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/binding.py +4 -3
  37. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/enforcer.py +1 -1
  38. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate.egg-info/PKG-INFO +91 -30
  39. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate.egg-info/SOURCES.txt +10 -0
  40. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate.egg-info/requires.txt +1 -0
  41. {hexgate-0.2.5 → hexgate-0.2.7}/pyproject.toml +6 -1
  42. {hexgate-0.2.5 → hexgate-0.2.7}/tests/test_bootstrap.py +9 -12
  43. hexgate-0.2.5/hexgate/cli/serve.py +0 -337
  44. hexgate-0.2.5/hexgate/config/settings.py +0 -50
  45. {hexgate-0.2.5 → hexgate-0.2.7}/LICENSE +0 -0
  46. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/__init__.py +0 -0
  47. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/__init__.py +0 -0
  48. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/google/__init__.py +0 -0
  49. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/langchain/__init__.py +0 -0
  50. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/langchain/agent.py +0 -0
  51. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/openai/__init__.py +0 -0
  52. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/pydantic_ai/__init__.py +0 -0
  53. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/adapters/pydantic_ai/agent.py +0 -0
  54. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/agents/__init__.py +0 -0
  55. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/agents/builtin/__init__.py +0 -0
  56. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/agents/builtin/researcher/agent.yaml +0 -0
  57. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/agents/builtin/researcher/policy.yaml +0 -0
  58. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/agents/builtin/researcher/system.md +0 -0
  59. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/agents/models.py +0 -0
  60. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/agents/prompts/agent_system.md +0 -0
  61. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/__init__.py +0 -0
  62. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/policy/__init__.py +0 -0
  63. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/policy/main.py +0 -0
  64. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/register/__init__.py +0 -0
  65. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/register/hexgate.py +0 -0
  66. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/register/langchain.py +0 -0
  67. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/register/main.py +0 -0
  68. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/register/manifest.py +0 -0
  69. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/register/models.py +0 -0
  70. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/register/openai.py +0 -0
  71. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/register/pydantic_ai.py +0 -0
  72. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cli/state.py +0 -0
  73. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/cloud/__init__.py +0 -0
  74. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/config/__init__.py +0 -0
  75. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/runtime/__init__.py +0 -0
  76. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/runtime/command_policy.py +0 -0
  77. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/runtime/context.py +0 -0
  78. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/runtime/sandbox_runtime.py +0 -0
  79. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/runtime/srt.py +0 -0
  80. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/runtime/workspace.py +0 -0
  81. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/__init__.py +0 -0
  82. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/bundle.py +0 -0
  83. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/constraints.py +0 -0
  84. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/decision.py +0 -0
  85. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/errors.py +0 -0
  86. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/file_scope.py +0 -0
  87. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/models.py +0 -0
  88. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/policy.py +0 -0
  89. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/policy_set.py +0 -0
  90. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/rego.py +0 -0
  91. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/rego_wasm.py +0 -0
  92. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/signing.py +0 -0
  93. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/source.py +0 -0
  94. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/security/wasm_engine.py +0 -0
  95. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/streaming/__init__.py +0 -0
  96. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/streaming/events.py +0 -0
  97. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/streaming/normalize.py +0 -0
  98. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tools/__init__.py +0 -0
  99. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tools/bash.py +0 -0
  100. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tools/decorators.py +0 -0
  101. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tools/fetch.py +0 -0
  102. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tools/files/__init__.py +0 -0
  103. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tools/files/_common.py +0 -0
  104. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tools/files/edit_file.py +0 -0
  105. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tools/files/glob.py +0 -0
  106. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tools/files/grep.py +0 -0
  107. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tools/files/read_file.py +0 -0
  108. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tools/files/write_file.py +0 -0
  109. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tools/refund.py +0 -0
  110. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tools/websearch.py +0 -0
  111. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tracing/__init__.py +0 -0
  112. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/tracing/langfuse.py +0 -0
  113. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/utils/__init__.py +0 -0
  114. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate/utils/retry.py +0 -0
  115. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate.egg-info/dependency_links.txt +0 -0
  116. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate.egg-info/entry_points.txt +0 -0
  117. {hexgate-0.2.5 → hexgate-0.2.7}/hexgate.egg-info/top_level.txt +0 -0
  118. {hexgate-0.2.5 → hexgate-0.2.7}/setup.cfg +0 -0
  119. {hexgate-0.2.5 → hexgate-0.2.7}/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.7
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
@@ -1268,7 +1329,7 @@ FastAPI over SQLite. Run with:
1268
1329
 
1269
1330
  ```bash
1270
1331
  cd platform/api
1271
- uv run uvicorn main:app --reload --port 8000
1332
+ uv run uvicorn hexgate_api.main:app --reload --port 8000
1272
1333
  ```
1273
1334
 
1274
1335
  The default `support-bot` project is seeded on first boot with two agents — `default` (broad access, side-effects gated by `approval_required`) and `read_only` (everything mutating denied).
@@ -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
@@ -1228,7 +1288,7 @@ FastAPI over SQLite. Run with:
1228
1288
 
1229
1289
  ```bash
1230
1290
  cd platform/api
1231
- uv run uvicorn main:app --reload --port 8000
1291
+ uv run uvicorn hexgate_api.main:app --reload --port 8000
1232
1292
  ```
1233
1293
 
1234
1294
  The default `support-bot` project is seeded on first boot with two agents — `default` (broad access, side-effects gated by `approval_required`) and `read_only` (everything mutating denied).
@@ -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;
@@ -0,0 +1,95 @@
1
+ """Google ADK adapter for :class:`~hexgate.mcp.MCPToolset`.
2
+
3
+ Every :class:`~hexgate.mcp.MCPToolProxy` produced by the toolset becomes
4
+ a :class:`google.adk.tools.BaseTool` subclass whose ``_get_declaration``
5
+ returns a ``FunctionDeclaration`` carrying the MCP tool's raw
6
+ JSON Schema (via ``parametersJsonSchema``) and whose ``run_async``
7
+ forwards to the proxy's ``call``. Once wrapped, the resulting tools
8
+ are indistinguishable from ADK-native :class:`FunctionTool` instances
9
+ to the rest of the Google ADK path — attach them to an ``Agent``, then
10
+ wrap via :func:`~hexgate.adapters.google.wrap_google_agent` so the
11
+ existing per-tool policy gate covers MCP invocations too.
12
+
13
+ Usage::
14
+
15
+ from google.adk.agents import Agent
16
+ from hexgate.adapters.google import wrap_google_agent
17
+ from hexgate.adapters.google.mcp import wrap_mcp_toolset
18
+ from hexgate.mcp import MCPServerConfig, MCPToolset
19
+
20
+ slack = MCPServerConfig(name="slack", transport="stdio", command="slack-mcp")
21
+ async with MCPToolset(slack) as mcp:
22
+ agent = Agent(
23
+ name="bot",
24
+ tools=[*wrap_mcp_toolset(mcp), *native],
25
+ )
26
+ wrapped, binding = wrap_google_agent(agent, api_key=api_key)
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from typing import Any
32
+
33
+ from google.adk.tools import BaseTool
34
+ from google.adk.tools.tool_context import ToolContext
35
+ from google.genai import types as genai_types
36
+
37
+ from hexgate.mcp.proxy import MCPToolProxy, MCPToolset
38
+
39
+
40
+ class _MCPProxyTool(BaseTool):
41
+ """One :class:`BaseTool` that forwards to an :class:`MCPToolProxy`.
42
+
43
+ ADK's :class:`FunctionTool` derives its schema from a Python
44
+ callable's signature via reflection, which can't express the shapes
45
+ MCP servers advertise (partial ``required``, ``anyOf`` unions,
46
+ nested objects with dynamic keys). Subclassing :class:`BaseTool`
47
+ and returning a hand-built :class:`FunctionDeclaration` from
48
+ ``_get_declaration`` bypasses the reflection path and hands the
49
+ server's raw JSON Schema straight to the model — via ADK's
50
+ ``parametersJsonSchema`` alias which accepts JSON Schema dicts.
51
+ """
52
+
53
+ def __init__(self, proxy: MCPToolProxy) -> None:
54
+ super().__init__(name=proxy.qualified_name, description=proxy.description)
55
+ # Store the schema + call as private attrs — ADK's BaseTool has
56
+ # no field for them, so we ride on the object dict.
57
+ self._input_schema = proxy.input_schema
58
+ self._call = proxy.call
59
+
60
+ def _get_declaration(self) -> genai_types.FunctionDeclaration:
61
+ # Gemini's FunctionDeclaration validator rejects any
62
+ # parametersJsonSchema missing a top-level `type` — literal `{}`,
63
+ # or a partial like `{"properties": {...}}`, or a bare
64
+ # `{"anyOf": [...]}`. All three shapes cause the whole tool
65
+ # list to fail at declaration time, so the agent never gets to
66
+ # run. Fill in `type: "object"` (the only shape Gemini accepts
67
+ # for a function-args container) whenever it's absent, and
68
+ # ensure a `properties` map so the LLM sees an argument surface
69
+ # rather than an opaque object. LangChain and OpenAI Agents
70
+ # both tolerate the missing `type` unchanged.
71
+ schema = self._input_schema if isinstance(self._input_schema, dict) else {}
72
+ if "type" not in schema:
73
+ schema = {"type": "object", "properties": {}, **schema}
74
+ return genai_types.FunctionDeclaration(
75
+ name=self.name,
76
+ description=self.description,
77
+ parametersJsonSchema=schema,
78
+ )
79
+
80
+ async def run_async(
81
+ self, *, args: dict[str, Any], tool_context: ToolContext
82
+ ) -> Any:
83
+ return await self._call(**(args or {}))
84
+
85
+
86
+ def wrap_mcp_toolset(toolset: MCPToolset) -> list[BaseTool]:
87
+ """Wrap every proxy in ``toolset`` as a Google ADK :class:`BaseTool`.
88
+
89
+ The returned tools share the toolset's connection lifecycle — they
90
+ stop working (returning a ``use_after_close`` envelope) once the
91
+ ``async with MCPToolset(...)`` block exits. Combine with
92
+ :func:`~hexgate.adapters.google.wrap_google_agent` to gate every
93
+ invocation through :class:`~hexgate.security.PolicyEnforcer`.
94
+ """
95
+ return [_MCPProxyTool(p) for p in toolset.proxies]