阅读需 8 分钟

智能体进程授权:撤销分叉访问

智能体进程授权需要一个能跨越 fork 的边界。了解如何检查子进程、撤销访问权限、停止进程组并保留证据。

智能体进程授权:撤销分叉访问

自主智能体很少像任务日志显示的那样干净地停止。智能体可能完成一次编程请求,打印一条令人安心的完成消息,却留下格式化工具、测试运行器、隧道、Shell 或辅助进程继续运行。如果这个残留进程仍能调用生产 API 或建立 SSH 会话,那么任务实际上并没有在界面所显示的地方结束。

解决办法不是执着于杀死进程。清理进程确实必要,但它不是授权系统。请把授权的生命周期与 Unix 进程的生命周期分开定义,让边界可被观察,并在开始追踪后代进程前先撤销访问权限。我见过一些团队把父 PID 当作隔离证明。只要智能体能够调用 Shell,这个 PID 几乎就不能证明什么。

进程树不是授权边界

父子关系只能告诉你是谁创建了某个进程,不能告诉你父进程结束后它是否应该继续拥有权限。这是两个不同的问题。把它们混在一起,就会出现常见的故障:有人取消了智能体任务,看到父进程消失,五分钟后却发现某个子进程仍在发起请求。

Unix 为进程提供了多种脱离预期结构的方式。子进程可以继续 fork,可以用 setsid 创建新会话,也可以请求服务管理器接管。它还可能留在 Shell 管道中,而信号只发送给其中一个成员。甚至可能只是因为父进程根本没有发送信号,所以一直存活。

进程携带的信息也不止命令行。它可能继承环境变量、当前工作目录、打开的文件、管道、套接字和文件描述符。如果父进程把 bearer token 放在环境变量中,继承该环境的任何子进程都会拥有这个令牌。之后杀死子进程并不能让秘密回到保险库,也无法撤销已经发出的请求。

因此,授权边界应该回答一个明确而有限的问题:哪个正在运行的进程实例可以请求特权操作,授权到什么时候,以及我们如何立即拒绝它?答案不应是「从启动任务的终端派生出来的所有进程」。那只是进程布局带来的结果,不是安全决策。

对于需要 HTTP 或 SSH 访问的智能体,请把凭据放在智能体进程之外。让持有凭据的组件在确认调用方获得批准后执行操作。这样一来,清理就不再是拼命抹除泄露权限的行为,而会成为日常运维的一部分。

在启动任务前定义生命周期

一次智能体运行需要明确的开始条件、结束条件和撤销条件。在决定 UI 中的一次点击、一个进程组或一个 Shell 包装器是否算作控制手段之前,先把这些条件写下来。

对于交互式工作,合理的默认做法通常是,一次授权只对应一个根智能体进程。用户批准这次具体运行时,授权开始;根进程退出或你撤销授权时,授权结束。新的智能体调用需要重新做决定,即使它从同一目录运行同一个二进制文件。

对于无人值守的工作,不要因为重复提示令人厌烦,就悄悄把一次批准延长到全天。应为任务指定负责人、有限的有效时长、受限的目标范围,以及一个可以在任务运行期间使用的取消途径。如果任务需要在父任务结束后继续运行,就把它当作独立任务,并让它单独请求授权。

下面三个问题能很快暴露模糊的设计:

  • 用户批准的是哪个可执行文件和哪个进程实例?
  • 如果进程从未发出正常完成信号,什么事件会结束它的权限?
  • 该事件发生后,后代进程还能获得新的特权操作吗?

如果最后一个问题的答案是肯定的,因为后代继承了令牌,那么你就在没有记录委托的情况下委托了权限。如果答案是肯定的,因为网关仍把所有后代都视为可信,那么你就把进程祖先关系变成了策略语言。在安全事件中,这两种选择都很难解释。

不要把便利误当成边界。终端标签页、项目目录、智能体账户和代码签名身份都能提供有用的上下文,但没有任何一项能单独标识一次具体运行。代码签名身份只能告诉你谁签署了可执行文件,不能告诉你它启动的是预期的辅助工具、旧版本辅助工具,还是取消后脱离的子进程。

终止前先检查实时进程树

在 macOS 上,应从内核提供的进程事实开始,而不是根据任务标签猜测。ps 手册将 pidppidpgidsid 记录为不同字段。它们分别展示进程祖先关系、进程组成员关系和会话成员关系。当智能体被允许调用 Shell 和工具时,这些信息都需要查看。

调查一次运行时,执行下面的命令并保存输出:

ps -axo pid,ppid,pgid,sid,stat,etime,command

你可能会看到这样的结构:

  PID  PPID  PGID   SID STAT ELAPSED COMMAND
48102 47790 48102 48102 S    00:18:04 agent-cli run build
48131 48102 48102 48102 S    00:17:59 /bin/sh -c make test
48144 48131 48102 48102 S    00:17:56 test-runner --watch
48209     1 48209 48209 S    00:16:02 helper --upload-results

前面三个进程共享同一个进程组和会话。最后一个进程的 PPID 是 1,并且使用了不同的进程组和会话。它可能已经脱离,也可能已经由某个启动器接管。无论是哪种情况,只向 PID 48102 发送信号都无法停止它。

如果要查找已知根 PID 的直接子进程,可以使用:

pgrep -P 48102 -alf

这个命令只查找一代子进程。如果要快速手动遍历,可以对每个子进程重复执行。对于安全事件记录,应在撤销前后分别保存 ps 输出,并记录准确的根 PID、启动时间、命令、进程组和会话。单独记录进程名称并不是可靠证据,因为名称会重复,命令行也会变化。

如果风险涉及外部调用,请检查打开的网络连接。在 macOS 上,lsof 可以显示进程的网络文件:

lsof -nP -p 48209 -i

监听套接字、已建立的出站连接或长期存在的 SSH 传输都会提高紧迫性,但它们不能证明存在恶意行为。它们只能证明,一个你原以为已经消失的进程仍保留着值得调查的通信渠道。

不要在生产环境中构建依赖解析人类可读 ps 输出的安全控制。ps 适合调查和测试。真正的启动器应在启动时记录标识符,并在授权网关处持有直接的撤销句柄。

进程组有帮助,但脱离的子进程会绕过它

专用进程组能为启动器提供一种实用方式,用来取消普通的任务树。请在智能体启动前创建进程组,让根进程成为组长,并向进程组发送信号,而不是只向根进程发送信号。这样可以覆盖 Shell、编译器、测试运行器和管道仍留在同一进程组中的常见情况。

在支持常见信号语法的系统上,负的组 ID 可以定位进程组:

kill -TERM -48102
sleep 3
kill -KILL -48102 2>/dev/null || true

TERM 信号会给普通工具机会,让它们关闭文件并报告取消。稍后发送的 KILL 信号则处理拒绝退出或无法退出的进程。在确认 48102 确实是目标进程组之前,不要把这段命令复制到自动化流程中。错误的组 ID 可能终止你自己的 Shell 或无关任务。

这种方法有局限。子进程可以调用 setsid,创建新的会话,通常也会创建新的进程组。任务还可以把工作提交给本地服务、远程构建系统或队列。Shell 也可能在进程组之外启动后台进程。一旦发生这些情况,终止进程组就只是清理,不再是隔离控制。

在 Linux 上,服务管理器可以把任务放进专用 cgroup,并将整个 cgroup 作为一个单位终止。这通常比进程组清理更强,因为内核会持续跟踪成员关系,而不只依赖普通的父子关系。不要暗示 macOS 菜单栏应用也具备这种控制能力。macOS 和 Linux 使用不同的进程监管模型,便携式智能体设计不应假装它们相同。

「直接杀掉整个进程树」之所以一直流行,是因为它在演示中有效。但在真正让智能体访问权限变得危险的条件下,它会失败,包括长时间运行的任务、后台辅助进程、包装器和部分取消。使用进程组可以减少残留,但不要把它当作唯一的撤销机制。

完全不要让进程树接触秘密

检查取消任务后的调用
Activity 日志从 Sallyport 加密且采用哈希链的审计日志中生成每次调用的记录。

最安全的子进程,仍然是无法读取凭据的子进程。通过环境变量传递令牌,会让继承该环境的所有后代都能访问令牌,也可能让诊断信息、崩溃报告或不谨慎的日志记录暴露令牌。传递临时文件只好一点点,如果子进程能在你删除文件前复制它,风险依然存在。

避免把下面这些模式用于智能体启动的命令:

export DEPLOY_TOKEN='token-value'
agent-cli run deploy
agent-cli run deploy --token "$(cat ~/.config/deploy-token)"

这两种方式都会把原始权限放进智能体的执行环境。第二种方式还可能把它放进进程参数、Shell 历史记录或日志中。出现问题后轮换令牌可能是必要的,但轮换是恢复措施,不应成为正常的取消路径。

改用本地操作网关。智能体应请求某项操作,例如向获准的端点发起 HTTP 请求,或执行 SSH 命令,但不应接触凭据材料。网关注入相关凭据、执行操作并返回结果。这样,即使某个残留子进程仍在运行,你也能阻止它发起下一次调用。

Sallyport 对 HTTP API 和 SSH 采用了这种方式:加密保险库留在应用中,智能体通过 sp mcp shim 连接,并接收操作结果,而不是接收秘密。这个设计比任何巧妙的进程终止脚本都更重要,因为子进程不可能继承它从未拥有的令牌。

不要夸大这项优势。拥有已批准网关会话的进程,仍然可以在会话结束或被撤销前请求操作。让秘密远离进程树可以限制凭据被窃取的风险,但不会让一个已获批准的智能体变得无害。

审批应绑定一次运行,而不是一个名称

只根据可执行文件名称授权是不可靠的。任何人都可以把二进制文件复制到其他路径,用 Shell 脚本包装它,或稍后再次运行另一个实例。只根据签名者授权有利于追溯来源,但范围仍然过宽,尤其是在它悄悄批准同一方签署的所有未来运行时。

一张实用的审批卡,应使用人能够核对的信息标识请求进程,包括代码签名机构、可执行文件路径、根 PID 和启动时间。决定应只适用于这一次进程运行。子进程不应仅仅因为历史上某处存在一个已获批准的祖先,就获得无限期行动权。

对于后代进程,有两种合理模式。更严格的模式要求每个不同的请求方都获得审批。更实用的模式允许一次已批准的根进程运行期间产生的调用,并在该运行结束后拒绝所有新调用。对于确实需要启动短期工具的智能体,第二种模式通常很好用,前提是网关能判断根运行已经结束,而且授权无法被重新绑定到一个名称相同的后续进程。

Sallyport 默认使用按会话授权:新智能体进程首次调用时,会显示一张审批卡,首先展示该进程的代码签名机构;授权会持续到这次运行退出。保险库锁定时,保险库网关会拒绝所有操作;按调用设置还可以要求每次使用某项凭据时都进行审批。这些控制有意保持简单。堆叠一大批策略规则,只会掩盖究竟是谁批准了什么。

按调用审批适合每次操作都值得认真审核的凭据,例如生产部署账户或具有破坏性的管理 API。但它不适合所有只读请求。如果每个无害操作都要求用户确认,人们最终会对所有提示一律点击批准。应把阻力放在后果真正严重的地方,在其他场景缩短并明确会话边界。

先撤销授权,再追踪进程

无需访问保险库即可验证记录
无需保险库密钥,即可通过 sp audit verify 离线检查密文中的哈希链。

当父任务意外结束时,首先撤销它发起新的特权调用的能力。然后终止根进程组,检查幸存进程,再清理逃脱的部分。如果顺序反过来,可能会留下一个漏洞:在你查看进程表时,尚未找到的子进程仍然可以继续向外发起调用。

合理的响应顺序如下:

  1. 在操作网关处撤销会话或锁定保险库。
  2. 保存根 PID、进程信息、最近的操作记录和取消时间。
  3. 向已知进程组发送 TERM,并检查剩余进程。
  4. 只对仍属于该任务且拒绝退出的进程升级发送 KILL
  5. 检查脱离的本地进程或远程任务是否需要单独取消。

即使根 PID 已经消失,第一步也必须有效。如果智能体故意试图让辅助进程存活,这一步也必须有效。要求调用方仍然存在才能撤销授权的网关,把方向弄反了。

即时撤销应该拒绝未来的请求,而不是改写历史。保留能显示既有授权和调用的记录。如果网关只记录成功的操作,那么调查期间的重要证据就会被隐藏。撤销后的拒绝调用能告诉你,某个对象仍在继续尝试行动。

保险库锁定是针对所有活动会话的紧急制动器。当你无法快速识别受影响的运行时,使用它很合适。能够识别具体会话时,应采用更精确的会话撤销。把这两种操作区分开,操作人员就不必在袖手旁观和中断所有工程师工作之间二选一。

审计轨迹必须与进程证据关联

没有进程上下文的活动记录只能回答一半问题。它可能说明发生了一次 HTTP 请求,却无法说明哪个获准运行发起了请求。只有会话记录而没有具体调用,也存在相反的问题。你需要同时保留两种视图,并且要有办法确认事件发生后记录没有被人悄悄修改。

至少记录授权事件、请求进程身份、会话开始和结束时间、每次特权操作、撤销事件,以及撤销后的任何拒绝。加入时间戳和稳定的关联标识符。不要记录原始秘密。也不要想当然地认为命令行可以安全保存,因为命令行经常包含本不应该在那里出现的值。

Sallyport 从一份加密且采用哈希链的审计日志中生成 Sessions 和 Activity 两个日志视图。它的 sp audit verify 命令可以在离线状态下对密文检查哈希链,无需保险库密钥。如果你需要把导出的记录交给某人,让对方确认完整性,却又不能让对方读取存储的凭据,这种验证方式很有用。

哈希链不能让不完整的日志变得完整。如果启动器从未记录根 PID,审计记录之后也无法重建它。如果远程任务接收 API 请求后在另一台机器上继续运行,本地进程证据不会显示远程进程。应审计发起远程工作的请求,并要求远程系统提供自己的取消机制和事件记录。

一个常见故障需要两项独立修复

阻止继承的 API 令牌
Sallyport 将 API 密钥保存在加密保险库中,在不向智能体暴露密钥的情况下执行 HTTP 调用。

设想一个编程智能体被要求运行集成测试并发布报告。它启动一个 Shell,Shell 启动测试运行器,测试运行器又启动一个上传结果的辅助进程。用户看到测试失败后取消智能体。父智能体退出,Shell 消失,但辅助进程已经脱离,仍保留出站连接,并在取消后发送报告。

如果上传凭据放在环境变量中,辅助进程可能在你撤销本地批准后仍然拥有它。此时你需要轮换凭据、检查日志,甚至可能启动安全事件响应。进程清理来得太晚,因为权限已经进入了子进程。

如果辅助进程通过网关请求上传,撤销父会话就能阻止新的上传请求。如果上传已经开始,网关审计也会记录这一事实。你仍然要终止辅助进程,但不再依赖终止进程作为唯一控制手段。

更棘手的是,有些辅助进程本来就需要比智能体存活更久,例如本地预览服务器或排队中的发布任务。不要把它称为子进程,然后让它永久继承权限。为它指定明确的负责人、独立的授权记录、明确的过期时间和可见的停止控制。一旦工作超出发起它的任务生命周期,它就已经成为独立的运维对象。

用顽固的后代进程测试取消功能

从未测试过的控制,往往会在最不方便的时候失效。构建一个无害的测试智能体,让它启动一个会休眠的子进程,打开一个无害的本地连接,并在父进程退出后尝试通过网关执行操作。然后在多个时点取消父进程:子进程启动前、子进程运行期间、子进程脱离后,以及操作正在执行期间。

预期结果必须具体。普通子进程应随进程组一起退出。脱离的子进程可能仍然可见,这正好说明为什么还要继续检查。撤销后,每个新的特权请求都应得到拒绝。会话记录应显示批准和撤销,活动记录则应显示边界前后的调用。

不要把测试通过理解成 UI 状态发生了变化。要验证操作系统中的进程状态、网关的决策和审计记录。这是三种不同的观察结果。如果取消按钮只是隐藏任务卡片,那只是表演。

最实际的第一步很简单:每当你授权智能体时,记录根进程身份,并让授权在这次运行结束时独立过期。然后,在你把能改变生产环境的凭据交给取消功能之前,先用一个故意顽固的子进程进行测试。

常见问题

什么是分叉的 AI 智能体进程?

分叉的智能体进程,是由智能体或其工具创建的子进程。即使父任务报告完成,它仍可能继续运行,并保留打开的网络连接、继承的文件描述符、环境变量,或访问凭据代理的能力。

智能体的子进程会在父进程退出时停止吗?

不会。父进程退出会改变进程的父子关系,但无法可靠地终止所有后代。子进程可能忽略正常关闭信号,脱离到另一个会话,或者被操作系统的服务管理器接管。

如何找到智能体启动的子进程?

使用 ps 检查 PID、PPID、进程组、会话、运行时长和命令,再使用 pgrep -P 查找直接子进程。在 macOS 上,ps -axo pid,ppid,pgid,sid,stat,etime,command 能提供比图形化活动监视器更有用的证据。

可以安全地终止整个进程组吗?

如果任务启动器在智能体启动前创建了专用进程组,进程组就很有用。你可以向负的进程组 ID 发送信号,但前提是子进程仍在该组中,并且没有主动创建新会话。

应该先撤销智能体访问权限,还是先终止它的进程?

两者解决的是不同问题。终止进程可以停止后续工作,但可能留下短暂的时间窗口,也无法说明发生了什么。撤销授权会在网关处阻止新的特权操作。当进程可能仍在运行时,先撤销授权更安全。

代码签名足以授权自主智能体吗?

代码签名只能标识谁签署了可执行文件,不能说明它启动的每个进程会做什么。可以把签名信息作为审批界面的归属依据,然后将审批绑定到一次具体、已观察到的进程运行,并明确规定过期时间。

子进程可以从父进程继承 API 凭据吗?

Shell 变量、导出的令牌、配置文件、临时文件、打开的套接字以及继承的文件描述符,都可能让权限传播得比预期更远。不要把原始密钥交给智能体进程,因为进程清理无法可靠地取回它已经读取的数据。

智能体授权应该持续多久?

授权应在获得授权的进程运行结束时结束,即使某个后代仍然存活。需要继续访问的子进程应通过自己的明确边界重新请求权限,而不是悄悄继承父进程的批准。

审计日志能阻止失控的智能体行动吗?

防篡改审计日志可以证明记录未经检测就无法被修改,但它不能阻止不应发生的调用。你仍然需要保险库网关、审批边界,以及切断正在运行任务的方式。

长期运行的智能体任务应该首先增加什么控制?

先让每个智能体任务在专用进程组中启动,记录根 PID 和启动时间,并用一个故意难以终止的子进程测试取消流程。然后让撤销独立于进程清理,因为当你最需要进程清理时,它最终总会失败。

Sallyport

Sallyport 替你的 AI 智能体执行 API 调用和 SSH 命令。密钥留在你 Mac 上的本地密钥库里;每次运行由你批准,每个操作都落入一份密封的审计日志。

© 2026 Sallyport · 依据 Apache-2.0 开源 · Oleg Sotnikov