Approval Settings

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

  1. Agent decides to call tool with requireApproval
  2. Execution pauses with status awaiting_approval
  3. User sees the tool call and can approve or reject
  4. If approved: tool executes normally
  5. If rejected: agent receives rejection and can try alternatives

Approval Flow

code
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: true

Types: 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 Tool or harness Harness, depending on how the chat runs). Each choice is labeled in plain language (for example npm run commands on laptop); the same label appears on the rule in chat settings → Permissions. See Always allow tool.
  • Allow all tools in chat settings (or allow_all_tools on POST /chats/:id/settings) runs every tool without asking and approves anything already waiting. It appears as the allow_all_tools rule 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 / DELETE on /chats/:id/rules. Chat rules can only narrow what layers above allow. Test a call with POST /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:

  1. GET /chats/:chatId/tools/:toolInvocationId/always-allow/options — choices ordered narrowest first (exact remote_exec command, command prefix, every command on the remote, whole tool, …). Each option includes a stable key, human label, scope, and the PolicyRuleDTO rows it would write. When nothing fits, options is empty and unavailable explains why.
  2. POST /chats/:chatId/tools/:toolInvocationId/always-allow with { "option": "<key>" } — saves those rules and executes the pending call. Omit option for the legacy default (exact command pieces for remote_exec, whole-tool allow otherwise). 409 means the key is stale (re-list options); 400 means the call is no longer waiting.
  3. 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:

bash
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.

→ CLI setup


Handling Rejections

When a user rejects a tool call, the agent receives feedback and can try alternatives:

code
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})

we use cookies

we use cookies to ensure you get the best experience on our website. for more information on how we use cookies, please see our cookie policy.

by clicking "accept", you agree to our use of cookies.
learn more.