08/Documentation
持久 agent 的安全性
持久 agent 的权限边界、socket 与 token 的信任规则、Collie 对持久 agent 文本的处理方式,以及审计日志记录的内容
实验性功能。 传输协议、SDK 和本页面可能会在后续次要版本中发生变更,该功能也可能被移除。持久 agent 说明了决定其是否保留的标准。
durable agent 是由你运行或部署的代码,Collie 将其文本视为不可信文本。本页说明 durable agent 可以访问的内容、防止外部未授权访问的机制、Collie 对其文本的处理方式,以及审计日志保留的内容。安全性 涵盖 Collie 的整体内容。
durable agent 能做什么和不能做什么
durable agent 发起提问,由配对设备作出决定。durable agent 接收你的回答、发送给它的内容以及网络请求。Collie 不给它进入你终端的途径。
| durable agent 可以 | durable agent 不能 |
|---|---|
| 提出问题:是或否、单选、一行文本或简短表单 | 通过 Collie 回答自己的提问 |
| 列出 starter 并在 belt 中最多放置四个自己的按钮 | 向你的终端输入内容,或显示屏幕 |
在对话中放置文本、代码和 https:// 链接 | 显示原始 HTML、图片或非网页地址的链接 |
| 在限额内选择自己推送的文案 | 推送超出守卫限制的内容 |
只有人工点击才能回答问题。回答路由在写入网关后方为配对设备运行,bridge 中没有任何部分会代 durable agent、hook 或模型输出调用它。在手机上,“始终允许”需要第二次点击。提供此选项的 durable agent 持有需要最高警惕的授权,因为来自恶意工具的一次回答就会让后续的每次调用都不经提问直接通过。
执行获批操作的具体逻辑由 durable agent 负责,而非 Collie。durable agent 的按钮以该 durable agent 的权限运行,Collie 仅转发点击事件。
durable agent 无法轰炸你的锁屏。Collie 对每个问题仅判定一次(基于 durable agent 和工具调用生成键值),且至多推送一次。每个对话每分钟允许 3 次推送,每个主机每分钟允许 10 次、每小时允许 30 次。原本第 31 次的推送会合并为一条摘要推送,此后该主机在一小时内不再发送推送。卡片则始终会显示。
注意。 同一用户可以在 socket 上回答问题。socket 只能将其他用户挡在外面,无法阻止当前用户自己的进程,这包括 agent 运行的工具。后代检查是 durable agent 自身的防御措施,Collie 无法判断 durable agent 是否启用了该检查。
启用检查后,durable agent 会拒绝来自自身、自身启动的任何进程以及无法与之区分的调用者的 answer、send、abort、action、rename、close、reopen 和 starter。拒绝状态为 403 caller-refused,读取通道保持开启。该检查需要 Linux 上的 Bun,SDK 会在 Linux 上读取 socket 的调用者。下表说明了它在 durable agent 运行环境中的具体执行情况:
| durable agent 运行环境 | 后代检查执行的操作 |
|---|---|
| Linux 上的 Bun | 默认运行。SDK 读取每个连接的调用者,并拒绝来自 durable agent、其后代进程或无法识别归属的调用者的操作,返回 403 caller-refused |
| 其他系统上的 Bun,例如 macOS | 无法运行。openCollie 在启动时报错中止,除非 durable agent 传入 callerCheck: "off"。在 off 模式下,当前用户的任何进程均可回答 |
| Node | 无法运行。durable agent 启动并输出一行警告。当前用户的任何进程均可回答 |
将 agent 的工具放入无法访问 socket 的沙箱中、将卡片作为通知而非关卡对待,或者在 Bun 上运行 durable agent。callerCheck: "off" 是 durable agent 自身选择在没有检查的情况下运行。
socket 传输
私有 socket 会阻止所有其他用户,其唯一的门禁就是路径。
ls -ld /run/user/1000 /run/user/1000/desk /run/user/1000/desk/app.socksocket 不携带 token,也不携带 Authorization 请求头。Collie 仅在整个路径都私有时才建立连接,并在每次连接前使用 lstat 从 / 向下执行此遍历,因此不会跟随链接:
| 部分 | Collie 的要求 | 原因说明的内容 |
|---|---|---|
| 路径的每一部分,包含 socket | 不是符号链接,即使指向有效位置也不行 | socket-not-private: <path> is a symlink |
| socket 所在的文件夹 | 一个真实的文件夹,归你所有,权限模式必须为 0700 | the socket's folder <path> has mode 0755, not 0700、belongs to user <a>, not to <b> 或 <path> is not a directory |
| 其上层的每个文件夹 | group 和 others 均无写权限的真实文件夹。允许使用带有 sticky bit 的文件夹(如 /tmp),但绝不能作为 socket 所在的文件夹。不会检查其所有者,因为具有私有 /tmp 的服务会将 root 的文件夹显示为用户 65534 | <path> has mode 0775, so group or others can write to it |
| socket | 一个 socket,归你所有,不授予 group 或 others 任何权限 | the path is not a socket、the socket belongs to user <a>, not to <b>、the socket has mode 0666, not 0600 |
| 没有用户 ID 的平台 | 默认关闭:Collie 从不发起连接 | this platform shows no file owner |
| Windows | 仅 HTTPS 传输可用:collie durable attach --socket 在写入前就会拒绝 | a private socket needs Unix user ids; on Windows use --url <https://...> --token-env VAR |
出现故障的路径会立即无法访问,没有任何宽限期。collie doctor 在其 durable-socket 行中指明了相同的检查项并给出了修复方法。空路径会读取为 no socket at <path>,这不属于隐私错误。
使用 SDK 构建的 durable agent 在 socket 文件夹不存在时会以权限模式 0700 创建它。若文件夹存在但为其他模式,它会拒绝启动。它将 socket 设置为权限模式 0600,并在开始监听前删除已失效的 socket 文件。
socket 传输适用于 Linux 和 macOS。Windows 没有 socket 传输,因此在那里请使用 token 传输。
token 传输
通过 URL 和 token 访问另一台主机上的 durable agent,token 是唯一的防线。
openssl rand -hex 32| 规则 | 值 |
|---|---|
| Token | 去除首尾空白后为 32 到 4096 字节,不含空格的可打印 ASCII 字符 |
| URL | 本机的 durable agent 使用 https://,或者 http://127.0.0.1:<port> 或 http://localhost:<port>。最多 2048 字节。不包含用户名、密码、查询参数或片段 |
| 证书 | 按照普通客户端的方式进行检查。Collie 没有跳过检查的开关 |
| 重定向 | Collie 不遵循任何重定向,因此 token 绝不会发送到其他主机 |
持有 token 的任何人都可以充当你的手机。他们可以读取所有对话、回答所有问题、发送文本、停止当前轮次、重命名或关闭,以及运行 starter。来自互联网的 TLS 连接无法验证调用方,因此这里不存在后代检查。通过这种方式访问的 durable agent 应仅提供 Allow 和 Deny,绝不能提供 always-allow。
主机可以看到文本。TLS 在主机处终止,因此它可以查看每个请求(包括请求头中的 token),并且对话存储在其存储空间中。
Collie 在两处以明文形式保存 token。sources.json 为条目保存该 token,配置文件夹的备份或同步会包含它。Collie 将该文件保持为模式 0600,启动时会收紧权限过宽的文件并在日志中记录一行,如果 group 或 others 可读,collie doctor 会显示红色的 sources 行。仅 durable 形式将其保存在 .env 或 config.toml 中,保存它的文件必须为模式 0600。Collie 从不输出 token。collie durable list 显示为 (token set),collie doctor 显示为 set, <n> bytes,任何错误或日志行中都不会包含该值。attach 从 --token-env 指定的变量中读取它,因此它不会出现在命令行中。
token 永不过期。要轮换 token,请在重启 Collie 之前向 durable agent 提供新 token,否则手机会提示 durable agent 拒绝了它。durable agent 可以在轮换期间接受第二个 token:停放的 Cloudflare 示例接收 COLLIE_TOKEN,并在其设置时接收 COLLIE_TOKEN_NEXT(ADR 0103)。具体步骤见 Token。
这是 Collie 发送到你配置的 URL 的出站调用,它通过不附加任何内容来表示拒绝。每个请求都携带 Authorization: Bearer <token>、传输协议版本、上次读取的 ETag,对于 tap 操作,还包含你回复或发送的内容主体。读取操作是不带主体的 GET。
通过 https 进行的每次轮询都是一次请求,你的主机可能会对此计费,"nothing changed" 的应答也会计入。在未打开手机的情况下,空闲轮询每 10 秒运行一次,因此 Collie 每天请求 8,640 次;手机查看期间每 2 秒请求一次。处于停机状态或拒绝 token 的 durable agent 最多每 10 秒被请求一次,这些请求同样会产生费用。对于 Collie 全天轮询的 durable agent,请使用付费计划。
Collie 如何处理 durable agent 的文本
在其他组件读取之前,Collie 会清理来自 durable agent 的每个字符串,并将其绘制为文本。
durable agent 发送的字符串应当在上限以内。Collie 不默认其符合规范。每个字符串都会经过三个步骤:
- 移除控制字符。 Collie 会移除 C0 控制字符、DEL 以及 C1 控制字符。容纳多行的字段会保留换行符和制表符:条目文本、工具调用的输出与摘要,以及问题的理由。
- 移除不可见字符和方向字符。 Collie 会移除方向标记 U+200E、U+200F 与 U+061C,覆盖与隔离字符 U+202A 至 U+202E 与 U+2066 至 U+2069,零宽字符 U+200B 至 U+200D,词连接符 U+2060,字节顺序标记 U+FEFF,以及可能携带整句不可见内容的标签字符 U+E0000 至 U+E007F。
- 字符串被截断。 Collie 会在完整字符处将其截断至字节上限,并以
…结尾。
标题或理由在清理中发生改变的问题,其卡片上会带有“Text shortened”标记。清理后不含任何字符的选项或 belt 按钮会被丢弃,因此按钮绝不会是空白的。无内容的字段以其 id 命名。
| 文本 | 上限 |
|---|---|
| 工作区、标签页、对话或问题的标题 | 256 字节 |
| 选项、字段或 starter 标签 | 128 字节 |
| belt 按钮标签 | 16 个字符 |
| 问题的理由 | 2048 字节 |
| 工具调用的输出 | 16384 字节 |
| 消息或回复的文本 | 65536 字节 |
markdown 或 code 条目的文本 | 32768 字节 |
| 发送给 durable agent 的文本 | 1 到 32768 字节,发出前会被拒绝 |
| 来自 durable agent 的单条回复 | hello 为 16 KiB,快照为 4 MiB |
id 从不被清理或截断。id 为 1 到 128 字节,不含控制字符、空格,且不含 /、? 或 #,否则携带该 id 的对象会被丢弃。
Collie 将文本按纯文本绘制。markdown 项会显示标题、列表、强调、代码片段和链接,且绝不渲染原始 HTML:标签会直接显示为字符本身。markdown 项内的链接只能打开网址。link 项需要带主机的 https:// 地址,不能包含用户名或密码,不能包含空格、控制字符或不可见字符,长度最多为 2048 个字符。否则 Collie 会丢弃该项,并针对每个对话在其日志中写入一行。图片和文件不属于项。
记录的内容
Collie 会将手机对 durable agent 的每次写入记录在 <state-dir>/audit.log 中,模式为 0600。
每行都包含 durable agent 的名称(表现为 session)、已配对的设备以及事件:
| 事件 | 当 | 该行保留的内容 |
|---|---|---|
answer | 点击回答了问题 | 请求 id、问题及其修订版本、选项 id,或答案的类型(optionIds、text 或 values),以及结果。绝不记录你输入的文字 |
send | 你发送或引导了文本 | 请求 id、其指定的轮次或问题、steer、结果,以及前 120 个字符的文本预览 |
abort | 你停止了一个轮次 | 请求 id、其指定的轮次或问题,以及结果 |
action | 你点击了工具栏按钮 | 请求 id、操作 id、轮次以及结果 |
starter.start | 你点击了启动器 | 启动器 id、请求 id 以及结果 |
durable.reopen | 你重新打开了已关闭的对话 | 对话 id |
space.rename | 你重命名了工作区 | 空间 id 和新名称 |
关闭和重命名对话或标签页使用常规事件:pane.close、pane.rename 和 tab.rename。COLLIE_AUDIT_CONTENT=none 会遮蔽详情中除少数指定字段(例如请求 id)以外的所有字符串,因此 send 的预览也会一同被遮蔽。日志用于在事后展示发生的情况。它不会阻止任何操作。
Collie 还会为每个回答、发送、停止、按钮点击和启动器保留回执,其中不包含输入的文本。Collie 保存 durable agent 数据的位置 说明了保留时长。