Skip to content

feat(approval): generic config-driven tool-call approval gate - #662

Open
hashbender wants to merge 1 commit into
mainfrom
mirror/pr-56811
Open

feat(approval): generic config-driven tool-call approval gate#662
hashbender wants to merge 1 commit into
mainfrom
mirror/pr-56811

Conversation

@hashbender

Copy link
Copy Markdown
Owner

Summary

Adds a human-in-the-loop approval gate for designated tool calls. Default OFF — with no config the behaviour is unchanged everywhere.

Why

Some deployments need certain tools (e.g. send_email, delete_*, API write-back tools) to require human sign-off before executing, without blocking every call and without a custom wrapper per tool. The existing dangerous-command engine has all the right primitives (inline ask, session/permanent allowlist, Kanban staging); this wires those to named or glob-matched tool names.

Two approval modes

Mode When Behaviour
Inline Live gateway session, channel present Blocks the call; sends approve/deny prompt to the user (same UX as dangerous commands)
Deferred Unattended (cron, background, no live channel), or force_deferred Stages to pending/actions/<id>.json, opens a Kanban approval card, returns a non-error "staged" result so the agent continues

Execution-on-approval is Hermes-native: approving the card mints an agent-assigned execution card; the dispatcher wakes a worker, which replays the staged call with a one-shot per-pending-id token (consumed on first match so it passes through exactly once; TTL + status state machine guard against double-execution).

Decision ladder in check_tool_approval

  1. Gate disabled → allow.
  2. Tool not designated → allow.
  3. YOLO / approvals.mode == off → allow.
  4. One-shot replay token for this pending_id → allow + consume.
  5. Prior inline session/permanent approval → allow.
  6. Select mode (inline vs deferred) → resolve.

Config example

approvals:
  tool_gate:
    require_approval:
      - send_email      # exact name
      - delete_*        # glob pattern
    default_mode: deferred    # or inline
    force_deferred: [send_email]
    allow_inline: [delete_file]

Files

File Change
tools/tool_gate.py New — staged-approval mechanics, config reader, replay token, summarize_tool_call, approve_action, replay_pending_action, Kanban card helpers. Optional Mattermost notification is no-op unless credentials are set.
tools/approval.py Add check_tool_approval + helpers before check_execute_code_guard.
tools/write_approval.py Add ACTIONS subsystem + update_pending() atomic helper.
agent/tool_executor.py Gate at sequential dispatch choke point.
agent/agent_runtime_helpers.py Gate at concurrent dispatch choke point (invoke_tool).
gateway/run.py Tool-flavoured approval header for kind == "tool" prompts.
tests/tools/test_tool_gate.py 27 unit tests (gate on/off, modes, staging, inline approve/deny, session/permanent, replay, TTL, double-exec).
tests/run_agent/test_tool_gate_dispatch.py 3 integration tests at each choke point.

Test plan

  • tests/tools/test_tool_gate.py — 27 unit tests pass
  • tests/run_agent/test_tool_gate_dispatch.py — 3 integration tests pass (sequential + concurrent)
  • Gate is default-OFF: no config → every existing test unaffected
  • Branch is from upstream/main (not the fork's feature branches)

🤖 Generated with Claude Code


Mirror-of: NousResearch#56811
NousResearch#56811

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant