HexgateContext— the per-request scope. Marks “this invocation acts on behalf of alice, with roles X, and these attributes.” Async context manager; pushes a fact-bearing Biscuit through the agent runtime.- Role policies — one
policy.yamlper role, optionally inheriting from a base mixin. At call time the runtime evaluates every role the active context carries and takes the most permissive outcome.
Minimal example
attenuate_for_user, extract_facts, or ToolUseContext plumbing at
the call site. The runtime mints the per-request token, picks the billing
role’s policy file, and evaluates its constraints against each tool call. Pass
more than one role and each is evaluated — see
most permissive wins.
Filtering on attributes (ctx.*)
Beyond the role, a context can carry an open attributes bag that policy
constraints read through the ctx.* namespace — turning role-based access into
attribute-based (ABAC) filtering:
ctx.<key> fails closed (deny), like any absent reference.
FastAPI pattern
The scope must enclose the streaming iteration itself, because the role is resolved lazily — read on each tool call as the agent runs (see Notes). The robust shape is to open the scope inside the generator that produces the response:async with HexgateContext(...): return await invoke_agent(...) is fine — the agent runs to completion inside the scope.
HexgateContext fields
Multiple roles: most permissive wins
Every role inuser_roles is evaluated and the outcomes are combined, so
access is granted if any single role grants it:
user_roles=["support", "billing"] can do everything support can
do plus everything billing can do. Adding a role can only ever widen access,
never narrow it. Evaluation stops at the first role that allows the call, and the
role that granted it is recorded on the decision as deciding_role (granted by: in hexgate chat, and on the Decision your approval handler and any
decision observer receive).
The persisted audit trail does not carry
deciding_role yet. Its role column
keeps its existing meaning — the caller’s first role — so a multi-role decision
is stored as who was calling, not which role granted the call. Nothing
recorded is wrong, but for genuinely multi-role callers it is incomplete until
the audit pipeline lands the full role set. Read provenance from the Decision
in-process in the meantime.Role policies — one file per role
Agents that need per-role behaviour ship apolicies/ directory instead of a
single policy.yaml:
AgentPolicy. Inheritance is left-to-right, child
wins on conflicts:
ctx.*) and a role-scoped end-to-end walkthrough.
Notes
- Single-file policies still work. A legacy
policy.yamlis treated as thedefaultrole — no migration needed. - Lazy attenuation.
HexgateContext.__aenter__only pushes a contextvar — the cryptographic work happens insidestream_agent/invoke_agentthe first time the agent runs. Errors surface at first agent call, not at scope entry. - Local agents skip attenuation. A context scope around a
load_local_agentagent logs a warning and runs with no facts. Thedefaultpolicy still applies — useload_hexgate_agentfor the full signed chain. - Explicit override. Passing
tool_use_context=explicitly tostream_agent/invoke_agentwins over an active context scope. Useful for tests or one-off bypass. - Sync callers.
HexgateContextexposes bothasync with ctx:andctx.sync_scope(). The async form is the primary API; the sync mirror exists for CLI loops andRunner.run_sync-style callers.