Skip to main content

Push approval for agent actions

Sometimes an agent is about to do something it should not do silently — wire money, send a customer-facing email, delete data, deploy to production. Push approval lets the agent call a single SDK method and get a human-in-the-loop tap on their phone before it proceeds.

This reuses the same push pipeline that powers human MFA: each prompt is bound to the phone's own signing key (ECDSA P-256) and expires after 120 s. The user's phone is registered once; agents borrow that established trust path for action approvals.

The demo​

npm install @lumoauth/agent
import { LumoAgent } from '@lumoauth/agent';

// Reads LUMOAUTH_URL, LUMOAUTH_ORG_ID, AGENT_CLIENT_ID and AGENT_CLIENT_SECRET;
// the agent authenticates itself on first use.
const agent = new LumoAgent();

const approval = await agent.approvals.require({
taskId: `wire-${Date.now()}`,
reason: 'Wire $4,500 to vendor INV-7741',
impact: 'high',
onBehalfOf: 'ada@acme.com',
meta: { action: 'wire-transfer', amount: 4500, vendor: 'INV-7741' },
});

if (approval.status !== 'approved') {
throw new Error(`agent action denied: ${approval.status}`);
}

// approval.token is short-lived and bound to this specific task — use as
// the auth header for the actual side-effecting call.

Calling from your own backend instead of from the agent? @lumoauth/backend exposes the same approvals.require() on LumoAuthBackend.

What the user sees on their phone:

  • Agent name at the top, not a generic "sign-in" prompt
  • Action description (reason) in a card with severity-tinted border
  • Impact tier badge — Low / Medium / High / Critical
  • Action metadata — every key/value in meta rendered as a row
  • Approve / Deny buttons that sign with the device's stored ECDSA key

Python SDK​

pip install lumoauth
from lumoauth import LumoAuthAgent, require_approval

agent = LumoAuthAgent()
agent.authenticate()

result = require_approval(
agent,
task_id="wire-001",
reason="Wire $4,500 to vendor INV-7741",
impact="high",
on_behalf_of="ada@acme.com",
meta={"amount": 4500, "vendor": "INV-7741"},
)
if result.status != "approved":
raise RuntimeError(f"denied: {result.status} ({result.reason})")

Impact levels​

TierWhen to use itUI treatment on phone
lowRead-only enrichment, lookupsGreen accent
mediumReversible writes (draft an email, create a ticket)Amber accent
highIrreversible writes with bounded blast radius (send email, charge $X)Orange accent
criticalCatastrophic if wrong (delete data, wire money over $threshold)Red accent + extra friction

The UI styling is hard-coded; you don't need to do anything beyond pick the right tier.

Endpoints​

  • POST /orgs/{orgId}/api/v1/agents/me/approvals — create a request
  • GET /orgs/{orgId}/api/v1/agents/me/approvals/{token}/status — poll

The SDK helpers handle the polling for you.

Batch responses (approver device)​

Approvers reviewing JIT permission requests (just-in-time permissions: narrow, short-lived access an agent asks for at the moment it needs it) from the mobile app can respond to several at once: POST /orgs/{orgId}/push-auth/jit/respond-batch takes up to 25 request ids and a single device signature over the sorted id list (jit-approve-batch:{id1,id2,...}:{challenge_nonce}:{iat} / jit-deny-batch:..., with a single-use server nonce from POST /push-auth/jit/challenge and 300 s freshness — identical signature rules to the single respond endpoint). Results are per-id, so a batch can partially succeed. See JIT permissions for the wire format.

The organization portal inbox has matching multi-select batch actions, a pending badge that updates live, and auto-approve rules for recurring low-risk requests — all aimed at reducing approval fatigue without weakening the human gate on high/critical actions.

Security notes​

  • Approve/deny challenges are bound to a server-generated nonce (auth-request:{request_id}:{challenge_nonce} / deny-request:{request_id}:{challenge_nonce}), fetched via /request-details, single-use, and rotated on consumption — a captured signature cannot be replayed. Deny requires a signature too. The device-signed JIT endpoints (/jit/list, /jit/details, /jit/respond, /jit/respond-batch) use the same scheme since mobile contract 1.3: a single-use nonce from /jit/challenge plus iat.
  • Browser login completion is bound to the initiating session (session_nonce issued at /initiate, verified at /push-auth-login, plus initiating-IP matching), closing the detached-approval / push-bombing account-takeover vector.
  • Every push-auth endpoint is rate-limited, including device lifecycle (update-token, unenroll) and the device-signed JIT endpoints.

Required setup​

  1. The user must have enrolled the LumoAuth mobile app on their phone: in My account → Sign-in & security, click Add a sign-in method and choose LumoAuth app. Without an enrolled device the API responds 428 Precondition Required with { "error": "no_enrolled_device" }.

    The Sign-in & security page of the end-user account with the Add a sign-in method menu open

  2. The agent must authenticate with its own identity — workload attestation (proving where it runs with a token from its cloud platform) or AAuth (the agent identity protocol, with the agent's own signing key) — and present an agent token. Approvals are scoped to the calling agent.

  3. Set on_behalf_of to the email or numeric user_id of the user who must approve.

Rate limits​

Per (tenant, agent, user) tuple: 10 approvals / 5 minutes by default. Push bombing a single user via a rogue agent is therefore bounded by both a hard ceiling and the audit log.

Unified approvals inbox​

The organization portal has a single approvals inbox, AI Agents → Approvals (/orgs/{orgId}/portal/approvals), that consolidates every "waiting on a human" surface into one newest-first list, superseding the older JIT-only /orgs/{orgId}/portal/jit-approvals view. Its status tabs are Waiting, Approved, Denied and All permission requests, and a second row filters by type: Everything, Permission requests, User consents and Action approvals, each with a badge showing how many items are waiting. It aggregates three item types:

The Approvals inbox with the Waiting, Approved, Denied and All permission requests tabs and the Everything, Permission requests, User consents and Action approvals filters

  • JIT permission requests — always deep-link to the JIT approvals surface (filter Permission requests), where you approve or deny.
  • Push approvals (agent action requests) — deep-link to the subject user's review page only when the viewer is the subject (that page authorizes the subject only); for every other admin they are informational (answered on-device).
  • AAuth user-consent requests (a user approving what an AAuth agent may do on their behalf) — informational by design: the end user grants consent from the agent redirect, so there is no portal action to take.

The inbox is a read model — it never mutates state; each actionable item links into the native flow that owns the decision.

See also​