Skip to content

[bug] BLOCK/HITL enforcement is missing for sync ToolNode.invoke #45

@Muhammad-usman92

Description

@Muhammad-usman92

What went wrong

The SDK enforces BLOCK/HITL policy for async LangGraph tool execution through ToolNode.ainvoke, but the sync ToolNode.invoke patch only injects callbacks and then calls the original tool execution.

That means sync agent runs can execute tools without waiting for a verdict, even when the backend policy is in BLOCK or HITL mode. Async and sync execution should have equivalent policy behaviour.

Relevant code:

  • sdk/adrian/__init__.py: _patch_tool_node()
  • patched_invoke injects callbacks and calls original_invoke
  • patched_ainvoke waits for LoginAck, checks active policy, waits for the tool-call verdict, and can return blocked tool messages

Reproduction steps

  1. Configure Adrian policy mode as block or hitl, with M3/M4 in scope.
  2. Initialise the SDK with WebSocket enabled.
  3. Build a LangGraph agent or ToolNode that emits a tool call which receives an in-scope blocking verdict.
  4. Run the agent using sync execution, for example graph.invoke(...) or ToolNode.invoke(...).
  5. Compare with the async path, for example await graph.ainvoke(...) or await ToolNode.ainvoke(...).

Expected behaviour

Sync ToolNode.invoke should enforce the same BLOCK/HITL policy as async ToolNode.ainvoke.

If the verdict is in scope for the active policy, the tool should not execute and the SDK should return the same blocked tool response shape used by the async path.

Actual behaviour

Sync ToolNode.invoke currently forwards directly to the original tool execution after callback injection. It does not wait for a verdict and does not call the existing halt logic.

As a result, tools can execute in sync agent runs even though the same tool call would be blocked in async execution.

Environment

  • Adrian version / commit: current main
  • OS: not expected to be OS-specific
  • Docker version: not required to reproduce
  • GPU model: not required to reproduce

Logs

Relevant code path

Sync path currently only injects callbacks:

def patched_invoke(self, input, config=None, **kwargs):
    config = _inject_callbacks(config)
    return original_invoke(self, input, config=config, **kwargs)
verdict = await ws.wait_for_tool_call_verdict(tool_call_id, timeout)

if _should_halt(verdict):
    return _build_blocked_response(tool_calls)


</details>

Suggested fix

Mirror the async policy gate in the sync ToolNode.invoke path, or fail closed when sync enforcement cannot safely wait for a verdict under an active BLOCK/HITL policy.

A regression test should cover sync ToolNode.invoke with an in-scope blocking verdict and verify that the underlying tool does not execute.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions