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 decision | Strands action | Effect |
|---|---|---|
| allow | Proceed | The tool runs as the model intended. |
| deny | Deny | Cancelled, and the model is told why so it can choose differently. |
| compute | Transform | Re-pointed at the safer tool your policy names. |
| human_review | Confirm | Paused for a person through Strands' interrupt system. |
Installation
# 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.
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
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.
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.
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.