企业级 AI Agent 执行控制与审计架构——Plugin+Hook实现

0 阅读19分钟
**方案逻辑:**企业级 AI Agent 执行控制与审计架构:Skill 治理、工具代理与多源审计管控 落地形态调整为 Plugin + Hook + ToolProxy;方案框架与控制逻辑保持不变

摘要

随着 AI Agent 从内容生成走向任务执行,企业安全治理的重点也从“模型输出是否合规”逐步延伸到“执行过程是否受控”。对企业而言,真正需要治理的不是单一回答,而是 Skill 如何接入、如何加载、如何调用工具、如何访问业务系统、是否存在执行旁路,以及异常行为出现后是否能够被核验、分级和处置。 本文给出一套面向企业业务落地过程的执行控制与审计架构。方案框架与核心逻辑保持不变,仍然围绕 Skill 治理、ToolProxy 统一代理、业务侧入口收口以及多源日志核验展开;差异在于运行时落地方式从默认 AST 插桩调整为 Plugin + Hook + ToolProxy:由治理插件在框架主路径上完成 Skill 生命周期拦截、上下文注入、票据绑定、审计采集与规则触发,由 ToolProxy 承担统一出口控制,由业务系统仅接受来自 ToolProxy 的受信流量,最终通过声明、代理日志与业务日志的三方对比输出分级裁决结果。 **提示:**本文讨论范围聚焦于企业最关心的执行链路控制,并不试图覆盖全部 AI 安全议题;目标是建立一条可控、可查、可核验、可分级处置的业务执行主链路。

一、整体架构:设计目标、原则与作用逻辑

1. 设计目标

企业环境中的 AI Agent 往往不只是生成文本,而是通过 Skill 调用文件系统、HTTP 接口、数据库、消息能力和内部业务服务,从而完成检索、编排、写入、审批、同步等任务。此时企业真正需要治理的,不只是“模型生成了什么”,更是“执行了什么、访问了什么、是否在授权范围内、是否能够追溯”。 因此,这套架构的设计目标是:在不显著改变业务逻辑的前提下,为 Agent 的执行链路建立统一入口、统一代理、统一日志与统一裁决机制,让企业能够对 Skill 的接入、加载、调用和业务落地过程形成可操作的治理闭环。

2. 设计原则

  • 主路径治理优先:控制逻辑应挂载在 Agent 框架主执行路径上,而不是作为可选旁路存在。
  • 出口统一收口:高风险工具和业务访问必须经由 ToolProxy 统一代理。
  • 业务入口可验证:业务系统仅接受来自 ToolProxy 或受信入口的调用。
  • 多源事实核验:裁决不依赖单一日志,而是基于声明、代理日志和业务日志的交叉比对。
  • 分级处置而非单点阻断:低风险偏差优先告警与确认,高风险偏差才进入冻结、吊销与阻断。

3. 实现逻辑

可以把整套机制理解为一条“先登记、再执行、再核验、再处置”的业务控制链。

  • Skill 在进入执行体系之前,先提交结构化声明并被纳入白名单;
  • 运行时由治理插件通过 hook 拦截关键生命周期事件,为请求注入上下文和票据;
  • 所有高风险工具访问再统一交给 ToolProxy 代理;
  • 业务系统仅接收来自 ToolProxy 的受信请求;
  • 执行完成后,平台将 Skill 声明、代理日志和业务日志统一汇总,对照访问对象、操作类型、时间窗口和身份信息进行三方核验;
  • 最后按照偏差等级选择告警、人工确认、限制权限或中止执行。 换句话说,Plugin + Hook 负责把控制能力准确挂到框架主路径上,ToolProxy 负责把外部访问统一收口,业务系统负责把入口边界收严,多源核验负责把结果变成真正可用的治理依据。

二、核心组件与核心安全机制

从逻辑上看,Security Core 仍然存在,但其运行时落地形态从“默认 AST 插桩型内核”调整为“Plugin + Hook 驱动的执行治理层”,并与 ToolProxy、业务系统入口约束及审计裁决服务共同组成完整闭环。

**组件** **核心职责** **关键安全机制** **说明** 治理插件层(Plugin + Hook) 拦截 Skill 生命周期与 Tool 调用主路径 白名单、声明读取、上下文注入、票据绑定、审计采集、规则触发 承担框架内治理职责,替代默认 AST 插桩作为首选落地方式 ToolProxy 统一代理高风险工具与业务访问 票据校验、实例身份校验、资源边界判断、标准化代理日志 承担统一出口控制,不与普通 Skill 混用身份 业务系统受信入口 只接受来自 ToolProxy 的访问 mTLS、服务身份、签名、网关校验、网络 ACL 用于保证无旁路控制,否则代理层无法形成真正门禁 审计与裁决服务 汇总多源日志并输出分级结论 声明比对、代理日志核验、业务日志核验、异常等级映射 裁决结果作为告警、确认、限制或阻断的依据

1. 治理插件层(Plugin + Hook)

治理插件层不是普通业务插件的简单并列项,而应作为框架主执行链路中的受管组件存在。 它的核心职责是:在 Skill 加载、Tool 调用、执行结束和异常处理等关键节点挂载 hook,完成声明读取、白名单匹配、上下文注入、票据绑定和审计事件采集。 在这种模式下,默认不再要求对每个 Skill 做 AST 改写;只要业务 Skill 通过框架标准接口访问工具和业务能力,治理插件就可以在主路径上完成大部分控制与采集。 对于非标准或遗留 Skill,AST 插桩可以作为兼容性补强手段保留,但不再是默认前提。

2. ToolProxy

ToolProxy 是统一出口控制组件,负责接收来自治理插件的代理请求,对票据、实例身份、声明边界与资源范围进行校验,再将符合条件的请求转发给业务系统或外部工具。 它的意义不是简单做一层转发,而是将高风险访问从 Skill 运行环境中剥离出来,建立真正可收口、可核验、可追溯的执行出口。

3. 业务系统受信入口

ToolProxy 是否真正具备控制意义,取决于业务系统是否只接受来自 ToolProxy 的调用。 如果业务接口仍允许 Skill 容器或普通执行节点直接访问,那么无论治理插件还是代理层都可能被绕过。因此,业务入口约束是整套方案能否成立的关键。

4. 审计与裁决服务

裁决不依赖单一日志,而是依赖三类事实源:Skill 声明、ToolProxy 代理日志、业务系统访问日志。 前者给出“原本允许做什么”,中者给出“代理层实际转发了什么”,后者给出“业务系统最终确认收到了什么”。 三者统一汇总后,平台才能输出具有处置意义的偏差等级。

三、整体执行流程

1. Skill 生命周期管理流程

  • Skill 接入:业务方提交 Skill 元数据、工具声明、资源范围和操作类型,进入白名单审核与登记流程。
  • Skill 加载:框架加载 Skill 前,由治理插件执行 before_load hook,读取声明、校验版本与状态,并为实例建立登记上下文。
  • 运行上下文注入:治理插件在执行前将 skill_id、instance_id、ticket_id、session_id 等上下文注入到请求链路中。
  • 调用前校验:Skill 发起 tool call 时,before_tool_call hook 对目标工具、目标资源和动作类型进行匹配判断。
  • 统一代理访问:符合条件的调用由治理插件转交 ToolProxy;对于明确禁止或高风险越权的调用,可直接拒绝或进入审批。
  • 审计回收:调用完成后,after_tool_call 与 on_error hook 统一回收结果、异常与上下文,送入审计与裁决服务。

2. ToolProxy 的实现逻辑

  • 接收来自治理插件的代理请求;
  • 校验 ticket、skill_id、instance_id 与声明边界;
  • 检查当前实例是否处于允许执行状态;
  • 判断动作是否需要放行、确认或阻断;
  • 将合法请求以受信身份转发至业务系统;
  • 记录标准化代理日志并异步上报裁决服务。

3. 无旁路保证:业务仅接受来自 ToolProxy 的流量

在工程上,要让 ToolProxy 成为真正的执行门禁,业务系统必须明确只接受来自 ToolProxy 或受信网关的流量。生产环境通常通过以下方式实现:

  • 业务接口只开放给 ToolProxy 所在网段、服务账户或受信服务身份;
  • 业务系统通过 mTLS、签名、服务网格、专用网关或网络 ACL 校验调用来源;
  • Skill 所在容器或普通执行节点不持有业务系统长期凭据;
  • 高风险动作必须由 ToolProxy 代发,业务系统不接受直连。

**提示:**如果业务系统无法落实“仅接受来自 ToolProxy 的流量”,那么治理插件和代理层仍然具有审计与编排价值,但不能视为严格意义上的强控制门禁。

4. 三方日志汇总与裁决流程

每次执行完成后,平台将 Skill 声明、ToolProxy 代理日志与业务日志统一汇总。 核验重点通常包括:

  • 目标资源是否在声明范围内
  • 调用动作是否与声明一致
  • 代理层与业务侧记录是否一致
  • 是否存在票据缺失
  • 是否存在非 ToolProxy 来源访问 系统基于这些差异输出分级结果,用于告警、人工确认、权限限制或阻断。

四、治理插件层与 ToolProxy 的安全保障机制

1. 治理插件层的安全保障

采用 Plugin + Hook 作为落地方式后,安全重点不再是“如何大规模做 AST 改写”,而是“如何确保治理插件本身处于框架主路径且不可被业务 Skill 轻易绕开或替换”。因此,治理插件层至少需要满足以下条件:

  • 优先加载:作为框架内建组件或高优先级受管插件存在,而不是普通业务插件;
  • 配置与版本可校验:白名单、策略、规则和 hook 配置应具备签名或摘要校验能力;
  • 关键 hook 不可关闭:before_load、before_tool_call、after_tool_call 等关键 hook 不应被业务面任意绕过;
  • 最小权限:治理插件只暴露必要接口,不与业务 Skill 共享高风险凭据;
  • 独立日志通道:关键审计事件直接写入独立通道,避免被业务链路静默吞没;
  • 兼容兜底:对于无法纳入标准 hook 主路径的遗留 Skill,可引入局部插桩或 SDK 适配作为补强。 需要强调的是,Plugin 本身并不会天然比独立 Security Core 更可信。 其可靠性来自于它是否由框架主进程优先加载、是否拥有不可绕开的 hook 覆盖、是否具备独立配置校验与日志通道,以及业务 Skill 是否无法在运行时卸载或替换它。

2. ToolProxy 的安全保障

ToolProxy 需要重点防范身份伪造、票据滥用、代理绕过和日志失真等问题。建议至少从以下方面进行加固:

  • 校验 skill_id、instance_id、ticket_id 与时间窗口,避免仅凭 skill_name 判断合法性;
  • 代理请求使用短周期票据,不向 Skill 长期暴露业务系统访问凭据;
  • 代理日志采用标准结构输出,记录调用目标、动作类型、结果状态和关联上下文;
  • 业务系统使用受信入口校验 ToolProxy 身份,并拒绝普通执行环境的直连访问;
  • 高风险调用失败、身份不一致或票据异常时,应触发告警并可按策略拒绝执行。

五、分级管理机制

三方核验的结果不宜被简单理解为“发现异常就一律阻断”。更稳妥的做法是根据偏差程度进行分级管理。

**等级** **典型情形** **建议处置** 轻微异常 声明与代理日志存在小幅偏差;低风险访问缺少部分上下文;日志时间窗轻微错位 记录告警、补充采集、提升观察等级,不中断主流程 中等异常 代理日志与业务日志多次不一致;调用目标偏离声明范围但未触及高危资源;票据多次校验失败 触发人工确认或审批,临时限制部分权限,对后续步骤附加校验 严重异常 无票据访问;身份伪造;高危工具越权调用;业务系统确认收到非 ToolProxy 来源的敏感访问 中止当前执行,冻结相关 Skill 权限,吊销票据并触发事件升级与溯源处理

六、技术落地可行性判断与评估

将落地方式从默认 AST 插桩调整为 Plugin + Hook 之后,工程可行性通常会更高。原因在于:Hook 更容易绑定到框架主路径,多语言兼容压力更小,存量 Skill 的适配成本更低,且治理逻辑更容易平台化和灰度发布。 真正的实施难点仍然集中在以下几类问题:业务框架是否存在标准化 hook 主路径、业务系统是否愿意落实受信入口、日志格式能否统一、遗留 Skill 是否仍有旁路能力,以及治理插件是否能获得高于普通业务插件的加载优先级和控制权。 因此,这套方案的价值不在于宣称“绝对安全”,而在于在企业既有基础设施之上,建立一条清晰的受控执行主链路。 较为稳妥的落地顺序通常是:先规范 Skill 声明和主路径 hook,再引入 ToolProxy 和业务入口收口,然后接入统一日志与分级裁决,最后视遗留系统情况补充插桩、SDK 改造或运行时约束。

七、演示代码

下面的演示代码以 Python 为例,目标是体现模块关系和控制逻辑,而不是直接作为生产代码使用。 相比旧版本,代码不再以 AST 插桩为默认前提,而是以治理插件注册 hook、统一拦截 tool call、转交 ToolProxy 代理为主线。

模块作用
manifest.py定义 Skill 声明结构,作为权限判断、工具边界和审计对账的基础。
ticket.py签发与校验短周期访问票据,为代理请求建立可信身份链路。
audit_center.py统一收集声明、代理日志与业务日志,并执行三方核验与分级裁决。
tool_proxy.py统一代理工具与业务访问,校验票据和声明边界,并输出代理日志。
plugin_runtime.py以 Plugin + Hook 方式实现 Security Core 的运行时治理能力。
business_system.py模拟业务系统仅接受来自 ToolProxy 的受信调用。
demo.py演示从 Skill 加载、工具调用、代理转发到裁决输出的完整闭环。

manifest.py :


from dataclasses import dataclass, field
from typing import Dict, List, Optional


@dataclass(frozen=True)
class ToolRule:
    tool_name: str
    actions: List[str]
    resources: List[str]
    risk_level: str = "medium"

    def allows(self, action: str, resource: str) -> bool:
        return action in self.actions and resource in self.resources


@dataclass(frozen=True)
class SkillManifest:
    skill_id: str
    skill_name: str
    version: str
    owner: str
    tools: List[ToolRule]
    labels: Dict[str, str] = field(default_factory=dict)

    def find_rule(self, tool_name: str) -> Optional[ToolRule]:
        for rule in self.tools:
            if rule.tool_name == tool_name:
                return rule
        return None

ticket.py

from __future__ import annotations

import base64
import hashlib
import hmac
import json
import time
from dataclasses import dataclass
from typing import Dict


class TicketError(Exception):
    pass


@dataclass
class TicketClaims:
skill_id: str
instance_id: str
tool_name: str
resource: str
exp: int


class TicketService:
def __init__(self, secret: str, ttl_seconds: int = 120) -> None:
    self.secret = secret.encode("utf-8")
    self.ttl_seconds = ttl_seconds

def issue(self, *, skill_id: str, instance_id: str, tool_name: str, resource: str) -> str:
    claims = {
        "skill_id": skill_id,
        "instance_id": instance_id,
        "tool_name": tool_name,
        "resource": resource,
        "exp": int(time.time()) + self.ttl_seconds,
    }
    payload = json.dumps(claims, separators=(",", ":"), sort_keys=True).encode("utf-8")
    sig = hmac.new(self.secret, payload, hashlib.sha256).hexdigest().encode("utf-8")
    return base64.urlsafe_b64encode(payload).decode() + "." + sig.decode()

def verify(self, token: str) -> TicketClaims:
    try:
        payload_b64, sig = token.split(".", 1)
        payload = base64.urlsafe_b64decode(payload_b64.encode("utf-8"))
    except Exception as exc:
        raise TicketError("invalid ticket format") from exc

    expected = hmac.new(self.secret, payload, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):
        raise TicketError("ticket signature mismatch")

    data: Dict[str, object] = json.loads(payload.decode("utf-8"))
    exp = int(data["exp"])
    if exp < int(time.time()):
        raise TicketError("ticket expired")

    return TicketClaims(
        skill_id=str(data["skill_id"]),
        instance_id=str(data["instance_id"]),
        tool_name=str(data["tool_name"]),
        resource=str(data["resource"]),
        exp=exp,
    )

audit_center.py

from __future__ import annotations

from dataclasses import dataclass
from typing import Dict, List

from manifest import SkillManifest


@dataclass
class Decision:
level: str
reason: str


class AuditCenter:
def __init__(self) -> None:
    self.proxy_logs: List[Dict[str, object]] = []
    self.business_logs: List[Dict[str, object]] = []

def log_proxy_call(self, entry: Dict[str, object]) -> None:
    self.proxy_logs.append(entry)

def log_business_call(self, entry: Dict[str, object]) -> None:
    self.business_logs.append(entry)

def reconcile(self, manifest: SkillManifest, correlation_id: str) -> Decision:
    proxy = next((x for x in self.proxy_logs if x["correlation_id"] == correlation_id), None)
    business = next((x for x in self.business_logs if x["correlation_id"] == correlation_id), None)

    if not proxy:
        return Decision("high", "missing proxy log")
    if not business:
        return Decision("high", "missing business log")
    if business.get("source") != "tool_proxy":
        return Decision("high", "business traffic is not from tool_proxy")

    rule = manifest.find_rule(str(proxy["tool_name"]))
    if rule is None:
        return Decision("high", "tool not declared in manifest")
    if not rule.allows(str(proxy["action"]), str(proxy["resource"])):
        return Decision("medium", "proxy action exceeds declared manifest boundary")
    if proxy["resource"] != business["resource"] or proxy["action"] != business["action"]:
        return Decision("medium", "proxy log does not match business log")

    return Decision("low", "execution path is consistent across manifest, proxy log and business log")

tool_proxy.py

 from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict

from audit_center import AuditCenter
from business_system import BusinessSystem
from manifest import SkillManifest
from ticket import TicketService


class ProxyPolicyError(Exception):
pass


@dataclass
class ToolCall:
tool_name: str
resource: str
action: str
payload: Dict[str, object] = field(default_factory=dict)
metadata: Dict[str, object] = field(default_factory=dict)


class ToolProxy:
def __init__(
    self,
    *,
    ticket_service: TicketService,
    audit_center: AuditCenter,
    business_system: BusinessSystem,
) -> None:
    self.ticket_service = ticket_service
    self.audit_center = audit_center
    self.business_system = business_system

def forward(self, *, correlation_id: str, manifest: SkillManifest, instance_id: str, call: ToolCall) -> Dict[str, object]:
    ticket = str(call.metadata.get("ticket", ""))
    claims = self.ticket_service.verify(ticket)
    if claims.skill_id != manifest.skill_id or claims.instance_id != instance_id:
        raise ProxyPolicyError("ticket identity mismatch")
    if claims.tool_name != call.tool_name or claims.resource != call.resource:
        raise ProxyPolicyError("ticket target mismatch")

    rule = manifest.find_rule(call.tool_name)
    if rule is None or not rule.allows(call.action, call.resource):
        raise ProxyPolicyError("tool call exceeds declared boundary")

    result = self.business_system.invoke(
        correlation_id=correlation_id,
        source="tool_proxy",
        tool_name=call.tool_name,
        resource=call.resource,
        action=call.action,
        payload=call.payload,
    )
    self.audit_center.log_proxy_call(
        {
            "correlation_id": correlation_id,
            "skill_id": manifest.skill_id,
            "instance_id": instance_id,
            "tool_name": call.tool_name,
            "resource": call.resource,
            "action": call.action,
            "result": result,
        }
    )
    return result

plugin_runtime.py

from __future__ import annotations

from dataclasses import dataclass
from typing import Dict
from uuid import uuid4

from audit_center import AuditCenter, Decision
from manifest import SkillManifest
from ticket import TicketService
from tool_proxy import ToolCall, ToolProxy


class PolicyError(Exception):
    pass


@dataclass
class ExecutionContext:
skill_id: str
instance_id: str
session_id: str


class RuntimeRegistry:
def __init__(self) -> None:
    self.manifests: Dict[str, SkillManifest] = {}
    self.instances: Dict[str, str] = {}

def register_skill(self, manifest: SkillManifest) -> None:
    self.manifests[manifest.skill_id] = manifest

def activate_instance(self, skill_id: str, instance_id: str) -> None:
    self.instances[instance_id] = skill_id

def get_manifest(self, skill_id: str) -> SkillManifest:
    return self.manifests[skill_id]


class SecurityPlugin:
def __init__(self, *, registry: RuntimeRegistry, ticket_service: TicketService, tool_proxy: ToolProxy, audit_center: AuditCenter) -> None:
    self.registry = registry
    self.ticket_service = ticket_service
    self.tool_proxy = tool_proxy
    self.audit_center = audit_center

def before_load_skill(self, manifest: SkillManifest) -> ExecutionContext:
    self.registry.register_skill(manifest)
    instance_id = f"inst-{uuid4().hex[:8]}"
    self.registry.activate_instance(manifest.skill_id, instance_id)
    return ExecutionContext(skill_id=manifest.skill_id, instance_id=instance_id, session_id=f"sess-{uuid4().hex[:8]}")

def before_tool_call(self, ctx: ExecutionContext, call: ToolCall) -> Dict[str, object]:
    manifest = self.registry.get_manifest(ctx.skill_id)
    rule = manifest.find_rule(call.tool_name)
    if rule is None or not rule.allows(call.action, call.resource):
        raise PolicyError("tool call is not allowed by manifest")

    call.metadata["ticket"] = self.ticket_service.issue(
        skill_id=ctx.skill_id,
        instance_id=ctx.instance_id,
        tool_name=call.tool_name,
        resource=call.resource,
    )
    correlation_id = f"corr-{uuid4().hex[:8]}"
    return self.tool_proxy.forward(
        correlation_id=correlation_id,
        manifest=manifest,
        instance_id=ctx.instance_id,
        call=call,
    )

def reconcile(self, manifest: SkillManifest, correlation_id: str) -> Decision:
    return self.audit_center.reconcile(manifest, correlation_id)

business_system.py

from __future__ import annotations

from typing import Dict

from audit_center import AuditCenter


class BusinessSystem:
    def __init__(self, audit_center: AuditCenter) -> None:
        self.audit_center = audit_center

def invoke(
    self,
    *,
    correlation_id: str,
    source: str,
    tool_name: str,
    resource: str,
    action: str,
    payload: Dict[str, object],
) -> Dict[str, object]:
    if source != "tool_proxy":
        raise PermissionError("direct traffic is not allowed")

    self.audit_center.log_business_call(
        {
            "correlation_id": correlation_id,
            "source": source,
            "tool_name": tool_name,
            "resource": resource,
            "action": action,
            "payload": payload,
        }
    )
    return {"status": "ok", "resource": resource, "action": action}

demo.py

from __future__ import annotations

from audit_center import AuditCenter
from business_system import BusinessSystem
from manifest import SkillManifest, ToolRule
from plugin_runtime import RuntimeRegistry, SecurityPlugin
from ticket import TicketService
from tool_proxy import ToolCall, ToolProxy


def main() -> None:
manifest = SkillManifest(
    skill_id="skill-finance-sync",
    skill_name="finance_sync",
    version="1.0.0",
    owner="ops@example.com",
    tools=[
        ToolRule(
            tool_name="invoice_api",
            actions=["read", "write"],
            resources=["invoice:2026-q2"],
            risk_level="high",
        )
    ],
    labels={"department": "finance"},
)
audit_center = AuditCenter()
ticket_service = TicketService(secret="demo-secret", ttl_seconds=120)
business_system = BusinessSystem(audit_center)
tool_proxy = ToolProxy(
    ticket_service=ticket_service,
    audit_center=audit_center,
    business_system=business_system,
)
registry = RuntimeRegistry()
plugin = SecurityPlugin(
    registry=registry,
    ticket_service=ticket_service,
    tool_proxy=tool_proxy,
    audit_center=audit_center,
)

ctx = plugin.before_load_skill(manifest)
call = ToolCall(
    tool_name="invoice_api",
    resource="invoice:2026-q2",
    action="write",
    payload={"invoice_id": "INV-001", "amount": 8000},
)

result = plugin.before_tool_call(ctx, call)
correlation_id = audit_center.proxy_logs[0]["correlation_id"]
decision = audit_center.reconcile(manifest, correlation_id)

print("tool result:", result)
print("proxy log:", audit_center.proxy_logs[0])
print("business log:", audit_center.business_logs[0])
print("decision:", decision.level, decision.reason)


if __name__ == "__main__":
main()

1. 代码流程简要说明

  • 业务 Skill 先提交 manifest,并被治理插件登记到运行时注册表;
  • Skill 发起工具调用时,插件的 before_tool_call hook 先校验目标工具与资源范围;
  • 插件为本次请求签发短周期票据,并将调用转交 ToolProxy;
  • ToolProxy 再次核验票据、实例状态与声明边界后,以受信身份调用业务系统;
  • 业务系统仅接受来自 ToolProxy 的请求,同时输出业务访问日志;
  • 审计中心汇总代理日志与业务日志,结合 manifest 做三方核验并输出分级裁决。

2. 关键代码片段

plugin_runtime.py:before_tool_call hook
class SecurityPlugin:
    def before_tool_call(self, ctx, call):
        manifest = self.registry.get_manifest(ctx.skill_id)
        self.policy.ensure_allowed(manifest, call)
        ticket = self.ticket_service.issue(
            skill_id=ctx.skill_id,
            instance_id=ctx.instance_id,
            tool_name=call.tool_name,
            resource=call.resource,
        )
        call.metadata["ticket"] = ticket
        return self.tool_proxy.forward(ctx, call)
tool_proxy.py:统一代理与二次校验
class ToolProxy:
    def forward(self, ctx, call):
        claims = self.ticket_service.verify(call.metadata["ticket"])
        self.policy.ensure_proxy_allowed(ctx, call, claims)
        result = self.business_client.invoke(
            source="tool_proxy",
            tool_name=call.tool_name,
            resource=call.resource,
            action=call.action,
            payload=call.payload,
        )
        self.audit.log_proxy_call(ctx, call, result)
        return result
business_system.py:仅接受来自 ToolProxy 的访问
class BusinessSystem:
    def invoke(self, source, tool_name, resource, action, payload):
        if source != "tool_proxy":
            raise PermissionError("direct traffic is not allowed")
        self.audit.log_business_call(tool_name, resource, action, payload)
        return {"status": "ok", "resource": resource, "action": action}

八、总结

企业级 AI Agent 的治理重点,正在从“模型输出管理”延伸到“执行过程控制”。当 Agent 已经能够通过 Skill 调用工具、访问资源并推动业务落地时,仅依赖 Prompt 约束或单一日志审计,往往不足以支撑企业级安全要求。 方案框架保持不变、将落地方式调整为 Plugin + Hook + ToolProxy,是一种更贴近工程现实的演进路线:

  • 治理插件把控制能力准确挂到框架主路径上
  • ToolProxy 把高风险访问统一收口
  • 业务系统把入口边界收严
  • 多源核验把执行结果转化为可处置的治理依据 对于希望稳步引入 AI Agent 的企业而言,这是一条更容易落地、也更容易持续演进的执行治理路径。