# tmux 代理会话归属：授权与审计

终端复用器解决了一个真实的运维问题：即使 SSH 连接中断、笔记本休眠，或者终端窗口意外关闭，工作仍能继续。可这种持久性也会模糊几个关键事实：AI 代理由谁负责，它之前获得的授权是否仍然适用，以及究竟是哪个进程发送了经过身份验证的请求。

把 tmux 窗口当成安全会话，是一个常见错误。它不是。tmux 是管理终端会话的进程管理器。它的服务器可以在终端客户端退出后继续运行，它的窗格也可以在创建者离开后继续存在。GNU screen 也有相同的基本特性。如果你的授权模型、事件记录或审计流程使用“tmux 会话”这个说法，仿佛它能代表单一操作者和单次代理运行，那么调查从一开始就比必要的复杂。

当代理可以通过人工控制的网关访问 API 或 SSH 主机时，这个问题尤其重要。某个人可能在窗格中启动代理，随后分离会话，把会话名称交给同事，再从另一台机器重新连接，之后又在同一个窗格中启动第二个代理。对操作人员来说，这些事件似乎是连续的；在进程表中，它们却是彼此独立的事实。

## tmux 会话不是一次代理运行

tmux 会话是由长期运行的 tmux 服务器持有的一组命名窗口和窗格。一次代理运行则是一个进程及其创建的子进程，边界由进程启动和退出决定。这是两种不同的对象。把它们混为一谈，会产生范围过大的授权，也会让事后解释变得不可能。

运行 `tmux new-session -s build` 时，tmux 会启动服务器或连接到已有服务器。服务器为初始窗格创建伪终端并启动 shell。shell 启动代理后，代理就成为 shell 的后代进程。分离操作只是移除了客户端与 tmux 的连接，通常不会终止服务器、shell 或代理。

一个看似无害的时间线可能是这样：

1. Maya 运行 `tmux new -s release`，并在窗格 0 中启动代理。
2. Maya 去开会并分离会话，代理继续工作。
3. 她的同事稍后连接到 `release`，查看窗格输出并启动后续命令。
4. 当晚，Maya 又把窗格 0 用于另一个代理进程。

会话名称始终是 `release`，但与安全相关的实体已经变了。至少出现了两个代理进程、两次人工交互，也可能有两次独立的授权事件。终端历史无法把这些事实重新变成一个连贯的身份。

GNU screen 的行为也类似。分离后的 screen 服务器负责终端窗口和子进程。具体命令不同，但安全结论不变：连接状态说明的是谁能访问终端复用器，而不是谁拥有其中的每一个进程。

tmux 手册介绍了客户端和服务器模型，并说明服务器负责管理会话、窗口和窗格。这不只是实现细节。它告诉你连续性存在于服务器中，而不是存在于某个人当时看到的终端应用里。授权应围绕发起操作的进程建立，而不是围绕恰好容纳它的复用器容器建立。

## 进程祖先能回答窗格名称无法回答的问题

进程树可以显示代理如何启动，以及哪个 shell 和 tmux 服务器是它的父级。窗格标题只能显示一个可能被修改、复制或长期未更新的标签。

在 macOS 或 Linux 上，如果进程还在运行，可以先用下面的命令查看有限范围的进程信息：

```sh
ps -axo pid,ppid,lstart,tty,stat,command | grep -E '[t]mux|[s]creen|[a]gent'
```

输出的结构比具体命令名称更重要：

```text
 8421     1 Tue Mar 12 09:14:03 2025 ??       Ss   tmux new-session -s release
 8430  8421 Tue Mar 12 09:14:04 2025 ttys002  S    -zsh
 9917  8430 Tue Mar 12 10:02:51 2025 ttys002  S+   agent-tool run deploy check
```

PID 9917 是候选代理进程，PID 8430 是启动它的 shell，PID 8421 是 tmux 服务器。`tty` 值可以帮助你在进程仍存在时把它与窗格关联起来，但不要把它当作持久身份。进程退出后，伪终端分配可能消失；重新创建终端后，分配结果也可能不同。

在 macOS 上，系统默认没有安装 `pstree`。下面的命令无需安装软件，也能提供可用的父子关系视图：

```sh
ps -axo pid,ppid,user,lstart,command | sort -n
```

针对某个 PID，反复查看其父进程，直到追溯到 tmux 服务器或启动服务：

```sh
ps -p 9917 -o pid=,ppid=,user=,lstart=,tty=,command=
ps -p 8430 -o pid=,ppid=,user=,lstart=,tty=,command=
```

在终止任何进程前记录这些值。若操作人员先运行 `tmux kill-session`，可能会抹掉最有用的实时证据，包括命令行、父子关系和启动时间。终止会话可能仍是正确的遏制措施，但条件允许时，应先收集一份简短快照。

进程祖先也有局限。进程可以创建守护进程、派生子进程，或故意重新设置父进程。代理还可能请求外部辅助程序执行工作，形成在辅助程序处中断的子进程树。因此，祖先关系只能作为启动环境的证据，不能证明某个人的意图。你仍然需要能够识别请求进程以及授权来源的操作记录。

## 授权必须跟随代理进程，而不是连接的终端

按会话授权应当适用于一次代理进程运行，并在进程退出时结束。连接、分离、窗格焦点变化和终端模拟器重启，都不应授予、续期或悄悄转移授权。

Sallyport 的按会话授权遵循这一原则：新代理进程第一次调用时请求授权，通过代码签名者识别进程，并且只在这次进程运行期间保留授权。授权不会变成 tmux 会话名称或 shell 提示符的属性。

这个边界可以解决多个容易混淆的情况。如果某个人分离后又重新连接，而同一个代理进程一直在运行，那么进程没有发生变化。在保险库门控以及某些每次调用都要求授权的凭据规则下，原有的会话授权仍然可以有效。如果代理退出，shell 在窗格中启动新的代理，而窗格看起来没有变化，那么新进程必须获得新的授权决定。

不要把连接 tmux 当成授权事件。这个想法很常见，因为人工执行 `tmux attach` 看起来像一个有用的信号。但对于凭据使用来说，它是错误的信号。连接可能发生在代理启动之后，也可能由使用同一台本地账户的其他人发起，甚至可能只是为了查看输出。更重要的是，即使没有任何 tmux 客户端连接，进程也可以继续执行操作。

另一种相反的错误是把连接状态下在窗格中输入的每条命令都视为有人监督。某个人可以连接后离开，让代理继续工作。能看到终端不等于有人在场。

对于敏感凭据，每次使用都要求授权有不同目的。它并不负责确定进程由谁负责，而是在某项具体凭据操作被请求的时刻要求人工决定。请把这些概念分开：

- 保险库门控决定在秘密仍受保护时，任何操作是否可以继续。
- 按进程授权决定这次代理运行是否可以发起调用。
- 按凭据授权决定某次具体凭据使用是否需要新的人工决定。

这些控制回答的是不同问题。团队如果只对长期运行的终端会话做一次宽泛授权，就是用便利性换取一个无法有力辩护的边界。

## 复用的窗格会把授权混淆带到下一次运行

问题通常从一个持久的“工作”会话开始。它逐渐变成了存放未完成命令的共享抽屉。有人启动代理，分离会话，几小时后回来停止代理，然后在同一个 shell 中启动另一个代理。窗格仍保留之前的输出、环境变量和提示符，于是人们推断出操作系统实际上并不存在的连续性。

设想一次事件：某个 API 在 16:43 收到破坏性请求。活动记录识别出一个代理进程。团队打开 tmux，发现名为 `prod-fix` 的窗格，输出从 09:00 开始。于是他们假定创建 `prod-fix` 的工程师批准了这个请求。但这个结论可能以多种方式出错：

- 原代理可能在 10:15 已退出，另一个进程在 16:40 启动。
- 第二名工程师可能连接会话并执行了新的启动命令。
- 窗格标题可能沿用了更早任务的名称。
- shell 启动文件可能导出了凭据或目标环境，而它们已经与当前任务不一致。
- 进程可能在 tmux 外部启动，再通过其他机制把输出写入窗格。

正确的问题应当更具体：16:43 发起操作的是哪个可执行进程，谁批准了这个进程，授权界面显示的代码签名者是谁？然后再追查这个进程如何进入机器的进程树。

不要因此禁止使用 tmux。一个新建且名称明确的会话，可以在连接不稳定时提供可见的工作区，并保留输出记录，有助于处理事件。风险来自把通用会话反复用作无关代理运行的容器。

名称应包含用途和短期运行标识符，例如使用 `deploy-4812`，而不是 `work` 或 `main`。名称只是帮助人理解的辅助信息，不应作为授权输入。运行结束后应关闭会话。

## 分离后的工作需要负责人和过期决定

分离的 tmux 或 screen 会话只有在有人明确接受它会在没有终端连接的情况下继续运行时，才可以安全执行工作。“我以为它已经停止了”不是责任模型。

在分离一个能够执行经过身份验证操作的代理前，应在工单、事件记录或交接记录中写下四项信息：代理进程 PID、tmux 会话名称、任务说明，以及负责停止它的人。还要写明必须重新评估是否继续运行的时间。这些工作很普通，却比复杂的窗格命名方案更能避免问题。

下面这种启动方式会为一次运行创建新的 socket 命名空间，而不是向长期存在的个人服务器中再添加一个窗口：

```sh
tmux -L agent-4812 new-session -d -s agent-4812 \
  'exec agent-tool run "verify deployment 4812"'

tmux -L agent-4812 display-message -p \
  '#{session_name} #{session_created} #{pane_pid} #{pane_tty}'
```

第一条命令会启动一个分离会话。`exec` 很重要，因为它会用代理命令替换 shell 进程，使窗格 PID 更直接地指向调查起点。不使用 `exec` 时，窗格最初属于 shell，之后 shell 再创建代理作为子进程。这仍然可以管理，但调查时要多检查一层。

第二条命令会输出类似下面的元数据：

```text
agent-4812 1731000000 9917 /dev/ttys002
```

将输出保存到任务记录中。进程之后可能派生子进程或退出，因此这不是完整的审计记录。但当响应人员需要把进程数据与操作日志进行比较时，它能提供一个早期锚点。

任务结束后，明确终止指定的 socket：

```sh
tmux -L agent-4812 kill-session -t agent-4812
```

之后确认预期的代理 PID 已经退出。不要假设 `kill-session` 清理了所有后代进程。创建自身会话或后台辅助程序的应用，可能在原始伪终端结束后仍然存活。再次检查进程列表；如果记录显示该代理运行仍有授权，还要通过操作网关撤销它。

## screen 也有同样的身份陷阱，而且线索更少

GNU screen 会创建可以稍后恢复的分离会话，它常见的数字会话标识符看起来比实际更加精确。这个标识符代表的是 screen 服务器实例，不是某个具体代理进程，也不是某次人工授权。

GNU screen 手册记录了分离会话以及 `screen -ls`、`screen -r` 等命令。这些命令能回答 screen 服务器是否可供连接，但不能说明窗口中的进程是否仍是之前运行的那个进程，也不能说明当前操作人员是否是早先作出授权决定的人。

当 screen 出现在事件中时，可以先运行：

```sh
screen -ls
ps -axo pid,ppid,lstart,tty,stat,command | grep -E '[s]creen|[a]gent'
```

像 `12345.build (Detached)` 这样的结果表示有一个值得检查的 screen 会话。连接前，应先根据进程启动时间和终端进行匹配。连接操作可能改变用户看到的内容、触发 shell 钩子，或让交互式程序继续执行。在敏感事件中，应先保存证据，再由指定响应人员决定是否需要交互。

screen 的多用户模式需要额外谨慎。它可以让其他本地用户访问共享终端会话。在受控机器上这可能合适，但它会削弱“连接的人就是进程负责人”这种随意判断。授权仍应识别发起请求的代理进程，并要求控制凭据网关的本地人员作出决定。

## 审计记录需要在 tmux 外部建立关联点

一次可以经得起审查的调查，需要把三类记录关联起来：操作系统进程记录、授权记录和单项操作记录。tmux 或 screen 的输出可以补充证据，但不能替代其中任何一类。

对于每一项正在调查的凭据操作，应建立以下顺序：

1. 在活动记录中确认操作的时间、目标、方法和结果。
2. 确认发起请求的代理运行，以及与该运行关联的授权决定。
3. 将该运行的进程信息和启动时间与实时或已捕获的进程树进行比较。
4. 只使用 tmux 或 screen 的会话元数据来还原操作人员的工作区和交接过程。
5. 确认审计记录自捕获以来没有发生变化。

操作日志与终端回滚内容之间有明显区别。回滚内容可能遗漏输出、被轮换清除、包含从另一个进程粘贴来的文本，也可能被能访问会话的用户修改。操作日志应说明尝试执行的操作及其结果。如果日志具备篡改可见性，调查人员就能检查记录集是否保持完整，而无需信任显示这些内容的终端。

Sallyport 会把代理运行和单项调用记录在加密、带哈希链的审计日志中，`sp audit verify` 可以在离线且不需要保险库密钥的情况下验证这条链。验证只能证明已记录序列的完整性，不能证明 tmux 标题准确识别了输入命令的人。在事件报告中，应把这两种结论分开。

有用的事件记录不应写成“是 release tmux 会话执行的”。可以这样写：“16:43，代理运行 [标识符] 请求执行 [操作]。请求来自进程 [标识符]，该进程于 [时间] 启动。捕获时，该进程是 [shell 或服务] 的后代，并与 tmux 服务器 [PID] 关联。[人员] 在查看显示的进程签名者后批准了此次运行。”只填写有证据支持的事实。

这种写法也会暴露证据缺口。如果遏制前没有捕获父进程链，就明确写出这一点。如果系统没有保留所需的进程身份信息，不要用窗格名称来填补缺口。

## 代码签名身份和本地账户身份解决的是不同问题

本地 macOS 账户标识操作系统用户。代码签名者标识可执行文件的签署者。tmux 会话标识终端服务器。这些标识并不等价，每一种都能发现不同的故障。

本地账户身份可以帮助回答哪个账户拥有该进程，但不能说明二进制文件是否来自预期发布者，也不能说明是否有其他人使用了未锁定的账户。代码签名者能帮助人确认请求访问的可执行文件，但不能说明进程是否在正确的 tmux 窗格中启动，也不能说明任务本身是否合适。

因此，当新的代理进程请求执行操作时，让授权卡片优先显示代码签名者很有用。这个信号不会因窗格重命名、复制 tmux 配置或重启终端模拟器而改变。它还可以让意外的未签名程序或签名不同的可执行文件在关键时刻显现出来。

不要把它简化成“签名者相同就安全”的盲目规则。可信签名者可能发布有问题的版本，本地包装程序可能在不安全的环境中调用已批准的二进制文件，签名进程也可能收到危险指令。进程身份可以缩小授权目标。选定凭据的逐次调用授权和完整操作记录，则负责处理身份本身无法解决的风险。

在 macOS 调查中，可以使用 `codesign` 检查二进制文件的签名信息：

```sh
codesign -dv --verbose=4 "$(command -v agent-tool)" 2>&1 | \
  grep -E 'Identifier=|TeamIdentifier=|Authority='
```

输出通常包括 `Identifier=`、`TeamIdentifier=` 以及一个或多个 `Authority=` 条目。需要解释授权界面为何识别某个进程时，应保存这些信息。不要用 `/usr/local/bin/agent-tool` 这样的路径代替签名证据。路径很容易被遮蔽、替换或通过符号链接指向其他位置。

## 不要再用永久共享会话运行高权限代理

名为 `dev`、`main` 或 `shared` 的永久 tmux 服务器，不适合运行能够访问生产凭据的代理。它虽然便于工作，却会把人员、任务、shell 历史、环境状态和进程生命周期混在一起，直到没人能说清一次运行在哪里结束。

把个人交互式复用器与代理执行分开。每次代理运行都使用自己的会话或 socket。通过一条有记录的命令启动，捕获 PID 和启动时间，并在分离前指定负责人。如果另一名工程师需要接手任务，应启动新的代理进程并获得新的授权决定，而不是按惯例继承旧窗格。

团队常常反对这种做法，因为复用会话看起来更高效。但这种效率只持续到代理在原任务结束后又发出请求为止。之后，响应团队可能花费一个小时阅读回滚内容。建立清晰的进程边界只需几秒，重建模糊的边界则要花更久，而且往往只能靠猜测，而不是得到答案。

第一项运维改动很简单：禁止使用未命名的长期 tmux 和 screen 会话来运行具有身份验证访问权限的代理。每次运行创建一个短期会话，记录启动身份，并在运行结束时关闭它。这样，授权系统和调查人员才有真正可以使用的边界。
