Human-in-the-loop (HIL) for tool execution.
Overview
Require user approval before tools execute:
1from inferencesh import tool, string23delete_file = (4 tool("delete_file")5 .describe("Delete a file from the system")6 .param("path", string("File path"))7 .require_approval() # ← Requires approval8 .handler(lambda args: delete(args["path"]))9 .build()10)How It Works
- Agent decides to call tool with
requireApproval - Execution pauses with status
awaiting_approval - User sees the tool call and can approve or reject
- If approved: tool executes normally
- If rejected: agent receives rejection and can try alternatives
Approval Flow
1Agent: "I need to delete old-file.txt"2 [calls delete_file(path="old-file.txt")]34System: ⏸ Awaiting approval5 Tool: delete_file6 Args: {"path": "old-file.txt"}7 [Approve] [Reject]89User: [clicks Approve]1011Tool: Executes delete("old-file.txt")1213Agent: "Done! I've deleted old-file.txt"Any Tool Type
All tool types support approval:
1# Client Tools2tool("dangerous_action").require_approval().handler(do_dangerous).build()34# App Tools5app_tool("expensive_model", "expensive/model@latest").require_approval().build()67# Agent Tools8agent_tool("expert", "my-team/expert@latest").require_approval().build()910# Webhook Tools11webhook_tool("external_api", "https://api.example.com").require_approval().build()Use Cases
Destructive Operations
1delete_tool = (2 tool("delete")3 .describe("Delete data")4 .require_approval()5 .build()6)External Integrations
1send_email = (2 webhook_tool("send_email", "https://api.email.com/send")3 .describe("Send an email")4 .require_approval() # Don't spam!5 .build()6)Expensive Operations
1premium_analysis = (2 app_tool("analyze", "expensive/analyzer@latest")3 .describe("Premium analysis ($5/call)")4 .require_approval()5 .build()6)Agent template defaults
Saved agent versions (AgentVersionDTO / version on POST /agents) may include a permissions object. Today the only field is allow_all_tools.
When permissions.allow_all_tools is true, each new chat in the agent's home workspace starts with allow all tools enabled (agent_data.allow_all_tools and the allow_all_tools chat rule). Chats another workspace starts with a published agent do not copy this default — they run on that workspace's remotes and tools, and the agent author cannot disable approvals there. The chat owns the setting from then on — toggling it in chat settings or via POST /chats/:id/settings only affects that conversation; redeploying the agent does not change existing chats.
Use this for agents that run unattended (webhook, cron, or trigger dispatches) where pausing on every tool would stall the run. Team and org policy layers can still ask or deny over the chat default.
Set defaults in the agent template (REST version.permissions, SDK types, or top-level permissions in belt agent deploy YAML). See Create or update agent template.
1# agent.yaml (belt agent deploy)2name: oncall-triage3permissions:4 allow_all_tools: trueTypes: AgentPermissions in the Python and JavaScript SDKs.
Per-chat permissions
Approvals are scoped to each chat through policy rules. The workspace chat settings → Permissions pane lists every layer in force — chat, agent, user, team, and org — most specific first.
- Always allow on one pending tool approves that call and adds an allow rule on the chat layer (loop
Toolor harnessHarness, depending on how the chat runs). Each choice is labeled in plain language (for examplenpm run commands on laptop); the samelabelappears on the rule in chat settings → Permissions. See Always allow tool. - Allow all tools in chat settings (or
allow_all_toolsonPOST /chats/:id/settings) runs every tool without asking and approves anything already waiting. It appears as theallow_all_toolsrule on the chat layer; turn it off to require approval again for tools not covered by other allow rules. - Add or revoke chat rules with
POST/DELETEon/chats/:id/rules. Chat rules can only narrow what layers above allow. Test a call withPOST /chats/:id/rules/explain.
Governance rules from a team or org are read-only in chat settings; they can loosen what a stricter layer above would require, while non-governance layers only narrow.
Per-chat changes do not update the agent version's permissions object — they only affect the current conversation (plus shared policy layers attached to the agent, user, team, or org). New chats in the agent's home workspace pick up the agent version defaults again unless you change the template.
Published agents: Allow rules on an agent template apply only in that agent's home workspace. In another workspace's chat with the same agent, ask and deny rules on the template still apply, but allow rules do not — the caller's remotes and tools are never opened by the publisher's defaults. Chat-level allow rules the caller adds still apply.
Scoped always allow and explain
For a single invocation in awaiting_approval, always allow can save a narrow chat rule instead of approving only this once:
GET /chats/:chatId/tools/:toolInvocationId/always-allow/options— choices ordered narrowest first (exactremote_execcommand, command prefix, every command on the remote, whole tool, …). Each option includes a stablekey, humanlabel,scope, and thePolicyRuleDTOrows it would write. When nothing fits,optionsis empty andunavailableexplains why.POST /chats/:chatId/tools/:toolInvocationId/always-allowwith{ "option": "<key>" }— saves those rules and executes the pending call. Omitoptionfor the legacy default (exact command pieces forremote_exec, whole-tool allow otherwise). 409 means the key is stale (re-list options); 400 means the call is no longer waiting.POST /chats/:chatId/tools/:toolInvocationId/explain— on-demand plain-language summary (risk_level,explanation,reasoning,risk). The model runs once per invocation and the API caches the result. Not available on harness-backed chats.
HTTP tools: The API resolves the call URL to a host the same way the HTTP tool does. When the pending approval is about that host (WebFetch(domain:…) — from governance or the default ask), chat rules cannot answer it, so always allow lists no options and unavailable names the host; posting without an option does not save a whole-tool allow (that would not stop the host question and would leave a tool-wide allow behind). When only the tool's own approval gate asked, the usual whole-tool option is offered.
→ Agents REST API — List always-allow options · Save always-allow · Explain tool call
JavaScript Agent SDK (@inferencesh/sdk ≥ v0.16.0): useAgentActions().getAlwaysAllowOptions, alwaysAllowTool(id, { option }), and explainTool(id). See JavaScript SDK — Scoped always allow and explain.
Terminal workflow
When an agent pauses for approval, use the CLI from any machine with your API key:
1belt chat pending # list invocations awaiting approval2belt chat pending ti_abc123 # inspect one invocation (tool, args, agent)3belt chat approve ti_abc123 # approve and execute4belt chat approve ti_abc123 --watch # approve, then stream the agent's reply5belt chat reject ti_abc123 --reason "not authorized"REST equivalents: POST /tools/:invocation_id/invoke and POST /tools/:invocation_id/reject. See Agents API.
You can also resolve approval gates through the generic interrupt API: list pending interrupts on the active run (GET /agent-runs/:runId/interrupts) and call POST /interrupts/:id/resolve with {"decision": "allow"} or "deny".
From the CLI, use belt interrupt list <run-id> and belt interrupt resolve <interrupt-id> allow|deny — see CLI setup — Interrupt.
Handling Rejections
When a user rejects a tool call, the agent receives feedback and can try alternatives:
1Agent: "Let me delete that file"2 [calls delete_file → REJECTED]34Agent: "I understand you don't want to delete it. 5 Would you like me to archive it instead?"Programmatic guardrails
For server-side policy checks (allow/deny based on tool name, arguments, or external rules), use lifecycle hooks on the agent.tool_call event. A synchronous webhook can return {"decision": "deny", "reason": "..."} to skip execution without pausing for human review.
Selective Approval
Mix approved and auto-execute tools:
1agent = client.agent({2 "core_app": { "ref": "infsh/claude-sonnet-4@latest" },3 "tools": [4 # Auto-execute (safe)5 tool("read_file").handler(read_file).build(),6 tool("list_files").handler(list_files).build(),7 8 # Require approval (dangerous)9 tool("delete_file").require_approval().handler(delete_file).build(),10 tool("modify_file").require_approval().handler(modify_file).build(),11 ]12})