New to the adapters? Start with the wrapping overview and comparison
table.
Compatible versions: see the compatibility
table for the verified
google-adk range.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.
Under the hood
- At construction,
HexgateRunnercallswrap_google_agent, which builds aPolicySet, constructs onePolicyEnforcer, and returnsagent.model_copy(update={"tools": guarded_tools})— your originalagentis untouched. - Each tool is normalized first: bare callables in
agent.toolsare wrapped intoFunctionTool(matching what ADK does internally) so the guard has a stableBaseToolsurface. Each tool is thencopy.copy’d and itsrun_asyncreplaced with an enforcer-gated version. - Each
run/run_asynccall opens aHexgateContextscope (hexgate_context.sync_scope()/async with hexgate_context:) and dispatches to the cached underlyingRunner. On non-allow, the guard returnsdecision.as_error_message()so the ADK runtime forwards it to the model as the tool output instead of aborting the run. - Observability is set up lazily on each call:
GoogleADKInstrumentor().instrument()plusnest_asyncio.apply()(ADK’s runner spins its own loop), and the run executes insidepropagate_attributes(user_id=..., session_id=..., metadata={"user_roles": ...}, tags=["google.runner.run.<agent_name>"]).
Agent-level enforcement
The Google runner has the fullest agent-level coverage, because ADK expresses both delegation modes as tool calls. It enforces admission at run entry, and reach atbefore_tool_callback for both
transfer_to_agent (handoff, agent.handoff:<name>) and AgentTool
(agent-as-tool, agent.tool:<name>) — a denied delegation is refused before it
runs. Set HexgateRunner(max_handoff_depth=N) to cap a runaway handoff chain.
Skills
ADK ships agent skills throughSkillToolset, and
HexgateRunner discovers and gates them with no extra wiring:
What registration records
For each skill in aSkillToolset, the manifest records its name, description,
the reference, asset and script files it ships, and a hash of its SKILL.md
body. The skill’s allowed-tools frontmatter is recorded too.
A skill whose frontmatter declares metadata.adk_additional_tools enlarges the
agent’s callable tool surface once it is activated: ADK only exposes those tools
to the model after the skill loads. Hexgate records them in the manifest’s tool
list as well — under the name the model will call, toolset prefix included — so
the generated policy covers them and they are gated like any other tool. A skill
that declares additional tools is treated like one that ships scripts in the
starter policy.
What is gated
EachSkillToolset tool maps to one level:
list_skills is left alone on purpose: the skill catalog is already in the system
prompt, so gating it would only hide what exists. The mapping is by tool class, so
a tool_name_prefix on the toolset does not change it, and a tool of your own
named load_skill is gated as an ordinary tool.
A run_skill_script decision sees one spelling of the path — scripts/x and x
both arrive as args.file_path == "scripts/x" — and the script’s invocation as
args.script_args, args.short_options and args.positional_args, so a
constraint can fence what a script is run with.
Until the policy lists a skill under skills:, these are ordinary tool calls,
decided under their tool names.
What a denial looks like
A denied or held skill comes back to the model as the tool result, marked like any other ADK denial:How reliably models honour that sentence has not yet been observed and written
up. Treat it as a nudge, not a guarantee: the tools the skill would have steered
toward are still gated only by their own rules.
Caveats
- Requires
google-adk >= 1.25.0, whereSkillToolsetwas added. Hexgate itself supportsgoogle-adk >= 1.14; on an older ADK there are no skills to discover and nothing changes. SkillToolsetis experimental upstream. Hexgate reaches some ADK internals to hash a skill’s body at decision time; if a future ADK moves them, the hash is omitted — and a pinned skill denies rather than passes.- Not containment. A denied
load_skillhides the instructions; it does not stop the model calling the agent’s other tools. See Agent skills.
Runnable example
examples/devops_google.py — HexgateRunner (Google ADK) end-to-end with
InMemorySessionService.