阅读需 8 分钟

到底是哪一个真实代理进程在请求审批?

了解如何在终端、IDE、shell、脚本和任务运行器之间识别真实的代理进程,让审查者能够批准一个可识别的操作方。

到底是哪一个真实代理进程在请求审批?

写着“Terminal”或“zsh”的审批提示通常太过模糊,无法支持真正的决策。这些程序可能属于启动路径,但往往只是共享的基础设施。审查者需要知道是哪一次代理运行请求执行操作、是什么启动了它,以及这条链中是否存在能提供持久身份的因素。

难点在于,进程树同时回答了几个不同的问题。它可以告诉你谁创建了谁,可以暴露出没人预料到的任务运行器,也可以显示图形化 IDE 启动了终端。但它本身无法证明,一个已签名的父进程批准了子进程将要执行的每一行代码。好的审批设计会把进程树当作证据,然后明确说明它能够支持的有限结论。

审批针对的是操作方,而不是终端

审查者批准的操作方,是发起请求的那次进程运行,并且要结合它的启动链来理解。终端窗口只是进程运行的地方,并不会自动成为负责方。

当一个团队在同一个 shell 中运行多个代理时,这个区别就很重要。一名开发者在 iTerm2 中打开终端,在一个标签页运行交互式编程代理,在另一个标签页启动仓库任务,同时在第三个标签页留下后台监视器。三个后代进程的上方都可能出现同一个终端应用。把每个请求都叫作“iTerm2”,几乎无法给审查者提供有用信息。

反方向的错误也很常见。审查者看到 nodepythonzsh,就以为离请求最近的二进制文件代表完整身份。这个二进制文件可能是代理包的解释器、包管理器钩子、IDE 扩展临时生成的脚本,或团队工具添加的包装器。名称在技术上没错,但在实际操作中毫无帮助。

即使审批卡只显示两个标签,内部也应使用以下三个标签:

  • 请求方:实际请求使用受保护通道的进程。
  • 执行链:说明请求方如何到达这里的相关父进程。
  • 可识别权限:审查者能够合理识别、并且可以评估其代码签名的最近祖先进程。

这些标签解决了许多审批系统混淆的问题。请求方回答“哪个进程发出了这个调用?”可识别权限回答“这次运行是通过谁或哪个组织来的?”二者有关联,但不能互换。

例如,一个请求可能看起来是这样:

Code Helper (signed application)
  └─ zsh -l
      └─ npm run agent
          └─ node ./tools/start-agent.mjs
              └─ agent-cli --workspace /Users/maya/src/payments

如果 agent-cli 发起 HTTP 请求,它就是请求方。shell 和 npm 说明了启动路径。已签名的 Code Helper 进程可能是最合适的可识别权限,前提是它确实位于当前的实时祖先链中,而不是仅仅在桌面上打开。写着“agent-cli,由 Code Helper 启动”的审批卡是诚实的。只写“Code Helper 请求访问”则丢掉了区分这次运行与另一个扩展或任务所需的信息。

进程树是证据,不是意图声明

父进程可以证明它创建了子进程,或与子进程建立了继承关系。但它不能证明父进程理解子进程后续的参数、提示词、仓库指令或远程响应。

当人们不严谨地使用签名术语时,这个限制尤其重要。Apple 将 designated requirement 描述为 macOS 用来判断不同版本代码是否为同一份代码的机制。它通常包含标识符和签名权限。Apple 也明确了边界:未签名代码没有持久的 designated requirement,而临时本地签名无法在不同版本之间提供稳定身份。

这些信息对审批很有用,但并不等于对操作的认可。已签名的 IDE 可以启动未签名的仓库脚本。已签名的终端可以运行下载的二进制文件。已签名的任务运行器可以将恶意环境变量传给一个看似普通的解释器。签名让审查者能够识别代码发布者,但不会自动清理它下面的链条。

应区分以下几种声明:

声明支持它的证据它不能证明什么
“这个请求来自 PID 81234。”本地进程记录谁编写了代码,或它为什么发起调用
“这个子进程由这个父进程启动。”父 PID 和实时祖先链父进程批准了子进程当前的行为
“这个可执行文件由该权限签名。”签名检查和 designated requirement可执行文件是无害的,或正在按预期运行
“这次运行从这个 IDE 或终端开始。”未中断的实时祖先链可见窗口启动了所有后代进程

实际后果很简单。不要把这四种声明压缩成一个友好的应用图标和一句话。审查者应该能看到自己接受的究竟是哪一种声明。

链条存在歧义时,要明确说出来。“由终端会话中的未签名脚本启动”比把请求归因于终端应用更好的审批标签,因为后者暗示终端编写了请求。虚假的精确性会让人逐渐忽略审批卡。

设计审批卡前先检查实时链条

找出正确操作方最快的方法,是在一次真实运行仍然存活时进行检查。让代理执行一个无害操作,因为工作从启动阶段进入任务运行器或子 helper 后,进程链可能会发生变化。

在 macOS 上,如果网关记录了请求方 PID,就从它开始。下面的命令会输出进程、父 PID、启动时间、可执行文件名称和参数:

ps -p "$PID" -o pid=,ppid=,lstart=,user=,comm=,args=

典型结果大致如下:

81234 80991 Tue Jul 21 14:08:31 2026 maya /usr/local/bin/agent-cli agent-cli --workspace /Users/maya/src/payments

然后向上检查。ps 不会自动递归,因此可以用一个简单的 shell 函数让检查过程保持一致:

ancestry() {
  local pid="$1"
  while [ "$pid" -gt 1 ] 2>/dev/null; do
    ps -p "$pid" -o pid=,ppid=,user=,comm=,args=
    pid=$(ps -p "$pid" -o ppid= | tr -d ' ')
  done
}

ancestry "$PID"

输出有意保持朴素。你要寻找的是进程类型的变化:代理可执行文件变成解释器,解释器变成包管理器,包管理器变成 shell,shell 变成终端或 IDE helper。不要因为认出了一个名称就停止。继续检查,直到找到能为审查者提供有意义来源的进程,或直到本地链条结束。

当存在多个后代进程时,下面的视图有助于获得整体快照:

ps -ax -o pid=,ppid=,user=,lstart=,comm=,args= | sort -n

它适合调查,不适合直接放进审批提示。完整进程列表包含太多无关细节,结构却不够清晰。实现时可以在需要时收集它,然后压缩为请求方、直接父进程、可识别权限,以及能够解释意外启动的中间跳转。

这里有两个时间陷阱。第一,进程退出后 PID 可能被重新使用。应连同 PID 一起记录启动时间,并在请求发生时解析链条,不要依赖早先的样本。第二,任务运行器可能创建 worker 后先退出,而受保护调用稍后才发生。如果父进程已经消失,就保留进程创建时观察到的链条,或报告缺失的链接。不要用猜测出来的父进程替代它。

VS Code 会改变启动路径,但不会成为请求方

VS Code 的集成终端在启动受支持的 shell 时,可以注入参数或环境变量来启用 shell 集成。其文档也说明,自动注入并不涵盖所有环境,包括某些子 shell、SSH 和复杂 shell 场景。这提醒我们,终端界面元数据和 Unix 父子关系是两种不同的证据来源。

在简单情况下,链条如下:

Visual Studio Code
  └─ terminal helper
      └─ login shell
          └─ agent-cli

终端 helper 的存在,是因为 IDE 需要管理伪终端及其子 shell。登录 shell 可能会加载配置文件,修改 PATH、激活语言运行时、安装 shell 钩子,或启动项目专用脚本。这些细节解释了为什么同一条命令在独立终端和 IDE 终端中的行为不同。

审批决策不应假装这些细节不存在。将代理可执行文件显示为请求方。只有在 VS Code 位于当前祖先链中时,才把它显示为启动权限。当 shell 和任务命令能够实质性地区分这次运行时,将它们放入可展开的路径中。

常见的失败情况如下:

Visual Studio Code
  └─ zsh
      └─ make test-agent
          └─ sh -c ./scripts/bootstrap-agent
              └─ python3 tools/agent.py

简单实现会看到 zsh,然后将请求标记为“VS Code 终端”。另一种实现看到 python3,就将它标记为“Python”。两者都无法帮助审查者判断这是已知的仓库任务,还是从同一个 shell 启动的任意进程。

更有用的说法接近这样:

Requester: python3 tools/agent.py
Run path: make test-agent > scripts/bootstrap-agent
Launched from: Visual Studio Code integrated terminal

这个标签有一个代价:它暴露了请求来自仓库控制的代码。它就应该这样做。如果审查者信任已签名的编辑器,却不信任当前检出的代码,审批卡必须为这种判断留下空间。

不要仅根据终端转义序列、环境变量或窗口标题推断 VS Code 的所有权。它们可能被继承、复制或过期。实时父子关系是更强的证据。IDE 提供的进程上下文可以在确认祖先关系后丰富显示,但不能取代它。

独立终端提供上下文,不提供全面权限

让保险库闸门真正生效
保险库始终由 Touch ID 保护,解锁前会拒绝所有操作。

iTerm2、Terminal 及类似应用为人提供了一个有意选择的入口。如果开发者打开干净的终端,输入代理命令,并立即收到审批提示,标明终端应用是有用的上下文,因为它说明运行从哪里开始。

但这并不能为恰好共享该会话的所有子进程提供全面审批。shell 的设计就是创建子进程,而长期存在的标签页往往会超过最初打开它的原因。终端标签页中 9:00 启动的命令,可能在午餐时间仍留下 worker,此期间还发生了 cd、分支切换、环境变化和三条无关命令。

应让区别清晰可见:

Requester: agent-cli --workspace /Users/maya/src/payments
Parent: zsh -l
Interactive origin: signed terminal application
Session started: Tue Jul 21 14:08:31 2026

交互来源只是辅助上下文。会话属于实际的进程运行。如果进程退出,就撤销会话审批,即使终端标签页仍然打开。同一标签页中启动的新代理进程是一次新的运行,拥有新的参数,PATH 中也可能是不同的可执行文件。

这正是可识别审查身份变得棘手的地方。终端应用可能由知名发布者签名,而 agent-cli 可能是安装在项目目录中的未签名可执行文件。诚实的审批卡应同时说明这两个事实。不要把终端的签名转移给未签名的子进程,好像终端为它提供了背书。

shell 函数和别名也有同样的问题。看起来像 agent 的命令可能会展开为一个函数,先切换目录、加载环境文件、启动包脚本,最后再运行另一个二进制文件。进程收集器只能看到最终产生的子进程。如果希望把用户输入的命令作为可选上下文,可以从 shell 集成或 shell 钩子中获取,并标记为用户输入的命令历史,而不是进程身份。

脚本和任务运行器会制造最容易误解的链条

任务运行器很受欢迎,因为它们能把复杂的本地设置变成容易记住的命令。但这种便利也常常让它们的进程链看起来比实际更可信。

考虑下面这个普通的启动过程:

zsh -l
  └─ just agent
      └─ /bin/sh -cu 'npm run agent -- --mode write'
          └─ npm run agent -- --mode write
              └─ sh -c node ./scripts/launch-agent.js --mode write
                  └─ node ./scripts/launch-agent.js --mode write
                      └─ agent-cli --mode write

每个中间进程都有合理用途。just 选择配方,/bin/sh 解释配方内容,npm 选择包脚本并启动 shell,Node 执行启动脚本。然而,这些名称都无法告诉审查者,受保护请求究竟来自已知的代理运行,还是来自恰好使用同一运行时的另一条命令。

不要从审计记录中删除中间层。调查修改过的包脚本、意外的 prepost 生命周期钩子、改变的 PATH,或用另一个可执行文件替代原程序的仓库脚本时,正是这些层最有价值。只有在将它们保留在不可变事件记录中后,才应在审批显示中压缩它们。

合理的显示规则是:

  1. 首先放置请求方可执行文件和参数。
  2. 当直接启动器会改变理解时显示它,例如 npm run deploy-agentmake migration-bot
  3. 将第一个可识别的已签名祖先进程标为启动权限。
  4. 在事件详情中保留完整的已观察祖先链。

这条规则可以避免两个坏习惯。第一个是隐藏所有包装器,让修改过的任务变得不可见。第二个是在审查者回答请求之前,先展示七行进程树。面对难以阅读的审批卡,人们审批的是卡片的形式,而不是具体操作。

我不赞成一个常见建议:由于任务名称更容易被人识别,就审批任务运行器而不是代理。任务名称通常只是仓库文本。任何可以修改检出内容的人,都可能修改任务的实际行为。可以将任务名称作为上下文,但应将审批会话绑定到观察到的代理进程运行,并在新的运行开始时重新评估。

仔细选择签名边界

让代理远离凭据
代理通过内置的 sp mcp shim 工作,而 Sallyport 自己执行 HTTP 和 SSH 操作。

最近的已签名祖先进程通常是最好的可识别权限,但“最近”需要判断。一个已签名的系统 shell 或解释器可能直接位于未签名脚本上方。把 shell 称为权限,会得到一个技术上已签名、却无法说明代码由谁提供的标签。

Apple 的代码签名指南区分可执行文件标识符和签名身份,并使用 designated requirement 建立代码身份。codesign 工具可以显示该要求。使用输出识别已签名的应用或 helper,但不要把单独的标识符字符串当作发布者关系的证明。

对于捆绑应用,应检查祖先链中出现的可执行文件或应用包:

codesign --display --verbose=4 --requirements - "/Applications/Example.app" 2>&1 \
  | sed -n '/Identifier=/p;/TeamIdentifier=/p;/Authority=/p;/designated =>/p'

典型结果包含如下形式的字段:

Identifier=com.example.editor
TeamIdentifier=AB12CDE345
Authority=Developer ID Application: Example Software (AB12CDE345)
designated => anchor apple generic and identifier "com.example.editor" and certificate leaf[subject.OU] = "AB12CDE345"

具体字段会因签名和分发路径而变化。重要的是,当 macOS 能提供稳定的代码身份时,审批系统应捕获它,然后以方便审查者理解的形式呈现,同时不隐藏原始证据。

选择显示权限时可以遵循以下规则:

  • 优先选择实际位于请求方祖先链中的已签名图形应用或专用已签名 helper。
  • 不要用通用的已签名解释器替代它执行的未签名脚本。
  • 不要根据可执行文件目录、仓库名称或 shell 提示符推断权限。
  • 清楚标记未签名和临时签名阶段,不要把它们埋在可识别父进程下面。
  • 如果不存在可识别的已签名权限,就明确说明这次运行没有经过验证的本地发布者身份。

从概念上说,一个进程可能有多个签名:应用包签名、嵌套 helper 的签名,以及解释器的签名。应检查实际出现在进程列表中的可执行文件。已签名应用可以启动另一个单独签名的 helper,helper 也可以启动用户安装的二进制文件。每一层都是独立的身份问题。

某些启动链应该降低可信度

干净的父子链并不总是存在,而缺失的情况也不是无关紧要的边缘问题。审批系统最容易在这些地方犯下严重的归因错误。

远程执行会打断本地祖先关系

如果本地代理调用 SSH,你的 Mac 可以识别本地 SSH 客户端及其父进程。但远程命令在另一台主机上拥有独立的进程树。除非你在远程机器上收集并验证证据,否则不要说远程的 agent-cli 是由本地 IDE 启动的。

本地审批卡仍然有用。它可以标明本地请求方、目标地址、已知的远程账户,以及传给 SSH 的完整命令。对于决定本地代理是否可以发起连接,这些信息已经足够。但它不足以建立远程进程身份。

脱离启动器的工作需要新的身份决定

子进程可能调用 setsid、进行两次 fork,或要求 supervisor 在原始启动器退出后继续工作。一旦发生这种情况,只绑定到终端或初始 shell 的审批就成了虚构。脱离启动器的 worker 需要自己的进程记录、自己的启动时间,以及新的审批范围,除非你设计了审查者可以检查的明确 supervisor 关系。

环境继承可能改变操作方,却不改变进程树

同一个 node 路径可能因为 PATHNODE_OPTIONS、语言特定的模块路径、代理设置或工作目录发生变化,而加载不同的代理包。进程祖先关系无法显示所有继承值。应为调查捕获受限的上下文指纹:可执行文件路径、参数、当前目录、经过脱敏的指定环境名称和值,以及适合你的模型的可执行文件摘要。

不要把完整环境导入审批卡或日志。环境中经常包含令牌、端点和个人路径。目标是区分不同运行,而不是在日志中再制造一个秘密存储。

审查者需要简短声明和可检查证据

将审批绑定到进程生命周期
每个新的代理进程都有独立的会话决策,进程退出后决策随之结束。

审批提示只有几秒钟来赢得注意。先把与决策有关的声明放在顶部,然后为审查者提供一条路径,让他们检查系统为何做出这个声明。

一张实用的卡片可能是这样:

Agent process requests an HTTP call

Requester
agent-cli --workspace /Users/maya/src/payments
PID 81234, started 14:08:31

Launched through
npm run agent > node scripts/launch-agent.js
from a signed editor process

Destination
api.example.internal / POST /deployments

它足够简短,便于阅读,也足够具体,能够提出质疑。审查者可以因为工作区不对、任务运行器出乎意料、目标地址异常,或请求来自未签名阶段而拒绝。它们都是阻止操作的真实理由。

将完整证据放在卡片后面。保留请求方 PID 和启动时间、可执行文件路径、参数、父进程链、所选权限的签名信息,以及审批决定。如果同一代理稍后在获批运行中发起另一个请求,记录应明确说明会话决定适用于该进程的生命周期,而不是适用于脱离进程的某个应用名称。

Sallyport 的按会话授权正是围绕这个实际边界构建的:新的代理进程首次调用时请求审批,审批只持续到该进程退出。它的审批卡首先显示进程的代码签名权限,为审查者提供一个可识别的起点,同时不假装签名能够解释每一条子命令。

按进程生命周期撤销,然后明确处理例外

会话审批应跟随代理进程的生命周期,因为真正发出请求的单位是进程。终端仍然打开、IDE 窗口仍然可见,或任务名称相同,都不应成为复用审批的理由,否则审批范围就无法诚实地检查。

这条规则确实会给短生命周期包装器带来摩擦。包管理器可能为每个子命令启动一个新的代理进程。不要因此悄悄把审批扩大到 npm 的所有未来子进程,或终端中的所有进程。应决定工作流是否需要稳定的已签名代理宿主、暴露单一清晰身份的长期进程,或对敏感凭据逐次确认。

当风险在于每次使用,而不是启动身份时,逐次审批才是正确选择。生产部署令牌、能够连接敏感主机的 SSH 私钥,或具有写入能力的管理 API,即使进程身份熟悉,也可能值得每次都由人决定。识别进程可以减少混乱,但不能替代判断。

实现测试很简单:结束已审批的进程,再次启动同一命令,确认第二次运行需要自己的审批。然后在同一个终端标签页中启动不同命令,确认它不能继承第一次运行的决定。如果任一测试失败,系统审批的就是一个位置或标签,而不是一个操作方。

审查者不应该被迫把终端名称当作进程身份的替代品。保留链条,识别发起请求的运行,显示你能够证明的最强可识别权限,并让缺失的证据保持可见。这比笼统的“可信 IDE”规则不那么华丽,但当任务脚本、shell 包装器或远程跳转最终成为关键因素时,它才能真正经得起检查。

常见问题

如何识别 AI 代理请求背后的真实进程?

先找到发起受保护请求的进程,然后沿父进程向上检查,直到找到人们可以识别的边界,通常是已签名的应用、终端应用、IDE 或受管理的运行器。不要因为最近的 shell 最容易显示,就用它来标记请求。shell 通常只能说明请求是如何启动的,并不一定是决定发起请求的程序。

终端应用是我应该审批的身份吗?

终端应用可以成为身份信息的一部分,尤其是在开发者有意通过终端启动一次性运行时。但当多个无关工具共享同一终端会话时,终端本身就不够了。应将终端作为启动上下文,同时显示代理可执行文件和相关的父进程链。

代理由未签名脚本启动时应该怎么办?

未签名脚本无法为审查者提供持久的发布者身份,文件名也很容易被复制。可以将脚本显示为执行细节,但如果存在已签名的祖先进程,应将审批绑定到它,并同时显示解释器和工作上下文。如果链中没有可识别的已签名权限,就应将其视为可信度较低的上下文,不要凭空制造确定性。

VS Code 集成终端能证明是 VS Code 启动了代理吗?

VS Code 可能会在启动受支持的 shell 时注入参数或环境变量,以实现 shell 集成,因此终端装饰信息无法准确说明每个子进程归谁所有。应检查实际的父进程 ID,不要根据集成终端界面推断身份。那里启动的子进程可能属于扩展主机、任务,或用户手动运行的 shell 命令。

npm、Make 或任务运行器会隐藏真实的代理进程吗?

不能。任务运行器可能调用包脚本,包脚本再调用 shell,shell 调用解释器,最后才启动代理。正确做法是保留这些跳转,同时用简洁的链条标出其上方第一个可识别的权限。隐藏中间层会让事故调查困难得多。

代码签名能证明代理操作安全吗?

代码签名权限可以告诉你程序由谁签名,以及 macOS 是否能将它识别为跨版本的同一份代码。但它不能说明程序当前的提示词、代码仓库、参数或远程指令是否安全。应将签名视为稳定的身份线索,而不是安全结论。

代理审批卡上应该显示哪些信息?

审查者应该看到发起请求的可执行文件、进程 ID、直接父进程、可识别的已签名祖先进程、启动路径,以及足以区分不同运行的命令细节。如果可以,还应显示工作目录和启动时间。不要让人在审批时从完整的进程表中自行寻找答案。

审查者应该如何处理通过 SSH 启动的代理?

远程 SSH 跳转会打断本地的进程关系。你的机器可以识别本地客户端进程及其启动者,但仅凭本地进程树无法真实识别远程程序。应记录目标地址和本地操作方。如果远程身份很重要,还需要远程主机上的独立证据。

已审批的代理创建另一个进程后会发生什么?

进程可能在原始启动者退出后创建子进程、重新托管进程、转为守护进程,或将工作交给其他服务。会话级审批应在已审批的进程运行结束时终止,之后独立运行的进程必须重新获得决定。长期运行的 helper 需要自己的可识别身份,并清楚说明它为何持续存在。

每次审批都应该显示完整的进程树吗?

在诊断启动链、调查意外请求或设计审批界面时,可以使用进程树。如果系统已经将链条压缩为清晰的操作方标签和支持细节,普通审批不必每次都显示完整进程树。进程树是供审查的证据,不应该要求每位审查者都去解读它。

Sallyport

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

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