# Core concepts: human in the loop MCP approvals

flowifai is a human in the loop MCP approval layer: it intercepts an agent's MCP tool
calls, adjudicates each one against your policy, and pauses high-impact calls until a
human approves them. This page explains the moving parts and the guarantees behind
them.

## The three processes

- **`flowifai` (CLI)** — the operator tool. It enrolls agents, manages the broker
  service, prints client config, and manages policies and approvals.
- **`flowifai-broker`** — the trusted local process on the tools host. It owns the
  enrollment credential, launches the real MCP server, asks the backend to adjudicate
  every call, and performs the actual tool execution.
- **`flowifai-wrapper`** — the untrusted stdio shim your MCP client (Claude Code, VS
  Code, Cursor, …) launches instead of the real server. It presents the wrapped
  server's tools to the agent and forwards every call to the broker over loopback. It
  holds no flowifai credential, by design: bypassing it just reaches the broker, which
  still gates.

## The credential boundary

Credentials configured in your local MCP server — API keys, service tokens — stay in
the local tools sandbox with that server. The flowifai enrollment credential is
separate: the broker uses it to authenticate *outbound* calls to the backend.

The cloud backend is an approval authority, never a credential holder. It stores
policy, approval tickets, audit records, and registered tool metadata. It has no
downstream credential columns by construction. The backend also never initiates a
connection into your machine — the model is pull-only: the broker asks the backend for
decisions through outbound requests, and execution happens locally.

One honest caveat: tool arguments and execution results are sent to the backend for
policy, approval, and audit. Any value embedded in them — including an accidental
secret — may be stored. See the [security model](/docs/security-model/).

## Adjudication: default-deny policy

Every tool call the broker receives is adjudicated against your org's policy rules.
A rule matches on the tool name (exact, or `*` for any tool) plus an optional CEL
predicate over the call's arguments. Among matching rules, the most restrictive effect
wins (`deny` > `needs_approval` > `allow`), and if no rule matches, the call is denied.
The engine fails closed: unparseable arguments and predicate evaluation errors are
denials, never silent allows. Full semantics in the
[policy reference](/docs/policy-reference/).

## The approval ticket lifecycle

A call whose decision is `needs_approval` becomes an approval ticket:

1. **pending** — the agent's tool call returns a pending result carrying an
   `approval_ref`. The human sees the request in the console inbox (and, in supported
   clients, gets the approval link delivered
   [protocol-natively](/docs/protocol-native-approvals/)).
2. **approved** or **denied** — an eligible human decides in the console. Approving
   requires a user identity distinct from the broker's machine identity, so an agent
   can never approve its own call. Policies can require n-of-m approvals across
   approver groups; one eligible deny vetoes.
3. **executing → executed** — when the agent retries with the `approval_ref`, the
   broker claims the ticket and runs the real tool. The result returns to the agent.
4. Terminal failures — **expired** (nobody decided in time, or approved-but-unexecuted
   lapsed), **revoked**, or **execution_failed**. Terminal states never silently
   re-arm: re-running the action requires a fresh call and a fresh human approval.

## Two-phase execution: exactly once

Approved actions execute through a two-phase claim/complete protocol. The backend
performs the atomic `approved → executing → executed` transition with a claimant token
and a lease, so concurrent retries of the same ticket yield exactly one execution.
If the broker crashes between invoking the tool and completing the ticket, the ticket
becomes `execution_failed` and any re-run needs a new approval — the failure is
reported honestly, never silently retried.

## Schema drift blocks calls

At enrollment, `flowifai connect` inspects the MCP server and registers its tool
schema. If the server's live schema later drifts from the registered one, the broker
refuses all calls to it (fail closed) until you re-run the app-generated
`flowifai connect` command. Policy rules that reference arguments of a changed schema
are disabled until re-confirmed, so a renamed field can never quietly widen an allow
rule.

## Audit

Adjudications, approvals, denials, execution outcomes, and lifecycle events are
written to an append-only audit log (updates and deletes are rejected at the database
level). Security-critical transitions write their audit row in the same transaction as
the state change, so a committed execution can never lack its audit record.

## What flowifai is not

flowifai is a cooperative approval gate, not a sandbox. It gates the MCP path routed
through the wrapper and broker. If an agent also holds direct API keys, shell access,
or another ungated tool path, those paths remain outside flowifai's control. The
[security model](/docs/security-model/) spells out the boundary.