hexgate 0.1.2__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hexgate-0.1.2 → hexgate-0.2.1}/PKG-INFO +174 -158
- {hexgate-0.1.2 → hexgate-0.2.1}/README.md +172 -156
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/__init__.py +14 -14
- hexgate-0.2.1/hexgate/adapters/google/__init__.py +3 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/adapters/google/runner.py +6 -6
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/adapters/google/tools.py +1 -1
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/adapters/google/wrapper.py +6 -6
- hexgate-0.2.1/hexgate/adapters/langchain/__init__.py +7 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/adapters/langchain/agent.py +5 -5
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/adapters/langchain/tools.py +9 -9
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/adapters/langchain/wrapper.py +14 -14
- hexgate-0.2.1/hexgate/adapters/openai/__init__.py +3 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/adapters/openai/runner.py +9 -9
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/adapters/openai/tools.py +1 -1
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/adapters/openai/wrapper.py +2 -2
- hexgate-0.2.1/hexgate/adapters/pydantic_ai/__init__.py +7 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/adapters/pydantic_ai/agent.py +4 -4
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/adapters/pydantic_ai/tools.py +1 -1
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/adapters/pydantic_ai/wrapper.py +13 -13
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/agents/__init__.py +5 -5
- hexgate-0.2.1/hexgate/agents/builtin/__init__.py +1 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/agents/factory.py +94 -67
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/agents/loader.py +145 -48
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/audit.py +58 -9
- hexgate-0.2.1/hexgate/bootstrap.py +62 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/__init__.py +8 -8
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/_common.py +136 -38
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/chat.py +101 -18
- hexgate-0.2.1/hexgate/cli/policy/__init__.py +17 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/policy/main.py +11 -11
- hexgate-0.2.1/hexgate/cli/register/__init__.py +8 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/register/google.py +1 -1
- hexgate-0.1.2/fortify/cli/register/fortify.py → hexgate-0.2.1/hexgate/cli/register/hexgate.py +10 -10
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/register/langchain.py +1 -1
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/register/main.py +10 -10
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/register/manifest.py +9 -9
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/register/models.py +4 -4
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/register/openai.py +1 -1
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/register/pydantic_ai.py +2 -2
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/register/register.py +6 -6
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/serve.py +25 -25
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cli/state.py +2 -2
- hexgate-0.2.1/hexgate/cloud/__init__.py +32 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cloud/attenuate.py +4 -4
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cloud/biscuit.py +2 -2
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/cloud/client.py +49 -39
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/config/settings.py +3 -3
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/runtime/__init__.py +5 -5
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/runtime/context.py +4 -4
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/runtime/workspace.py +5 -5
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/__init__.py +13 -13
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/binding.py +25 -19
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/bundle.py +14 -14
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/constraints.py +1 -1
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/decision.py +3 -3
- hexgate-0.2.1/hexgate/security/enforcer.py +136 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/file_scope.py +1 -1
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/models.py +1 -1
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/policy.py +10 -10
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/policy_set.py +4 -4
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/rego.py +11 -11
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/rego_wasm.py +8 -8
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/signing.py +1 -1
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/source.py +41 -41
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/wasm_engine.py +8 -8
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/streaming/__init__.py +3 -3
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/streaming/events.py +1 -1
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/streaming/normalize.py +1 -1
- hexgate-0.2.1/hexgate/tools/__init__.py +21 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/tools/bash.py +3 -3
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/tools/decorators.py +3 -3
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/tools/fetch.py +1 -1
- hexgate-0.2.1/hexgate/tools/files/__init__.py +9 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/tools/files/_common.py +1 -1
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/tools/files/edit_file.py +3 -3
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/tools/files/glob.py +3 -3
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/tools/files/grep.py +3 -3
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/tools/files/read_file.py +3 -3
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/tools/files/write_file.py +3 -3
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/tools/refund.py +1 -1
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/tools/websearch.py +1 -1
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/tracing/langfuse.py +2 -2
- {hexgate-0.1.2 → hexgate-0.2.1}/hexgate.egg-info/PKG-INFO +174 -158
- hexgate-0.2.1/hexgate.egg-info/SOURCES.txt +104 -0
- hexgate-0.2.1/hexgate.egg-info/entry_points.txt +2 -0
- hexgate-0.2.1/hexgate.egg-info/top_level.txt +1 -0
- {hexgate-0.1.2 → hexgate-0.2.1}/pyproject.toml +10 -10
- hexgate-0.2.1/tests/test_bootstrap.py +136 -0
- {hexgate-0.1.2 → hexgate-0.2.1}/tests/test_demo.py +1 -1
- hexgate-0.1.2/fortify/adapters/google/__init__.py +0 -3
- hexgate-0.1.2/fortify/adapters/langchain/__init__.py +0 -7
- hexgate-0.1.2/fortify/adapters/openai/__init__.py +0 -3
- hexgate-0.1.2/fortify/adapters/pydantic_ai/__init__.py +0 -7
- hexgate-0.1.2/fortify/agents/builtin/__init__.py +0 -1
- hexgate-0.1.2/fortify/bootstrap.py +0 -37
- hexgate-0.1.2/fortify/cli/policy/__init__.py +0 -17
- hexgate-0.1.2/fortify/cli/register/__init__.py +0 -8
- hexgate-0.1.2/fortify/cloud/__init__.py +0 -32
- hexgate-0.1.2/fortify/security/enforcer.py +0 -102
- hexgate-0.1.2/fortify/tools/__init__.py +0 -21
- hexgate-0.1.2/fortify/tools/files/__init__.py +0 -9
- hexgate-0.1.2/hexgate.egg-info/SOURCES.txt +0 -104
- hexgate-0.1.2/hexgate.egg-info/entry_points.txt +0 -2
- hexgate-0.1.2/hexgate.egg-info/top_level.txt +0 -1
- hexgate-0.1.2/tests/test_bootstrap.py +0 -37
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/adapters/__init__.py +0 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/agents/builtin/researcher/agent.yaml +0 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/agents/builtin/researcher/policy.yaml +0 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/agents/builtin/researcher/system.md +0 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/agents/models.py +0 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/agents/prompts/agent_system.md +0 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/config/__init__.py +0 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/runtime/command_policy.py +0 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/runtime/sandbox_runtime.py +0 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/runtime/srt.py +0 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/security/errors.py +0 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/tracing/__init__.py +0 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/utils/__init__.py +0 -0
- {hexgate-0.1.2/fortify → hexgate-0.2.1/hexgate}/utils/retry.py +0 -0
- {hexgate-0.1.2 → hexgate-0.2.1}/hexgate.egg-info/dependency_links.txt +0 -0
- {hexgate-0.1.2 → hexgate-0.2.1}/hexgate.egg-info/requires.txt +0 -0
- {hexgate-0.1.2 → hexgate-0.2.1}/setup.cfg +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexgate
|
|
3
|
-
Version: 0.1
|
|
4
|
-
Summary: HexaGate
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: HexaGate — authorization infrastructure for AI agents (agent runtime + cloud client).
|
|
5
5
|
Requires-Python: >=3.13
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
Requires-Dist: bashlex>=0.18
|
|
@@ -34,9 +34,9 @@ Requires-Dist: pytest>=8.4.1; extra == "dev"
|
|
|
34
34
|
Requires-Dist: pytest-asyncio>=1.0.0; extra == "dev"
|
|
35
35
|
Requires-Dist: ruff>=0.12.2; extra == "dev"
|
|
36
36
|
|
|
37
|
-
#
|
|
37
|
+
# hexgate
|
|
38
38
|
|
|
39
|
-
`
|
|
39
|
+
`hexgate` is a lightweight LangChain-based agent runtime built around:
|
|
40
40
|
|
|
41
41
|
- `langchain`
|
|
42
42
|
- `gpt-5.4`
|
|
@@ -65,7 +65,7 @@ The runtime preflights `ripgrep` at agent build time and refuses to start when i
|
|
|
65
65
|
|
|
66
66
|
## ⚡ Quick Start — Local CLI
|
|
67
67
|
|
|
68
|
-
If you just want to install `
|
|
68
|
+
If you just want to install `hexgate` and try the terminal chat:
|
|
69
69
|
|
|
70
70
|
1. Install the package in editable mode.
|
|
71
71
|
2. Copy the sample environment file.
|
|
@@ -75,7 +75,7 @@ If you just want to install `fortify` and try the terminal chat:
|
|
|
75
75
|
```bash
|
|
76
76
|
python -m pip install -e .
|
|
77
77
|
cp .env.sample .env
|
|
78
|
-
|
|
78
|
+
hexgate chat --agent example_agent
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
Required keys for the example CLI flow:
|
|
@@ -84,15 +84,16 @@ Required keys for the example CLI flow:
|
|
|
84
84
|
- `LINKUP_API_KEY`
|
|
85
85
|
- `TAVILY_API_KEY`
|
|
86
86
|
|
|
87
|
-
Run `
|
|
87
|
+
Run `hexgate --help` to see all subcommands (`chat`, `serve`, `register`), and `hexgate <subcommand> --help` for the flags each one accepts.
|
|
88
88
|
|
|
89
89
|
Useful next commands:
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
hexgate chat --list-agents
|
|
93
|
+
hexgate chat --agent researcher # by id
|
|
94
|
+
hexgate chat --agent examples.customer_bot:agent # by module:attr (same shape as `hexgate serve`)
|
|
95
|
+
hexgate chat --use examples/file_agents.py --agent workspace_explorer
|
|
96
|
+
hexgate chat --use examples/research_agents.py --agent update_researcher
|
|
96
97
|
```
|
|
97
98
|
|
|
98
99
|
The included local agent lives in `examples/example_agent/`, and the CLI can also load:
|
|
@@ -101,9 +102,24 @@ The included local agent lives in `examples/example_agent/`, and the CLI can als
|
|
|
101
102
|
- code-defined agents registered from `examples/file_agents.py`
|
|
102
103
|
- code-defined research agents registered from `examples/research_agents.py`
|
|
103
104
|
|
|
105
|
+
## 🧭 Which path do I pick?
|
|
106
|
+
|
|
107
|
+
The two Quick Starts above aren't competing — they answer different questions.
|
|
108
|
+
|
|
109
|
+
**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.
|
|
110
|
+
|
|
111
|
+
**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.
|
|
112
|
+
|
|
113
|
+
| Path | Needs platform? | Audit destination | Policy edits visible at | Best for |
|
|
114
|
+
|------|-----------------|-------------------|--------------------------|----------|
|
|
115
|
+
| `hexgate chat --agent ...` | No | Local terminal panel | Edit + restart (hot-reload only when `HEXGATE_LOCAL_POLICY` is set) | Inner loop, policy authoring |
|
|
116
|
+
| `hexgate serve --agent ...` + Playground | Yes | ClickHouse via `/v1/audit/decisions` | Per-turn fetch from dashboard | Team review, demos, integration testing |
|
|
117
|
+
|
|
118
|
+
Both commands accept either a plain agent id (`--agent researcher`) or a uvicorn-style `module.path:attr` spec (`--agent examples.customer_bot:agent`), so the same entry-point string works in both workflows.
|
|
119
|
+
|
|
104
120
|
## 🚀 Quick Start — Platform
|
|
105
121
|
|
|
106
|
-
To run the full
|
|
122
|
+
To run the full HexaGate control plane locally (FastAPI backend + dashboard + your local agent serving over WebSocket), you need **three terminals**. The Makefile has a target that prints the recipe:
|
|
107
123
|
|
|
108
124
|
```bash
|
|
109
125
|
make demo-platform # prints the 3-terminal recipe below
|
|
@@ -118,15 +134,15 @@ make dashboard
|
|
|
118
134
|
|
|
119
135
|
# Terminal 3 — mint a token, then serve your local agent
|
|
120
136
|
# 1. Open http://localhost:5173/tokens, click "Mint new token", copy the value.
|
|
121
|
-
# 2. Add to asianf/.env:
|
|
137
|
+
# 2. Add to asianf/.env: HEXGATE_KEY=fty_live_...
|
|
122
138
|
# 3. Pick the agent's Python entrypoint (module:attr — uvicorn-style)
|
|
123
|
-
# and let `
|
|
139
|
+
# and let `hexgate serve` take over:
|
|
124
140
|
make serve # default — examples.customer_bot:agent
|
|
125
141
|
# or, for a different agent:
|
|
126
|
-
uv run
|
|
142
|
+
uv run hexgate serve my_app.agents:my_agent
|
|
127
143
|
```
|
|
128
144
|
|
|
129
|
-
On first serve, `
|
|
145
|
+
On first serve, `hexgate serve` auto-registers the agent's manifest on
|
|
130
146
|
the platform (the server generates a starter role-aware policy from the
|
|
131
147
|
tool list). Subsequent serves short-circuit if the manifest hasn't
|
|
132
148
|
changed. Pass `--no-auto-register` for CI / deliberate-deployment flows.
|
|
@@ -171,7 +187,7 @@ curl -X POST localhost:8000/v1/audit/decisions \
|
|
|
171
187
|
|
|
172
188
|
Integration tests (`pytest -m integration`) round-trip rows through the live ClickHouse — opt-in so the default `make platform-api-test` stays offline-friendly.
|
|
173
189
|
|
|
174
|
-
The dashboard's `/policies` page lets you edit each agent's policy. `
|
|
190
|
+
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.
|
|
175
191
|
|
|
176
192
|
## ✨ Core Primitives
|
|
177
193
|
|
|
@@ -183,7 +199,7 @@ The two main primitives are:
|
|
|
183
199
|
Use them when you want to define everything directly in Python.
|
|
184
200
|
|
|
185
201
|
```python
|
|
186
|
-
from
|
|
202
|
+
from hexgate import agent_tool, create_agent
|
|
187
203
|
|
|
188
204
|
|
|
189
205
|
@agent_tool(name="my_lookup")
|
|
@@ -207,10 +223,10 @@ Devs pick one of two shapes. Both end up at the same enforcement seam — they d
|
|
|
207
223
|
Dev wrote an OpenAI Agents / LangChain / Google ADK / Pydantic AI agent. They wrap it once and they're done:
|
|
208
224
|
|
|
209
225
|
```python
|
|
210
|
-
from
|
|
211
|
-
from
|
|
226
|
+
from hexgate.adapters.openai import HexgateRunner # or .langchain.wrap_langchain_agent, .google.HexgateRunner, .pydantic_ai.wrap_pydantic_agent
|
|
227
|
+
from hexgate.runtime import User
|
|
212
228
|
|
|
213
|
-
runner =
|
|
229
|
+
runner = HexgateRunner() # picks up HEXGATE_KEY from env
|
|
214
230
|
await runner.run(
|
|
215
231
|
my_agent,
|
|
216
232
|
"refund 30",
|
|
@@ -230,9 +246,9 @@ That's it. They get:
|
|
|
230
246
|
Dev authored the agent's `agent.yaml` / `policy.yaml` / `system.md` in the dashboard. SDK fetches them:
|
|
231
247
|
|
|
232
248
|
```python
|
|
233
|
-
from
|
|
249
|
+
from hexgate import load_hexgate_agent, stream_agent, User
|
|
234
250
|
|
|
235
|
-
agent, handler =
|
|
251
|
+
agent, handler = load_hexgate_agent("default") # explicit name — the SDK's loader requires it
|
|
236
252
|
|
|
237
253
|
async with User(user_id="alice", role="billing"):
|
|
238
254
|
async for ev in stream_agent(agent, handler, "refund 30"):
|
|
@@ -245,12 +261,12 @@ Same enforcement seam, same `User` scope. The difference is whose system of reco
|
|
|
245
261
|
|
|
246
262
|
| What dev sets | What changes |
|
|
247
263
|
|---|---|
|
|
248
|
-
| `
|
|
249
|
-
| `
|
|
250
|
-
| `
|
|
251
|
-
| `
|
|
252
|
-
| `
|
|
253
|
-
| `
|
|
264
|
+
| `HEXGATE_KEY=fty_live_<project>_…` | Wakes up the platform path. Without it, adapters / `load_agent` fall back to local / builtin. |
|
|
265
|
+
| `HEXGATE_API_URL=http://localhost:8000` *(optional)* | Platform endpoint. Defaults to localhost. |
|
|
266
|
+
| `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. |
|
|
267
|
+
| `HEXGATE_BUNDLE_SIGN_KEY_PATH=./keys/dev.private` *(optional)* | Sign locally-recompiled yaml so `bundle.is_signed` reads True. |
|
|
268
|
+
| `HEXGATE_BUNDLE_PUBKEY_PATH=./keys/prod.public` *(optional)* | Verify a pre-built bundle dir against this pubkey on every reload. |
|
|
269
|
+
| `HEXGATE_BUNDLE_REQUIRE_SIGNATURE=true` *(optional)* | Strict mode — refuse any unsigned or unverifiable bundle at startup. |
|
|
254
270
|
|
|
255
271
|
No config object to instantiate, no `enforce_policy(...)` call to remember on the platform path. The adapter / loader threads it all through.
|
|
256
272
|
|
|
@@ -266,18 +282,18 @@ Walk through one tool call:
|
|
|
266
282
|
|
|
267
283
|
`_policy_source` is set automatically by the loader based on env:
|
|
268
284
|
|
|
269
|
-
- `
|
|
270
|
-
- `
|
|
285
|
+
- `HEXGATE_LOCAL_POLICY` set → `YamlPolicySource` or `BundleDirPolicySource` (mtime-driven refresh)
|
|
286
|
+
- `HEXGATE_KEY` set, no local override → `PlatformPolicySource` (ETag / `304 Not Modified` refresh)
|
|
271
287
|
- Neither → no source attached; enforcement uses whatever was loaded once
|
|
272
288
|
|
|
273
|
-
**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 `
|
|
289
|
+
**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.
|
|
274
290
|
|
|
275
291
|
### Two carve-outs worth knowing
|
|
276
292
|
|
|
277
293
|
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).
|
|
278
|
-
2. **`approval_required` tools.** If the policy uses that mode, dev decides what happens — pass `approval_handler=` (True / False / callable) when wrapping. Default for `
|
|
294
|
+
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.
|
|
279
295
|
|
|
280
|
-
Everything else — fetch, verify, hot-reload, role selection, signature check, decision rendering, tracing — the runtime handles. Set `
|
|
296
|
+
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.
|
|
281
297
|
|
|
282
298
|
## 📦 What You Can Import
|
|
283
299
|
|
|
@@ -292,7 +308,7 @@ The current curated surface includes:
|
|
|
292
308
|
- `stream_agent_raw`
|
|
293
309
|
- `load_builtin_agent`
|
|
294
310
|
- `list_builtin_agents`
|
|
295
|
-
- `
|
|
311
|
+
- `load_hexgate_agent`
|
|
296
312
|
- `User` — async context manager for per-request user attenuation (see [User Scope](#-user-scope))
|
|
297
313
|
- `agent_tool`
|
|
298
314
|
- `web_search`
|
|
@@ -301,7 +317,7 @@ The current curated surface includes:
|
|
|
301
317
|
Example:
|
|
302
318
|
|
|
303
319
|
```python
|
|
304
|
-
from
|
|
320
|
+
from hexgate import (
|
|
305
321
|
create_agent,
|
|
306
322
|
edit_file,
|
|
307
323
|
enforce_policy,
|
|
@@ -312,7 +328,7 @@ from fortify import (
|
|
|
312
328
|
agent_tool,
|
|
313
329
|
load_agent,
|
|
314
330
|
load_builtin_agent,
|
|
315
|
-
|
|
331
|
+
load_hexgate_agent,
|
|
316
332
|
register_agent,
|
|
317
333
|
fetch,
|
|
318
334
|
web_search,
|
|
@@ -322,7 +338,7 @@ from fortify import (
|
|
|
322
338
|
|
|
323
339
|
## 🤝 Framework Agent Wrapping
|
|
324
340
|
|
|
325
|
-
In addition to its native `create_agent(...)` runtime, `
|
|
341
|
+
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:
|
|
326
342
|
|
|
327
343
|
1. **Tool-call policy enforcement.** Each tool the agent can invoke is gated by a `PolicyEnforcer` that returns a typed `Decision` (allow / deny / needs-approval) per call. Non-allow outcomes render as a `[policy_denied]` / `[approval_required]` marker the model sees as tool output (or, for pydantic_ai, a `ModelRetry`) rather than aborting the run, so the agent can recover.
|
|
328
344
|
2. **User-aware observability.** Every run is traced through Langfuse with the active `User`'s identity (user id, session id, role) propagated onto the spans.
|
|
@@ -331,7 +347,7 @@ The four integrations differ in shape because the underlying SDKs do:
|
|
|
331
347
|
|
|
332
348
|
| | OpenAI Agents SDK | LangChain / LangGraph | Google ADK | Pydantic AI |
|
|
333
349
|
| --- | --- | --- | --- | --- |
|
|
334
|
-
| Entry point | `
|
|
350
|
+
| Entry point | `HexgateRunner` (replaces `Runner`) | `wrap_langchain_agent` (returns a proxy) | `HexgateRunner` (replaces `Runner`) | `wrap_pydantic_agent` (returns a proxy) |
|
|
335
351
|
| Tool wrapping | Copies each `FunctionTool`, replaces `on_invoke_tool` with a `PolicyEnforcer`-gated version | Mutates each `BaseTool` in place (`install_enforcer_on_tool`), replaces `func`/`coroutine` with enforcer-gated versions, sets `handle_tool_error=True` | Copies each `BaseTool` (normalizing bare callables to `FunctionTool`), replaces `run_async` with a gated version | Copies each `Tool` and overrides `function_schema.call` with a gated version |
|
|
336
352
|
| Denial behavior | Returns `decision.as_error_message()` as the tool output (`[policy_denied]` / `[approval_required]` markered string) | Returns `{"ok": False, "error": decision.as_error_payload()}` so LangChain emits the structured dict as the tool result | Returns `decision.as_error_message()` as the tool output | Raises `ModelRetry(decision.as_error_message())`; pydantic_ai surfaces it back to the model as a tool-result message |
|
|
337
353
|
| Tracing | `OpenAIAgentsInstrumentor` + `propagate_attributes` | Langfuse `CallbackHandler` injected into each call's `RunnableConfig` + `propagate_attributes` | `GoogleADKInstrumentor` + `propagate_attributes` | `Agent.instrument_all()` + `propagate_attributes` |
|
|
@@ -339,19 +355,19 @@ The four integrations differ in shape because the underlying SDKs do:
|
|
|
339
355
|
|
|
340
356
|
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.
|
|
341
357
|
|
|
342
|
-
All adapters resolve the API key the same way: from the explicit `api_key=` argument, falling back to the `
|
|
358
|
+
All adapters resolve the API key the same way: from the explicit `api_key=` argument, falling back to the `HEXGATE_KEY` environment variable.
|
|
343
359
|
|
|
344
|
-
### OpenAI Agents SDK — `
|
|
360
|
+
### OpenAI Agents SDK — `HexgateRunner`
|
|
345
361
|
|
|
346
|
-
`
|
|
362
|
+
`HexgateRunner` is a drop-in replacement for `agents.Runner`. It wraps the agent's tools with a `PolicyEnforcer` at construction time and opens a `User` scope around each `Runner.run` / `run_sync` / `run_streamed` call so role resolution happens at call time.
|
|
347
363
|
|
|
348
364
|
```python
|
|
349
365
|
import asyncio
|
|
350
366
|
from agents import Agent, function_tool
|
|
351
367
|
from dotenv import load_dotenv
|
|
352
368
|
|
|
353
|
-
from
|
|
354
|
-
from
|
|
369
|
+
from hexgate.runtime import User
|
|
370
|
+
from hexgate.adapters.openai import HexgateRunner
|
|
355
371
|
|
|
356
372
|
|
|
357
373
|
@function_tool
|
|
@@ -369,7 +385,7 @@ async def main():
|
|
|
369
385
|
model="gpt-4o-mini",
|
|
370
386
|
)
|
|
371
387
|
|
|
372
|
-
runner =
|
|
388
|
+
runner = HexgateRunner() # picks up HEXGATE_KEY from env
|
|
373
389
|
result = await runner.run(
|
|
374
390
|
agent,
|
|
375
391
|
"What's the weather in Cherbourg?",
|
|
@@ -384,7 +400,7 @@ if __name__ == "__main__":
|
|
|
384
400
|
|
|
385
401
|
What happens under the hood:
|
|
386
402
|
|
|
387
|
-
- `
|
|
403
|
+
- `HexgateRunner.run` calls `wrap_openai_agent`, which builds a `PolicySet` for `(api_key, agent.name, tool_names)`, constructs one `PolicyEnforcer`, and returns a `dataclasses.replace`'d copy of the agent with policy-gated tool copies — your original `agent` is untouched.
|
|
388
404
|
- The runner opens an `async with user:` scope around the underlying `Runner.run*` call. When the model calls a tool, the guard asks `enforcer.decide(...)` for a `Decision`. On non-allow, it returns `decision.as_error_message()` — a `[policy_denied]` or `[approval_required]` markered string the model can interpret and recover from.
|
|
389
405
|
- The run executes inside `propagate_attributes(user_id=..., session_id=..., metadata={"user_role": ...})`, so Langfuse spans carry the caller identity.
|
|
390
406
|
|
|
@@ -392,7 +408,7 @@ What happens under the hood:
|
|
|
392
408
|
|
|
393
409
|
### LangChain / LangGraph — `wrap_langchain_agent`
|
|
394
410
|
|
|
395
|
-
`wrap_langchain_agent` builds a `PolicyEnforcer` once and installs it on each tool in place (`install_enforcer_on_tool`) so the same instances inside the compiled graph become policy-gated. It returns a `
|
|
411
|
+
`wrap_langchain_agent` builds a `PolicyEnforcer` once and installs it on each tool in place (`install_enforcer_on_tool`) so the same instances inside the compiled graph become policy-gated. It returns a `HexgateLangchainAgent` proxy that opens a `User` scope and injects a Langfuse callback into every `invoke` / `ainvoke` / `stream` / `astream` / `astream_events` call. The `user` is supplied **per call**, so a single wrapped agent can serve many users concurrently — role resolution happens at call time from the contextvar.
|
|
396
412
|
|
|
397
413
|
```python
|
|
398
414
|
import asyncio
|
|
@@ -401,8 +417,8 @@ from langchain_core.tools import tool
|
|
|
401
417
|
from langchain_openai import ChatOpenAI
|
|
402
418
|
from langgraph.prebuilt import create_react_agent
|
|
403
419
|
|
|
404
|
-
from
|
|
405
|
-
from
|
|
420
|
+
from hexgate.runtime import User
|
|
421
|
+
from hexgate.adapters.langchain import wrap_langchain_agent
|
|
406
422
|
|
|
407
423
|
|
|
408
424
|
@tool
|
|
@@ -429,7 +445,7 @@ async def main():
|
|
|
429
445
|
agent = wrap_langchain_agent(
|
|
430
446
|
agent=graph,
|
|
431
447
|
tools=TOOLS, # same list passed to create_react_agent — wrapped in place
|
|
432
|
-
api_key="sk-...", # or rely on
|
|
448
|
+
api_key="sk-...", # or rely on HEXGATE_KEY
|
|
433
449
|
)
|
|
434
450
|
|
|
435
451
|
result = await agent.ainvoke(
|
|
@@ -446,13 +462,13 @@ if __name__ == "__main__":
|
|
|
446
462
|
What happens under the hood:
|
|
447
463
|
|
|
448
464
|
- `wrap_langchain_agent` builds a `PolicySet` for the agent, constructs one `PolicyEnforcer(policy_set, agent_name=…)`, and calls `install_enforcer_on_tools(tools, enforcer=…)` to mutate each tool's `func` and `coroutine` with enforcer-gated closures. `handle_tool_error` is forced to `True`. Installation is idempotent — re-installing rebinds the captured originals to the new enforcer without stacking gates.
|
|
449
|
-
- Each invocation method on `
|
|
465
|
+
- Each invocation method on `HexgateLangchainAgent` takes `user=` and opens an `async with user:` (or `user.sync_scope()` for sync) around the delegated `CompiledStateGraph` call. The active `User` is pushed onto a contextvar; the guards read it at tool-call time to resolve the matching role's policy.
|
|
450
466
|
- A non-allow `Decision` is rendered as `{"ok": False, "error": decision.as_error_payload()}` so the LangChain runtime surfaces the structured dict as the tool result instead of raising.
|
|
451
467
|
- The wrapper also enters `propagate_attributes(user_id=..., session_id=..., metadata={"user_role": ...})` and merges a Langfuse `CallbackHandler` into the `RunnableConfig.callbacks` for the duration of the call. Anything not explicitly proxied falls through via `__getattr__`.
|
|
452
468
|
|
|
453
|
-
### Google ADK — `
|
|
469
|
+
### Google ADK — `HexgateRunner`
|
|
454
470
|
|
|
455
|
-
The Google ADK wrapper exposes its own `
|
|
471
|
+
The Google ADK wrapper exposes its own `HexgateRunner`. It's constructed up front with the agent, app name, and session service (mirroring the ADK `Runner` constructor) — the underlying ADK `Runner` is built once and reused since role resolution happens at call time. `run` / `run_async` then yield ADK events.
|
|
456
472
|
|
|
457
473
|
```python
|
|
458
474
|
import asyncio
|
|
@@ -464,8 +480,8 @@ from google.adk.models.lite_llm import LiteLlm
|
|
|
464
480
|
from google.adk.sessions import InMemorySessionService
|
|
465
481
|
from google.genai import types
|
|
466
482
|
|
|
467
|
-
from
|
|
468
|
-
from
|
|
483
|
+
from hexgate.runtime import User
|
|
484
|
+
from hexgate.adapters.google import HexgateRunner
|
|
469
485
|
|
|
470
486
|
|
|
471
487
|
def get_weather(city: str) -> str:
|
|
@@ -501,11 +517,11 @@ async def main():
|
|
|
501
517
|
session_id=user.session_id,
|
|
502
518
|
)
|
|
503
519
|
|
|
504
|
-
runner =
|
|
520
|
+
runner = HexgateRunner(
|
|
505
521
|
agent=agent,
|
|
506
522
|
app_name="google_runner_example",
|
|
507
523
|
session_service=session_service,
|
|
508
|
-
) # picks up
|
|
524
|
+
) # picks up HEXGATE_KEY from env
|
|
509
525
|
|
|
510
526
|
user_msg = types.Content(
|
|
511
527
|
role="user", parts=[types.Part(text="What is the weather in New Delhi?")]
|
|
@@ -522,22 +538,22 @@ if __name__ == "__main__":
|
|
|
522
538
|
|
|
523
539
|
What happens under the hood:
|
|
524
540
|
|
|
525
|
-
- At construction, `
|
|
541
|
+
- At construction, `HexgateRunner` calls `wrap_google_agent`, which builds a `PolicySet`, constructs one `PolicyEnforcer`, and returns `agent.model_copy(update={"tools": guarded_tools})` — your original `agent` is untouched.
|
|
526
542
|
- Each tool is normalized first: bare callables in `agent.tools` are wrapped into `FunctionTool` (matching what ADK does internally) so the guard has a stable `BaseTool` surface. Each tool is then `copy.copy`'d and its `run_async` replaced with an enforcer-gated version.
|
|
527
543
|
- Each `run` / `run_async` call opens a `User` scope (`user.sync_scope()` / `async with user:`) and dispatches to the cached underlying `Runner`. On non-allow, the guard returns `decision.as_error_message()` — a `[policy_denied]` or `[approval_required]` markered string — so the ADK runtime forwards it to the model as the tool output instead of aborting the run.
|
|
528
544
|
- Observability is set up lazily on each call: `GoogleADKInstrumentor().instrument()` plus `nest_asyncio.apply()` (ADK's runner spins its own loop), and the run executes inside `propagate_attributes(user_id=..., session_id=..., metadata={"user_role": ...}, tags=["google.runner.run.<agent_name>"])` so Langfuse spans carry the caller identity.
|
|
529
545
|
|
|
530
546
|
### Pydantic AI — `wrap_pydantic_agent`
|
|
531
547
|
|
|
532
|
-
`wrap_pydantic_agent` returns a `
|
|
548
|
+
`wrap_pydantic_agent` returns a `HexgatePydanticAgent` proxy backed by a clone of the original agent whose tools are gated by a freshly built `PolicyEnforcer`. Tools registered via the `Agent(...)` constructor or via `@agent.tool` / `@agent.tool_plain` are all picked up. The `user` is supplied **per call**, so a single wrapped agent can serve many users concurrently — role resolution happens at call time from the contextvar.
|
|
533
549
|
|
|
534
550
|
```python
|
|
535
551
|
import asyncio
|
|
536
552
|
from dotenv import load_dotenv
|
|
537
553
|
from pydantic_ai import Agent
|
|
538
554
|
|
|
539
|
-
from
|
|
540
|
-
from
|
|
555
|
+
from hexgate.runtime import User
|
|
556
|
+
from hexgate.adapters.pydantic_ai import wrap_pydantic_agent
|
|
541
557
|
|
|
542
558
|
|
|
543
559
|
async def main():
|
|
@@ -557,7 +573,7 @@ async def main():
|
|
|
557
573
|
|
|
558
574
|
agent = wrap_pydantic_agent(
|
|
559
575
|
agent=agent,
|
|
560
|
-
api_key="sk-...", # or rely on
|
|
576
|
+
api_key="sk-...", # or rely on HEXGATE_KEY
|
|
561
577
|
)
|
|
562
578
|
|
|
563
579
|
result = await agent.run(
|
|
@@ -578,7 +594,7 @@ if __name__ == "__main__":
|
|
|
578
594
|
What happens under the hood:
|
|
579
595
|
|
|
580
596
|
- `wrap_pydantic_agent` builds a `PolicySet`, constructs one `PolicyEnforcer`, reads tools off the agent's internal `_function_toolset`, copies each tool with an enforcer-gated `function_schema.call`, and returns a shallow-copied agent whose toolset holds those gated copies — your original `agent` is untouched, so it can be reused or wrapped again independently.
|
|
581
|
-
- Each invocation method on `
|
|
597
|
+
- Each invocation method on `HexgatePydanticAgent` (`run` / `run_sync` / `run_stream` / `iter`) takes `user=` and opens a `User` scope around the delegated `Agent` call. The contextvar is per-task, so concurrent `run` calls for different users do not see each other's policies.
|
|
582
598
|
- A non-allow `Decision` raises `ModelRetry(decision.as_error_message())`; pydantic_ai surfaces it back to the model as a tool-result message — `[policy_denied]` / `[approval_required]` markers in the same shape as the OpenAI/Google adapters — instead of aborting the run.
|
|
583
599
|
- Identity propagation uses `propagate_attributes(...)` so Langfuse spans carry the caller identity. Global tracing is enabled via `Agent.instrument_all()` on construction.
|
|
584
600
|
|
|
@@ -586,9 +602,9 @@ What happens under the hood:
|
|
|
586
602
|
|
|
587
603
|
Working scripts in `examples/`:
|
|
588
604
|
|
|
589
|
-
- `examples/customer_bot.py` — canonical
|
|
590
|
-
- `examples/openai_demo.py` — `
|
|
591
|
-
- `examples/google_demo.py` — `
|
|
605
|
+
- `examples/customer_bot.py` — canonical HexaGate path: `create_agent(...)` + the dashboard register/serve loop end-to-end.
|
|
606
|
+
- `examples/openai_demo.py` — `HexgateRunner` (OpenAI Agents SDK) end-to-end.
|
|
607
|
+
- `examples/google_demo.py` — `HexgateRunner` (Google ADK) end-to-end with `InMemorySessionService`.
|
|
592
608
|
- `examples/pydantic_ai_demo.py` — `wrap_pydantic_agent` (Pydantic AI) end-to-end.
|
|
593
609
|
|
|
594
610
|
> **Note on naming.** These demo files end in `_demo.py` so their filenames don't shadow the installed packages they import (`agents`, `google`, `langchain`, `openai`, `pydantic_ai`). Without the suffix, running any script inside `examples/` would put the directory on `sys.path[0]` and Python would import the demo files instead of the real packages.
|
|
@@ -615,14 +631,14 @@ It demonstrates:
|
|
|
615
631
|
For the CLI, you can import that script and then pick one of its registered agents:
|
|
616
632
|
|
|
617
633
|
```bash
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
634
|
+
hexgate chat --use examples/file_agents.py --agent workspace_explorer
|
|
635
|
+
hexgate chat --use examples/file_agents.py --agent repo_editor
|
|
636
|
+
hexgate chat --use examples/research_agents.py --agent update_researcher
|
|
621
637
|
```
|
|
622
638
|
|
|
623
639
|
## 🗂️ Builtin And Local Agents
|
|
624
640
|
|
|
625
|
-
The package now ships with a small `
|
|
641
|
+
The package now ships with a small `hexgate.builtin_agents` directory for official starter agents.
|
|
626
642
|
|
|
627
643
|
Current builtin agents:
|
|
628
644
|
|
|
@@ -631,7 +647,7 @@ Current builtin agents:
|
|
|
631
647
|
Example:
|
|
632
648
|
|
|
633
649
|
```python
|
|
634
|
-
from
|
|
650
|
+
from hexgate import load_builtin_agent
|
|
635
651
|
|
|
636
652
|
agent, handler = load_builtin_agent("researcher")
|
|
637
653
|
```
|
|
@@ -645,7 +661,7 @@ The CLI also discovers local agents from:
|
|
|
645
661
|
This repo ships a demo agent at `examples/example_agent/`, so from the project root you can simply run:
|
|
646
662
|
|
|
647
663
|
```bash
|
|
648
|
-
|
|
664
|
+
hexgate chat --agent example_agent
|
|
649
665
|
```
|
|
650
666
|
|
|
651
667
|
## 🔐 Policy Shape
|
|
@@ -685,7 +701,7 @@ Every tool call routes through a `PolicyEnforcer` that returns `allow` / `deny`
|
|
|
685
701
|
`create_agent(...)` stays close to LangChain. Policy enforcement is applied after agent creation:
|
|
686
702
|
|
|
687
703
|
```python
|
|
688
|
-
from
|
|
704
|
+
from hexgate import AgentPolicy, create_agent, enforce_policy, fetch, web_search
|
|
689
705
|
|
|
690
706
|
policy = AgentPolicy.model_validate(
|
|
691
707
|
{
|
|
@@ -721,7 +737,7 @@ That means the same agent code can stay simple in development, while deployment
|
|
|
721
737
|
|
|
722
738
|
## 🧩 Policy Bundles — Compile, Sign, Enforce (WASM)
|
|
723
739
|
|
|
724
|
-
|
|
740
|
+
HexaGate has **two policy enforcement engines** that return identical decisions (there's a parity test suite that proves it):
|
|
725
741
|
|
|
726
742
|
- **pydantic** (default) — evaluates constraints in-process. Zero setup; this is what every example above uses.
|
|
727
743
|
- **WASM** — compiles `policy.yaml` → Rego → a WebAssembly module evaluated via `wasmtime`. This is the path production ships: one compiled artifact, byte-for-byte reproducible, cryptographically signed by the platform.
|
|
@@ -737,24 +753,24 @@ brew install opa # macOS
|
|
|
737
753
|
# or see https://www.openpolicyagent.org/docs/latest/#running-opa
|
|
738
754
|
```
|
|
739
755
|
|
|
740
|
-
Without `opa` on `PATH`, `
|
|
756
|
+
Without `opa` on `PATH`, `hexgate policy build --no-wasm` still emits the yaml + rego (no `.wasm`), and the pydantic engine keeps working.
|
|
741
757
|
|
|
742
|
-
### The `
|
|
758
|
+
### The `hexgate policy` CLI
|
|
743
759
|
|
|
744
760
|
```bash
|
|
745
761
|
# Validate a policy.yaml without the network — parse + check every constraint
|
|
746
|
-
|
|
762
|
+
hexgate policy validate policy.yaml
|
|
747
763
|
|
|
748
764
|
# See the Rego your YAML compiles to (stdout)
|
|
749
|
-
|
|
765
|
+
hexgate policy show-rego policy.yaml
|
|
750
766
|
|
|
751
767
|
# Dry-run a single decision. --engine wasm compiles + evaluates in wasmtime
|
|
752
768
|
# (matching production); the default pydantic engine needs no opa.
|
|
753
|
-
|
|
769
|
+
hexgate policy test policy.yaml --role billing --tool refund_order \
|
|
754
770
|
--args '{"amount": 200, "currency": "USD"}' --engine wasm
|
|
755
771
|
|
|
756
772
|
# Compile a bundle: writes {stem}.yaml + .rego + .wasm + .bundle.json
|
|
757
|
-
|
|
773
|
+
hexgate policy build policy.yaml --out ./bundle
|
|
758
774
|
```
|
|
759
775
|
|
|
760
776
|
On a denied decision, `test` prints the reason; the wasm engine additionally lists each violated constraint string verbatim:
|
|
@@ -768,7 +784,7 @@ On a denied decision, `test` prints the reason; the wasm engine additionally lis
|
|
|
768
784
|
|
|
769
785
|
### What's in a bundle
|
|
770
786
|
|
|
771
|
-
`
|
|
787
|
+
`hexgate policy build` produces a directory:
|
|
772
788
|
|
|
773
789
|
| File | Contents |
|
|
774
790
|
|---|---|
|
|
@@ -780,29 +796,29 @@ On a denied decision, `test` prints the reason; the wasm engine additionally lis
|
|
|
780
796
|
|
|
781
797
|
The manifest's hashes authenticate the files; the signature authenticates the manifest. Verifying both proves the whole bundle came from the trusted signer, untampered.
|
|
782
798
|
|
|
783
|
-
### Local enforcement — `
|
|
799
|
+
### Local enforcement — `HEXGATE_LOCAL_POLICY`
|
|
784
800
|
|
|
785
801
|
Point an agent at a local source and every tool call routes through the WASM engine instead of pydantic — no platform needed. Two shapes are accepted, and both **hot-reload on save** (no restart, no manual rebuild between turns):
|
|
786
802
|
|
|
787
|
-
| `
|
|
803
|
+
| `HEXGATE_LOCAL_POLICY=…` | What happens | When to use |
|
|
788
804
|
|---|---|---|
|
|
789
|
-
| **`./bundle/`** (output of `
|
|
805
|
+
| **`./bundle/`** (output of `hexgate policy build`) | Stat the bundle manifest each turn; reload if its mtime changed. | Production-shaped local testing — exercises the exact signed-bundle path. |
|
|
790
806
|
| **`./policy.yaml`** | Stat the yaml each turn; recompile via `opa` when its mtime changed. | The tight dev loop — edit yaml, save, ask again. No build step. |
|
|
791
807
|
|
|
792
808
|
```bash
|
|
793
809
|
# Pre-built bundle dir — rebuild it mid-session, next chat picks it up
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
# [
|
|
810
|
+
hexgate policy build policy.yaml --out ./bundle
|
|
811
|
+
HEXGATE_LOCAL_POLICY=./bundle hexgate chat --agent researcher
|
|
812
|
+
# [hexgate] HEXGATE_LOCAL_POLICY active (bundle-dir): ./bundle (wasm_hash=7e6d1f8b..., unsigned)
|
|
797
813
|
|
|
798
814
|
# Raw yaml — edit policy.yaml in your editor, save, next chat sees the new policy
|
|
799
|
-
|
|
800
|
-
# [
|
|
815
|
+
HEXGATE_LOCAL_POLICY=./policy.yaml hexgate chat --agent researcher
|
|
816
|
+
# [hexgate] HEXGATE_LOCAL_POLICY active (yaml): ./policy.yaml (wasm_hash=ab12..., unsigned)
|
|
801
817
|
```
|
|
802
818
|
|
|
803
|
-
The bundle's integrity (files match the manifest) is verified on every reload — a stale or corrupt bundle fails immediately, not at the first tool call. Yaml sources default to **unsigned**: set `
|
|
819
|
+
The bundle's integrity (files match the manifest) is verified on every reload — a stale or corrupt bundle fails immediately, not at the first tool call. Yaml sources default to **unsigned**: set `HEXGATE_BUNDLE_SIGN_KEY_PATH=./keys/dev.private` to sign each recompile with your `hexgate policy keygen` key, so downstream gates that check `bundle.is_signed` see what they expect.
|
|
804
820
|
|
|
805
|
-
> **Same refresh seam as the platform.** Under the hood both sources implement `PolicySource.fetch()`; the agent runtime calls it at the top of every turn and only swaps the active policy when the returned bundle is a new instance. Unchanged → identity match → no work. That's the same hot-reload path `
|
|
821
|
+
> **Same refresh seam as the platform.** Under the hood both sources implement `PolicySource.fetch()`; the agent runtime calls it at the top of every turn and only swaps the active policy when the returned bundle is a new instance. Unchanged → identity match → no work. That's the same hot-reload path `hexgate serve` uses for platform-edited YAML.
|
|
806
822
|
|
|
807
823
|
### Signing & verification
|
|
808
824
|
|
|
@@ -811,22 +827,22 @@ Production bundles are signed so the runtime can prove a bundle is genuine befor
|
|
|
811
827
|
Generate a keypair and sign a bundle locally:
|
|
812
828
|
|
|
813
829
|
```bash
|
|
814
|
-
|
|
815
|
-
|
|
830
|
+
hexgate policy keygen --out ./keys/dev # → dev.private (0600) + dev.public
|
|
831
|
+
hexgate policy build policy.yaml --out ./bundle --sign-key ./keys/dev.private
|
|
816
832
|
# → ./bundle/policy.bundle.json.sig
|
|
817
833
|
```
|
|
818
834
|
|
|
819
835
|
At runtime, point the verifier at the public key:
|
|
820
836
|
|
|
821
837
|
```bash
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
# [
|
|
838
|
+
HEXGATE_LOCAL_POLICY=./bundle \
|
|
839
|
+
HEXGATE_BUNDLE_PUBKEY_PATH=./keys/dev.public \
|
|
840
|
+
HEXGATE_BUNDLE_REQUIRE_SIGNATURE=true \
|
|
841
|
+
hexgate chat --agent researcher
|
|
842
|
+
# [hexgate] HEXGATE_LOCAL_POLICY active (bundle-dir): ./bundle (wasm_hash=..., signed)
|
|
827
843
|
```
|
|
828
844
|
|
|
829
|
-
`
|
|
845
|
+
`HEXGATE_BUNDLE_REQUIRE_SIGNATURE` controls strictness — warn-by-default keeps local dev frictionless; opt into refusal for CI/prod:
|
|
830
846
|
|
|
831
847
|
| Bundle | `PUBKEY_PATH` set | `REQUIRE_SIGNATURE` | Outcome |
|
|
832
848
|
|---|---|---|---|
|
|
@@ -858,7 +874,7 @@ The `action` dict carries `{"tool_name", "arguments", "agent_name"}`; `context`
|
|
|
858
874
|
Example:
|
|
859
875
|
|
|
860
876
|
```python
|
|
861
|
-
from
|
|
877
|
+
from hexgate import (
|
|
862
878
|
AgentPolicy,
|
|
863
879
|
create_agent,
|
|
864
880
|
edit_file,
|
|
@@ -913,7 +929,7 @@ Supported on **macOS and Linux only** (Windows is unsupported). If `srt` is not
|
|
|
913
929
|
Tune the boundary through `LocalWorkspace`:
|
|
914
930
|
|
|
915
931
|
```python
|
|
916
|
-
from
|
|
932
|
+
from hexgate.runtime import LocalWorkspace
|
|
917
933
|
|
|
918
934
|
workspace = LocalWorkspace(
|
|
919
935
|
root_dir="./project",
|
|
@@ -988,11 +1004,11 @@ Policy-bundle enforcement (see [Policy Bundles](#-policy-bundles--compile-sign-e
|
|
|
988
1004
|
|
|
989
1005
|
| Env var | Purpose |
|
|
990
1006
|
|---|---|
|
|
991
|
-
| `
|
|
992
|
-
| `
|
|
993
|
-
| `
|
|
994
|
-
| `
|
|
995
|
-
| `
|
|
1007
|
+
| `HEXGATE_LOCAL_POLICY` | Path to a bundle directory **or** a `policy.yaml`; routes enforcement through the WASM engine and hot-reloads on save |
|
|
1008
|
+
| `HEXGATE_BUNDLE_PUBKEY_PATH` | base64url Ed25519 public key used to verify a bundle's signature |
|
|
1009
|
+
| `HEXGATE_BUNDLE_SIGN_KEY_PATH` | base64url Ed25519 private key used to sign locally-compiled yaml sources (so `bundle.is_signed` is True) |
|
|
1010
|
+
| `HEXGATE_BUNDLE_REQUIRE_SIGNATURE` | `true` to refuse unsigned or unverifiable bundles (default: warn only) |
|
|
1011
|
+
| `HEXGATE_OPA_BIN` | Override the `opa` binary location (default: search `PATH`) |
|
|
996
1012
|
|
|
997
1013
|
## 🧪 Tests & Dev Tooling
|
|
998
1014
|
|
|
@@ -1018,11 +1034,11 @@ Targets at a glance:
|
|
|
1018
1034
|
| **M2 policy demo** | |
|
|
1019
1035
|
| `policy-build` | Compile the example policy.yaml to a bundle |
|
|
1020
1036
|
| `policy-test-wasm` | Smoke a WASM-engine decision |
|
|
1021
|
-
| `demo-override` | Build a deny bundle + chat with `
|
|
1037
|
+
| `demo-override` | Build a deny bundle + chat with `HEXGATE_LOCAL_POLICY` |
|
|
1022
1038
|
| **Platform demo** (multi-terminal — see `make demo-platform`) | |
|
|
1023
1039
|
| `platform-api` / `platform-api-install` / `platform-api-test` | FastAPI control plane in `platform/api/` |
|
|
1024
1040
|
| `dashboard` / `dashboard-install` | Vite + React app in `platform/dashboard/` |
|
|
1025
|
-
| `serve` | `
|
|
1041
|
+
| `serve` | `hexgate serve` — bridge this SDK to the platform |
|
|
1026
1042
|
| `demo-platform` | Print the 3-terminal recipe |
|
|
1027
1043
|
| **Misc** | |
|
|
1028
1044
|
| `build` / `clean` | Package + tidy |
|
|
@@ -1060,40 +1076,40 @@ python examples/demo.py
|
|
|
1060
1076
|
Run the inline chat CLI with a local or builtin YAML agent:
|
|
1061
1077
|
|
|
1062
1078
|
```bash
|
|
1063
|
-
|
|
1079
|
+
hexgate chat --agent example_agent
|
|
1064
1080
|
```
|
|
1065
1081
|
|
|
1066
1082
|
Run the CLI with code-defined agents from a Python script:
|
|
1067
1083
|
|
|
1068
1084
|
```bash
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1085
|
+
hexgate chat --use examples/file_agents.py --agent workspace_explorer
|
|
1086
|
+
hexgate chat --use examples/file_agents.py --agent repo_editor
|
|
1087
|
+
hexgate chat --use examples/research_agents.py --agent update_researcher
|
|
1088
|
+
hexgate chat --use examples/research_agents.py --agent update_researcher --approval-mode ask
|
|
1073
1089
|
```
|
|
1074
1090
|
|
|
1075
1091
|
List what the CLI can currently resolve:
|
|
1076
1092
|
|
|
1077
1093
|
```bash
|
|
1078
|
-
|
|
1094
|
+
hexgate chat --list-agents
|
|
1079
1095
|
```
|
|
1080
1096
|
|
|
1081
|
-
### `
|
|
1097
|
+
### `hexgate register` — push a manifest to the platform
|
|
1082
1098
|
|
|
1083
|
-
Register a code-defined agent's manifest with the
|
|
1099
|
+
Register a code-defined agent's manifest with the HexaGate platform. `--agent`
|
|
1084
1100
|
takes a Python import path of the form `module.path:attribute`, the same shape
|
|
1085
1101
|
as ASGI/WSGI entrypoints. The CLI imports the module, grabs the agent object,
|
|
1086
|
-
and POSTs its manifest to `${
|
|
1087
|
-
`${
|
|
1102
|
+
and POSTs its manifest to `${HEXGATE_API_URL}/v1/agents` using
|
|
1103
|
+
`${HEXGATE_KEY}` as the bearer token:
|
|
1088
1104
|
|
|
1089
1105
|
```bash
|
|
1090
|
-
|
|
1091
|
-
|
|
1106
|
+
hexgate register --agent examples.customer_bot:agent
|
|
1107
|
+
hexgate register --agent my_app.agents:my_agent --description "Customer support bot"
|
|
1092
1108
|
```
|
|
1093
1109
|
|
|
1094
1110
|
On first register, the platform auto-generates a starter role-aware
|
|
1095
1111
|
policy from the manifest's tool list (`read_only` mixin + `default` +
|
|
1096
|
-
`member` + `admin`) and signs a WASM bundle so `
|
|
1112
|
+
`member` + `admin`) and signs a WASM bundle so `hexgate serve` runs
|
|
1097
1113
|
against real enforcement from the first request. Edit the policy in
|
|
1098
1114
|
the dashboard's `/policies` page; subsequent re-registers preserve
|
|
1099
1115
|
those edits — only the manifest snapshot grows.
|
|
@@ -1105,25 +1121,25 @@ you can pass each of those pieces explicitly. Only `--tools` is required;
|
|
|
1105
1121
|
fields on the manifest so the dashboard can show them:
|
|
1106
1122
|
|
|
1107
1123
|
```bash
|
|
1108
|
-
|
|
1124
|
+
hexgate register \
|
|
1109
1125
|
--agent my_app.agents:graph \
|
|
1110
1126
|
--tools my_app.tools:my_tools \
|
|
1111
1127
|
--model gpt-4o-mini \
|
|
1112
1128
|
--system-prompt prompts/support.md
|
|
1113
1129
|
```
|
|
1114
1130
|
|
|
1115
|
-
For everyone else — agents built with `
|
|
1131
|
+
For everyone else — agents built with `hexgate.create_agent(...)`, OpenAI
|
|
1116
1132
|
Agents, Pydantic AI, Google ADK — the manifest reads tools, model, and
|
|
1117
1133
|
system prompt directly off the object. No flags needed.
|
|
1118
1134
|
|
|
1119
1135
|
`--system-prompt` accepts either a literal string or a path to a `.md` /
|
|
1120
1136
|
`.txt` / `.jinja` file (read as text at register time).
|
|
1121
1137
|
|
|
1122
|
-
Supported frameworks: OpenAI Agents SDK, Google ADK, Pydantic AI, LangChain/LangGraph,
|
|
1138
|
+
Supported frameworks: OpenAI Agents SDK, Google ADK, Pydantic AI, LangChain/LangGraph, HexaGate agents.
|
|
1123
1139
|
|
|
1124
|
-
### `
|
|
1140
|
+
### `hexgate serve` — bridge a local agent to the platform's relay
|
|
1125
1141
|
|
|
1126
|
-
`
|
|
1142
|
+
`hexgate serve` takes the **same** `module:attr` spec as `hexgate register`.
|
|
1127
1143
|
The CLI imports the agent, derives the manifest in one call, auto-registers
|
|
1128
1144
|
on the platform (idempotent — content-hash short-circuits no-ops), fetches
|
|
1129
1145
|
the operator's policy from the cloud, and opens the WebSocket relay so the
|
|
@@ -1131,13 +1147,13 @@ dashboard's Playground can drive it. Policy edits in `/policies` take
|
|
|
1131
1147
|
effect at the next chat-turn boundary.
|
|
1132
1148
|
|
|
1133
1149
|
```bash
|
|
1134
|
-
|
|
1150
|
+
hexgate serve examples.customer_bot:agent
|
|
1135
1151
|
|
|
1136
1152
|
# CI / deliberate-deploy: error if not pre-registered
|
|
1137
|
-
|
|
1153
|
+
hexgate serve examples.customer_bot:agent --no-auto-register
|
|
1138
1154
|
```
|
|
1139
1155
|
|
|
1140
|
-
There is **no** `
|
|
1156
|
+
There is **no** `HEXGATE_AGENT_NAME` env var anymore — the name lives in
|
|
1141
1157
|
the agent's `.name` attribute (or the `name=` kwarg you passed to
|
|
1142
1158
|
`create_react_agent` / `create_agent`). The platform is the source of
|
|
1143
1159
|
truth for policy; your Python file is the source of truth for code.
|
|
@@ -1149,24 +1165,24 @@ inspect it, persist it elsewhere, diff it across versions, or wire it
|
|
|
1149
1165
|
into a custom registration flow — call `create_manifest` directly:
|
|
1150
1166
|
|
|
1151
1167
|
```python
|
|
1152
|
-
from
|
|
1168
|
+
from hexgate import create_manifest
|
|
1153
1169
|
|
|
1154
1170
|
manifest = create_manifest(agent, description="Customer support bot")
|
|
1155
1171
|
print(manifest.model_dump())
|
|
1156
1172
|
```
|
|
1157
1173
|
|
|
1158
1174
|
`create_manifest` dispatches on the framework of `agent`. The supported
|
|
1159
|
-
types are the same set `
|
|
1175
|
+
types are the same set `hexgate register` accepts: HexaGate, OpenAI Agents
|
|
1160
1176
|
SDK, Google ADK, Pydantic AI, and LangChain/LangGraph compiled graphs.
|
|
1161
1177
|
For LangGraph you must pass `tools=` explicitly, and may pass `model=` /
|
|
1162
1178
|
`system_prompt=`, since compiled graphs don't expose those fields after
|
|
1163
1179
|
compilation.
|
|
1164
1180
|
|
|
1165
1181
|
The return value is an `AgentManifest` (a Pydantic model, also re-exported
|
|
1166
|
-
from `
|
|
1182
|
+
from `hexgate` for type annotations) — the same schema the platform
|
|
1167
1183
|
stores and the dashboard renders.
|
|
1168
1184
|
|
|
1169
|
-
## 🌐
|
|
1185
|
+
## 🌐 HexaGate Platform
|
|
1170
1186
|
|
|
1171
1187
|
The `platform/` directory contains an optional control plane that hosts agent definitions, dev tokens, and a live debug surface. The SDK works fully without it (`load_local_agent`, `load_builtin_agent` keep their existing semantics) — but with it you get:
|
|
1172
1188
|
|
|
@@ -1194,10 +1210,10 @@ Endpoints:
|
|
|
1194
1210
|
- `GET /v1/projects/:id/agents` — list agents with their YAMLs
|
|
1195
1211
|
- `GET /v1/projects/:id/agents/:name` — read one agent
|
|
1196
1212
|
- `PUT /v1/projects/:id/agents/:name` — save agent / policy / system YAMLs
|
|
1197
|
-
- `WS /v1/projects/:id/serve` — producer socket (the `
|
|
1213
|
+
- `WS /v1/projects/:id/serve` — producer socket (the `hexgate serve` CLI dials here)
|
|
1198
1214
|
- `WS /v1/projects/:id/chat` — consumer socket (the dashboard Playground dials here)
|
|
1199
1215
|
|
|
1200
|
-
DB lives at `platform/api/
|
|
1216
|
+
DB lives at `platform/api/hexgate.db`. Delete it and restart to wipe state.
|
|
1201
1217
|
|
|
1202
1218
|
### Dashboard (`platform/dashboard/`)
|
|
1203
1219
|
|
|
@@ -1220,23 +1236,23 @@ Routes:
|
|
|
1220
1236
|
|
|
1221
1237
|
The dev server proxies `/v1/*` (HTTP and WebSocket) to `localhost:8000`, so HMR works through the same origin as the API.
|
|
1222
1238
|
|
|
1223
|
-
### Serve Mode (`
|
|
1239
|
+
### Serve Mode (`hexgate serve`)
|
|
1224
1240
|
|
|
1225
1241
|
Bridges your local agent runtime to the dashboard via the platform's WebSocket relay — same pattern as Cloudflare Tunnel or ngrok.
|
|
1226
1242
|
|
|
1227
1243
|
```bash
|
|
1228
1244
|
# in asianf/.env
|
|
1229
|
-
|
|
1230
|
-
|
|
1245
|
+
HEXGATE_KEY=fty_live_<project>_<biscuit>
|
|
1246
|
+
HEXGATE_API_URL=http://localhost:8000 # optional, defaults to localhost:8000
|
|
1231
1247
|
|
|
1232
1248
|
# pick an agent module:attr — uvicorn-style spec
|
|
1233
|
-
uv run
|
|
1249
|
+
uv run hexgate serve examples.customer_bot:agent
|
|
1234
1250
|
```
|
|
1235
1251
|
|
|
1236
1252
|
Behaviour:
|
|
1237
1253
|
|
|
1238
1254
|
- Loads the agent object from the `module:attr` spec — same form as
|
|
1239
|
-
`
|
|
1255
|
+
`hexgate register`. The agent's name, tools, model, and system
|
|
1240
1256
|
prompt come from the object directly (no flags duplicating
|
|
1241
1257
|
what's already in code).
|
|
1242
1258
|
- Auto-registers the manifest on first run via `POST /v1/agents`
|
|
@@ -1245,10 +1261,10 @@ Behaviour:
|
|
|
1245
1261
|
- Fetches the operator's policy from `GET /v1/agents/{name}`. Local
|
|
1246
1262
|
code is authoritative for code; the platform is authoritative for
|
|
1247
1263
|
policy.
|
|
1248
|
-
- Connects `wss://${
|
|
1264
|
+
- Connects `wss://${HEXGATE_API_URL}/v1/serve` with the bearer
|
|
1249
1265
|
percent-encoded into the WebSocket subprotocol (Phase 6 — the WS
|
|
1250
1266
|
handshake grammar doesn't allow `=` padding in plain headers).
|
|
1251
|
-
Server echoes `
|
|
1267
|
+
Server echoes `hexgate.v1` to confirm the contract.
|
|
1252
1268
|
- Sends a `hello` frame announcing the agent name (the dashboard's
|
|
1253
1269
|
"Serving" indicator reads this).
|
|
1254
1270
|
- On each inbound `chat` message, **refreshes the active policy**
|
|
@@ -1262,30 +1278,30 @@ Behaviour:
|
|
|
1262
1278
|
serve mode for prompts (planned: dashboard-side approval UI).
|
|
1263
1279
|
- Reconnects with exponential backoff on socket drop.
|
|
1264
1280
|
|
|
1265
|
-
There's no longer a `
|
|
1281
|
+
There's no longer a `HEXGATE_AGENT_NAME` env var, `--agent` flag, or
|
|
1266
1282
|
`--use` flag — the spec carries everything. If you've been setting
|
|
1267
|
-
`
|
|
1283
|
+
`HEXGATE_AGENT_NAME` in `.env`, drop it.
|
|
1268
1284
|
|
|
1269
|
-
### How `load_agent()` resolves with `
|
|
1285
|
+
### How `load_agent()` resolves with `HEXGATE_KEY`
|
|
1270
1286
|
|
|
1271
1287
|
```python
|
|
1272
|
-
from
|
|
1288
|
+
from hexgate import load_agent
|
|
1273
1289
|
|
|
1274
1290
|
agent, handler = load_agent("read_only") # explicit name required
|
|
1275
1291
|
```
|
|
1276
1292
|
|
|
1277
|
-
When `
|
|
1278
|
-
the platform (via `
|
|
1293
|
+
When `HEXGATE_KEY` is set, `load_agent(name)` fetches the named agent from
|
|
1294
|
+
the platform (via `load_hexgate_agent`). When `HEXGATE_KEY` is not set, it
|
|
1279
1295
|
falls back to local / registered / builtin resolution — no platform call.
|
|
1280
1296
|
|
|
1281
|
-
The legacy `
|
|
1282
|
-
direct callers of `
|
|
1283
|
-
explicit name. For the CLI workflow, `
|
|
1297
|
+
The legacy `HEXGATE_AGENT_NAME` env-var fallback was removed in Phase 7;
|
|
1298
|
+
direct callers of `load_hexgate_agent` / `load_agent` must pass an
|
|
1299
|
+
explicit name. For the CLI workflow, `hexgate serve <module:attr>` derives
|
|
1284
1300
|
the name from the loaded agent's `.name` attribute — no env var needed.
|
|
1285
1301
|
|
|
1286
1302
|
## 👤 User Scope + Roles
|
|
1287
1303
|
|
|
1288
|
-
Real backends serve many users, and different users get different capabilities.
|
|
1304
|
+
Real backends serve many users, and different users get different capabilities. HexaGate splits that into two pieces:
|
|
1289
1305
|
|
|
1290
1306
|
- **`User`** — the per-request scope. Marks "this invocation acts on behalf of alice, in role X." Async context manager; pushes a fact-bearing Biscuit through the agent runtime.
|
|
1291
1307
|
- **Role policies** — one `policy.yaml` per role, optionally inheriting from a base mixin. The runtime picks the right one at call time based on the active `User.role`.
|
|
@@ -1295,9 +1311,9 @@ The two are deliberately decoupled: tokens carry **identity** (who is calling),
|
|
|
1295
1311
|
### Minimal example
|
|
1296
1312
|
|
|
1297
1313
|
```python
|
|
1298
|
-
from
|
|
1314
|
+
from hexgate import User, load_hexgate_agent, stream_agent
|
|
1299
1315
|
|
|
1300
|
-
agent, handler =
|
|
1316
|
+
agent, handler = load_hexgate_agent("support-bot") # client + roles attached at load
|
|
1301
1317
|
|
|
1302
1318
|
async with User(user_id="alice", role="billing", ttl_seconds=300):
|
|
1303
1319
|
async for event in stream_agent(agent, handler, "refund customer 30"):
|
|
@@ -1312,10 +1328,10 @@ The cleanest production shape — set the scope once in middleware, every endpoi
|
|
|
1312
1328
|
|
|
1313
1329
|
```python
|
|
1314
1330
|
from fastapi import FastAPI
|
|
1315
|
-
from
|
|
1331
|
+
from hexgate import User, load_hexgate_agent, stream_agent
|
|
1316
1332
|
|
|
1317
1333
|
app = FastAPI()
|
|
1318
|
-
agent, handler =
|
|
1334
|
+
agent, handler = load_hexgate_agent("support-bot") # at startup
|
|
1319
1335
|
|
|
1320
1336
|
@app.middleware("http")
|
|
1321
1337
|
async def attach_user(request, call_next):
|
|
@@ -1415,7 +1431,7 @@ Switch to `User(user_id="alice", role="default")` and `refund_order` itself is m
|
|
|
1415
1431
|
|
|
1416
1432
|
- **Single-file policies still work.** A legacy `policy.yaml` is treated as the `default` role — no migration needed for agents that don't yet differentiate by role.
|
|
1417
1433
|
- **Lazy attenuation.** `User.__aenter__` only pushes a contextvar — the cryptographic work happens inside `stream_agent` / `invoke_agent` the first time the agent runs. Errors surface at first agent call, not at scope entry.
|
|
1418
|
-
- **Local agents skip attenuation.** A `User` scope around a `load_local_agent` / `load_builtin_agent` agent logs a warning and runs with no facts. The `default` policy still applies — use `
|
|
1434
|
+
- **Local agents skip attenuation.** A `User` scope around a `load_local_agent` / `load_builtin_agent` agent logs a warning and runs with no facts. The `default` policy still applies — use `load_hexgate_agent` for the full signed chain.
|
|
1419
1435
|
- **Explicit override.** Passing `tool_use_context=` explicitly to `stream_agent` / `invoke_agent` wins over an active `User` scope. Useful for tests or one-off bypass.
|
|
1420
1436
|
- **Sync callers.** `User` exposes both `async with user:` and `user.sync_scope()`. The async form is the primary API (room for KMS / audit / JWKS I/O in `__aenter__` / `__aexit__` later); the sync mirror exists for CLI loops and `Runner.run_sync`-style callers where the async ctxmgr protocol is unavailable.
|
|
1421
1437
|
|
|
@@ -1424,7 +1440,7 @@ Switch to `User(user_id="alice", role="default")` and `refund_order` itself is m
|
|
|
1424
1440
|
For direct Python usage, the simplest runtime path is:
|
|
1425
1441
|
|
|
1426
1442
|
```python
|
|
1427
|
-
from
|
|
1443
|
+
from hexgate import stream_agent
|
|
1428
1444
|
|
|
1429
1445
|
async for event in stream_agent(agent, handler, "latest AI breakthroughs"):
|
|
1430
1446
|
...
|