从 Scope State 到 Guardrails:Workrun 如何为 Agent 工作流建立安全边界

在构建复杂 Agent 工作流(Multi-Agent Workflow)时,开发者面临的最严峻挑战往往不在于 LLM 的推理逻辑,而在于数据流(Data Flow)与工具链(Tool Chain)的越权与泄露

在一个标准的自动化退款工作流中,extractor 节点解析出用户的退款原因与摘要;下一个负责"CRM 查询"的 crm-agent 需要使用初始输入中的真实 Token 和邮箱调用外部接口,而再下一个负责"最终审核"的 reviewer 节点,可能只需要知道退款摘要。

  • Token 和邮箱会不会直接暴露在 crm-agent 的 Prompt 里?
  • reviewer 会不会无意中读取到了上游敏感的客户联系方式?
  • 工具调用返回了用户的明文数据,会不会又通过模型回复或日志泄露出去?

传统做法依赖 Prompt 提示词告诉模型"不要泄露",这在工程上是不可靠的。作为一款基于 Tauri + Rust 构建的桌面自动化工具,Workrun 的策略是:用系统架构(Architecture Constraints)代替 Prompt 约束。我们将数据分为"模型可见"与"工具可用"两套视图,并在输入、输出、工具执行与日志等边界上补齐强硬的 Guardrails。

场景引入:一个真实的退款工作流

为了理解 Workrun 的安全机制,我们来看一个具体的退款工作流:

scss 复制代码
[用户提交退款申请] 
       │
       ▼
[提取节点 extractor] ──(提取基本信息与路由)──► [CRM 节点 crm-agent] ──► [审核节点 reviewer]

1. 工作流初始输入 (Workflow Input)

JSON

perl 复制代码
{
  "orderId": "20260903001",
  "customerEmail": "alice@example.com",
  "crmToken": "Bearer eyJhbGciOi..."
}

2. extractor 节点的原始输出

extractor 作为一个 LLM 节点,由于 Agent 看到的本身就是脱敏后的输入(customerEmail 显示为 [EMAIL REDACTED]),其生成的输出结构如下:

JSON

css 复制代码
{
  "summary": "用户申请订单 20260903001 的退款",
  "customer": {
    "email": "[SENSITIVE REDACTED]",
    "reason": "重复下单"
  },
  "internalScore": 82
}

我们的目标是:

  1. crmTokencustomerEmail 绝对不能注入到任何 Agent Prompt 中
  2. crm-agent 执行 CRM 工具时,必须能够通过受控方式拿到初始输入里的真实 crmTokencustomerEmail
  3. reviewer 节点只能读取到 summary ,绝对看不到 internalScore 或敏感联系方式。

下面是 Workrun 如何在配置层与运行时实现这一目标。

Scope State:数据隔离与双轨状态

Workrun 拒绝把工作流 State 当成一份所有节点平铺共享的 JSON。每个节点都有自己的 State Namespace,节点输出默认私有,按字段控制暴露边界

scss 复制代码
                       ┌───────────────────────────────┐
                       │       Workflow Raw State      │ (包含初始 raw input,仅在内存中保存)
                       └──────────────┬────────────────┘
                                      │
                       [PII & Sensitive Redaction]
                                      │
                                      ▼
                       ┌───────────────────────────────┐
                       │     Workflow Visible State    │ (用于 Agent Prompt/UI/日志)
                       └──────────────┬────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                             LLM Agent Prompt                                │
└─────────────────────────────────────────────────────────────────────────────┘

1. 工作流输入层配置 (Workflow Settings)

首先在工作流级别的输入配置(inputSchema)中,标记敏感字段与授权节点:

JSON

json 复制代码
{
  "inputSchema": {
    "fields": [
      { "key": "orderId", "type": "string" },
      { "key": "customerEmail", "type": "string" },
      { "key": "crmToken", "type": "string" }
    ],
    "sensitiveFields": ["crmToken", "customerEmail"],
    "rawReaders": ["crm-agent"]
  }
}

2. 节点层配置 (extractor Inspector)

extractor 节点中,我们可以进行如下配置(在 UI 的 Inspector 面板中操作):

JSON

json 复制代码
{
  "id": "extractor",
  "kind": "agent",
  "data": {
    "state": {
      "access": {
        "readers": ["crm-agent"],       // 允许读取该节点 State 的下游节点
        "rawReaders": ["crm-agent"]    // 允许使用原始数据的节点
      },
      "sensitiveFields": ["customer.email"], // 强制脱敏的业务字段
      "globalKeys": ["summary"]         // 发布到全局 Global State 的字段
    }
  }
}

这段配置在运行时会产生极严密的隔离效果:

  • 默认私有与全局发布internalScorecustomer 留在 extractor 私有作用域中。只有 summary 会发布到 Global State。下游的 reviewer 因为不在 readers 列表中,它完全读不到 extractor 的私有 State,只能读取已发布的 summary
  • Prompt 永远只看 Visible State :即便 crm-agent 被授权在 readers 中,其 Agent 绑定的 Prompt 拿到的 Context 中,敏感字段依然是脱敏状态:

JSON

json 复制代码
// crm-agent 的 Prompt 实际收到的 Context
{
  "orderId": "20260903001",
  "customerEmail": "[SENSITIVE REDACTED]",
  "crmToken": "[SENSITIVE REDACTED]",
  "summary": "用户申请订单 20260903001 的退款"
}

Tool State Binding:原始值不进 Prompt,只在工具执行前恢复

既然 Prompt 里只有 [SENSITIVE REDACTED]crm-agent 的 CRM 查询工具该如何拿到真实数据?

Workrun 实现了 Tool State Binding(工具状态绑定) 机制。在 crm-agent 节点的配置中,用户可以将拥有原始读取权限的 State 路径(如 Workflow Input 中的真实字段)显式映射到工具参数上:

JSON

json 复制代码
{
  "toolStateBindings": [
    {
      "toolId": "crm.lookup",
      "argumentPath": "email",
      "statePath": "customerEmail"  // 指向工作流初始输入中的原始邮箱字段
    },
    {
      "toolId": "crm.lookup",
      "argumentPath": "token",
      "statePath": "crmToken"       // 指向工作流初始输入中的原始 Token 字段
    }
  ]
}

当工作流运行到工具调用阶段时,数据的生命周期经历了如下蜕变:

perl 复制代码
1. LLM 推理生成参数  ──► { "email": "[SENSITIVE REDACTED]", "token": "[SENSITIVE REDACTED]" }
                                     │
2. ManagedTool 拦截   ──► 检查节点是否拥有针对该 statePath 的 `rawReaders` 授权?
                                     │
                 ┌───────────────────┴───────────────────┐
              [已授权]                                [未授权]
                 │                                       │
  根据 AST 路径在 `raw_state`             保持 `[... REDACTED]` 占位符,
  中查找真实值并精准替换                  ManagedTool 拒绝执行并硬阻断抛错
                 │
                 ▼
3. 最终发给 CRM 接口 ──► { "email": "alice@example.com", "token": "Bearer eyJhbGciOi..." }

图中包含 Global State、节点独立 namespace、Runtime State,以及 Visible State 与 Tool State Binding 的边界关系。

演示视频

其中 CRM Agent 调用的"模拟 CRM 查询" Tool 代码如下:

python 复制代码
"""Tool App entrypoint.

Workrun validates the Agent arguments against this App's input fields, then invokes the decorated function. Return a JSON object that matches the configured output fields.
"""

from workrun_sdk.tool import tool


@tool(
    name="mock_crm_lookup",
    description="查询模拟客户的退款资格。",
)
def mock_crm_lookup(email: str, token: str) -> dict[str, object]:
    # 该工具只用于演示;不记录也不返回传入的令牌。
    if not token:
        raise ValueError("缺少 CRM 演示令牌")

    # 仅用于本地演示:Tool output 会显示这两行日志。
    print("token: ", token); // 查看是否获取到原始的 crmToken 
    print("email: ", email); // 查看是否获取到原始的 customerEmail

    return {
        "status": "active",
        "customerName": "Alice",
        "email": email,
        "refundEligible": True,
    }

关键控制限制

  1. 严格按路径替换 :绑定仅替换模型已经显式生成的参数路径 (如 email),系统绝不会自动将整份 raw_state 塞进请求包里。
  2. 占位符防逃逸 :如果工具准备发出一瞬间,参数中依然包含 [... REDACTED],说明授权失败或路径匹配失败。Workrun 会直接拒绝调用,绝不会把占位符作为请求发送给外部 Process 或 MCP 工具。

Guardrails:边界上的硬核防线

如果说 Scope State 是可配置的数据隔离规则,那么 Guardrails 就是 Workrun 内置在 Agent 执行器层面的硬规则策略。

scss 复制代码
  [用户 Prompt] ──► [ 输入 Guardrail ] ──► [ LLM 模型 ]
                                              │
  [外部 API / 工具] ◄── [ 工具 Guardrail ] ◄──┤ (模型发出 Tool Call)
          │                                   │
          └────────► [ 输出 Guardrail ] ──► [ UI / 事件 / 日志 ]

1. 输入 Guardrail(硬阻断 vs 脱敏)

当用户输入进入模型前,引擎会进行扫描:

  • 常规 PII 脱敏 :邮箱、电话、IP 地址、中国手机号、身份证号等转换为 [REDACTED] 后放行;
  • 认证凭证直接阻断 :一旦检测到 Bearer Token、OpenAI sk- Key、GitHub/AWS Token 或 PEM 私钥,系统策略不是"替换后放行",而是直接拒绝执行(Input Error)

工程逻辑:用户把 Token 粘贴进 Prompt 往往是误操作。如果脱敏后放行,模型通常会因缺失密钥而产生无效对话,直接阻断能最快给用户明确的安全反馈。

2. 输出 Guardrail(阻断旁路泄漏)

如果 CRM 工具为了业务需要拿到了原始值,并返回了包含明文邮箱的数据:

JSON

json 复制代码
// CRM 工具返回给 Agent 的原始数据
{ "customerName": "Alice", "email": "alice@example.com", "ip": "10.0.0.8" }

输出 Guardrail 会在工具结果写回 Agent 上下文、工作流日志以及前端 UI 事件前再次介入,将数据强制清洗为:

JSON

json 复制代码
{ "customerName": "Alice", "email": "[EMAIL REDACTED]", "ip": "[IP ADDRESS REDACTED]" }

这彻底杜绝了"工具成功拿到秘密,但通过结果回显给模型/日志"的旁路泄漏路径。

3. 流式输出(Streaming)防泄露

流式输出(SSE Chunk)是脱敏引擎极易破防的场景。如果模型将 Token 拆成分块发送:

Chunk 1: "sk-" -> Chunk 2: "12345" -> Chunk 3: "67890"

单个 Chunk 无法匹配正则,明文就会直接暴露在前端。

Workrun 在 Rust 后端(agent.rs)的策略是:需要经过输出 Guardrail 的 Agent 消息会在后端做 Chunk 汇聚缓存,待完整消息形成并脱敏后统一推送到 UI 渲染;文本与 JSON 数据流则在 Rust 结构体层走递归过滤,封堵流式泄露。

UI 配置与实际可见效果对应

为了给开发者提供直观的心理模型,Workrun 中的配置项与运行时的实际表现有着严格的映射关系:

想解决的问题 Workrun 配置位置 运行时效果 UI 呈现位置
不希望所有节点看到输入中的 Token inputSchema.sensitiveFields: ["crmToken"] Prompt 中 crmToken 变成 [SENSITIVE REDACTED] Workflow Settings 输入字段旁打开 Sensitive 开关
CRM 工具必须使用真实 Token inputSchema.rawReaders: ["crm-agent"] 工具执行阶段拿到原值,模型 Prompt 仍保持脱敏 Workflow Settings 的 Raw input readers 勾选 crm-agent
extractor 客户资料不默认共享 不配置 globalKeys customer 等字段保留在 extractor 私有 State 中 节点 Inspector 的 State access
CRM 节点读取客户资料 readers: ["crm-agent"] crm-agent 可读取 extractor 的可见 State 节点 Inspector 的 Allowed readers
CRM 工具需要真实邮箱与 Token rawReaders: ["crm-agent"] + Tool State Binding 模型看脱敏邮箱,工具执行前绑定并替换成初始输入的真实值 节点 Inspector 的 Raw state readers + Tool State Bindings
审核节点只需要摘要 globalKeys: ["summary"] summary 发布到 Global State 节点 Inspector 的 Global State publication

在大盘展示(Workflow State 面板)中,Workrun 展示的永远是 Visible State。即便工具在幕后静默使用过真实凭证,开发者在 UI 和工作流日志中看到的永远是经过脱敏后的优雅视图。
如果你想尝试,可以动手复现:在 Workrun 配置"退款安全工作流" Workrun 仍在开发阶段,暂未提供工作流及关联配置的一键导入、导出能力。想亲自验证 Scope State、Tool State Binding 和 Guardrails 的效果,可以本地运行 Workrun,并按下面步骤手动配置。

示例仅使用演示邮箱和 Token,不要打印或填入生产环境密钥

1. 创建工作流和输入字段

新建一个 Task 工作流,名称设为「退款流程安全演示」。

添加三个输入字段:

Key 类型 必填 敏感字段
orderId string
customerEmail string
crmToken string

在 Input Schema 的 Raw readers 中,仅授权「CRM 查询」Agent。这样,CRM 工具可以在执行时拿到原始邮箱和 Token,但其他 Agent 的提示词仍只会看到脱敏后的 State。

2. 创建 Tool App

创建一个 Tool App,名称为「模拟 CRM 查询」,配置两个输入参数:

  • email:string,必填
  • token:string,必填

输出 Schema:

json 复制代码
{
  "type": "object",
  "properties": {
    "status": { "type": "string" },
    "customerName": { "type": "string" },
    "email": { "type": "string" },
    "refundEligible": { "type": "boolean" }
  },
  "required": ["status", "refundEligible"]
}

工具代码如下:

python 复制代码
from workrun_sdk.tool import tool


@tool(
    name="mock_crm_lookup",
    description="查询模拟客户的退款资格。",
)
def mock_crm_lookup(email: str, token: str) -> dict[str, object]:
    if not token:
        raise ValueError("缺少 CRM 演示令牌")

    # 仅用于本地演示:Tool output 会显示这两行日志。
    print("token:", token)
    print("email:", email)

    return {
        "status": "active",
        "customerName": "Alice",
        "email": email,
        "refundEligible": True,
    }

为保证演示连续完成,将该工具的执行策略设为 Run automatically

3. 配置节点与连线

创建并按顺序连接以下节点:

text 复制代码
Start → 提取退款信息 → CRM 查询 → 退款审核 → End

节点一:提取退款信息

类型:Agent

提示词:

text 复制代码
请读取当前工作流中可见的数据,提取一份结构化退款申请。

只返回符合输出 Schema 的 JSON:
- summary:用一句中文概括退款申请;
- customer.email:复制当前可见的 customerEmail;
- customer.reason:填写"重复下单";
- internalScore:填写 0 到 100 之间的整数。

不要尝试获取或输出 crmToken。

输出 Schema:

json 复制代码
{
  "type": "object",
  "properties": {
    "summary": { "type": "string" },
    "customer": {
      "type": "object",
      "properties": {
        "email": { "type": "string" },
        "reason": { "type": "string" }
      },
      "required": ["email", "reason"]
    },
    "internalScore": { "type": "integer" }
  },
  "required": ["summary", "customer", "internalScore"]
}

State 配置:

  • Global keys:summary
  • Sensitive fields:customer.email
  • 不授予 Raw State access

此时 Agent 在提示词中看到的 customerEmail[EMAIL REDACTED],因此它产生的 customer.email 也会是脱敏值。这正是 Scope State 的预期效果。

节点二:CRM 查询

类型:Agent

添加刚才创建的「模拟 CRM 查询」工具。

提示词:

text 复制代码
你负责查询客户的退款资格。

必须调用一次"模拟 CRM 查询"工具。
工具需要 email 和 token 两个参数;填写当前可见值即可,Workrun 会在工具执行前按 State Binding 注入已授权的原始值。

工具返回后,只返回符合输出 Schema 的 JSON。
不要在最终输出中包含邮箱、令牌或其他联系方式。

输出 Schema:

json 复制代码
{
  "type": "object",
  "properties": {
    "crmStatus": { "type": "string" },
    "refundEligible": { "type": "boolean" }
  },
  "required": ["crmStatus", "refundEligible"]
}

最关键的是配置 Tool State Binding:

Tool 参数 State path
email customerEmail
token crmToken

这里不要写 customer.email。上一个 Agent 没有 Raw State 权限,它写入的是已脱敏邮箱;而 customerEmailcrmToken 保存在原始输入 State 中,且只对已授权的 CRM 工具可用。

节点三:退款审核

类型:Agent

提示词:

text 复制代码
你负责退款审核。

只能依据当前可见的数据做判断,不要尝试索取、猜测或输出客户联系方式、内部风险评分、CRM Token 或其他敏感信息。

只返回符合输出 Schema 的 JSON:
- decision:填写"通过";
- reason:引用可见退款摘要,说明通过原因。

输出 Schema:

json 复制代码
{
  "type": "object",
  "properties": {
    "decision": { "type": "string" },
    "reason": { "type": "string" }
  },
  "required": ["decision", "reason"]
}

State 配置:

  • Global keys:decisionreason
  • 不配置 Sensitive fields
  • 不授予 Raw State access

4. 运行验证

使用以下输入运行:

json 复制代码
{
  "orderId": "演示订单-20260903-001",
  "customerEmail": "alice@example.com",
  "crmToken": "demo-crm-token-001"
}

预期现象:

  1. 「提取退款信息」输出中,customer.email[EMAIL REDACTED]
  2. 「CRM 查询」仍能成功返回 crmStatus: "active"refundEligible: true
  3. 展开 CRM 节点中的 Tool call,可以在 Output 区域看到 Tool App 的 stdout
  4. 「退款审核」只根据退款摘要给出结论,不会得到邮箱、Token 或内部评分。

这个流程刻意同时展示两件事:Agent 不会因为"下游工具需要"就自动获得敏感数据;而被明确授权的工具,又能通过 Binding 在执行瞬间使用原始值。

总结

Workrun 针对 Agent 工作流建立的安全边界,本质上不是靠一句 Prompt 劝导模型"不要泄露",而是从架构层面实现:

  1. 数据分视图(Scope State) :将数据拆分为"模型可见(Visible State)"与"工具可用(Raw State)",Prompt 只能看到脱敏后的减法视图,原始数据仅在受控的工具执行阶段按需恢复。
  2. 边界硬隔离(Guardrails) :在输入阻断、工具参数校验、流式输出汇聚以及磁盘 Checkpoint 加密等边界设立兜底护栏,拦截一切未授权的数据逃逸。

这种"模型与数据解耦"的架构设计,为企业与开发者构建高敏感的 Multi-Agent 自动化工程提供了坚实的基础。

👉 Workrun GitHub 项目地址github.com/1111mp/work...

相关推荐
luckystar513~1 小时前
Hermes 实战 :调度——cron 6:00 无人值守跑日报
人工智能·agent·智能体·hermes·实战专栏
Csvn1 小时前
第 15 章 规划 Planning
人工智能·aigc·agent
Neighbor_OldY1 小时前
云上证书过期与HTTPS安全治理实战:证书到期没人管、自动化续期与证书链排查
安全·https·自动化
咖啡星人k1 小时前
2026 智能体安全进阶:把注入和越权写进SPEC,MonkeyCode 云端跑通
人工智能·安全·机器学习
迅利科技2 小时前
航空 EWIS 线束数字化,CATIA 如何保障复杂机载电气流体系统安全合规
安全·系统安全
狗凯之家源码网2 小时前
iOS 系统安全机制与漏洞修复解析
安全·系统安全
DO_Community2 小时前
Baseten vs DigitalOcean、RunPod:7 个 AI 模型部署平台横向对比
人工智能·llm·aigc·agent·ai编程
小五传输2 小时前
聚焦卫健数字化建设 文件传输服务器守护跨机构敏感医疗数据交换
大数据·运维·安全
AI_Cloud_推荐2 小时前
SpringBoot集成百度人脸识别SDK实战:人脸检测、比对与注册
大数据·人工智能·spring boot·安全·百度·视觉检测