How the OpenHuman Approval Gate Mechanism Controls Interactive Turns

The OpenHuman approval gate mechanism functions as a runtime safety filter that evaluates every agent-generated tool call against configurable risk policies, returning either immediate execution, a human review request, or a denial before any external action occurs.

The tinyhumansai/openhuman repository implements this approval gate mechanism to ensure autonomous agents remain safely within human-defined operational boundaries. This system intercepts tool calls at the critical moment between model generation and execution, creating a deterministic checkpoint for sensitive operations. By centralizing safety logic into a single ToolHook implementation, OpenHuman provides consistent governance across all agent skills without requiring individual tool modifications.

Gate Installation and Tool Hook Registration

The approval gate mechanism installs itself during core initialization by registering as a tool hook within the turn-processing pipeline. In src/openhuman/security/approval/gate.rs, the ApprovalGate struct implements the ToolHook trait, which exposes a decide method that inspects every ParsedToolCall before execution.

// src/openhuman/security/approval/gate.rs
impl ToolHook for ApprovalGate {
    fn decide(&self, ctx: &ToolContext) -> ToolHookDecision {
        // … logic that examines the tool’s risk level and the config flag
    }
}

The decide method returns one of three variants from the ToolHookDecision enum:

  • Proceed – Executes the tool call immediately without human interaction
  • Ask(reason) – Parks the turn and persists an approval request for human review
  • Deny(reason) – Rejects the tool call outright with a provided justification

This tri-state design ensures that every tool invocation receives an explicit safety verdict before accessing external systems.

Risk Classification and Approval Modes

The approval gate mechanism classifies risk by examining each tool's approval mode annotation combined with runtime configuration flags. Each tool carries metadata specifying one of three modes: required, sometimes, or null.

The gate reads these annotations via internal metadata inspection (referenced as redact_args in the source) and evaluates them against the require_approval_for_medium_risk configuration flag. Medium-risk turns are defined as those lacking an explicit required mode but exceeding the configured sensitivity threshold. This dual-layer evaluation allows fine-grained control over which operations trigger human review without hardcoding behavior into individual tools.

Parking Turns in the Approval Store

When the gate returns Ask, the approval gate mechanism immediately parks the turn and persists the request to durable storage. The ApprovalStore::insert_pending function in src/openhuman/security/approval/store.rs writes to an SQLite database (approval.db located in the workspace's approval/ directory), recording the turn ID, thread ID, client ID, and a serialized snapshot of the model's original request.

// src/openhuman/security/approval/store.rs
pub fn insert_pending(cfg: &Config, pending: PendingApproval) -> Result<()> {
    // INSERT INTO pending_approvals …
}

This persistence guarantees that approval requests survive process restarts and enables asynchronous human review through external interfaces.

Human-in-the-Loop Decision Flow

External clients interact with parked turns through JSON-RPC methods defined in src/openhuman/security/approval/schemas.rs. The openhuman.approval_list_pending method allows UIs to retrieve queued requests, presenting users with three resolution options: Approve Once, Approve Always, or Deny.

Upon user selection, the client calls ApprovalStore::decide to record the verdict:

// src/openhuman/security/approval/store.rs
pub fn decide(cfg: &Config, id: &str, decision: ApprovalDecision) -> Result<()> {
    // UPDATE pending_approvals → decisions table
}

This function atomically moves the record from the pending queue to the decisions audit table, ensuring no requests are lost during high-concurrency scenarios.

Resuming Execution After Approval

After a decision is persisted, the gate's background task (demonstrated in tests/raw_coverage/worker_b_raw_coverage_e2e.rs) wakes the parked turn and re-injects the original tool arguments into the execution pipeline.

// tests/raw_coverage/worker_b_raw_coverage_e2e.rs
let (outcome, audit_id) = approval_task.await.expect("approval task join");

Approve Once permits the single pending execution to complete before returning to normal gate evaluation. Approve Always modifies the tool's future evaluation path, automatically returning Proceed for subsequent invocations without further human interaction. Deny terminates the turn with a failure status propagated back to the agent.

Audit Trails and Session Management

All approval decisions generate immutable audit records stored in the decisions table within approval.db. These records capture the decision type, timestamp, requesting agent, and human reviewer identity when applicable.

The gate periodically purges expired entries via purge_session to prevent database bloat while maintaining compliance history. Developers can query recent activity through the openhuman.approval_list_recent_decisions RPC method, enabling operational visibility into safety-critical agent behaviors.

Implementing the Approval Gate in Code

To enable the approval gate mechanism in your OpenHuman deployment, register the ApprovalGate during harness initialization:

use openhuman_core::Harness;
use openhuman_core::openhuman::security::approval::gate::ApprovalGate;

// Build a harness with the approval gate enabled
let harness = Harness::builder()
    .access(openhuman_core::Access::full())
    .gate(ApprovalGate::default())   // registers the hook
    .build()
    .await?;

// Prompt that triggers a medium‑risk tool (e.g., sending an email)
let turn = harness.run("Send an email to alice@example.com").await?;

Once a turn is parked, list pending approvals via RPC:

let pending = core_rpc_client.call("openhuman.approval_list_pending", &json!({})).await?;
println!("Pending approvals: {:?}", pending);

Approve a specific request from CLI or custom UI:

let decision = json!({
    "id": "pending-1234",
    "decision": "ApproveOnce"
});
core_rpc_client.call("openhuman.approval_decide", &decision).await?;

Retrieve the audit trail for compliance monitoring:

let recent = core_rpc_client
    .call("openhuman.approval_list_recent_decisions", &json!({"limit": 10}))
    .await?;
println!("Recent approvals: {:?}", recent);

Summary

  • The approval gate mechanism inserts a ToolHook into the OpenHuman turn pipeline to evaluate every ParsedToolCall before execution
  • Risk classification combines tool-level approvalMode annotations with runtime configuration flags like require_approval_for_medium_risk
  • Proceed, Ask, and Deny decisions provide granular control over tool execution safety
  • Parked turns persist to approval.db via ApprovalStore::insert_pending, enabling asynchronous human review through RPC methods
  • Resume logic handles one-time approvals, permanent whitelisting, and denial scenarios through background task coordination
  • Complete audit trails in the decisions table support operational visibility and compliance requirements

Frequently Asked Questions

What are the three possible decisions the approval gate mechanism can return?

The approval gate mechanism returns Proceed for immediate execution, Ask(reason) to pause the turn for human review, or Deny(reason) to reject the tool call entirely. These variants compose the ToolHookDecision enum returned by the decide method in src/openhuman/security/approval/gate.rs.

How does OpenHuman store pending approval requests?

OpenHuman persists pending approvals to a local SQLite database named approval.db using the ApprovalStore::insert_pending function. This stores the turn ID, thread ID, client ID, and serialized model request snapshot, ensuring durability across process restarts.

What happens to an agent turn while waiting for human approval?

While waiting for human input, the turn remains parked in a suspended state managed by the gate's background task. The original tool arguments are preserved in approval.db, and the execution thread awaits a signal from ApprovalStore::decide before resuming or terminating based on the human's selection.

How can developers query the approval audit history?

Developers retrieve audit records by calling the openhuman.approval_list_recent_decisions JSON-RPC method, which queries the decisions table in approval.db. This returns historical data including decision types, timestamps, and review metadata for compliance and debugging purposes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →