Python SDK
The armoriq_sdk package for Python agents. ArmorIQClient, for_user, from_config, sessions, exception handling, and the plan-token-invoke loop.
The Python SDK ships as armoriq-sdk on PyPI. One package gives you the CLI (armoriq) and the library (armoriq_sdk).
pipx install armoriq-sdk # CLI on $PATH
# or
pip install armoriq-sdk # into your project venvPublic API surface
From armoriq_sdk you import:
from armoriq_sdk import (
# Core
ArmorIQClient,
ArmorIQSession,
SessionOptions,
SessionMode,
EnforceResult,
ReportOptions,
# Plan helpers
build_plan_from_tool_calls,
default_tool_name_parser,
hash_tool_calls,
# Exceptions
ArmorIQException,
InvalidTokenException,
IntentMismatchException,
MCPInvocationException,
TokenExpiredException,
DelegationException,
ConfigurationException,
PolicyBlockedException,
PolicyHoldException,
# Models
IntentToken,
PlanCapture,
MCPInvocation,
MCPInvocationResult,
DelegationRequest,
DelegationResult,
)ArmorIQClient
The main entry point. One client per service; cheap to hold as a module-level singleton.
Minimum setup
from armoriq_sdk import ArmorIQClient
client = ArmorIQClient() # reads ~/.armoriq/credentials.jsonThat's the complete setup after armoriq login. Every other constructor argument is optional and auto-resolves.
Constructor
ArmorIQClient(
api_key: Optional[str] = None, # resolves env → credentials file
user_id: Optional[str] = None, # auto-defaults to "__sdk_multiuser__"
agent_id: Optional[str] = None, # auto-defaults to "__sdk_multiuser__"
iap_endpoint: Optional[str] = None, # auto-resolves to platform default
proxy_endpoint: Optional[str] = None,
backend_endpoint: Optional[str] = None,
proxy_endpoints: Optional[Dict[str, str]] = None, # per-MCP overrides
context_id: Optional[str] = None,
timeout: float = 30.0,
max_retries: int = 3,
verify_ssl: bool = True,
use_production: bool = True,
mcp_credentials: Optional[Mapping] = None,
)Only api_key really matters — and even that resolves from the env var (ARMORIQ_API_KEY) or the credentials file if you omit it.
from_config() — load an armoriq.yaml
For config-as-code teams. Scaffold the YAML with armoriq init, then:
client = ArmorIQClient.from_config("armoriq.yaml")The YAML drives identity, environment, MCP list, policy, and intent settings. See the YAML reference for every field.
The email + key pattern
The API key authenticates your service. An email identifies the end user your service is acting on behalf of. Together they drive audit trails, per-user policy enforcement, and delegation flows.
Option A — per-call user_email
The simplest path. Pass user_email on every invoke():
client = ArmorIQClient()
plan = client.capture_plan(
llm="gpt-4o",
prompt="Book a SFO→JFK flight",
plan={
"goal": "Book flight",
"steps": [
{"action": "book_flight", "mcp": "travel-mcp", "params": {...}},
],
},
)
token = client.get_intent_token(plan, validity_seconds=300)
result = client.invoke(
mcp="travel-mcp",
action="book_flight",
intent_token=token,
params={"from": "SFO", "to": "JFK"},
user_email="alice@customer.com", # ← end user
)The audit log row for this invocation is tagged with alice@customer.com; policies that scope access by email (or by the membership that email maps to) are evaluated against her.
Option B — session mode for long-running conversations
For multi-turn chat agents that make many tool calls, use for_user(email) to get a ArmorIQUserScope, then start a session. The session carries the user email across every subsequent call automatically.
client = ArmorIQClient()
alice = client.for_user("alice@customer.com")
session = alice.start_session(SessionOptions(mode="sdk"))
token = session.start_plan([
{"action": "search_hotels", "mcp": "travel-mcp"},
{"action": "book_hotel", "mcp": "travel-mcp"},
])
decision = session.check("search_hotels", {"city": "Paris"})
if decision.allowed:
... # forward to MCPOption C — pre-auth check without invoking
resolve_user(email) fetches the user's membership, role, applicable policies, and approver chain — without triggering an MCP call. Useful for pre-authorization screens.
context = client.resolve_user("alice@customer.com")
# context = {"user": {...}, "policies": [...], "approverChain": [...]}Cached 300 seconds per email to keep it cheap.
The plan → token → invoke loop
Every tool call follows the same three-step flow.
Your agent ArmorIQ SDK
│ │
│ capture_plan ──────────► │ structures the plan, computes
│ │ canonical hash (Merkle root)
│ │
│ get_intent_token ──────► │ calls IAP with plan hash,
│ │ receives signed JWT
│ │
│ invoke ─────────────────►│ routes through the proxy,
│ │ proxy verifies token + policy,
│ │ forwards to MCP, returns resultcapture_plan(llm, prompt, plan, metadata=None)
Capture the LLM's reasoning and the planned tool calls:
plan = client.capture_plan(
llm="gpt-4o",
prompt="Book flights and hotels for a Paris trip",
plan={
"goal": "Plan Paris trip",
"steps": [
{"action": "search_flights", "mcp": "travel-mcp", "params": {...}},
{"action": "book_flight", "mcp": "travel-mcp", "params": {...}},
{"action": "search_hotels", "mcp": "travel-mcp", "params": {...}},
{"action": "book_hotel", "mcp": "travel-mcp", "params": {...}},
],
"metadata": {"trip_id": "paris-2026-05"},
},
)
print(plan.plan_hash) # canonical hash
print(plan.merkle_root) # Merkle tree root
print(plan.ordered_paths) # per-step proofsYour plan dict must contain steps. The SDK rejects empty or malformed plans — this is what prevents an attacker from substituting a different plan behind a valid token.
get_intent_token(plan, validity_seconds=300)
Mint a short-lived signed token bound to the plan:
token = client.get_intent_token(plan, validity_seconds=600) # 10 min
print(token.token_id)
print(token.expires_at)
print(token.is_expired)
print(token.time_until_expiry) # secondsTokens are JWT-style. The proxy verifies the signature, checks the hash against the registered plan, and confirms the token hasn't expired.
invoke(mcp, action, intent_token, params, user_email=None)
Actually execute the tool call:
result = client.invoke(
mcp="travel-mcp",
action="book_flight",
intent_token=token,
params={"from": "SFO", "to": "CDG", "date": "2026-05-10"},
user_email="alice@customer.com",
)
print(result.status) # "success" | ...
print(result.data) # the MCP's response body
print(result.duration_ms)The call goes to the proxy, which verifies the token, checks policy, forwards to the MCP, and returns the response. params must match the steps in the plan — mismatch raises IntentMismatchException.
invoke_with_policy() — with hold / delegation
The richer variant. If the policy returns a hold (requires approval), invoke_with_policy can wait on the delegation and auto-retry once approved:
result = client.invoke_with_policy(
mcp="travel-mcp",
action="book_flight",
intent_token=token,
params={"amount": 5000},
user_email="alice@customer.com",
wait_for_approval=True, # poll until approved or timeout
)For high-stakes tool calls. See DelegationException and PolicyHoldException below.
Exception handling
Every exception inherits from ArmorIQException. Catch broadly for "something went wrong in ArmorIQ," or narrowly for specific conditions.
| Exception | You'll see it when… | What to do |
|---|---|---|
ConfigurationException | No API key resolved, or key is malformed | Run armoriq login or set ARMORIQ_API_KEY |
InvalidTokenException | Token signature doesn't verify, or token doesn't match the plan | Capture a fresh plan and token |
TokenExpiredException | Token was valid but validity_seconds elapsed | Get a new token with get_intent_token |
IntentMismatchException | Your invoke() args don't match the captured plan's steps | Align params with the plan, or capture a new plan |
PolicyBlockedException | The tool isn't in the caller's allow-list | Update policy in the dashboard |
PolicyHoldException | The tool requires human approval | Create a delegation; use invoke_with_policy(wait_for_approval=True) |
DelegationException | Delegation request failed or was denied | Inspect exception.delegation_id and retry |
MCPInvocationException | The MCP server itself returned an error | Check the MCP server logs; not an ArmorIQ-layer issue |
Example:
from armoriq_sdk import (
ArmorIQClient,
PolicyBlockedException,
PolicyHoldException,
TokenExpiredException,
)
try:
result = client.invoke(mcp="travel-mcp", action="book_flight",
intent_token=token, params={...},
user_email="alice@customer.com")
except TokenExpiredException:
token = client.get_intent_token(plan)
result = client.invoke(mcp="travel-mcp", action="book_flight",
intent_token=token, params={...},
user_email="alice@customer.com")
except PolicyBlockedException as e:
print(f"Blocked: {e.reason}")
except PolicyHoldException as e:
print(f"Approval required: delegation_id={e.delegation_id}")Plan helpers
Build plans from tool-call lists instead of writing the dict manually:
from armoriq_sdk import build_plan_from_tool_calls
# Your framework (LangChain, CrewAI, raw OpenAI) produced these tool calls:
tool_calls = [
{"name": "travel-mcp.book_flight", "arguments": {...}},
{"name": "travel-mcp.search_hotels", "arguments": {...}},
]
plan = build_plan_from_tool_calls(
tool_calls=tool_calls,
goal="Plan Paris trip",
)default_tool_name_parser and hash_tool_calls are lower-level helpers for custom integrations — see source for details.
Full worked example
from armoriq_sdk import ArmorIQClient, TokenExpiredException
client = ArmorIQClient()
plan = client.capture_plan(
llm="gpt-4o",
prompt="What's the weather in Paris?",
plan={
"goal": "Weather lookup",
"steps": [
{
"action": "get_weather",
"mcp": "weather-mcp",
"params": {"city": "Paris", "units": "celsius"},
},
],
},
)
token = client.get_intent_token(plan, validity_seconds=300)
try:
result = client.invoke(
mcp="weather-mcp",
action="get_weather",
intent_token=token,
params={"city": "Paris", "units": "celsius"},
user_email="alice@customer.com",
)
print(result.data)
finally:
client.close()Next steps
CLI Reference
Every armoriq command with flags, expected output, and copy-pasteable examples. login, orgs, switch-org, init, validate, register, status, logs, whoami, logout.
TypeScript SDK
The @armoriq/sdk package for Node.js agents. ArmorIQClient, capturePlan / getIntentToken / invoke loop, per-call user scoping, and known parity gaps with the Python SDK.