# OpenHuman Approval Gate Mechanism for Agent Tool Execution: 10-Minute TTL Policy Explained

> Learn about OpenHuman's agent tool execution approval gate and its 10-minute TTL policy. Ensure secure and controlled agent actions with human oversight.

- Repository: [Tiny Humans/openhuman](https://github.com/tinyhumansai/openhuman)
- Tags: deep-dive
- Published: 2026-08-28

---

**OpenHuman parks every agent tool call behind a global Approval Gate that requires explicit human approval before execution, automatically denying requests that remain pending longer than a configurable TTL that defaults to 10 minutes.**

The OpenHuman agent framework implements a fail-closed safety layer that intercepts tool calls before they reach external systems. This approval gate mechanism for agent tool execution ensures that every potentially side-effecting operation waits for human or policy-driven consent, while a built-in TTL policy prevents orphaned requests from blocking the agent indefinitely.

## How the Approval Gate Mechanism Works

### Gate Installation and Global Registration

When the core starts, `ensure_approval_gate` creates a global `ApprovalGate` instance in [`src/openhuman/security/approval/gate.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/security/approval/gate.rs). The gate is wrapped in an `Arc` and attached to the core’s audit store, making it discoverable throughout the system via `ApprovalGate::try_global`.

### Policy Hook Interception

Tools declare the `"approval_gate"` policy in [`src/openhuman/tools/policy.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/tools/policy.rs). The policy handler intercepts the tool call and routes it through the gate as `crate::openhuman::tools::policy::PolicyDecision::Ask`. If the core configuration sets `approval_gate: false`, the request bypasses the gate and executes immediately.

### Parking a Turn and Suspending Execution

When a tool request reaches the gate, `ApprovalGate::park` creates a pending-approval row in the store with an `expires_at` timestamp. The turn is suspended, and the UI surfaces a request card to the user. The park duration can be bounded by a caller-provided timeout; the effective timeout is the **minimum** of the caller bound and the gate’s TTL.

### Decision Processing and TTL Expiration

When the user sends an **Allow**, **Deny**, or **Ask** response, `ApprovalGate::decide` updates the pending row and un-parks the turn. If no decision arrives before the timeout expires, the gate TTL-denies the request (fail-closed), records a `Deny`, and removes the pending row.

## TTL Policy and Timeout Configuration

### Default 10-Minute TTL

The default approval window is defined in [`src/openhuman/security/approval/gate.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/security/approval/gate.rs) at line 66 as `Duration::from_secs(60 * 10)`, which equals **10 minutes**. This value is persisted in the audit store so that a core restart or crash does not lose the timeout.

### Copilot-Streaming TTL Override

For tools invoked from a copilot-streaming context, the gate applies a shorter `COPILOT_APPROVAL_TTL` of 3 minutes (`Duration::from_secs(180)`), defined at line 78 in the same file. As noted around line 103, this keeps streaming turns responsive by preventing long parks during active sessions.

### Debug Environment Overrides

On debug builds only, the `OPENHUMAN_APPROVAL_TTL_SECS` environment variable overrides the boot-time TTL. The implementation at lines 311–346 reads this variable, but the value is clamped to the copilot TTL when the copilot context applies.

### Caller-Bound Clamping

If a tool caller supplies its own timeout bound, the gate caps the park to `min(bound, gate_TTL)`, as described in the comments around line 103. This prevents any single tool from parking longer than the global policy allows.

## Source Code Examples

Here is how the gate is installed, invoked, and resolved in practice:

```rust
// Install the gate during core boot
let gate = Arc::new(ApprovalGate::new(config, session_id, DEFAULT_APPROVAL_TTL));
core_state.insert_approval_gate(gate);

```

```rust
// A tool that requires approval declares the policy
impl Tool for SomeNetworkTool {
    fn policy(&self) -> &'static str {
        "approval_gate"
    }
}

```

```rust
// Parking a request inside the gate implementation
let pending = gate.park(request_id, caller_bound);
// pending.expires_at is set to now + min(caller_bound, gate.ttl)

```

```rust
// UI sends an Allow decision via RPC
// ApprovalGate::decide(pending_id, Decision::Allow)
// The core then resumes the original turn.

```

Key files involved in this flow include:

- [`src/openhuman/security/approval/gate.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/security/approval/gate.rs) — Core implementation, TTL constants, parking, and decision logic
- [`src/openhuman/tools/policy.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/tools/policy.rs) — Policy declaration and routing
- [`src/core/types.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/core/types.rs) — Boot helper `approval_gate_boot_decision`
- [`tests/agent_harness_e2e.rs`](https://github.com/tinyhumansai/openhuman/blob/main/tests/agent_harness_e2e.rs) — Integration tests such as `approval_gate_timeout` at line 1708
- [`src/openhuman/agent/triage/escalation.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/agent/triage/escalation.rs) and [`src/openhuman/channels/proactive.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/proactive.rs) — Gate lookup for audit logging and UI interaction

## Summary

- The approval gate mechanism for agent tool execution in OpenHuman intercepts every tool call through a global `ApprovalGate` instance registered at boot time.
- Tools opt into the gate by declaring the `"approval_gate"` policy in [`src/openhuman/tools/policy.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/tools/policy.rs).
- Requests are parked via `ApprovalGate::park`, creating a pending row with an `expires_at` timestamp.
- The default TTL is 10 minutes, with a 3-minute copilot-streaming override and debug-only environment variable customization.
- Unresolved requests are automatically denied when the TTL expires, ensuring the system fails closed.

## Frequently Asked Questions

### What happens when an agent tool request exceeds the 10-minute TTL?

When the TTL expires without a user decision, the gate logic around line 383 records an automatic `Deny` and removes the pending row. The suspended turn is un-parked with a denial result, preventing the tool from executing.

### How does the approval gate handle copilot-streaming contexts differently?

Copilot-streaming contexts use a shorter `COPILOT_APPROVAL_TTL` of 3 minutes (`Duration::from_secs(180)`) instead of the default 10 minutes. This prevents long-running parks from stalling active streaming sessions.

### Can the approval gate TTL be customized or disabled?

You can override the TTL in debug builds by setting the `OPENHUMAN_APPROVAL_TTL_SECS` environment variable. The gate can also be disabled entirely by setting the `approval_gate` configuration flag to `false`, which causes the policy handler to forward tool calls immediately without parking.

### Where is the approval gate state stored if the core restarts?

The pending-approval row and its `expires_at` timestamp are persisted in the audit store. Because the TTL is stored durably, a core restart or crash does not reset or lose the timeout state.