hexgate 0.2.3__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hexgate-0.2.3 → hexgate-0.2.4}/PKG-INFO +26 -1
- {hexgate-0.2.3 → hexgate-0.2.4}/README.md +25 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/langchain/agent.py +6 -3
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/agents/factory.py +23 -8
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/audit.py +30 -4
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/_common.py +17 -7
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/register/hexgate.py +1 -1
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/register/models.py +1 -1
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/runtime/context.py +12 -9
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/binding.py +16 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate.egg-info/PKG-INFO +26 -1
- {hexgate-0.2.3 → hexgate-0.2.4}/pyproject.toml +1 -1
- {hexgate-0.2.3 → hexgate-0.2.4}/LICENSE +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/google/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/google/runner.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/google/tools.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/google/wrapper.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/langchain/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/langchain/tools.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/langchain/wrapper.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/openai/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/openai/runner.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/openai/tools.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/openai/wrapper.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/pydantic_ai/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/pydantic_ai/agent.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/pydantic_ai/tools.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/adapters/pydantic_ai/wrapper.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/agents/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/agents/builtin/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/agents/builtin/researcher/agent.yaml +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/agents/builtin/researcher/policy.yaml +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/agents/builtin/researcher/system.md +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/agents/loader.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/agents/models.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/agents/prompts/agent_system.md +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/bootstrap.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/chat.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/policy/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/policy/main.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/register/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/register/google.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/register/langchain.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/register/main.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/register/manifest.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/register/openai.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/register/pydantic_ai.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/register/register.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/serve.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cli/state.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cloud/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cloud/attenuate.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cloud/biscuit.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/cloud/client.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/config/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/config/settings.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/runtime/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/runtime/command_policy.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/runtime/sandbox_runtime.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/runtime/srt.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/runtime/workspace.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/bundle.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/constraints.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/decision.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/enforcer.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/errors.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/file_scope.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/models.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/policy.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/policy_set.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/rego.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/rego_wasm.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/signing.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/source.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/security/wasm_engine.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/streaming/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/streaming/events.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/streaming/normalize.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tools/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tools/bash.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tools/decorators.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tools/fetch.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tools/files/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tools/files/_common.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tools/files/edit_file.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tools/files/glob.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tools/files/grep.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tools/files/read_file.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tools/files/write_file.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tools/refund.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tools/websearch.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tracing/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/tracing/langfuse.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/utils/__init__.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate/utils/retry.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate.egg-info/SOURCES.txt +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate.egg-info/dependency_links.txt +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate.egg-info/entry_points.txt +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate.egg-info/requires.txt +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/hexgate.egg-info/top_level.txt +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/setup.cfg +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/tests/test_bootstrap.py +0 -0
- {hexgate-0.2.3 → hexgate-0.2.4}/tests/test_demo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexgate
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Hexgate — authorization infrastructure for AI agents (agent runtime + cloud client).
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.13
|
|
@@ -223,6 +223,31 @@ make dashboard-install # pnpm install inside platform/dashboard/
|
|
|
223
223
|
|
|
224
224
|
Then open http://localhost:5173/playground — type a message, watch the live stream of tool calls and policy decisions from your local agent.
|
|
225
225
|
|
|
226
|
+
### Control-plane database (SQLite / Postgres)
|
|
227
|
+
|
|
228
|
+
`make platform-api` uses a local **SQLite** file — zero setup, fine for
|
|
229
|
+
dev and the test suite. Set `DATABASE_URL` to run on **Postgres** instead
|
|
230
|
+
(what deployments use); bare `postgres://` URLs are normalized to the
|
|
231
|
+
`asyncpg` driver. A local Postgres is in the dev compose:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
make platform-api-pg # starts Postgres (Docker) + runs the API against it
|
|
235
|
+
# equivalent to:
|
|
236
|
+
make postgres-up # start Postgres, wait until healthy
|
|
237
|
+
DATABASE_URL=postgresql+asyncpg://hexgate:hexgate-dev-password@localhost:5433/hexgate make platform-api
|
|
238
|
+
make postgres-reset # wipe ONLY the Postgres data volume
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
No migration system: the schema is created with `create_all`, so a model
|
|
242
|
+
change means resetting the volume (`make postgres-reset`), not migrating —
|
|
243
|
+
data is treated as disposable. A managed Postgres (e.g. Scaleway) needs SSL
|
|
244
|
+
in the DSN, e.g. `…/hexgate?ssl=require`.
|
|
245
|
+
|
|
246
|
+
`DATABASE_URL` is read from the real environment or `platform/api/.env`
|
|
247
|
+
(see `.env.sample`). The opt-in `tests/test_postgres_smoke.py` is the only
|
|
248
|
+
test that exercises the Postgres path (the rest run on SQLite); it runs
|
|
249
|
+
when `DATABASE_URL` points at Postgres.
|
|
250
|
+
|
|
226
251
|
### Audit log (ClickHouse)
|
|
227
252
|
|
|
228
253
|
Policy decisions are written to a local ClickHouse instance for the audit dashboard. Requires Docker.
|
|
@@ -184,6 +184,31 @@ make dashboard-install # pnpm install inside platform/dashboard/
|
|
|
184
184
|
|
|
185
185
|
Then open http://localhost:5173/playground — type a message, watch the live stream of tool calls and policy decisions from your local agent.
|
|
186
186
|
|
|
187
|
+
### Control-plane database (SQLite / Postgres)
|
|
188
|
+
|
|
189
|
+
`make platform-api` uses a local **SQLite** file — zero setup, fine for
|
|
190
|
+
dev and the test suite. Set `DATABASE_URL` to run on **Postgres** instead
|
|
191
|
+
(what deployments use); bare `postgres://` URLs are normalized to the
|
|
192
|
+
`asyncpg` driver. A local Postgres is in the dev compose:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
make platform-api-pg # starts Postgres (Docker) + runs the API against it
|
|
196
|
+
# equivalent to:
|
|
197
|
+
make postgres-up # start Postgres, wait until healthy
|
|
198
|
+
DATABASE_URL=postgresql+asyncpg://hexgate:hexgate-dev-password@localhost:5433/hexgate make platform-api
|
|
199
|
+
make postgres-reset # wipe ONLY the Postgres data volume
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
No migration system: the schema is created with `create_all`, so a model
|
|
203
|
+
change means resetting the volume (`make postgres-reset`), not migrating —
|
|
204
|
+
data is treated as disposable. A managed Postgres (e.g. Scaleway) needs SSL
|
|
205
|
+
in the DSN, e.g. `…/hexgate?ssl=require`.
|
|
206
|
+
|
|
207
|
+
`DATABASE_URL` is read from the real environment or `platform/api/.env`
|
|
208
|
+
(see `.env.sample`). The opt-in `tests/test_postgres_smoke.py` is the only
|
|
209
|
+
test that exercises the Postgres path (the rest run on SQLite); it runs
|
|
210
|
+
when `DATABASE_URL` points at Postgres.
|
|
211
|
+
|
|
187
212
|
### Audit log (ClickHouse)
|
|
188
213
|
|
|
189
214
|
Policy decisions are written to a local ClickHouse instance for the audit dashboard. Requires Docker.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator
|
|
5
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator, Literal
|
|
6
6
|
|
|
7
7
|
from langchain_core.runnables import RunnableConfig
|
|
8
8
|
from langfuse import get_client, propagate_attributes
|
|
@@ -134,10 +134,10 @@ class HexgateLangchainAgent:
|
|
|
134
134
|
async def astream_events(
|
|
135
135
|
self,
|
|
136
136
|
input: dict[str, Any],
|
|
137
|
-
version: str,
|
|
138
137
|
*,
|
|
139
138
|
user: User,
|
|
140
139
|
config: RunnableConfig | None = None,
|
|
140
|
+
version: Literal["v1", "v2"] = "v2",
|
|
141
141
|
**kwargs: Any,
|
|
142
142
|
) -> AsyncIterator[dict[str, Any]]:
|
|
143
143
|
"""Stream the agent events asynchronously inside a User scope."""
|
|
@@ -145,7 +145,10 @@ class HexgateLangchainAgent:
|
|
|
145
145
|
async with user:
|
|
146
146
|
with propagate_attributes(**self._propagate_kwargs(user, "astream_events")):
|
|
147
147
|
async for event in self._agent.astream_events(
|
|
148
|
-
input,
|
|
148
|
+
input,
|
|
149
|
+
config=self._with_callbacks(config),
|
|
150
|
+
version=version,
|
|
151
|
+
**kwargs,
|
|
149
152
|
):
|
|
150
153
|
yield event
|
|
151
154
|
|
|
@@ -190,6 +190,13 @@ def extract_input_text(input: AgentInput) -> str:
|
|
|
190
190
|
return _extract_query_from_messages(input)
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
# One-shot dedupe for the "local agent with active User scope" warning
|
|
194
|
+
# inside `_resolve_user_facts`. The dashboard playground hits this once
|
|
195
|
+
# per turn (and the platform-served sessions never hit it at all), so
|
|
196
|
+
# without a flag the warning floods the log on every chat message.
|
|
197
|
+
_warned_local_agent_user_scope: bool = False
|
|
198
|
+
|
|
199
|
+
|
|
193
200
|
def _resolve_user_facts(agent: HexgateAgent) -> dict[str, list[str | int]] | None:
|
|
194
201
|
"""Lazily attenuate when a :class:`User` scope is active.
|
|
195
202
|
|
|
@@ -206,14 +213,22 @@ def _resolve_user_facts(agent: HexgateAgent) -> dict[str, list[str | int]] | Non
|
|
|
206
213
|
client = agent.hexgate_client
|
|
207
214
|
if client is None:
|
|
208
215
|
# Local agent or test stub — User scope is set but there's nothing to
|
|
209
|
-
# attenuate against. Surface a single warning so devs see why their
|
|
210
|
-
# `requires_user` predicate isn't firing on a local-loaded agent
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
216
|
+
# attenuate against. Surface a *single* warning so devs see why their
|
|
217
|
+
# `requires_user` predicate isn't firing on a local-loaded agent, then
|
|
218
|
+
# stay quiet: every subsequent turn would re-fire the same message
|
|
219
|
+
# otherwise (3-5x per chat session in the dashboard playground was
|
|
220
|
+
# the symptom).
|
|
221
|
+
global _warned_local_agent_user_scope
|
|
222
|
+
if not _warned_local_agent_user_scope:
|
|
223
|
+
import logging
|
|
224
|
+
|
|
225
|
+
logging.getLogger(__name__).warning(
|
|
226
|
+
"User scope active but agent has no hexgate_client; "
|
|
227
|
+
"biscuit_facts will be empty (use load_hexgate_agent for "
|
|
228
|
+
"attenuation). Subsequent occurrences in this process are "
|
|
229
|
+
"suppressed."
|
|
230
|
+
)
|
|
231
|
+
_warned_local_agent_user_scope = True
|
|
217
232
|
return None
|
|
218
233
|
from hexgate.cloud.attenuate import attenuate_for_user
|
|
219
234
|
from hexgate.cloud.biscuit import (
|
|
@@ -149,7 +149,13 @@ class AuditSender:
|
|
|
149
149
|
# latch onto the first loop that drives them and reject any other
|
|
150
150
|
# (e.g. a second asyncio.run()). Build them eagerly so configure()
|
|
151
151
|
# stays sync, but track the loop and rebuild if it rotates.
|
|
152
|
-
|
|
152
|
+
#
|
|
153
|
+
# Capture the build-time loop so emit() can reach it from an executor
|
|
154
|
+
# thread (a sync tool under run_in_executor has no loop of its own).
|
|
155
|
+
try:
|
|
156
|
+
self._loop: asyncio.AbstractEventLoop | None = asyncio.get_running_loop()
|
|
157
|
+
except RuntimeError:
|
|
158
|
+
self._loop = None
|
|
153
159
|
self._semaphore = asyncio.Semaphore(max_in_flight)
|
|
154
160
|
self._client: httpx.AsyncClient | None = self._new_client()
|
|
155
161
|
self._tasks: set[asyncio.Task[None]] = set()
|
|
@@ -182,11 +188,31 @@ class AuditSender:
|
|
|
182
188
|
try:
|
|
183
189
|
loop = asyncio.get_running_loop()
|
|
184
190
|
except RuntimeError:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
191
|
+
# Called off-loop (sync tool on a run_in_executor thread): route
|
|
192
|
+
# to the build-time loop instead of dropping the event.
|
|
193
|
+
loop = self._loop
|
|
194
|
+
if loop is None or loop.is_closed():
|
|
195
|
+
if not self._warned_no_loop:
|
|
196
|
+
_log.warning(
|
|
197
|
+
"audit emit called with no running loop and no live "
|
|
198
|
+
"bound loop; skipping"
|
|
199
|
+
)
|
|
200
|
+
self._warned_no_loop = True
|
|
201
|
+
return
|
|
202
|
+
try:
|
|
203
|
+
loop.call_soon_threadsafe(self._spawn_send, event)
|
|
204
|
+
except RuntimeError:
|
|
205
|
+
pass # loop torn down between the is_closed() check and the call
|
|
188
206
|
return
|
|
189
207
|
self._ensure_loop_state(loop)
|
|
208
|
+
self._spawn_send(event)
|
|
209
|
+
|
|
210
|
+
def _spawn_send(self, event: AuditEvent) -> None:
|
|
211
|
+
"""Create the send task. MUST run on the bound loop's thread —
|
|
212
|
+
``create_task`` and the loop-bound semaphore require the running loop.
|
|
213
|
+
Reached on-loop from :meth:`emit`, or via ``call_soon_threadsafe``."""
|
|
214
|
+
if self._closing or self._client is None:
|
|
215
|
+
return
|
|
190
216
|
if self._semaphore.locked():
|
|
191
217
|
self._dropped += 1
|
|
192
218
|
if self._dropped % 100 == 1:
|
|
@@ -209,13 +209,11 @@ def build_runtime_from_local_agent(
|
|
|
209
209
|
"""
|
|
210
210
|
import os
|
|
211
211
|
|
|
212
|
-
import yaml
|
|
213
|
-
|
|
214
212
|
from hexgate.agents.factory import enforce_policy
|
|
215
213
|
from hexgate.cli.register.manifest import create_manifest
|
|
216
214
|
from hexgate.cli.register.register import post_manifest
|
|
217
215
|
from hexgate.cloud.client import HexgateClient, HexgateConfig
|
|
218
|
-
from hexgate.security.
|
|
216
|
+
from hexgate.security.binding import platform_policy_from_payload
|
|
219
217
|
from hexgate.tracing.langfuse import get_langfuse_handler
|
|
220
218
|
|
|
221
219
|
manifest = create_manifest(agent_obj, description=description)
|
|
@@ -239,7 +237,7 @@ def build_runtime_from_local_agent(
|
|
|
239
237
|
|
|
240
238
|
config = HexgateConfig.from_env()
|
|
241
239
|
client = HexgateClient(config)
|
|
242
|
-
payload,
|
|
240
|
+
payload, initial_etag = client.get_agent(agent_name)
|
|
243
241
|
if payload is None:
|
|
244
242
|
# Invariant: no If-None-Match was sent, so a 304 is impossible.
|
|
245
243
|
# Raise so `python -O` can't strip the check.
|
|
@@ -248,10 +246,22 @@ def build_runtime_from_local_agent(
|
|
|
248
246
|
"on initial fetch (no If-None-Match was sent)"
|
|
249
247
|
)
|
|
250
248
|
|
|
251
|
-
|
|
252
|
-
|
|
249
|
+
# platform_policy_from_payload returns the canonical (engine, source)
|
|
250
|
+
# pair: handles signed-bundle vs pydantic fallback, and seeds the
|
|
251
|
+
# PlatformPolicySource with the bundle + ETag so the next refresh is
|
|
252
|
+
# a 304 unless policy changed. Without the source kwarg, refresh_policy()
|
|
253
|
+
# at the top of every stream_agent() is a no-op and dashboard edits
|
|
254
|
+
# only land at the next `hexgate serve` restart.
|
|
255
|
+
policy, refresh_source = platform_policy_from_payload(
|
|
256
|
+
client, agent_name, payload, initial_etag
|
|
257
|
+
)
|
|
253
258
|
|
|
254
|
-
enforced = enforce_policy(
|
|
259
|
+
enforced = enforce_policy(
|
|
260
|
+
agent_obj,
|
|
261
|
+
policy,
|
|
262
|
+
approval_handler=approval_handler,
|
|
263
|
+
source=refresh_source,
|
|
264
|
+
)
|
|
255
265
|
|
|
256
266
|
# Fresh handler for the streaming layer. The user's create_agent() call
|
|
257
267
|
# built its own handler but discarded it; we make a new one bound to
|
|
@@ -28,7 +28,7 @@ def create_hexgate_manifest(
|
|
|
28
28
|
return AgentManifest(
|
|
29
29
|
name=agent.name,
|
|
30
30
|
description=description,
|
|
31
|
-
framework=AgentFramework.
|
|
31
|
+
framework=AgentFramework.HEXGATE,
|
|
32
32
|
model=_extract_model(agent.model),
|
|
33
33
|
system_prompt=_extract_system_prompt(agent.system_prompt),
|
|
34
34
|
tools=[_to_tool_definition(t) for t in agent.tools],
|
|
@@ -9,7 +9,6 @@ from collections.abc import Iterator
|
|
|
9
9
|
from contextlib import contextmanager
|
|
10
10
|
from contextvars import ContextVar, Token
|
|
11
11
|
from dataclasses import dataclass
|
|
12
|
-
from typing import Any
|
|
13
12
|
|
|
14
13
|
from pydantic import BaseModel, ConfigDict, PrivateAttr
|
|
15
14
|
|
|
@@ -96,26 +95,30 @@ class User(BaseModel):
|
|
|
96
95
|
session_id: str | None = None
|
|
97
96
|
ttl_seconds: int | None = None
|
|
98
97
|
|
|
99
|
-
# Stack
|
|
100
|
-
|
|
98
|
+
# Stack of shadowed values (supports nested scopes). We save/restore via
|
|
99
|
+
# set() rather than reset(token): async-generator finalizers run __aexit__
|
|
100
|
+
# in a different Context, where a token reset would raise — set() doesn't.
|
|
101
|
+
_saved: list["User | None"] = PrivateAttr(default_factory=list)
|
|
101
102
|
|
|
102
103
|
async def __aenter__(self) -> "User":
|
|
103
|
-
self.
|
|
104
|
+
self._saved.append(_CURRENT_USER.get())
|
|
105
|
+
_CURRENT_USER.set(self)
|
|
104
106
|
return self
|
|
105
107
|
|
|
106
108
|
async def __aexit__(self, *_: object) -> None:
|
|
107
|
-
if self.
|
|
108
|
-
_CURRENT_USER.
|
|
109
|
+
if self._saved:
|
|
110
|
+
_CURRENT_USER.set(self._saved.pop())
|
|
109
111
|
|
|
110
112
|
@contextmanager
|
|
111
113
|
def sync_scope(self) -> Iterator["User"]:
|
|
112
114
|
"""Sync mirror of ``async with self`` for sync entry points."""
|
|
113
|
-
self.
|
|
115
|
+
self._saved.append(_CURRENT_USER.get())
|
|
116
|
+
_CURRENT_USER.set(self)
|
|
114
117
|
try:
|
|
115
118
|
yield self
|
|
116
119
|
finally:
|
|
117
|
-
if self.
|
|
118
|
-
_CURRENT_USER.
|
|
120
|
+
if self._saved:
|
|
121
|
+
_CURRENT_USER.set(self._saved.pop())
|
|
119
122
|
|
|
120
123
|
|
|
121
124
|
_CURRENT_USER: ContextVar[User | None] = ContextVar(
|
|
@@ -170,6 +170,22 @@ def platform_policy_from_payload(
|
|
|
170
170
|
"Refusing to fall back to the pydantic engine."
|
|
171
171
|
)
|
|
172
172
|
else:
|
|
173
|
+
# Loud one-shot signal (fires at load time, not per turn) so an
|
|
174
|
+
# operator running `hexgate serve` doesn't silently get the
|
|
175
|
+
# pydantic engine when they expected the production-shaped
|
|
176
|
+
# WASM path. Common cause: `opa` not installed on the platform
|
|
177
|
+
# host — see compile_bundle() in platform/api/services.py, which
|
|
178
|
+
# logs "opa not on PATH" on the server side too.
|
|
179
|
+
logger.warning(
|
|
180
|
+
"policy for %r served without a WASM bundle — falling back to "
|
|
181
|
+
"the pydantic engine. Decisions are equivalent (parity-tested), "
|
|
182
|
+
"but signature verification and signed-artifact distribution "
|
|
183
|
+
"are off. Install `opa` on the platform host and re-save the "
|
|
184
|
+
"policy to get the WASM path; set %s=true to refuse the "
|
|
185
|
+
"fallback entirely.",
|
|
186
|
+
agent_name,
|
|
187
|
+
_REQUIRE_SIGNATURE_ENV_VAR,
|
|
188
|
+
)
|
|
173
189
|
policy = load_policy_set_from_dict(
|
|
174
190
|
yaml.safe_load(payload.get("policy_yaml") or "") or {}
|
|
175
191
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexgate
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Hexgate — authorization infrastructure for AI agents (agent runtime + cloud client).
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.13
|
|
@@ -223,6 +223,31 @@ make dashboard-install # pnpm install inside platform/dashboard/
|
|
|
223
223
|
|
|
224
224
|
Then open http://localhost:5173/playground — type a message, watch the live stream of tool calls and policy decisions from your local agent.
|
|
225
225
|
|
|
226
|
+
### Control-plane database (SQLite / Postgres)
|
|
227
|
+
|
|
228
|
+
`make platform-api` uses a local **SQLite** file — zero setup, fine for
|
|
229
|
+
dev and the test suite. Set `DATABASE_URL` to run on **Postgres** instead
|
|
230
|
+
(what deployments use); bare `postgres://` URLs are normalized to the
|
|
231
|
+
`asyncpg` driver. A local Postgres is in the dev compose:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
make platform-api-pg # starts Postgres (Docker) + runs the API against it
|
|
235
|
+
# equivalent to:
|
|
236
|
+
make postgres-up # start Postgres, wait until healthy
|
|
237
|
+
DATABASE_URL=postgresql+asyncpg://hexgate:hexgate-dev-password@localhost:5433/hexgate make platform-api
|
|
238
|
+
make postgres-reset # wipe ONLY the Postgres data volume
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
No migration system: the schema is created with `create_all`, so a model
|
|
242
|
+
change means resetting the volume (`make postgres-reset`), not migrating —
|
|
243
|
+
data is treated as disposable. A managed Postgres (e.g. Scaleway) needs SSL
|
|
244
|
+
in the DSN, e.g. `…/hexgate?ssl=require`.
|
|
245
|
+
|
|
246
|
+
`DATABASE_URL` is read from the real environment or `platform/api/.env`
|
|
247
|
+
(see `.env.sample`). The opt-in `tests/test_postgres_smoke.py` is the only
|
|
248
|
+
test that exercises the Postgres path (the rest run on SQLite); it runs
|
|
249
|
+
when `DATABASE_URL` points at Postgres.
|
|
250
|
+
|
|
226
251
|
### Audit log (ClickHouse)
|
|
227
252
|
|
|
228
253
|
Policy decisions are written to a local ClickHouse instance for the audit dashboard. Requires Docker.
|
|
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
|
|
|
9
9
|
# 0.2.0 (the original package name was taken on PyPI by a 2014
|
|
10
10
|
# abandoned project; the team consolidated on `hexgate` for everything).
|
|
11
11
|
name = "hexgate"
|
|
12
|
-
version = "0.2.
|
|
12
|
+
version = "0.2.4"
|
|
13
13
|
description = "Hexgate — authorization infrastructure for AI agents (agent runtime + cloud client)."
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
license = "MIT"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|