# 未签名的本地构建应如何获得批准？

未签名的本地构建不一定可疑，但它也不代表一个可识别的身份。这两点必须同时成立，否则审批流程很容易滑向两个坏习惯：开发者为了工作而批准每个匿名进程，或者放弃审批，让代理直接持有凭据。

真正有用的问题更具体：在不假定熟悉目录中的每个进程都值得同等信任的前提下，哪些证据能让人批准某个本地编译代理的这一次运行？答案是一套围绕会话边界、启动来源、凭据范围设计的流程，并且对无法说明自身来历的请求坚决拒绝。

## 未签名意味着缺少声明，不代表风险评级

未签名代码没有通过证书链验证的发布者身份声明。这本身无法说明代码是否恶意、是否经过审查、是否被本地修改，或是否刚刚从你打开的代码库构建完成。它只是移除了一类证据。

团队常常在两个方向上犯错。一类团队把所有未签名可执行文件都当作恶意程序，结果把正常开发推向旁路和个人 API 密钥。另一类团队把未签名简单理解为“我的本地代码”，于是建立一个过于宽泛的批准类别，让无关进程也能利用这个例外。

请把下面几种概念分开：

- **发布者身份**回答谁为分发的构建签名。
- **构建来源**回答哪个源代码、哪个修订版本、哪台机器以及哪条命令生成了这个可执行文件。
- **运行时来源**回答此刻发起访问请求的进程是由什么启动的。
- **操作权限**回答该进程可以使用什么凭据或远程访问能力。

签名可以帮助回答第一个问题。干净的本地流程还必须回答另外三个问题。如果开发者从工作区编译代理客户端，他们的信心通常来自代码库状态和刚刚执行的命令，而不是公共证书。审批界面应展示这些证据，而不是把没有证书当成结论。

Apple 的代码签名文档也以更正式的方式划出了这条边界。指定要求会根据检查策略识别已签名代码的某个实例，但它不能证明该代码适合访问每项资源或执行未来的每个操作。Apple 还指出，macOS 的每个子系统都有自己的信任策略。因此，操作网关不应把代码签名状态变成通用的权限决定。

## 本地构建通道需要其他进程无法借用的证据

为本地编译的代理客户端设置独立的审批通道。不要把它们归入“未签名”“开发”或“终端”这样的类别。这些标签覆盖的进程太多，实际帮助不大。

开发者批准会话前，通道应要求四项证据：

1. 可执行文件路径必须位于开发者控制的代码检出目录或构建输出目录中。
2. 父进程必须是预期的启动器，通常是终端、IDE 任务或团队维护的包装脚本。
3. 代码库状态和构建命令应易于检查，不需要凭记忆还原当天发生的事情。
4. 请求访问的目标和凭据必须符合当前任务。

这些信号单独看都不完美，但合在一起，就很难让随机下载程序、后台助手或遭入侵的依赖伪装成普通的本地运行。

这时，团队往往会想使用策略引擎：允许目录下的路径、放行名称以 `agent` 开头的命令，或豁免某个 IDE 启动的所有进程。不要这样做。静态规则看似能减少打扰，却会把本来容易检查的决定变成长久存在的例外。任何能安排出正确路径或父进程链的进程，都可能继承这个例外。

应在会话边界使用人工批准。批准卡片可以在存在签名时显示进程的代码签名权威，但未签名构建还需要其他上下文：可执行文件路径、父命令、可用时的工作目录，以及它准备访问的目标。开发者随后可以回答一个真正的问题：“这是我刚为当前任务构建的客户端吗？”

Sallyport 的逐会话授权适合这种场景，因为它只会为新的代理进程请求一次批准，并且在进程退出后结束。决定单位是具体的进程，而不是模糊的未签名软件类别。

## 熟悉的路径是证据，不是身份

`~/src/agent-client/dist/agent` 这样的路径让人安心，因为它讲述了一个看似合理的故事。但它不是身份边界。如果某个恶意进程能够写入该路径、替换输出文件、修改符号链接，或诱使启动器解析到另一个可执行文件，它同样可以从这个路径运行。

首先，让合法的启动故事变得简单、重复且容易检查。每位开发者都应拥有固定的代码检出位置。构建输出应保留在该检出目录中，或放在只有开发者账户可写的可预测目录中。避免共享构建目录、Downloads、临时目录、同步文件夹，以及包脚本经常重写可执行文件的项目根目录。

实际的启动约定可以简单到这样：

```sh
#!/bin/zsh
set -eu

repo="$HOME/src/agent-client"
cd "$repo"

git status --short
git rev-parse --short HEAD
exec ./build/agent-client --mcp
```

有用的不是 shell 语法，而是它留下的证据。父 shell 有已知的脚本路径，工作目录指向代码检出目录，Git 修订版本会在客户端启动前打印出来，`exec` 会用预期程序替换 shell，而不是留下令人困惑的进程链。

不要让这个脚本下载发布版本、从环境变量选择分支、运行包管理器钩子，或读取代码检出目录之外的可变配置文件。否则启动约定会失去价值，因为审批者无法判断脚本实际选择了什么。

如果代理客户端需要生成代码，请把生成过程作为明确构建命令的一部分。如果它需要本地配置文件，应清楚地传入文件路径；只有在文件包含机器特定设置时，才把它放在代码库之外。不要把凭据偷偷放进去。网关存在的目的就是让客户端不需要凭据。

## 批准会话前检查进程

好的审批决定只需几秒，但不应依赖回忆。当新的本地进程请求调用 API 或打开 SSH 时，在点击批准前检查它的可执行文件和父进程链。

在 macOS 上，下面的命令可以提供第一轮检查。把示例 PID 换成进程查看器或终端显示的 PID：

```sh
pid=48271

ps -o pid=,ppid=,user=,lstart=,command= -p "$pid"
ps -o pid=,ppid=,command= -p "$(ps -o ppid= -p "$pid" | tr -d ' ')"
lsof -a -p "$pid" -d cwd -Fn
```

输出大致应如下：

```text
48271 47990 alex Tue Jul 22 10:14:03 2026 /Users/alex/src/agent-client/build/agent-client --mcp
47990 23112 /bin/zsh /Users/alex/src/agent-client/scripts/run-local-agent
n/Users/alex/src/agent-client
```

你要检查的是一条链，而不是收集零散信息。二进制文件应位于预期的构建位置，父进程应是已知启动器，当前目录应与代码检出目录吻合。如果进程来自 `/private/var/folders`、`~/Downloads`、陌生的包缓存，或来自你没预料到的包装器，即使命令名称看起来正确，也应判定为未通过审查。

接着检查可执行文件本身：

```sh
client="$HOME/src/agent-client/build/agent-client"
file "$client"
codesign --display --verbose=4 "$client" 2>&1 | sed -n '1,18p'
shasum -a 256 "$client"
```

未签名二进制可能会让 `codesign` 报告它完全没有签名。临时签名的二进制会报告存在签名，但没有公共签名权威。这仍然是有用的信息，但不要把它理解为团队身份。Apple 将临时签名描述为“Sign to Run Locally”，并说明其指定要求与特定代码版本绑定。重新构建会改变证据，这正是会话批准不应跨越进程替换继续有效的原因。

哈希是比较工具，不是信任信号。当两位开发者需要确认自己运行的是同一修订版本生成的同一输出时，它很有帮助。但哈希旁边有 64 个十六进制字符，并不会让二进制文件自动安全。

只要链条中有任何环节出乎意料，开发者就应拒绝请求。不要先批准 API 调用，再事后调查。审批正是应该让不确定性付出几分钟代价，而不是付出一次事故复盘代价的时刻。

## 用会话作为不同构建之间的边界

会话边界解决了路径白名单无法解决的问题：本地代码会持续变化。开发者可能在一小时内重建客户端十次。每次输出都可能拥有不同的依赖图、不同的命令处理逻辑，或包含一个会把请求发送到意外位置的临时调试分支。

让启动过程可以随时结束。为一个任务启动代理客户端，检查后批准该进程，并在进程退出时让批准失效。当开发者重新构建、切换分支、修改包装器或重启客户端时，应得到新的进程和新的审批决定。

只有在会话定义不清晰时，这才显得不方便。如果代理客户端每次工具调用都会启动和停止，就修正客户端生命周期，或使用一个在进程树中保持透明的本地监督程序。不要为了减少变化而让批准永久有效。短生命周期进程本来就应该是短生命周期的。

开发者从本地编译的代码代理连接测试环境时，下面的流程通常很好用：

1. 从代码检出目录构建，并打印修订版本和未提交的更改。
2. 通过已提交到代码库的包装脚本启动。
3. 第一次授权请求出现时，确认显示的进程路径、父命令和目标服务。
4. 如果这些细节与任务一致，就批准会话。
5. 任务结束时退出客户端；下一个不同的任务开始前重新构建或重新启动。

这种流程为开发者提供了清晰的恢复路径。如果构建有疑点，就停止它。不需要处理隐藏的授权，也不会有一份悄悄为半个团队积累例外的规则文件。

不要把会话批准和代码库批准混为一谈。代码库可以是干净的，但启动命令可能错误。启动命令可以正确，但当前分支可能仍处于实验阶段。会话决定表示：在当前这次运行中，这个带有这些上下文的进程可以执行普通调用。

## 把逐次调用批准留给无法撤销的后果

对于可重复的开发流量，会话批准是合适的默认方式，例如读取问题元数据、查询沙盒 API、通过 SSH 获取代码库，或更新一次性的测试记录。要求人们对每次调用都做手势，只会训练他们不阅读内容就点击批准。

有些凭据仍应每次使用都要求批准。判断标准应是远程操作的后果，而不是围绕密钥本身制造的敏感性表象。

以下凭据应使用逐次调用批准：

- 写入生产环境；
- 发布包、版本或部署构件；
- 访问客户数据导出或其他高度集中的敏感数据集；
- 修改组织成员身份、身份验证设置或恢复控制项；
- 访问生产 SSH 主机。

不要把每个开发 API 令牌都设置成这样。那会制造审批疲劳，让谨慎的审查者变成只会按按钮的人。网关应让危险时刻足够醒目，使人们真正注意到它们。

请求详情和第二次提示同样重要。对于 HTTP，应显示方法和目标，并显示足以识别操作的路径，同时避免把敏感请求内容全部倾倒在审批界面中。`GET /v1/test-runs/123` 和 `DELETE /v1/projects/123` 绝不能看起来没有区别。对于 SSH，应显示主机和账户，并让开发者确认该主机为什么属于当前任务。

逐次调用要求也能有效约束被本地修改的客户端。你可能愿意为一个读取暂存服务的新实验构建批准会话，但同一个构建若要发布生产版本，就应再次停下来确认。这种犹豫正是它的作用。

## 把重新构建、切换分支和修改包装器视为新证据

开发者经常在早上做出一次批准决定，然后一整天不断改变事实。他们拉取分支、运行代码生成器、更新依赖、修改提示文件、添加 shell 别名或替换包装脚本。二进制文件可能仍然使用同一个名称和路径，但行为已经发生实质变化。

采用一条简单规则：如果可执行文件、启动器或目标环境发生变化，就结束会话并重新启动。无需为每次编辑举行正式仪式，但必须避免把昨天的上下文带入新的构建。

在自己的流程中，以下三个事件应自动触发暂停：

- 修改了分支、提交、依赖锁定文件或生成的输出。
- 修改了启动客户端的脚本，或修改了影响其行为的环境变量。
- 代理现在想访问不同的 API 主机、不同的 SSH 主机，或后果更严重的凭据。

切换分支常被低估，因为二进制名称没有变化。但功能分支可能包含实验性集成、修改过的工具清单或调试端点。正确的做法不是禁止分支，而是让新运行的第一次请求清晰可见并且可以审查。

环境变量和可执行参数一样值得警惕。使用 `API_BASE_URL`、`SSH_AUTH_SOCK`、`PATH`、`DYLD_*` 或自定义配置位置启动的客户端，行为可能与检查过的源代码不同。包装器只应设置客户端需要的变量，打印会影响路由的非机密值，并在缺少值时拒绝运行，而不是加载内容庞杂的 shell 配置文件。

例如，下面的写法容易审查：

```sh
export AGENT_ENVIRONMENT=staging
export AGENT_API_ORIGIN=https://staging.example.internal
exec ./build/agent-client --mcp
```

下面的写法则不容易审查：

```sh
source "$HOME/.agent-env"
eval "$(tooling configure-agent)"
exec "$AGENT_BIN" "$@"
```

第二种形式可能更方便，但它隐藏了可执行文件、配置来源、参数和副作用。批准进程的人只能信任一堆间接调用。本地开发本来就有足够多的变量，不需要再增加这种不透明性。

## 即使客户端属于你，也要让凭据留在客户端之外

本地编译的客户端最容易被信任的情况，是它从未拿到想使用的凭据。如果客户端从环境变量读取 API 令牌，或从文件读取 SSH 密钥，它就可能打印密钥、转发密钥、缓存密钥、把密钥写进崩溃报告，或交给子进程。开发者对自己构建的信心无法改变这种暴露风险。

把密钥放入操作网关，让客户端通过引用请求操作。客户端应提供执行任务所需的 HTTP 请求或 SSH 命令上下文。网关注入凭据、执行调用并返回结果。这样，审批决定关注的是具体操作，而不是客户端能否长期持有 bearer token。

对于 HTTP，应为不同环境和用途设置不同凭据。不要因为客户端提供了另一个主机，就让暂存令牌可以访问生产环境。对于 SSH，不同角色应使用不同的主机条目或凭据记录。不要依赖一把全能密钥，再靠人的承诺来选择正确目标。

Sallyport 将 API 和 SSH 凭据保存在加密保管库中，并自行执行操作，因此代理收到的是结果，而不是明文密钥。对于本地构建，这种设计尤其重要，因为源代码会不断变化，而泄露的环境变量可能只差一次调试打印就会暴露。

这种分离也能改善事故响应。如果客户端行为异常，你可以停止或撤销它的会话，而不必轮换它可能已加载到内存中的所有密钥。保管库锁定时，网关边界仍然有效，Sallyport 会拒绝操作，而不是试图猜测开发者的意图。

## 审计记录应该解决争议，而不只是收集事件

本地构建审批流程运行起来后，人们偶尔会问代理为什么访问某项服务，或在交接后进程是否仍在运行。答案应来自事件记录，而不是猜测、终端历史或事后写下的聊天消息。

对同一活动保留两个视角。一种显示代理运行并支持立即撤销，另一种显示单独的 HTTP 和 SSH 操作。把证据关联起来，让审查者可以从可疑请求追溯到授权它的会话，再追溯到导致该决定的进程上下文。

防篡改日志在这里尤其有用，因为修改本地构建的人可能正是审查它的人。Sallyport 从一份加密且采用哈希链的审计日志生成 Sessions 和 Activity 日志；`sp audit verify` 可以在离线状态下对密文检查哈希链，而无需保管库密钥。即使审查者不应接触操作背后的密钥，团队仍然可以检查记录完整性。

把验证作为审查或事故演练的一部分：

```sh
sp audit verify
```

预期结果应是清晰的成功提示，或指出链条问题的报告。在理解原因前，应把验证失败视为运营问题。不要删除日志、重新安装应用，或在保存受影响记录前接受新的基线。

日志不会取代批准时的审查，但它可以帮助你检验流程是否真的产生了可用证据。如果日志只说“一个未签名客户端”发起了调用，就改进启动约定和审批上下文。如果日志记录了进程、会话、目标、时间和操作，团队就能开展调查，而不必把每台开发者电脑都变成取证项目。

## 团队约定能防止审批提示变成背景噪声

本地构建审批流程最难的部分不是命令行，而是数月正常工作之后，仍然保持批准所代表的人类判断。

写下一份简短的本地代理约定，并把它放在代码库附近，不要埋在安全手册里。内容应说明支持的代码检出位置、启动器脚本的形式、哪些环境属于普通开发、哪些凭据需要逐次调用批准，以及提示显示异常进程时开发者应怎么做。

让拒绝路径变得正常。开发者因为父进程看起来异常而点击拒绝，不应觉得自己阻碍了进度。他们应停止进程、检查它，通过已知包装器重新启动，然后批准干净的运行。这比把神秘进程正常化、事后再解释要快得多。

在真实意外发生后复查约定：包脚本重写了输出，IDE 启动了预料之外的助手，分支指向错误环境，或代理请求了任务之外的凭据。这些失败很有价值，因为它们揭示了缺少哪些证据。不要通过添加永久允许规则来应对。应收紧启动约定，改进审批界面显示的信息，或缩小凭据范围。

编译自己代理的开发者不应在安全表演和持续打扰之间二选一。为他们提供针对可见进程的会话批准，把有重大后果的调用放在单独的审批之后，并在本地构建故事发生变化时要求重新审查。这样既足够严格，能发现错误进程，也足够实用，让人们愿意持续使用。
