integrate-arcjet-guard-strands-agents-py

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Integrate Arcjet Guard into Python Strands Agents

将Arcjet Guard集成到Python Strands Agents中

arcjet.guard.strands_agents
wraps the agent's existing Arcjet client. It never talks to the Arcjet API itself. Shared Guard fundamentals (client, rules, labels, decisions, capture, registration) live in ../arcjet/references/guards_python.md. Load that reference for anything that is not Strands-specific.
Official Python
strands
/
strands-agents>=1.11.0,<2
only — not JS
@arcjet/guard/strands-agents/v1
(docs https://docs.arcjet.com/guards/strands-agents/), not community forks. Importing
arcjet.guard.strands_agents
does not load LangChain, CrewAI, or JS
@arcjet/guard/strands-agents/v1
. Unlike CrewAI (no extra), the
arcjet[strands-agents]
extra exists at the pin (
strands-agents = ["strands-agents>=1.11.0,<2"]
in
pyproject.toml
). The extra is safe (no chromadb).
Exports:
guard_tool
,
guard_hooks
,
strands_agent_context
. Authored
@tool
plus
Agent(hooks=)
/
add_hook
.
Three surfaces, one decision rule:
  • An authored
    @tool
    guard_tool
    . Hand the agent the copy this returns — the original stays unguarded.
  • Unwrapped / MCP tools
    guard_hooks
    . Gate is per-tool
    BeforeToolCallEvent.cancel_tool
    (
    True
    or
    str
    ). Already-wrapped tools are skipped so Guard is not called twice.
  • Correlation
    strands_agent_context
    reads a caller-owned id from
    invocation_state
    . It never mints. It never reads
    trace_id
    .
Docs: https://docs.arcjet.com/guards/strands-agents-py/. Example:
examples/fastapi-strands-agents-guard
(pins
a6308061
; no verify fixture). Do not invent a second example name.
arcjet.guard.strands_agents
包装代理现有的Arcjet客户端。它本身不会调用Arcjet API。通用的Guard基础功能(客户端、规则、标签、决策、捕获、注册)位于../arcjet/references/guards_python.md中。任何非Strands专属的内容,请参考该文档。
仅支持官方Python
strands
/
strands-agents>=1.11.0,<2
—— 不支持JS
@arcjet/guard/strands-agents/v1
(文档地址:https://docs.arcjet.com/guards/strands-agents/),也不支持社区复刻版本。导入`arcjet.guard.strands_agents`不会加载LangChain、CrewAI或JS
@arcjet/guard/strands-agents/v1
。与CrewAI不同(无需额外依赖),
arcjet[strands-agents]
存在额外依赖(在
pyproject.toml
中定义为
strands-agents = ["strands-agents>=1.11.0,<2"]
)。该额外依赖是安全的(不包含chromadb)。
导出内容:
guard_tool
guard_hooks
strands_agent_context
。需结合已编写的
@tool
Agent(hooks=)
/
add_hook
使用。
三种应用场景,一套决策规则:
  • 已编写的
    @tool
    → 使用
    guard_tool
    。将该方法返回的副本交给代理——原工具保持未受防护状态。
  • 未包装的/MCP工具 → 使用
    guard_hooks
    。通过每个工具的
    BeforeToolCallEvent.cancel_tool
    (值为
    True
    或字符串)实现防护。已包装的工具会被跳过,避免Guard被重复调用。
  • 关联
    strands_agent_context
    invocation_state
    中读取调用方拥有的ID。它不会生成ID,也不会读取
    trace_id

The gate is per-tool
BeforeToolCallEvent.cancel_tool

防护基于每个工具的
BeforeToolCallEvent.cancel_tool

guard_hooks
registers on that event so the tool never runs on
DENY
(or unevaluated Guard under the default
on_guard_error="deny"
). A string is the cancel message (JSON of
ArcjetDenialResult
);
True
uses Strands' default message and drops the fields. Fail closed: always set
cancel_tool
on error — do not leave it unset and do not raise. Same fail-closed default as #196: only
"allow"
fails open; a
DENY
always blocks. Core
guard()
still fails open (
has_failed_open()
).
guard_hooks
会注册该事件,因此当Guard返回
DENY
(或在默认
on_guard_error="deny"
下未完成评估)时,工具将不会运行。字符串值为取消消息(
ArcjetDenialResult
的JSON格式);
True
使用Strands的默认消息并忽略字段。默认故障关闭(fail closed):在错误时始终设置
cancel_tool
——不要留空也不要抛出异常。与#196的默认故障关闭逻辑一致:仅
"allow"
会故障开放;
DENY
始终会阻止操作。核心
guard()
仍为故障开放(
has_failed_open()
)。

event.interrupt()
is not a policy gate

event.interrupt()
并非策略防护

BeforeToolCallEvent.interrupt()
/ resume is human-in-the-loop. Same trap as CrewAI
human_input
, JS
humanInTheLoopMiddleware
, LangGraph
interrupt()
, OpenAI Agents
needsApproval
, and Genkit
interrupt()
. There is no inbound helper and no approval helper.
BeforeToolCallEvent.interrupt()
/ 恢复属于人工介入(HITL)场景。这与CrewAI的
human_input
、JS的
humanInTheLoopMiddleware
、LangGraph的
interrupt()
、OpenAI Agents的
needsApproval
以及Genkit的
interrupt()
属于同一类机制。目前没有入站辅助工具和审批辅助工具。

Screen inbound before
Agent(...)
/
__call__
/
stream_async

Agent(...)
/
__call__
/
stream_async
之前筛选入站消息

There is no inbound helper. Call
aj.guard(...)
in the application and act on the decision. Core
guard()
fails open:
ALLOW
is not proof the rules ran. Gate on
decision.has_failed_open()
if this call site must fail closed;
guard_tool
/
guard_hooks
already default to that.
没有入站辅助工具。请在应用中调用
aj.guard(...)
根据决策执行操作。核心
guard()
为故障开放:
ALLOW
并不代表规则已执行。如果该调用点必须故障关闭,则需基于
decision.has_failed_open()
进行防护;
guard_tool
/
guard_hooks
已默认采用此逻辑。

Questions to ask the human first

首先要向用户确认的问题

Ask only what you cannot infer from the code; suggest defaults.
  1. Which tools are risky (external side effects, irreversible, spends money, sends messages)? Those get
    guard_tool
    . MCP / unwrapped tools you did not author get
    guard_hooks
    .
  2. What limits? (e.g. "10 lookups/min per user" →
    TokenBucket
    .)
  3. Who is the user for metadata — an opaque user/tenant ID (never PII)? Default: none. Put the conversation / session id you already have on
    invocation_state
    and on
    guard_hooks(...)
    . That id is the correlation id, not the user.
  4. Is an Arcjet outage unacceptable? Every helper defaults to
    on_guard_error="deny"
    . Ask explicitly about inbound screening before the agent runs: failing closed there means the agent does not run, so
    "allow"
    is a routine and legitimate choice at that one call site.
仅询问无法从代码中推断的内容,并给出建议默认值。
  1. 哪些工具具有风险(存在外部副作用、不可逆转、产生费用、发送消息)?这些工具需要使用
    guard_tool
    。你未编写的MCP/未包装工具则使用
    guard_hooks
  2. 需要设置哪些限制?(例如:“每个用户每分钟最多10次查询” → 使用
    TokenBucket
    。)
  3. 元数据对应的用户是谁——是不透明的用户/租户ID(绝不能是PII)?默认:无。将你已有的对话/会话ID放入
    invocation_state
    以及
    guard_hooks(...)
    中。该ID是关联ID,而非用户ID。
  4. Arcjet服务中断是否不可接受?所有辅助工具默认
    on_guard_error="deny"
    。请明确询问代理运行前的入站筛选:此处故障关闭意味着代理不会运行,因此
    "allow"
    是该调用点的常规合法选择。

The things readers get wrong

读者容易误解的点

  1. This is not JS
    @arcjet/guard/strands-agents/v1
    .
    Import
    arcjet.guard.strands_agents
    .
  2. There is no inbound helper. Screen with core
    guard()
    before
    Agent(...)
    /
    __call__
    /
    stream_async
    .
  3. event.interrupt()
    is HITL, not policy.
    Deny is
    cancel_tool
    (
    True
    or
    str
    ).
  4. Fail closed = always set
    cancel_tool
    on error.
    Do not raise and do not leave it unset.
  5. Correlation is read, never minted. Never
    trace_id
    , never
    agent.id
    , never SessionManager auto-ids.
  6. Already-wrapped tools are skipped by
    guard_hooks
    so Guard is not called twice. Hand the agent the copy
    guard_tool
    returns.
  7. Key rate limits on the authenticated caller, not a model-supplied order id.
  8. Do not hand-wrap every Strands tool with raw
    guard()
    .
  1. 这不是JS
    @arcjet/guard/strands-agents/v1
    请导入
    arcjet.guard.strands_agents
  2. 没有入站辅助工具。 请在
    Agent(...)
    /
    __call__
    /
    stream_async
    之前使用核心
    guard()
    进行筛选。
  3. event.interrupt()
    是人工介入(HITL),而非策略防护。
    拒绝操作需设置
    cancel_tool
    (值为
    True
    或字符串)。
  4. 故障关闭 = 错误时始终设置
    cancel_tool
    不要抛出异常,也不要留空。
  5. 关联ID仅读取,从不生成。 绝不会使用
    trace_id
    agent.id
    或SessionManager自动生成的ID。
  6. 已包装的工具会被
    guard_hooks
    跳过
    ,避免Guard被重复调用。请将
    guard_tool
    返回的副本交给代理。
  7. 关键速率限制基于已认证的调用方,而非模型提供的订单ID。
  8. 不要用原生
    guard()
    手动包装每个Strands工具。

Step 1: Install and find the guard client

步骤1:安装并找到Guard客户端

Until-published: PyPI
arcjet
0.9.0 does not include this module. Pin
arcjet
to git SHA
a630806169b92757192f3f5cce2e305827b26567
:
bash
pip install "arcjet[strands-agents] @ git+https://github.com/arcjet/arcjet-py.git@a630806169b92757192f3f5cce2e305827b26567"
If the agent has no guard client yet, launch one once at module scope:
python
import os
from arcjet.guard import launch_arcjet

aj = launch_arcjet(key=os.environ["ARCJET_KEY"])
在正式发布前:PyPI上的
arcjet
0.9.0不包含此模块。请将
arcjet
固定到Git SHA
a630806169b92757192f3f5cce2e305827b26567
bash
pip install "arcjet[strands-agents] @ git+https://github.com/arcjet/arcjet-py.git@a630806169b92757192f3f5cce2e305827b26567"
如果代理还没有Guard客户端,请在模块作用域中初始化一次
python
import os
from arcjet.guard import launch_arcjet

aj = launch_arcjet(key=os.environ["ARCJET_KEY"])

Step 2: Gate authored tools —
guard_tool

步骤2:防护已编写的工具 ——
guard_tool

python
from strands import Agent, tool
from arcjet.guard import DetectPromptInjection, TokenBucket, launch_arcjet
from arcjet.guard.strands_agents import guard_hooks, guard_tool, strands_agent_context

aj = launch_arcjet(key=os.environ["ARCJET_KEY"])
lookup_limit = TokenBucket(
    label="order.looked-up",
    bucket="lookups",
    refill_rate=10,
    interval_seconds=60,
    max_tokens=10,
)
mcp_limit = TokenBucket(
    label="mcp.invoked",
    bucket="mcp-access",
    refill_rate=20,
    interval_seconds=60,
    max_tokens=20,
)
inbound = DetectPromptInjection()
user_id = authenticated_user_id

@tool  # event.interrupt() is HITL — not this policy gate
def lookup_order(order_id: str) -> dict:
    """Look up an order by ID."""
    return {"order_id": order_id, "status": "shipped"}

lookup_order = guard_tool(
    guard=aj,
    tool=lookup_order,
    action="order.looked-up",
    rules=[lookup_limit(key=user_id, requested=1)],
    on_guard_error="deny",
)
python
from strands import Agent, tool
from arcjet.guard import DetectPromptInjection, TokenBucket, launch_arcjet
from arcjet.guard.strands_agents import guard_hooks, guard_tool, strands_agent_context

aj = launch_arcjet(key=os.environ["ARCJET_KEY"])
lookup_limit = TokenBucket(
    label="order.looked-up",
    bucket="lookups",
    refill_rate=10,
    interval_seconds=60,
    max_tokens=10,
)
mcp_limit = TokenBucket(
    label="mcp.invoked",
    bucket="mcp-access",
    refill_rate=20,
    interval_seconds=60,
    max_tokens=20,
)
inbound = DetectPromptInjection()
user_id = authenticated_user_id

@tool  # event.interrupt()是人工介入(HITL)——并非此策略防护
def lookup_order(order_id: str) -> dict:
    """根据ID查询订单。"""
    return {"order_id": order_id, "status": "shipped"}

lookup_order = guard_tool(
    guard=aj,
    tool=lookup_order,
    action="order.looked-up",
    rules=[lookup_limit(key=user_id, requested=1)],
    on_guard_error="deny",
)

Step 3: Gate unwrapped / MCP tools —
guard_hooks

步骤3:防护未包装的/MCP工具 ——
guard_hooks

python
mcp_tools = []  # from an MCP client you did not wrap with guard_tool
agent = Agent(
    tools=[lookup_order, *mcp_tools],
    # The agent-wide gate for tools guard_tool did not wrap.
    # Already-wrapped tools are skipped, so Guard is not called twice.
    # BeforeToolCallEvent.cancel_tool is True or a str (JSON of the payload).
    hooks=[
        guard_hooks(
            guard=aj,
            action="mcp.invoked",
            rules=[mcp_limit(key=user_id, requested=1)],
            session_id=conversation_id,
            on_guard_error="deny",
        ),
    ],
)
python
mcp_tools = []  # 来自你未用guard_tool包装的MCP客户端
agent = Agent(
    tools=[lookup_order, *mcp_tools],
    # 针对未被guard_tool包装的工具的代理级防护。
    # 已包装的工具会被跳过,避免Guard被重复调用。
    # BeforeToolCallEvent.cancel_tool的值为True或字符串(有效载荷的JSON格式)。
    hooks=[
        guard_hooks(
            guard=aj,
            action="mcp.invoked",
            rules=[mcp_limit(key=user_id, requested=1)],
            session_id=conversation_id,
            on_guard_error="deny",
        ),
    ],
)

Step 4: Screen inbound before the agent runs

步骤4:在代理运行前筛选入站消息

python
invocation_state = {"sessionId": conversation_id}
derived = strands_agent_context(invocation_state)
decision = await aj.guard(
    label="message.received",
    rules=[inbound(user_text)],
    correlation_id=derived.correlation_id,
)
if decision.conclusion == "DENY":
    raise RuntimeError("message blocked")
if decision.has_failed_open():
    raise RuntimeError("inbound guard unavailable")

agent(user_text, invocation_state=invocation_state)
There is no inbound helper.
python
invocation_state = {"sessionId": conversation_id}
derived = strands_agent_context(invocation_state)
decision = await aj.guard(
    label="message.received",
    rules=[inbound(user_text)],
    correlation_id=derived.correlation_id,
)
if decision.conclusion == "DENY":
    raise RuntimeError("消息被拦截")
if decision.has_failed_open():
    raise RuntimeError("入站Guard不可用")

agent(user_text, invocation_state=invocation_state)
没有入站辅助工具。

Step 5: Correlation

步骤5:关联

strands_agent_context
reads a caller-owned id from
invocation_state
:
correlationId
, then
sessionId
, then
requestId
. It never mints an id. It never reads
trace_id
. It never reads
agent.id
or SessionManager auto-ids. Do not invent a correlation id per turn. Put the same id on the invocation and on
guard_hooks(...)
.
strands_agent_context
invocation_state
中读取调用方拥有的ID:优先读取
correlationId
,其次是
sessionId
,最后是
requestId
。它不会生成ID,也不会读取
trace_id
,更不会读取
agent.id
或SessionManager自动生成的ID。请勿为每个回合创建新的关联ID。请在调用和
guard_hooks(...)
中使用同一个ID。

Verify the integration

验证集成

  1. python -m py_compile
    (or the project's type-check) passes.
  2. Exercise inbound PI (before the agent runs), a
    guard_tool
    deny, a
    guard_hooks
    deny on an unwrapped tool (
    cancel_tool
    is
    True
    or a str), a rate limit, and fail-closed (an unreachable guard). Confirm
    event.interrupt()
    is never called as the gate.
  3. Confirm in the Arcjet Console / CLI that decisions share the caller-owned session / request id — not a
    trace_id
    or
    agent.id
    .
  4. Manual E2E with a real
    ARCJET_KEY
    is still-to-verify until you run it.
Worked example:
examples/fastapi-strands-agents-guard
. Do not invent a second example name. Do not add an example in this skills repo.
  1. 执行
    python -m py_compile
    (或项目的类型检查)通过。
  2. 测试入站提示注入(代理运行前)、
    guard_tool
    拒绝、未包装工具的
    guard_hooks
    拒绝(
    cancel_tool
    True
    或字符串)、速率限制以及故障关闭(Guard不可达)场景。确认
    event.interrupt()
    从未被用作防护机制。
  3. 在Arcjet控制台/CLI中确认决策共享调用方拥有的会话/请求ID —— 而非
    trace_id
    agent.id
  4. 使用真实的
    ARCJET_KEY
    进行手动端到端测试,直到运行通过才算完成验证。
完整示例:
examples/fastapi-strands-agents-guard
。请勿创建第二个示例名称。请勿在此技能仓库中添加示例。