# 安全说明 · Security

> dsh-lark-bot 把本机 DeepSeek Harness（`dsh`）暴露给飞书 / Lark IM。本文件说明威胁模型、
> 默认安全姿态与报告渠道。Security model for a bridge that exposes a local coding agent to Feishu / Lark.

## 官方分发渠道 · Official distribution channels

- **唯一官方仓库**：<https://github.com/PlutoKeating/dsh-lark-bot>
- **唯一官方 npm 包**：`dsh-lark-bot`（同源双包 `dsh-feishu-bot`），维护者 `plutokeating`
- **唯一安装命令**：`npx dsh-lark-bot@latest setup --profile dsh-lark`
- **Releases 资产**：仅两个 npm tarball（`dsh-lark-bot-<ver>.tgz` / `dsh-feishu-bot-<ver>.tgz`），
  **从不发布 Windows/macOS 可执行文件**
- **校验承诺**：自本文档更新后的下一个 Release 起，每个发布资产随附 `<asset>.sha256` 校验文件；
  安装/使用前请核对 SHA-256，不一致即视为被篡改，请勿安装并报告
- **假冒识别**：任何以本项目名义提供“下载即运行”二进制（尤其 .exe）、或使用仿冒仓库名/包名的分发
  渠道均为**假冒 / 恶意来源**——请勿下载或运行，并截图按下方报告渠道反馈
- **取证存档**：假冒仓库 `tarraencompassing61/dsh-lark-bot` 的取证与处置约定见
  `docs/security/2026-08-17-impostor-repo-evidence/`
- **持续监控**：`pnpm security:monitor`（假冒仓库活动 / npm 仿冒包 / 相似包名抢注），建议每周运行或挂 cron

## 威胁模型 · Threat model

- **凭据泄露**：飞书 `app_id` / `app_secret`、DeepSeek API key、会话内容可能在日志、卡片或进程环境中出现。
- **越权访问**：未授权用户 / 群聊驱动本机 coding agent 执行命令、读写文件。
- **路径逃逸 / 符号链接逃逸**：附件、worktree、`/cd` 相关路径穿越到 bot 状态目录之外。
- **SSRF**：agent 或桥接层被诱导访问内网 / 环回地址。
- **消息重放 / 过期事件**：旧消息或重复事件被当作新指令处理。
- **交互工具不可达**：`ask_user_question`、终端类工具在 IM 场景下无法回达，应默认禁用。
- **救援通道被滥用**：dsh 下线后由守护接管飞书通道，若控制信号无鉴权，任何能私聊 bot 的人
  都能触发安全模式或重启完整 profile。

## 安全姿态 · Security posture

1. **默认拒绝**：
   - 群聊 / 话题普通消息必须 `@bot` 才响应；channel 以 `requireMention: false` 把事件交给
     bridge，由 bridge 在匹配 pending 问答卡回复后执行 mention gate。只有精确回复问答卡可免 @。
   - 配置了白名单后，私聊切换为 allowlist 模式（`dmMode: 'allowlist'`）。
   - 可通过 `DSH_LARK_ACCESS_DEFAULT_DENY=1` 在无白名单时也拒绝私聊（默认关闭以兼容首次扫码绑定）。
2. **密钥脱敏**：结构化日志按字段名（`secret/token/password/api_key`）脱敏；
   自由文本日志与卡片文本对 `Bearer …`、`sk-…`、`api_key=…` 做正则脱敏（`src/config/security.ts`）。
3. **路径 containment**：媒体下载目标、git worktree 目标必须落在各自根目录内
   （realpath 校验，拒绝符号链接逃逸，`isPathWithin`）。
4. **UTF-8 安全截断**：附件文本、卡片摘要按字节截断且不切断多字节字符（`truncateUtf8Safe`）。
5. **过期事件拒绝**：消息时间戳超出窗口即拒绝（`isEventFresh`）。
6. **SSRF 防护**：仅允许 http(s) 公网地址；环回、私有、链路本地、CGNAT、IPv6 ULA 全部拒绝
   （`isSafeHttpUrl`）。
7. **交互工具默认禁用**：SDK / ACP runtime profile 禁用 `user-questions`；
   `DEFAULT_DENIED_INTERACTIVE_TOOLS` 提供工具级黑名单。
8. **审批**：默认 SDK / Web 宿主在 `tools/pre-execute` 强制阻断高风险调用并通过 dsh rc.8
   `approval/request` 回调，ACP 使用 `session/request_permission`。按隔离 scope 持久化的
   `ask/allow/deny` 策略（0600，失败回滚且不报成功）决定弹一次性卡、自动放行或直接拒绝；只有管理员可修改，
   显式目标仅限当前 chat 内 scope，`deny`
   会在聊天中明确告知。该策略只作用于逐工具审批，不跳过后续独立的计划门禁；run 结束或 callback
   断连只结算所属 session 的挂起请求。legacy headless 无工具回调，因此不宣称受该策略保护。
   SDK / ACP / Web agent 对较大或高风险动作还会通过 `lark_request_plan_approval` 暂停；同一 turn
   未批准时 pre-execute 策略拒绝写入、删除、移动、命令执行与 `run_code`。完整计划发到当前飞书
   会话，只有卡片批准后才继续；继续规划会把可选文字意见返回 agent。run/callback 结束时只取消
   所属 session 的挂起门禁。该门禁是人机确认层，不替代 dsh sandbox 或 ACP 的逐工具权限审批。
9. **管理操作鉴权**：飞书会话内对 dsh 配置与访问白名单的写操作（`/model default`、
   `/model add|remove`、`/provider add|update|remove`、`/key set|remove`、`/invite user|admin|group`
   与 `/invite remove …`）、`/permission ask|allow|deny`，以及群聊会话隔离模式写操作（`/isolation group|topic|member`）仅 profile 管理员可执行；首个扫码绑定的 operator 自动成为管理员，之后由现有
   管理员经 `/invite admin <open_id>` 添加（`/invite list` 为只读、开放）。查看类命令
   （`/model`、`/providers`、`/key list`）开放。`/doctor` 因包含本机运行状态与最近日志，仅管理员可执行。
   `/replies set|default` 另允许当前群的群主/群管理员修改当前 scope；角色通过
   `im.v1.chat.get` 的 `owner_id/user_manager_id_list` 以 `open_id` 实时校验，查询失败时拒绝写入。
10. **本地回调隔离**：`lark_notify`、`lark_send_file`、`lark_ask_user`、`lark_request_plan_approval` 与
    `approval/request` answerer 的回调
    服务只绑定 `127.0.0.1`，每次启动生成随机
    token 鉴权（不落盘、不进日志），请求体限 1MB；`/notify` 与角色 / 配置写命令同为管理员操作。
    文件回传不信任 runtime 自报 cwd：bridge 以 native session 反查 scope/workspace，只允许该
    workspace、该 scope 实际 worktree/归档与实例日志内的 realpath 普通文件；以 no-follow 打开后
    在同一文件句柄复核文件身份并有界读取，拒绝竞态 / symlink 越界、非法文件名和默认超过
    20 MiB 的文件；agent 工具目标固定为该 session 的原 chat/thread。归档跨会话转发仅管理员可用。
    主动通知偏好默认关闭；普通用户只能为当前 scope 设置当前目标，跨会话目标要求管理员且必须已在
    `ScopeDirectory` 登记。偏好文件为 0600，提醒发送失败不回写或改变 durable job 终态。
    回复合并与近似去重默认关闭；`reply-policies.json` 为 0600 且写失败回滚。近似去重只在同发送者、
    同 immutable scope + workspace 与有限时间窗内生效，并对短文本要求规范化精确相等，降低误拦截
    其他成员或不同项目任务的风险；命中时向原消息明确回执。
11. **多机器人 peer 鉴权与防循环**：只有 `fleet.json` 中已启用且 identity 唯一的 bot open_id，
    在群内真实 @ 当前 bot 时才可交接；未知 bot、未 @、system/anonymous 消息拒绝。bot 文本不进入
    slash-command 管理管线。连续交接由跨进程 `handoffs.json` 原子计数、按 messageId 去重，超过
    `DSH_LARK_BOT_HANDOFF_MAX` fail closed；只有通过 freshness 检查的真人消息能重置计数。fleet、
    handoff 与共享 `config.json` 写入均使用原子 owner 目录 + 唯一 token 子文件的 lease 锁并心跳续租；
    回收/释放只删除精确 token，再对空目录 `rmdir`，不会误删替代 owner。dead-owner / 遗弃 lease
    仍可回收。附加实例拒绝无法隔离广播 session 的共享 `web` adapter。
12. **安全网守护（默认随 `setup` 安装）**：
    - 守护是独立于 dsh / Cordis 的最小进程，只读取本地状态与进程命令行（`ps`，不读内存），
      不导入任何 dsh 代码、不监听公网端口；
    - dsh 在线时守护**不连接飞书**（同 app 长连接仅允许单连接，避免抢占正常通道）；仅在
      「曾观察 dsh 在线 且 心跳过期 + 无 dsh 进程」时接管通道；
    - 控制信号默认拒绝：仅管理员（`access.admins`，无管理员时回退 `allowedUsers`）可触发
      `/safemode` 系列命令，未授权消息静默丢弃；
    - 过期事件复用 `DSH_LARK_EVENT_FRESHNESS_MS` 窗口拒绝；
    - 心跳 / 守护状态文件以 `0600` 写入；安全模式仅挂载官方核心 bundle（headless：
      `dsh-base` + `dsh-headless`；SDK 流式优先：`dsh-base` + `dsh-sdk-jsonrpc-server`，
      均不挂载第三方插件与 bridge 回调工具），避免把故障面带进救援通道。

## 数据与凭据 · Data & credentials

### DSH session 投影与 TUI 信任边界

- Session 投影只在飞书用户显式确认后建立；WebUI/TUI 的 open、resume、switch 或 activity 不得
  自动改变 binding，也不得把外部 session 广播给所有已知 scope。
- 选择器只枚举当前 canonical workspace 的非 subagent session 元数据，不展示正文。确认卡在历史
  披露前列出标题/ID、workspace、更新时间、回填数量、scope、替换/迁移；取消、超时或 operator/
  scope/workspace 不匹配时不绑定也不发送历史。
- 私聊遵循 allowlist；member 仅 scope owner；共享 group/topic 和跨 scope 独占迁移仅 profile
  管理员。确认副作用在原子 store 事务中复核披露时 owner 与迁移授权；owner 变化必须重新开卡。
  迁移同时清除旧 scope 的兼容 session mapping。一个 DSH session 默认最多绑定一个飞书 scope，
  避免跨私聊/群聊的数据泄露。
- `session-projections.json` 为 0600 原子状态，只保存 routing/cursor/message mapping/rpcId，不复制
  transcript。DSH append-only session log 是唯一真源，bridge 不直接修改 JSONL、不启动第二 writer。
- 来源标签只依据 DSH 事件提供的可信 provenance 或 bridge prompt correlation；无法确认时显示
  “其他 DSH 客户端”，不根据进程、窗口或文本相似度猜测。飞书原始用户消息不被编辑。
- 根 `dsh-plugin.json` 的 host facet 运行于 `trusted-in-process`：它与 dsh-TUI 宿主共享进程权限，
  **不是安全沙箱**。可选 seam 缺失时 no-op，注册、定时器和状态均随插件 lifecycle 清理；不拦截
  input/session switch，也不把 TUI observation/storage 当同步真源。

- 本地配置 `~/.dsh-lark/config.json` 以 `0600` 权限写入。
- 飞书凭据明文保存在本机配置文件；日志与卡片不输出真实密钥。
- dsh Web 设置页把 App Secret 声明为 Schemastery `role('secret')`：Host→browser 的 resolved/base/user
  层均脱敏，只允许 write payload 单向进入官方 settings provider；设置卡不读取、预填或比较旧密钥。
  Web 提交触发的 bridge reload 串行等待旧 generation 完整停止，避免新旧凭据实例同时连接。
- 卡片语言由飞书/Lark 客户端根据 Card JSON 2.0 的 `zh_cn` / `en_us` variant 本地选择；bridge
  不读取、不推断也不持久化成员 locale。无法 per-viewer 选择的 Markdown/toast 直接并列中英文。
- 多机器人 registry `~/.dsh-lark/fleet.json` 只保存实例/profile 名与 bot open_id/name；共享
  `handoffs.json` 保存 chat id、最近 message id 和轮数（均 0600）。这些标识会让本机用户看到
  哪些机器人/群参与过交接；peer name/open_id 会进入每轮 agent prompt 并随任务上下文发送给
  当前模型 provider，以支持精确 @ 交接。交接内容仍发送到共享群，不构成消息隐私隔离。
- 每个额外实例的 dsh provider 设置与凭据位于独立
  `~/.dsh-lark/bots/<name>/dsh/{settings.yaml,.credentials.yaml}`；service env 快照标准 DeepSeek key
  与该实例配置中已引用的 credential 环境键。`bot remove` 删除 `.credentials.yaml` 与 service env，
  保留不含字面密钥的 settings/runtime session 以便恢复；其余 DSH_HOME 数据需由用户备份后手工清除。
- 桥接引擎始终在 dsh 宿主进程内运行。可选 `service install` 会将启动所需的
  `DSH_LARK_*`、运行路径及实际 provider `credentialRef` 环境键白名单快照到
  `~/.dsh-lark/service/<profile>.env`（POSIX 0600；Windows 用 `icacls` 移除继承并只授予当前用户）；macOS plist / Windows 计划任务不嵌入密钥，
  隐藏 runner 在启动时读取快照。敏感值不进日志与卡片；环境变更后需 `service restart` 刷新。
- 正常服务生命周期以 profile 级原子锁串行化；portable status 的 PID 必须同时匹配 Linux
  `/proc` starttime、`service-supervise` 命令和 profile 后才可发送信号，强制停止作用于已验证的
  独立进程组，避免 PID 复用误杀或遗留孤儿 dsh。stop/uninstall intent 会阻止 guardian 回拉。
- 桥接引擎日志以 JSON Lines 输出到 stderr（由 dsh 宿主进程捕获），密钥字段脱敏后输出；
- `/doctor` 诊断文件在内存生成并直接上传，不创建临时文件；仅包含非敏感配置计数、当前 workspace
  的运行摘要、服务状态与最多 64 KiB 的当前 bridge 进程内结构化事件；不读取共享 dsh 宿主 stdout。
  结构化事件只保留代码内固定枚举的 category/event 与固定数值字段，
  时间被规范化，所有其他字段名和值均丢弃，
  因而不含消息正文/transcript/凭据标识或值。
  导出前会再次对 Bearer、`sk-`、`api_key`、当前进程已知敏感环境值及主目录脱敏。群中上传的文件
  对群成员可见，因此命令仅限管理员，仍建议私聊生成并由发送者转发前复核。
  `logs/bot.log` 是 0.6.0 独立服务时代的遗留路径，0.7.0 起不再写入。
- 聊天命令管理的 dsh 配置按官方存储协议写入：`~/.dsh/settings.yaml`（只存 `apiKeyEnv`
  引用，不落字面密钥）与 `~/.dsh/.credentials.yaml`（目录 0700、文件 0600）。bot 永不回显
  密钥值；群聊中粘贴密钥会对群成员可见，建议私聊使用或改用环境变量 / dsh Web 页面录入。
- 所有数据仅在本机、飞书开放平台与 DeepSeek API 之间流转；无遥测。
- 安全网守护相关文件：`~/.dsh-lark/guardian.json` 与
  `~/.dsh-lark/profiles/<profile>/guardian/heartbeat.json`（均 `0600`）；守护读取的飞书凭据
  来自 `~/.dsh-lark/config.json`（`0600`），日志按既有规则脱敏。
- 群聊隔离模式保存在 `~/.dsh-lark/profiles/<profile>/isolation.json`（`0600`）。成员模式会把
  飞书 `open_id` 作为 durable scope owner，因而该标识也会出现在对应 session、scope directory、
  worktree 与 archive 的本地索引或路径中，并显示在共享群的运行卡片上。成员模式隔离的是 agent
  上下文与会话数据，**不是群消息可见性**：任务输入、进度卡和回复仍发送到共享群，群内其他成员
  仍可看到；其他成员不能操作该 member scope 的停止、审批或问答卡，缺失 operator identity 时也
  拒绝操作。涉及私密内容时请改用私聊。
- adapter 实际上报的 input/output/cache token 与 context used/limit 保存在同一 profile 的
  `sessions.json`（`0600`），并按 scope + canonical workspace cwd 隔离；最近 context 快照同时保存
  产生它的 native sessionId 与 canonical provider/model 身份，并可由
  `/status` 卡在身份匹配时展示。未知或身份不匹配字段不估算；member scope 的刷新动作
  校验 operator `open_id` 与 owner，但共享群里已发送的状态卡仍对群成员可见。
- bridge 接收的普通 agent 消息在入队前写入 `profiles/<profile>/jobs.json`（0600）：包含原始正文、
  附件/提及元数据、chat/thread/scope、workspace、状态及受控 checkpoint，最多保留 500 条终态记录。
  checkpoint 不含隐藏推理正文或工具参数；`/jobs` 展示先脱敏并按 scope + workspace 隔离。原始 prompt
  仍可能包含用户主动输入的密钥，安全边界与 `sessions.json` 相同。running 崩溃后只标记 interrupted，
  不自动重跑可能已有副作用的工具；显式 retry 会再次执行，用户必须先对账。
- 审批卡会把工具名、理由、调用标识及可取得的执行参数发送到当前会话；member scope 只限制谁能
  点击，并不隐藏卡片正文。涉及密钥、私有路径或敏感命令时应使用私聊。
- 群消息在底层 channel 进入 bridge 后执行 mention gate：普通群消息仍需 @bot（或管理员明确开启
  no-at 模式）；仅当 `replyToMessageId` 命中当前进程内 pending 问答卡时可免 @。文字答案必须属于
  同 chat/topic，member scope 还要求 sender `open_id` 等于 owner；拒绝的回复不会结算问题或进入任务队列。
  no-at 的实时事件与历史轮询都再次校验当前 `allowedUsers` / `allowedChats`。`scopes.json` 会保存
  每个 scope 最近一次入站 messageId，作为 topic 问答卡的 reply anchor。

## 报告渠道 · Reporting

发现安全漏洞请通过 GitHub Security Advisory 私下报告，**不要**公开 issue。
