lelu
Integrations

Strands Agents

Authorize every Strands tool call before it runs. Lelu registers as an intervention handler, so denials, safe-tool redirects, and human approval all use machinery Strands already has.

How decisions map

Lelu returns one of four decisions for any action, and each corresponds to an intervention action Strands already understands.

Lelu decisionStrands actionEffect
allowProceedThe tool runs as the model intended.
denyDenyCancelled, and the model is told why so it can choose differently.
computeTransformRe-pointed at the safer tool your policy names.
human_reviewConfirmPaused for a person through Strands' interrupt system.

Installation

terminal
# Python
pip install "lelu-agent-auth-sdk[strands]"

# TypeScript
npm install lelu-agent-auth @strands-agents/sdk

Python

Pass the handler in interventions. Strands evaluates handlers in order and recommends putting cheap authorization checks first, so it belongs at the front of the list.

agent.py
from strands import Agent
from lelu import LeluClient
from lelu.strands import LeluIntervention

guard = LeluIntervention(
    LeluClient(base_url="http://localhost:8080"),
    actor="invoice_bot",
)

agent = Agent(tools=[refund, lookup_invoice], interventions=[guard])

TypeScript

agent.ts
import { Agent } from "@strands-agents/sdk";
import { LeluClient } from "lelu-agent-auth";
import { LeluIntervention } from "lelu-agent-auth/strands";

const agent = new Agent({
  tools: [refund, lookupInvoice],
  interventions: [new LeluIntervention({ client, actor: "invoice_bot" })],
});

Mapping tools to permissions

By default the tool name is the permission checked. Pass action_for when your policy uses a different vocabulary.

agent.py
LeluIntervention(
    client,
    actor="invoice_bot",
    action_for=lambda call: f"tool:{call.name}",
)

Human review

A human_review decision returns Confirm, pausing the agent so a person can approve in the flow your application already has.

If approval happens in Lelu's own review queue instead, set on_review="deny" and resume it yourself. Redemption re-checks the payload against what the reviewer actually approved, so an approval cannot be spent on a call they never saw.

resume.py
outcome = await guard.evaluate(call)
if outcome.action == "review":
    result = await guard.redeem(outcome, timeout_ms=60_000)

Failure behaviour

Two independent failures, both closed by default. If the engine is unreachable the call is denied — an authorization engine that permits everything when it breaks is not an authorization engine. If the handler itself throws, on_error defaults to "deny" rather than Strands' own "throw".

Set fail_open=True to override the first, deliberately.