Skip to content

feat(adk): add standard tool policy and human approval middleware - #1260

Open
Dragonzz27 wants to merge 1 commit into
cloudwego:mainfrom
Dragonzz27:feat/tool-policy-middleware
Open

Dragonzz27 wants to merge 1 commit into
cloudwego:mainfrom
Dragonzz27:feat/tool-policy-middleware

Conversation

@Dragonzz27

Copy link
Copy Markdown

Implements #996

Motivation

Eino has powerful primitives (tool middleware, interrupt/resume), but application developers still hand-roll a policy layer for deciding whether a tool call should be allowed, denied, rewritten, audited, or interrupted for human approval. This PR adds the standard, reusable abstraction proposed in #996, following the thin-hook direction that converged in the issue discussion: the middleware owns only the invocation seam and lifecycle integration; policy storage, approval storage, and audit persistence stay deployment-specific (audit can plug into existing callback handlers).

Design

New adk/middlewares/toolpolicy package:

  • Policy interfaceDecide(ctx, *Request) (*Result, error); Request carries tool name, call ID, and raw JSON arguments. A PolicyFunc adapter is provided.
  • Three verdicts
    • allow: execute, optionally with rewritten/sanitized arguments;
    • deny: the model receives a safe tool result (default or custom message) without executing the tool;
    • require_approval: the run interrupts with the policy's ApprovalInfo and waits.
  • Approval via existing interrupt/resume — no second checkpoint mechanism: approval state is persisted in the checkpoint, and the approver resumes with *toolpolicy.Approval as resume data (Approved, optional denial Message, optional RewrittenArguments). Unapproved sibling calls re-interrupt with the same user-facing info.
  • Full coverage of the invocation seam — all four tool-call kinds are wrapped (invokable, streamable, enhanced invokable, enhanced streamable), for both Message and AgenticMessage agents.
  • Fails closed — policy errors, nil results, and unknown decisions surface as errors; a missing Policy is rejected at construction.

Example

mw, _ := toolpolicy.New(ctx, &toolpolicy.Config{Policy: toolpolicy.PolicyFunc(
    func(ctx context.Context, req *toolpolicy.Request) (*toolpolicy.Result, error) {
        if req.ToolName == "delete_database" {
            return &toolpolicy.Result{
                Decision:     toolpolicy.DecisionRequireApproval,
                ApprovalInfo: fmt.Sprintf("approve %s(%s)?", req.ToolName, req.Arguments),
            }, nil
        }
        return &toolpolicy.Result{Decision: toolpolicy.DecisionAllow}, nil
    })})

agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
    ...
    Handlers: []adk.ChatModelAgentMiddleware{mw},
})

// after the run interrupts:
runner.ResumeWithParams(ctx, checkPointID, &adk.ResumeParams{
    Targets: map[string]any{interruptID: &toolpolicy.Approval{Approved: true}},
})

Tests

18 tests, all green with -race:

  • endpoint-level: allow / rewritten args / deny (default & custom message) / approval interrupt / policy error / nil result / unknown decision, across all four tool-call kinds;
  • end-to-end through ChatModelAgent + Runner + checkpoint store: approval granted (tool runs with original args), rejected (denial message reaches the model), granted with rewritten arguments, and two parallel approvals where the unapproved sibling re-interrupts before the run completes;
  • genericity: the same middleware serves *schema.AgenticMessage agents.

Add adk/middlewares/toolpolicy implementing the thin-hook direction
discussed in cloudwego#996: a reusable ChatModelAgentMiddleware that gates every
tool call through a small Policy interface and rides on ADK's existing
interrupt/resume machinery instead of introducing a second checkpoint
or approval-storage mechanism.

Per tool call, the policy can:
- allow execution, optionally with rewritten/sanitized arguments;
- deny execution, returning a safe tool result to the model;
- require human approval, interrupting the run with user-facing
  ApprovalInfo until the run is resumed with an *Approval decision.

The middleware wraps all four tool-call kinds (invokable, streamable,
enhanced invokable, enhanced streamable), works for both Message and
AgenticMessage agents, keeps approval state in the checkpoint so
unapproved sibling calls re-interrupt with the same info, and fails
closed on policy errors. Decision auditing can be layered on via the
existing callback handlers.
@CLAassistant

CLAassistant commented Sep 8, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants