Agent OS 信号处理机制详解:用 POSIX 风格信号实现 AI Agent 生命周期治理
【免费下载链接】agent-governance-toolkitAI Agent Governance Toolkit — Policy enforcement, zero-trust identity, execution sandboxing, and reliability engineering for autonomous AI agents. Covers 10/10 OWASP Agentic Top 10.项目地址: https://gitcode.com/GitHub_Trending/ag/agent-governance-toolkit
Agent OS(agent-governance-toolkit 中的 Agent 操作系统内核)借鉴 POSIX 信号模型,为 AI Agent 提供了一套可强制干预的生命周期控制机制:暂停、恢复、优雅中断、立即终止、策略违规升级、预算超限挂起等。本文以 signal-handling.md 为核心骨架,逐项解析 12 个信号的语义、全部边界行为与状态转换规则,并结合 signals.py 的源码实现与 test_kernel_critical.py 的测试用例,讲清信号从发送、屏蔽、排队到交付的完整链路,帮助你掌握如何用信号体系在 Agent 失控时实现"0 容忍"的即时干预。
为什么 Agent 需要"信号":内核与用户空间分离
传统 Agent 框架(如 LangChain、CrewAI)本质上是库——它们提供工具而非治理能力。当 Agent 行为失控时,框架只能"建议",无法"强制执行"。Agent OS 反转了这一设计:内核拥有执行权,所有 Agent 动作必须经过内核空间,策略在内核中确定性执行后才进入用户空间(参见 kernel-internals.md 的架构说明)。
信号正是这个模型中"内核控制用户空间"的关键通道。在内核/用户空间分离设计中(实现于 kernel_space.py),信号分发器(Signal Dispatcher)与策略引擎(Policy Engine)、飞行记录器(Flight Recorder)、VFS 挂载管理器、IPC 路由器共同构成受保护的内核空间组件,它们必须在用户空间 Agent 崩溃甚至幻觉时依然存活。用户空间的 LLM 生成、工具执行、Agent 逻辑、自定义处理器则全部位于内核之外。
这套设计有四个核心不变量:
- 无旁路(No Bypass):所有 Agent I/O 必须经过系统调用(syscall);
- 失败即关闭(Fail Closed):未知动作一律拒绝;
- 全面审计(Audit Everything):飞行记录器记录所有决策;
- 崩溃隔离(Crash Isolation):用户空间崩溃不等于内核崩溃。
信号机制让运维者可以在不依赖 Agent 自身配合的前提下,从内核侧直接暂停、恢复或终止一个 Agent,这正是它与传统框架"只能请求、不能强制"的本质区别。
信号全览:12 个 POSIX 风格 Agent 信号
AgentSignal在源码中定义为IntEnum(signals.py),共 12 个信号,其中前 7 个对应经典 POSIX 控制/诊断信号,后 5 个是 Agent 特有的治理信号:
| 信号 | 值 | 描述 | 可屏蔽(Maskable) |
|---|---|---|---|
SIGSTOP | 1 | 暂停执行(进入影子/审查模式) | ✅ |
SIGCONT | 2 | 恢复执行 | ✅ |
SIGINT | 3 | 优雅中断(完成当前动作后停止) | ✅ |
SIGKILL | 4 | 立即终止(不可屏蔽、不可捕获) | ❌ |
SIGTERM | 5 | 请求优雅关闭 | ✅ |
SIGUSR1 | 6 | 进入诊断模式 | ✅ |
SIGUSR2 | 7 | 触发检查点/快照 | ✅ |
SIGPOLICY | 8 | 策略违规(升级为 SIGKILL) | ❌ |
SIGTRUST | 9 | 越过信任边界 | ❌ |
SIGBUDGET | 10 | 资源预算超限 | ✅ |
SIGLOOP | 11 | 检测到死循环 | ✅ |
SIGDRIFT | 12 | 检测到目标漂移 | ✅ |
从源码结构看,SignalMask.UNMASKABLE集合目前明确包含SIGKILL与SIGPOLICY(signals.py),即这两个信号永远无法被屏蔽;而SIGTRUST的默认处理器会直接升级为内核恐慌(kernel panic),因此在效果上同样无法被用户空间"接住"。信号的分派行为由SignalDisposition枚举描述(signals.py):
DEFAULT:使用默认处理器;IGNORE:忽略该信号(对SIGKILL等不可屏蔽信号禁止);CUSTOM:使用自定义处理器;BLOCK:屏蔽/排队该信号。
与经典 POSIX 信号的对应与差异
与 Unix 进程信号相比,Agent OS 的语义有几处关键差异:
SIGSTOP= 影子模式:不是冻结进程,而是让 Agent 进入影子/审查模式(shadow/inspection mode),便于人类或治理系统检查其内部状态;SIGKILL= 内核恐慌:源码注释将其定义为 Agent 的 "KERNEL PANIC",在策略违规或 Agent 不可恢复时使用(signals.py),并抛出AgentKernelPanic异常强制终止——该异常被设计为用户空间代码无法捕获的致命错误;- 新增治理信号:
SIGPOLICY(策略违规)、SIGTRUST(信任违规)、SIGBUDGET(预算超限)、SIGLOOP(死循环)、SIGDRIFT(目标漂移)是经典 POSIX 没有的,它们专门服务于 Agent 治理场景。
SignalDispatcher:发送、排队与交付的完整链路
SignalDispatcher(signals.py)是内核空间中的信号管理组件,构造时接收agent_id,并维护四个核心数据结构:
_handlers:信号 → 处理器的映射,构造时自动安装默认处理器;_dispositions:每个信号的处置方式(默认全部为DEFAULT);_mask:信号屏蔽掩码;_pending:被屏蔽信号的排队队列;_signal_history:全部信号历史(用于审计)。
发送信号的核心方法是signal(sig, source, reason, context),它封装为携带元数据的SignalInfo(含信号名、值、时间戳、来源、原因、上下文,to_dict()输出审计友好的字典),并遵循如下交付流程:
- 先记录、后处理:无论信号最终是否被屏蔽,都会先追加到
_signal_history,保证可审计性; - 检查屏蔽:若信号被
_mask屏蔽,则进入_pending排队队列并返回False; - 检查处置:若处置为
IGNORE且信号可忽略,则直接返回; - 调用处理器:查找对应 handler 并执行;若处理器抛异常,则对
SIGPOLICY/SIGTRUST这类关键信号升级为 SIGKILL(防止治理信号被用户代码"吞掉")。
默认处理器的行为
| 处理器 | 行为 |
|---|---|
_handle_stop | 置_is_stopped = True,Agent 主循环在安全点检查后进入影子模式 |
_handle_continue | 若当前未停止则忽略(no-op);否则置_is_stopped = False |
_handle_interrupt | 置_is_stopped = True,等待当前动作完成后停止(优雅) |
_handle_kill | 置_is_terminated = True、_is_stopped = True,抛出AgentKernelPanic强制终止 |
_handle_term | 置_is_terminated = True,请求优雅关闭 |
_handle_policy_violation | 记录违规原因后升级为 SIGKILL(0% 违规容忍) |
_handle_trust_violation | 记录越界原因后升级为 SIGKILL |
_handle_budget_exceeded | 不杀进程,置_is_stopped = True暂停等待人工干预 |
状态查询通过三个只读属性暴露:is_stopped、is_terminated、is_running(等于"未停止且未终止")。
API 命名说明:signal-handling.md 文档示例使用
dispatcher.send_signal(...)与dispatcher.mask_signals(...)作为教学记法;当前仓库源码中实际提供的对应方法名为signal(...)与block_signals(...)/unblock_signals(...)。下文示例同时给出两者,以源码为准。
边界情况:信号系统的全部细节行为
原文档对信号系统的 8 类边界情况做了明确规定,这也是编写可靠治理逻辑时必须理解的部分。
向已停止的 Agent 发送 SIGSTOP
行为:no-op。信号被确认接收并记入飞行记录器,但状态不变。
dispatcher.signal(AgentSignal.SIGSTOP) # Agent 现已停止 dispatcher.signal(AgentSignal.SIGSTOP) # No-op,已停止向运行中的 Agent 发送 SIGCONT
行为:no-op。源码中_handle_continue首先检查if not self._is_stopped,未停止则直接返回并记录调试日志(signals.py):
# Agent 正在运行 dispatcher.signal(AgentSignal.SIGCONT) # No-op,已在运行SIGSTOP 后紧跟 SIGCONT
行为:先停后恢复。由于信号按 FIFO 顺序处理,SIGSTOP 总是先于 SIGCONT 被处理,因此可能存在一个短暂的暂停窗口:
dispatcher.signal(AgentSignal.SIGSTOP) dispatcher.signal(AgentSignal.SIGCONT) # Agent 立即恢复执行向已终止的 Agent 发送 SIGKILL
行为:仅记录日志,不抛异常。信号仍被追加到历史中用于审计,但不会再次触发内核恐慌:
dispatcher.signal(AgentSignal.SIGKILL) # Agent 已终止 dispatcher.signal(AgentSignal.SIGKILL) # No-op,已终止多次策略违规(SIGPOLICY)
行为:第一次 SIGPOLICY 升级为 SIGKILL 并终止 Agent;后续信号仅记录,因为 Agent 已死。
# 策略违规 1 → SIGPOLICY → SIGKILL → Agent 终止 # 策略违规 2 → SIGPOLICY → No-op(Agent 已终止)关键段中的信号屏蔽
你可以在关键操作期间临时屏蔽信号:被屏蔽的信号进入_pending队列而非交付,解除屏蔽时按 FIFO 顺序补投。文档给出的上下文管理器用法如下:
with dispatcher.mask_signals({AgentSignal.SIGINT, AgentSignal.SIGTERM}): # SIGINT 与 SIGTERM 被排队,不会交付 await critical_operation() # 解除屏蔽后,排队信号被补投源码中对应的底层实现是block_signals(signals)与unblock_signals(signals)(signals.py)。unblock_signals的补投逻辑会遍历_pending,仅对仍处于屏蔽状态的信号继续排队,其余按序交付:
dispatcher.block_signals([AgentSignal.SIGINT, AgentSignal.SIGTERM]) try: await critical_operation() finally: dispatcher.unblock_signals([AgentSignal.SIGINT, AgentSignal.SIGTERM])注意:SIGKILL、SIGPOLICY与SIGTRUST不可屏蔽。源码中SignalMask.block()对不可屏蔽信号返回False并记录警告;同时set_disposition()也禁止对不可屏蔽信号设置IGNORE,确保内核权威信号永远生效(signals.py)。
执行期间的信号竞态
当动作执行中到达信号时,不同信号给出不同的交付时机保证:
- SIGSTOP:当前动作先完成,然后 Agent 停止;
- SIGKILL:动作被立即中断(可能留下部分状态);
- SIGINT:当前动作完成,然后 Agent 优雅停止。
信号顺序保证
信号按 FIFO 顺序处理。发送 SIGSTOP 再发送 SIGCONT,则 SIGSTOP 必然先于 SIGCONT 被处理——这一保证同样作用于被屏蔽信号的排队与补投,避免"先恢复后暂停"这类乱序竞态。
信号处理器抛出异常时
- 可屏蔽信号(SIGSTOP、SIGINT 等):异常被记录,Agent 继续运行;
- 不可屏蔽信号(SIGKILL 等):Agent 无论如何都会被终止。
需要特别强调的是,_deliver中还有一个隐性升级规则:若SIGPOLICY或SIGTRUST的处理器在执行中抛出异常,内核会立即以AgentKernelPanic的方式升级为 SIGKILL(signals.py),防止治理信号被用户代码静默吞掉。
信号状态转换:STOPPED / RUNNING / TERMINATED
信号驱动的状态机在原文档中以 ASCII 图给出,核心转换如下:
SIGSTOP ┌─────────────────────────────────────┐ │ │ ▼ │ ┌─────────┐ SIGCONT ┌─────────┐ │ │ STOPPED │◄─────────────►│ RUNNING │────┘ └─────────┘ └─────────┘ │ │ │ SIGKILL │ │ SIGTERM │ └──────────┬──────────────┘ │ ▼ ┌────────────┐ │ TERMINATED │ └────────────┘要点归纳:
- RUNNING → STOPPED由
SIGSTOP(暂停审查)或SIGBUDGET(预算超限挂起等待干预)触发; - STOPPED → RUNNING仅由
SIGCONT触发,且仅在确实处于停止态时生效; - RUNNING/STOPPED → TERMINATED由
SIGKILL(立即)或SIGTERM(优雅)触发;SIGINT则先完成当前动作再进入停止态,属于"优雅的暂停式停止"。
该状态机与SignalAwareAgent基类(signals.py)配合使用:Agent 继承该类后在主循环的"安全点"调用check_signals(),若已终止则抛出AgentKernelPanic,若已停止则回调可覆写的on_pause()进入影子模式——这就是"内核发信号、用户空间在安全点响应"的协作模型。
自定义信号处理器
对于可屏蔽信号,你可以注册自定义处理器,用于注入诊断逻辑、检查点逻辑等。文档中的教学写法:
def my_handler(info: SignalInfo) -> None: print(f"Custom handling: {info.signal.name}") dispatcher.register_handler(AgentSignal.SIGUSR1, my_handler) dispatcher.send_signal(AgentSignal.SIGUSR1) # Prints: "Custom handling: SIGUSR1"源码中对应 API 为set_handler(sig, handler)(signals.py),它返回被替换的旧处理器,并把该信号的处置置为CUSTOM:
def my_handler(info: SignalInfo) -> None: print(f"Custom handling: {info.signal.name} (source={info.source})") dispatcher.set_handler(AgentSignal.SIGUSR1, my_handler) dispatcher.signal(AgentSignal.SIGUSR1) # Prints: "Custom handling: SIGUSR1 (source=kernel)"限制:SIGKILL的处理器不可覆盖。源码对set_handler(AgentSignal.SIGKILL, ...)直接拒绝并返回None,这保证了"内核恐慌"通道永远属于内核。该约束由测试 test_kernel_critical.py(test_sigkill_cannot_be_caught)显式验证:即使尝试注册自定义 SIGKILL 处理器,发送 SIGKILL 后调用的仍是内核默认处理器,自定义处理器不会被调用。此外,set_disposition()也禁止对不可屏蔽信号设置IGNORE。
Flight Recorder 集成:信号审计与可追溯性
所有信号(无论是否被屏蔽、是否 no-op)都会自动记入飞行记录器(Flight Recorder),通过get_signal_history()获取完整历史:
history = dispatcher.get_signal_history() # [ # {"signal": "SIGSTOP", "signal_value": 1, "timestamp": "...", "source": "user", "reason": None, "context": {...}, ...}, # {"signal": "SIGCONT", "signal_value": 2, "timestamp": "...", "source": "user", ...}, # ]源码层面,SignalInfo.to_dict()输出包含signal(名称)、signal_value(数值)、timestamp(UTC ISO 格式)、source(来源,默认"kernel")、reason(原因)与context(上下文)六个字段(signals.py)。signal()方法在记录日志与屏蔽检查之前就先追加历史(signals.py),确保"即使信号被屏蔽或 no-op,审计线索也不丢失"。这正是内核"审计一切"不变量的具体落地。
与策略引擎的联动:从违规到内核恐慌
信号系统是策略执行的最终强制手段。在 kernel-internals.md 描述的请求生命周期中,Agent 的每个动作先经系统调用进入内核空间,策略引擎逐条检查blocked_actions、blocked_patterns与约束,任一检查失败即触发 SIGKILL(ExecutionResult(signal=SIGKILL))。
为了便于治理组件接入,源码在 signals.py 底部提供了四个便捷函数:
kill_agent(dispatcher, reason):向 Agent 发送 SIGKILL(内核恐慌);pause_agent(dispatcher, reason="Inspection requested"):发送 SIGSTOP 暂停 Agent;resume_agent(dispatcher):发送 SIGCONT 恢复 Agent;policy_violation(dispatcher, policy_name, details, context=None):上报策略违规,源码自动构造reason=f"Violated policy '{policy_name}': {details}"并以source="policy_engine"发送 SIGPOLICY,随后升级为 SIGKILL。
策略引擎、信任引擎(trust engine)等内核组件通过这些通道实现"0% 违规容忍":一旦检测到策略违规或信任越界,信号链路会立即将 Agent 升级至内核恐慌终止,而不会留给用户空间任何延迟或绕过空间。
测试验证:关键信号行为的自动化保障
信号机制的关键行为在 test_kernel_critical.py 中有系统性的回归测试(测试以"信号强制执行"为主题组织,跳过条件为agent_control_plane.signals不可用):
test_sigkill_cannot_be_caught(test_kernel_critical.py):验证set_handler(AgentSignal.SIGKILL, handler)返回None(不可覆盖),且发送 SIGKILL 后自定义处理器不会被调用——SIGKILL 永远走内核处理器;test_sigstop_pauses_agent(test_kernel_critical.py):验证 SIGSTOP 可以使用自定义处理器接管(可屏蔽信号的可扩展性);test_sigint_graceful_interrupt(test_kernel_critical.py):注册 SIGINT 清理处理器后发送信号,断言清理逻辑确实被触发,验证优雅中断语义。
这些测试同时印证了"可屏蔽信号可自定义、SIGKILL 永不可覆盖"的对称设计,是信号契约的行为级文档。
后续深入阅读
- kernel-internals.md:内核如何处理信号的完整架构(syscall → 策略检查 → SIGKILL 的请求生命周期);
- security-spec.md:策略违规升级与安全边界规范;
- troubleshooting.md:常见的信号相关问题排查;
- signals.py:信号系统的完整源码实现(枚举、掩码、分发器、默认处理器、
SignalAwareAgent基类); - kernel_space.py:内核空间组件划分与保护环设计;
- test_kernel_critical.py:信号关键行为与策略执行延迟的回归测试。
【免费下载链接】agent-governance-toolkitAI Agent Governance Toolkit — Policy enforcement, zero-trust identity, execution sandboxing, and reliability engineering for autonomous AI agents. Covers 10/10 OWASP Agentic Top 10.项目地址: https://gitcode.com/GitHub_Trending/ag/agent-governance-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考