Strands
Add ArmorIQ to an AWS Strands agent
Strands Integration
ArmorIQStrands wires ArmorIQ into AWS Strands so every tool call an agent
makes goes through ArmorIQ's plan, token, policy, and audit pipeline. You keep
your existing Agent, model, and tools. You just attach the ArmorIQ hooks for
the duration of each request.
Live in Python and TypeScript. Python ships in armoriq-sdk 0.6.0+,
TypeScript in @armoriq/sdk 0.6.0+. See Integrations
for the full status matrix.
Install
pip install "armoriq-sdk[strands]"npm install @armoriq/sdk
# plus your Strands agent packageMinimal example
import os
from strands import Agent
from armoriq_sdk import ArmorIQClient
from armoriq_sdk.integrations.strands import ArmorIQStrands
# Once per process
armoriq = ArmorIQStrands(
armoriq_client=ArmorIQClient(api_key=os.environ["ARMORIQ_API_KEY"]),
mode="sdk",
)
# Per request: for_user returns a hook provider you pass to the Agent
hooks = armoriq.for_user("alice@example.com", goal=user_message)
agent = Agent(model=model, tools=tools, hooks=[hooks])
async for event in agent.stream_async(user_message):
handle(event)import { ArmorIQClient } from '@armoriq/sdk';
import { ArmorIQStrands } from '@armoriq/sdk/dist/integrations/strands';
// Once per process
const armoriq = new ArmorIQStrands({
client: new ArmorIQClient({ apiKey: process.env.ARMORIQ_API_KEY! }),
mode: 'sdk',
});
// Per request: forUser returns a plugin you attach to the Agent
const plugin = await armoriq.forUser('alice@example.com', { goal: userMessage });
const agent = new Agent({ model, tools, plugins: [plugin] });That's it. The SDK attaches three Strands lifecycle hooks:
| Hook | What ArmorIQ does |
|---|---|
AfterModelCallEvent | Builds a plan from the tool calls the model chose and mints an intent token for exactly those tools |
BeforeToolCallEvent | Enforces the user's policy before the tool runs: allow, hold (wait for approval), or block |
AfterToolCallEvent | Reports the executed tool so the plan lifecycle closes (see below) |
Enforcement is fail-closed: a tool runs only on an explicit allow. A block, a hold that is not approved in time, or any enforcement error stops the tool.
Live enforcement events
Pass an on_event callback (onEvent in TypeScript) to stream the enforcement
lifecycle into your own UI. The kinds are hold, approved, rejected,
timeout, block, and error.
def on_event(kind, payload):
# kind in {"hold","approved","rejected","timeout","block","error"}
stream_to_ui(kind, payload)
hooks = armoriq.for_user("alice@example.com", goal=user_message, on_event=on_event)const plugin = await armoriq.forUser('alice@example.com', {
goal: userMessage,
onEvent: (kind, payload) => streamToUi(kind, payload),
});On a hold, the hook emits hold, then waits asynchronously for the delegation
decision. Approved lets the tool run; rejected or timeout stops it. The wait is
non-blocking, so your app can stream held to approved to success live.
Plan lifecycle: the execution report
After each tool actually runs, the AfterToolCallEvent hook reports that step
back to ArmorIQ. This advances the plan to completed once every declared step
has run, instead of leaving it active forever. Tools that were cancelled by
enforcement never ran, so they are not reported.
ArmorIQStrands parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
armoriq_client | ArmorIQClient | - | The client (holds your API key and endpoints) |
mode | str | "sdk" | Session mode |
validity_seconds | int | 3600 | Intent-token validity window |
default_mcp_name | str | None | MCP name to attribute tools that can't be auto-mapped |
tool_name_parser | callable | built-in | Custom tool_name -> (mcp, action) mapper |
approval_wait_seconds | float | 300 | How long to wait for a held tool's approval before failing closed |
approval_poll_interval | float | 5.0 | How often to poll for the approval decision |
for_user(email, *, goal=None, on_event=None)
Returns a per-request hook provider. Pass it to Agent(hooks=[...]) in Python
or Agent({ plugins: [...] }) in TypeScript. The provider caches plan state for
the request, so tool calls that aren't in the plan get blocked before they hit
the MCP.
Reference
For a complete example, wire ArmorIQStrands into any Strands Agent that
already has tools attached: the accounts-payable and finance-workflow demos
built internally follow exactly the pattern shown above: one ArmorIQStrands
instance per process, one for_user(...) scope per request.