dsh-lark-bot 0.17.3 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,7 +52,7 @@
52
52
  - 流式过程卡以飞书原生折叠面板实时展示阶段、耗时以及工具名称与状态,完成后最终回答单独发送,支持交互按钮(停止 / 计划门禁 / 审批 / 问答卡);原始推理、工具输入输出与底层错误不会进入卡片;卡片更新失败会有限重试并降级为普通提示,Agent 与最终回答继续,不会拖垮 bridge 进程;
53
53
  - Git 仓库内为每个会话自动创建隔离 worktree 项目工作区,多项目互不干扰。
54
54
 
55
- **十一项全网独有组合**:
55
+ **十二项全网独有组合**:
56
56
 
57
57
  - 🆘 **Guardian 安全网守护——“永远叫得应”**:DSH 崩溃后飞书仍会回复你,`/safemode` 进入仅核心安全模式直接重启。
58
58
  - 👥 **多角色 Agent——“一个机器人,一整个团队”**:`/role` 切换或指派 PM / 开发 / 文档等角色,每个角色独立人设、模型偏好与规则。
@@ -64,6 +64,7 @@
64
64
  - ⚙️ **dsh Web 可视化设置——“不用背环境变量”**:在官方 Settings → Plugins 页面点选应用、工作目录、模型、并行数与提醒,并可直达诊断。
65
65
  - 🔑 **对话内管理模型和密钥——“不用离开飞书”**:`/providers` `/provider` `/key` 直接查看、切换供应商、热更新密钥。
66
66
  - 🎚️ **快速 / 平衡 / 深度模式——“任务强度一键选”**:`/mode` 按 scope 持久选择,下一轮生效且不打断当前任务。
67
+ - 🔄 **飞书内自更新——“离开命令行也能升级”**:管理员发送 `/upgrade` 检查 npm 官方包并通过确认卡交给 Guardian 后台更新、验证和重载;`/new` 发现新版本时只发一条简短提醒。
67
68
  - 🧭 **关键任务先拍板——“计划看清再动手”**:完整计划先单独发出,再用卡片批准执行或附意见继续规划,原任务自动续跑。
68
69
 
69
70
  ## 30 秒上手
@@ -117,6 +118,8 @@ Markdown、toast 与旧客户端降级路径同时显示中英文。agent 最终
117
118
  | `/ws use <name>` | 切换到命名工作空间|
118
119
  | `/ws remove <name>` | 删除命名工作空间|
119
120
  | `/status` | 查看可刷新状态卡(工作区 / 模型 / session / run / context / token / pending / 任务账本)|
121
+ | `/version` | 查看当前版本与 npm 最新版本 |
122
+ | `/upgrade` | 检查更新并通过确认卡让 Guardian 后台更新、验证和重载(管理员) |
120
123
  | `/doctor` | 生成脱敏诊断包并作为文件发送(管理员;可下载转发)|
121
124
  | `/jobs [list\|show <消息ID>\|retry <消息ID>]` | 对账排队/运行/完成/失败/中断任务;确认后显式重试 |
122
125
  | `/resume` | 查看当前会话最近上下文|
@@ -143,11 +146,19 @@ Markdown、toast 与旧客户端降级路径同时显示中英文。agent 最终
143
146
  | `/model default <id>` | 写入 dsh 默认模型 `agent-default-model`(管理员)|
144
147
  | `/model add\|remove <provider> <modelId> [--input-modalities text,image]` | 添加 / 删除 provider 的模型,可声明视觉输入能力(管理员)|
145
148
  | `/provider add\|update\|remove <id>` | 管理 provider(管理员;deepseek-official 与自定义 pi-ai)|
146
- | `/key set\|remove\|list <引用名>` | 管理 dsh 凭据(set / remove 需管理员)|
149
+ | `/key set <引用名>`、`/key remove\|list <引用名>` | 通过仅请求者可提交的安全表单设置 dsh 凭据;remove 需管理员 |
150
+ | `/secret status\|set\|remove <dsh-credential\|app-secret> <引用>` | 查询状态或安全采集/删除受支持密钥(写操作需管理员;值不进入 Agent) |
151
+ | `/language show\|set plain\|agent …\|reset …` | 管理普通文本与 Agent 回答语言策略(写操作需管理员) |
147
152
  | `/ask <问题>` | 发送问答卡,回答写入会话上下文|
148
153
  | `/invite user\|admin\|group <id>`、`/invite list`、`/invite remove user\|group <id>` | 管理访问白名单(写操作需管理员)|
149
154
  | `/help` | 查看帮助|
150
155
 
156
+ 每轮 SDK / ACP / Web 请求都会注入结构化、无密钥的频道上下文,并注册官方 runtime Skill
157
+ `dsh-lark-bot`。API Key、token 与 App Secret 必须经 `/key set <引用名>`、`/secret set …` 或 Agent
158
+ 工具 `lark_request_secret` 打开的密码表单提交;普通聊天、旧 `/key set <引用名> <值>` 与
159
+ `--api-key` 不再消费值。表单只允许发起者提交,值直接写入本机受支持目标,不进入 prompt、session、
160
+ 任务账本、归档、日志、诊断包或回复。Guardian 安全模式是降级恢复面,不提供该完整配置与密钥工具。
161
+
151
162
  飞书消息中的图片会按文件内容识别 PNG/JPEG/WebP/GIF 并补全安全扩展名;默认 SDK 会经 dsh
152
163
  附件存储校验后发送原生 image block,而不是把路径当作图片。无法读取或模型不支持视觉时会明确
153
164
  失败,agent 被要求不得用工作区内其他图片替代。文本类文件会读取内容并注入任务上下文。
@@ -236,13 +247,13 @@ guardian 仍只救援其配置的主实例。
236
247
 
237
248
  **结果文件直接回传**:SDK / ACP / Web agent 可调用 `lark_send_file`,把当前会话 workspace、实际执行 worktree、当前 scope 归档或实例日志中的文件直接上传到原飞书聊天 / 话题;普通 `/archive [note]` 会在落盘后立即发送 Markdown + JSONL,失败时保留路径并可用 `/archive send <id> [scope|chatId]` 重试或由管理员转发到指定会话。上传只接受普通文件,默认单文件不超过 20 MiB;真实路径必须位于 bridge 计算的会话目录内,runtime 自报 cwd 不能扩大边界。
238
249
 
239
- **逐操作审批与 scope 权限策略**:默认 SDK Web 宿主在 `tools/pre-execute` 强制拦截高风险调用,并接入 dsh rc.8 官方 `approval/request` seam;ACP 走原生 `session/request_permission`。默认 `ask` 会弹出“允许执行一次 / 拒绝”卡。管理员可用 `/permission allow` 对当前隔离 scope 自动放行逐工具审批,或用 `/permission deny` 直接拒绝并向聊天给出明确反馈;`/permission ask` 恢复逐次询问。member 隔离下可从目标 `/status` 复制 scope,执行 `/permission <策略> <scope>`;只允许修改当前聊天内 scope。策略成功落盘后才确认,持久化到 profile 的 `permission-policies.json`(0600),重启不丢,且显示在 `/status`。该策略不绕过较大/高风险任务的计划门禁;legacy `headless` 不具备工具回调能力。
250
+ **逐操作审批与 scope 权限策略**:SDK / ACP / Web runtime 在任何本地快速通道和计划门裁决前,先通过鉴权回环读取当前 immutable scope `ask|allow|deny`。`deny` 对低风险与高风险工具都先行拒绝并返回 `permission-policy` 来源;`ask` 对保守只读自省静默放行、对高风险调用弹“允许执行一次 / 拒绝”卡;`allow` 自动放行逐工具审批,但仍不替代高风险任务的计划确认或 Harness 文件沙箱。管理员可用 `/permission allow|deny|ask [scope]` 修改当前聊天内 scope;策略成功落盘后才确认,持久化到 profile 的 `permission-policies.json`(0600),重启不丢并显示在 `/status`。legacy `headless` 不具备工具回调能力。
240
251
 
241
252
  **关键任务计划门禁**:SDK / ACP / Web agent 在修改文件、运行脚本等较大或高风险动作前使用
242
253
  `lark_request_plan_approval`;同一 turn 未获批准时,runtime pre-execute 策略会拒绝写入、删除、
243
254
  移动、非只读 shell 命令与 `run_code`。一次计划批准只放行随后一次高风险调用,计划外的后续调用必须
244
- 重新确认。`date`、`pwd`、`ls`、`find`、`rg`、`git status/log/diff` 等单条
245
- 只读检查直接放行;SDK `bash` 自动附带的 `description`、`workdir` 与
255
+ 重新确认。快速通道只保留无路径的 `date`、`id`、`pwd`、`uname`、`whoami` 与受限的
256
+ `git status/log/diff` 等仓库内检查;`cat`、`grep`、`find`、`head`、`tail`、`rg`、`ls` 等可读取文件或枚举路径的命令不在快速通道,避免借工作区外路径读取环境或凭据。SDK `bash` 自动附带的 `description`、`workdir` 与
246
257
  `run_in_background:false` 经无副作用校验后不会改变只读判定。包含未知参数、串联、重定向、
247
258
  命令替换或未知程序的 shell 调用仍保守地走计划门禁。
248
259
  bridge 先把完整 Markdown 计划作为普通消息发出,再弹出“批准,开始执行 /
@@ -254,8 +265,8 @@ bridge 先把完整 Markdown 计划作为普通消息发出,再弹出“批准
254
265
  插件可控的拒绝统一为 `[policy-denial layer=<plan-gate|permission-policy|tool-approval>]`,随后给出
255
266
  `reason` 与 `to change`;Harness 自己的 `[sandbox: ...]` 明确归类为 `file-sandbox`。高风险分类器、
256
267
  persona 中的只读说明和拒绝文本由 `src/policy/tool-policy.ts` 同一来源生成,因此策略调整不会只改
257
- 提示词或只改执行钩子。计划门与逐工具审批仍保留不同语义,`/permission allow` 不扩大文件沙箱,
258
- 也不替代计划确认。
268
+ 提示词或只改执行钩子。persona 同时要求任一层拒绝后停止,不得换用等价命令、工具或路径绕行。
269
+ 计划门与逐工具审批仍保留不同语义,`/permission allow` 不扩大文件沙箱,也不替代计划确认。
259
270
 
260
271
  **任务中向你提问(问答卡)**:agent 需要你拍板、确认或补充信息时,通过 `lark_ask_user` 工具弹**问答卡**(单选 / 多选 / 自由文本)。可提交卡片,也可直接回复该卡片输入任意文字;单选/多选没有合适项时,回复文字就是补充答案。系统按被回复的 card messageId 精确匹配 pending 问题,回答后任务自动继续,等待期间运行超时看门狗暂停。(与 `/ask` 的“你主动提问”方向相反。)
261
272
 
@@ -307,7 +318,7 @@ profile 的前台进程会拒绝并提示先停止,生命周期锁阻止并发
307
318
  (需 `--api` / `--base-url` / 至少一个 `--model`,与官方 schema 一致)或 `deepseek-official`。
308
319
  - `/key set|remove|list`:读写 `~/.dsh/.credentials.yaml`(0600);settings 只存 `apiKeyEnv` 引用,
309
320
  字面密钥不进 settings / 聊天记录。
310
- - **凭据引用必须关联**:`/key set <引用名> <值>` 只写入凭据文件;provider 要生效还须在其
321
+ - **凭据引用必须关联**:`/key set <引用名>` 通过安全表单写入凭据文件;provider 要生效还须在其
311
322
  `apiKeyEnv` 字段引用同一名字(`/provider add|update ... --api-key-env <引用名>`,或向导中填写)。
312
323
  引用名与 provider ID 相同且 provider 未设 `apiKeyEnv` 时,`/key set` 会自动补关联;
313
324
  已存在的老配置在下次运行时也会自动补齐。
@@ -316,12 +327,14 @@ profile 的前台进程会拒绝并提示先停止,生命周期锁阻止并发
316
327
  `https://www.kingapi.xyz`)会自动补全为 `/v1`。dsh runtime 启动后需几百毫秒才注册
317
328
  pi-ai 路由,桥接会重试握手直到注册完成(避免 “no adapter registered for provider”)。
318
329
 
319
- 安全提醒:在飞书会话输入密钥会对可见成员暴露,建议私聊使用或 `--api-key-env` 引用环境变量;bot 不在任何回复中回显密钥值。
330
+ 安全提醒:不要在普通飞书消息中输入密钥;使用安全表单或 `--api-key-env` 引用环境变量。bot 不读取旧式带值命令,也不在回复中回显密钥值。
320
331
 
321
332
  ## 升级、禁用与卸载
322
333
 
323
334
  ### 升级
324
335
 
336
+ **完全不接触命令行:** profile 管理员在飞书发送 `/upgrade`。有新版本时 bot 弹出只允许发起人操作的确认卡;点击“确认更新”后,Guardian 通过独立 worker 安装卡片中确认的精确 npm 版本,复用完整升级、runtime profile 修复、guardian/profile 重启和 doctor 验证链,并在重载后回到原会话报告结果。点击“取消”不会产生任何变更。更新会重启机器人,正在执行的任务可能被中断;配置、会话、归档和凭据保持不变。每次 `/new` / `/reset` 都会 best-effort 查询一次 npm;仅在有新版本时额外发送一条简短普通消息。
337
+
325
338
  **推荐:一行命令彻底升级(v0.12.0+ 新增,issue #10)**
326
339
 
327
340
  ```bash
@@ -394,7 +407,7 @@ dsh plugin --profile dsh-lark remove dsh-lark-bot
394
407
 
395
408
  **Q: dsh-lark-bot 和其他 DeepSeek Harness 飞书插件(如 harness-lark)有什么区别?**
396
409
 
397
- **A:** 功能组合最全:安全网守护、多角色 Agent、多机器人可信交接、并行多任务、持久任务对账、会话归档、跨会话主动通知、dsh Web 可视化设置、对话内模型 / 密钥管理、执行模式与关键任务计划门禁十一项合一;标准 dsh profile bundle,`setup` 是唯一安装路径;可选 `service install` 只负责把同一 profile 交给 OS 常驻,不是第二套运行时。
410
+ **A:** 功能组合最全:安全网守护、多角色 Agent、多机器人可信交接、并行多任务、持久任务对账、会话归档、跨会话主动通知、dsh Web 可视化设置、对话内模型 / 密钥管理、执行模式、关键任务计划门禁与飞书内自更新十二项合一;标准 dsh profile bundle,`setup` 是唯一安装路径;可选 `service install` 只负责把同一 profile 交给 OS 常驻,不是第二套运行时。
398
411
 
399
412
  **Q: 项目从哪下载?会不会有假冒版本?**
400
413
 
@@ -506,11 +519,16 @@ SDK 模式下 dsh 原生 session 续跑,headless 模式则把历史注入下
506
519
  | `DSH_LARK_GUARDIAN_SAFE_TIMEOUT_MS` | `600000` | 安全模式单任务空闲超时(持续无活动事件才停止并出超时卡)|
507
520
  | `DSH_LARK_GUARDIAN_CARD_DENSITY` | `detailed` | 安全模式任务卡片密度(compact / standard / detailed)|
508
521
  | `DSH_LARK_UPGRADE_REGISTRY` | `https://registry.npmjs.org` | `upgrade` 探测最新版本的 npm registry(可指向镜像)|
509
- | `DSH_LARK_UPGRADE_CHECK` | `1` | `doctor` / `/version` 是否探测 npm 最新版本(`0` 关闭,best-effort)|
522
+ | `DSH_LARK_UPGRADE_CHECK` | `1` | `doctor` / `/version` / `/upgrade` / `/new` 是否探测 npm 最新版本(`0` 关闭,best-effort)|
510
523
  | `DSH_LARK_UPGRADE_CHECK_INTERVAL_MS` | `21600000` | 桥接引擎检查新版本的间隔(`0` 关闭,默认 6h)|
511
524
  | `DSH_LARK_UPGRADE_NOTIFY` | `false` | `true` 时发现新版本向指定 chat 推送飞书通知(默认仅日志)|
512
525
  | `DSH_LARK_UPGRADE_NOTIFY_CHAT` | — | 接收更新通知的 chat id(配合 `DSH_LARK_UPGRADE_NOTIFY=true`)|
513
526
 
527
+ SDK / ACP 启动会先解析完整 provider/model route:显式双字段优先;缺失时读取 dsh 对象形式
528
+ `agent-default-model: { provider, model }`;仍无法得到完整 route 时在 bridge/doctor 入口给出本项目的
529
+ 明确配置错误,不把空 provider 传给上游 runtime。受管 service 的 install/start/restart 会把旧 env
530
+ 文件中的受管键与当前 shell 合并(当前 shell 显式值优先),避免从另一个终端重启时静默丢失已有 route。
531
+
514
532
  启动时会自动查找本机常见的 `@deepseek-ai/dsh` 安装位置。只有自动发现失败或需要指定特殊 profile 时,才需要设置这两个变量。
515
533
 
516
534
  ## 权限与数据
@@ -603,10 +621,18 @@ pnpm check:publish-bundle # 校验 dist 与全部 exports/bin 入口一致(
603
621
  pnpm ci:local
604
622
  pnpm release:check # ci:local + 上游一致性检查
605
623
  pnpm compat:probe # 临时安装锁定版 dsh,验证 SDK/ACP 握手及 SDK 工具/续接
606
- pnpm dsh:upstream # 对比 npm 上游 stable 与锁定矩阵
624
+ pnpm upstream:report # 只读检查 dsh + dsh-TUI 的 GitHub Release/npm 双源发布
625
+ pnpm dsh:upstream # upstream:report 的兼容别名
607
626
  pnpm security:monitor # 假冒仓库与仿冒包监控(建议每周)
608
627
  ```
609
628
 
629
+ 仓库的 `upstream-release-watch` GitHub Actions 每天运行,也可手动触发。它以
630
+ `scripts/upstream-release-config.mjs` 中经人工确认的 `trackFrom` 为首次基线,合并全部非 draft
631
+ GitHub Releases 与 npm 全版本/发布时间/dist-tags;每个新“上游 + 版本”创建一个带
632
+ `upstream-update` label 的跟踪 Issue。同一版本的 npm-only Issue 会在 GitHub Release 后续补发时
633
+ 原地补充,closed Issue 也参与隐藏标记去重。该自动化只搬运并安全截断外部发布信息,不分析兼容性、
634
+ 不修改依赖或代码,也不自动创建适配 PR。
635
+
610
636
  开发规范见 [`AGENTS.md`](AGENTS.md),模块契约见 [`docs/API.md`](docs/API.md),架构见 [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)。
611
637
  兼容矩阵的升级政策与自动化见 [`docs/COMPATIBILITY.md`](docs/COMPATIBILITY.md)。
612
638
 
@@ -760,7 +786,7 @@ pnpm publish:dual
760
786
  - omdsh-dev/community 收录:[Discussion #11](https://github.com/orgs/omdsh-dev/discussions/11) — ✅ 通过,讨论活跃(最新更新说明 v0.10.2);v0.15.1 更新说明 — 📨 已备妥,待人工粘贴
761
787
  - 平台数据刷新(v0.14.0 → v0.15.1)— ✅ 已恢复提交(2026-08-17):awesome-dsh-plugins [PR #230](https://github.com/AdamPlatin123/awesome-dsh-plugins/pull/230) · dshfind [#6 跟进](https://github.com/hikariming/dshfind/issues/6#issuecomment-5317081509) · omdsh 说明备妥
762
788
 
763
- **历史亮点跟进**(当时六项独家能力与 issue #6 设计实现;当前能力见上方十一项清单):
789
+ **历史亮点跟进**(当时六项独家能力与 issue #6 设计实现;当前能力见上方十二项清单):
764
790
 
765
791
  - awesome-dsh-plugins 榜单行同步(仓库描述 → 最新)与 agent-test 报告名称异常:[#139](https://github.com/AdamPlatin123/awesome-dsh-plugins/issues/139) — 📨 已提交(维护方已确认,等待渲染周期同步)
766
792
  - dshfind 详情页补「对话内管理模型和密钥」亮点:[#2 跟进评论](https://github.com/hikariming/dshfind/issues/2#issuecomment-5301019067) — 📨 已提交
package/README_EN.md CHANGED
@@ -54,7 +54,7 @@ Bot-owned command help, status/error messages and interactive cards are availabl
54
54
  - A streaming process card with a native collapsible panel for phase, elapsed time, and tool names/statuses; raw reasoning, tool payloads, and underlying errors stay out of the card. The final answer arrives separately, with interactive buttons for stop / plan gate / approval / questions. Failed card patches are retried finitely and degrade to a plain notice—the agent and final reply continue instead of taking down the bridge;
55
55
  - Automatic session archival and retention policies; per-session isolated git worktrees inside Git repositories, so multiple projects never interfere with each other.
56
56
 
57
- **Eleven exclusive capabilities**:
57
+ **Twelve exclusive capabilities**:
58
58
 
59
59
  - 🆘 **Guardian safety net — "always reachable"**: Feishu still replies after dsh crashes; `/safemode` enters core-only safe mode to locate the problem and restart directly.
60
60
  - 👥 **Multi-role agents — "one bot, a whole team"**: switch or assign PM / dev / docs roles with `/role`; each role has its own persona, model preference and rules.
@@ -67,6 +67,7 @@ Bot-owned command help, status/error messages and interactive cards are availabl
67
67
  - ⚙️ **Visual dsh Web settings — "no environment variables to memorize"**: edit the app, workspace, model, concurrency and reminders from the official Plugins settings page, with diagnostic shortcuts.
68
68
  - 🔑 **In-chat model & key management — "never leave Feishu"**: `/providers` `/provider` `/key` to view, switch vendors and hot-update keys.
69
69
  - 🎚️ **Quick / balanced / deep modes — "pick the right task intensity"**: `/mode` persists per scope and applies on the next turn without interrupting active work.
70
+ - 🔄 **In-chat self-update — "upgrade without a terminal"**: an admin sends `/upgrade`, confirms an owner-bound card, and Guardian updates, verifies, and reloads the bot in the background; `/new` emits only a short reminder when a newer version exists.
70
71
 
71
72
  ## Quick Start
72
73
 
@@ -115,6 +116,8 @@ Send a normal message to the bot in Feishu to get started. Common commands:
115
116
  | `/ws use <name>` | Switch to a named workspace |
116
117
  | `/ws remove <name>` | Remove a named workspace |
117
118
  | `/status` | Show a refreshable status card (workspace / model / session / runs / context / tokens / pending cards / job ledger) |
119
+ | `/version` | Show the running version and npm latest version |
120
+ | `/upgrade` | Check for an update and confirm a Guardian-managed background update, verification, and reload (admin) |
118
121
  | `/doctor` | Generate and upload a redacted diagnostic bundle (admin; downloadable and forwardable) |
119
122
  | `/jobs [list\|show <message-id>\|retry <message-id>]` | Reconcile queued/running/completed/failed/interrupted jobs and explicitly retry after review |
120
123
  | `/resume` | Show the session's recent context |
@@ -141,11 +144,22 @@ Send a normal message to the bot in Feishu to get started. Common commands:
141
144
  | `/model default <id>` | Write the dsh default model `agent-default-model` (admin) |
142
145
  | `/model add\|remove <provider> <modelId> [--input-modalities text,image]` | Add / remove a provider model and declare vision input (admin) |
143
146
  | `/provider add\|update\|remove <id>` | Manage providers (admin; deepseek-official and custom pi-ai) |
144
- | `/key set\|remove\|list <ref>` | Manage dsh credentials (set / remove require admin) |
147
+ | `/key set <ref>`, `/key remove\|list <ref>` | Set dsh credentials through an owner-only secure form; remove requires admin |
148
+ | `/secret status\|set\|remove <dsh-credential\|app-secret> <ref>` | Inspect status or securely collect/remove a supported secret (admin writes) |
149
+ | `/language show\|set plain\|agent …\|reset …` | Manage plain-message and agent-answer language policy (admin writes) |
145
150
  | `/ask <question>` | Send a Q&A card; the answer is written back to session context |
146
151
  | `/invite user\|admin\|group <id>`、`/invite list`、`/invite remove user\|group <id>` | Manage the access allowlist (mutating commands require admin) |
147
152
  | `/help` | Show help |
148
153
 
154
+ Every SDK, ACP, and Web turn receives structured, secret-free channel context and the official runtime
155
+ `dsh-lark-bot` skill. API keys, tokens, and App Secrets must be entered through the owner-only password form opened
156
+ by `/key set <ref>`, `/secret set …`, or `lark_request_secret`. Ordinary chat, legacy `/key set <ref> <value>`, and
157
+ `--api-key` no longer consume values. The value never enters prompts, sessions, jobs, archives, logs, diagnostics,
158
+ or replies. Guardian safe mode is a degraded recovery surface without the full configuration/secret seam.
159
+ The form payload still traverses Feishu/Lark; platform-side auditing and retention are outside this project's control,
160
+ so use a trusted direct chat. The bridge guarantees that the value is not an ordinary chat message and is never sent
161
+ to the cloud LLM.
162
+
149
163
  Feishu images are detected by content as PNG/JPEG/WebP/GIF and receive a safe extension. The default
150
164
  SDK validates them through dsh's attachment store and sends native image blocks instead of path text.
151
165
  Unreadable or unsupported images fail explicitly, and the agent is instructed never to substitute another
@@ -319,12 +333,14 @@ Configuration is persisted the official dsh way (the same storage protocol as th
319
333
  changes (effective on the next message). A bare pi-ai Base URL (e.g. `https://www.kingapi.xyz`)
320
334
  is completed with `/v1` automatically.
321
335
 
322
- Security note: typing a key in a Feishu conversation exposes it to everyone who can see that chat; prefer private chats or `--api-key-env` references to environment variables. The bot never echoes key values in any reply.
336
+ Security note: never type a key in ordinary Feishu chat. Use the secure form or an `--api-key-env` environment reference. Legacy value-bearing commands are not consumed, and replies never echo values.
323
337
 
324
338
  ## Upgrade, Disable & Uninstall
325
339
 
326
340
  ### Upgrade
327
341
 
342
+ **No terminal required:** a profile admin sends `/upgrade` in Feishu/Lark. When a newer release exists, the bot sends an owner-bound confirmation card. Confirming hands the exact npm version to an isolated Guardian worker, which runs the full package/runtime-profile/guardian/profile upgrade and verification path, reloads the bot, and reports the result back to the original chat or thread. Cancel makes no changes. Reloading can interrupt active tasks; configuration, sessions, archives, and credentials are preserved. Every `/new` or `/reset` performs a best-effort npm check and emits one short plain reminder only when a newer version exists.
343
+
328
344
  **Recommended: one-command full upgrade (new in v0.12.0, issue #10)**
329
345
 
330
346
  ```bash
@@ -479,7 +495,7 @@ Core environment variables:
479
495
  | `DSH_LARK_GUARDIAN_SAFE_TIMEOUT_MS` | `600000` | Safe-mode per-task idle timeout (stops the run after it has been silent this long and renders a timeout card) |
480
496
  | `DSH_LARK_GUARDIAN_CARD_DENSITY` | `detailed` | Card density for safe-mode run cards (compact / standard / detailed) |
481
497
  | `DSH_LARK_UPGRADE_REGISTRY` | `https://registry.npmjs.org` | npm registry used by `upgrade` to discover the latest version (mirrors supported) |
482
- | `DSH_LARK_UPGRADE_CHECK` | `1` | Whether `doctor` / `/version` probe npm latest (`0` disables; best-effort) |
498
+ | `DSH_LARK_UPGRADE_CHECK` | `1` | Whether `doctor` / `/version` / `/upgrade` / `/new` probe npm latest (`0` disables; best-effort) |
483
499
  | `DSH_LARK_UPGRADE_CHECK_INTERVAL_MS` | `21600000` | Bridge new-version check interval (`0` disables; default 6h) |
484
500
  | `DSH_LARK_UPGRADE_NOTIFY` | `false` | Push a Feishu notification to the target chat when a newer version is found (default: log-only) |
485
501
  | `DSH_LARK_UPGRADE_NOTIFY_CHAT` | — | Chat receiving update notifications (with `DSH_LARK_UPGRADE_NOTIFY=true`) |
@@ -570,10 +586,18 @@ pnpm check:publish-bundle # verifies dist matches every export & the CLI entry
570
586
  pnpm ci:local
571
587
  pnpm release:check # ci:local + upstream consistency check
572
588
  pnpm compat:probe # temp-installs pinned dsh; probes SDK/ACP plus SDK tool/resume
573
- pnpm dsh:upstream # compares npm upstream stable with the pinned matrix
589
+ pnpm upstream:report # read-only dsh + dsh-TUI GitHub Release/npm source report
590
+ pnpm dsh:upstream # backward-compatible alias for upstream:report
574
591
  pnpm security:monitor # impostor-repo & npm copycat monitor (recommended weekly)
575
592
  ```
576
593
 
594
+ The repository's `upstream-release-watch` GitHub Actions workflow runs daily and can also be dispatched
595
+ manually. Its reviewed `trackFrom` baselines live in `scripts/upstream-release-config.mjs`. It merges every
596
+ non-draft GitHub Release with npm versions, publish times, and dist-tags, then creates one `upstream-update`
597
+ Issue per upstream/version. A later GitHub Release enriches the existing npm-only Issue, while hidden markers
598
+ deduplicate both open and closed Issues. This automation only transports sanitized, bounded release data; it
599
+ does not assess compatibility, modify dependencies/code, or create adaptation PRs.
600
+
577
601
  See [`AGENTS.md`](AGENTS.md) for the development workflow, [`docs/API.md`](docs/API.md) for module contracts, and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the architecture. See [`docs/COMPATIBILITY.md`](docs/COMPATIBILITY.md) for the compatibility matrix, upgrade policy and automation.
578
602
 
579
603
  Contributions are welcome via Issues and PRs; see [`AGENTS.md`](AGENTS.md) for the workflow (required reading, commit conventions, push policy) and [`docs/ECOSYSTEM.md`](docs/ECOSYSTEM.md) for ecosystem delivery standards.
package/cordis.patch.yml CHANGED
@@ -44,10 +44,23 @@
44
44
  endpoint: !!js process.env.DSH_LARK_FILE_URL
45
45
  token: !!js process.env.DSH_LARK_NOTIFY_TOKEN
46
46
 
47
+ - id: lark-secret
48
+ name: 'dsh-lark-bot/secret'
49
+ config:
50
+ endpoint: !!js process.env.DSH_LARK_SECRET_URL
51
+ token: !!js process.env.DSH_LARK_NOTIFY_TOKEN
52
+
53
+ - id: lark-ask
54
+ name: 'dsh-lark-bot/ask'
55
+ config:
56
+ endpoint: !!js process.env.DSH_LARK_ASK_URL
57
+ token: !!js process.env.DSH_LARK_NOTIFY_TOKEN
58
+
47
59
  - id: lark-plan-approval
48
60
  name: 'dsh-lark-bot/plan'
49
61
  config:
50
62
  endpoint: !!js process.env.DSH_LARK_PLAN_URL
63
+ policyEndpoint: !!js process.env.DSH_LARK_APPROVAL_URL
51
64
  token: !!js process.env.DSH_LARK_NOTIFY_TOKEN
52
65
  mode: !!js process.env.DSH_LARK_PLAN_GATE
53
66
 
package/dist/approval.js CHANGED
@@ -1,28 +1,10 @@
1
1
  // src/policy/tool-policy.ts
2
2
  var READ_ONLY_SHELL_TOOLS = /* @__PURE__ */ new Set(["bash", "shell"]);
3
3
  var READ_ONLY_COMMANDS = /* @__PURE__ */ new Set([
4
- "basename",
5
- "cat",
6
4
  "date",
7
- "df",
8
- "dirname",
9
- "du",
10
- "find",
11
- "grep",
12
- "head",
13
5
  "id",
14
- "jq",
15
- "ls",
16
- "pgrep",
17
- "ps",
18
6
  "pwd",
19
- "readlink",
20
- "realpath",
21
- "rg",
22
- "stat",
23
- "tail",
24
7
  "uname",
25
- "wc",
26
8
  "whoami"
27
9
  ]);
28
10
  var READ_ONLY_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set([
@@ -80,22 +62,7 @@ function isSimpleReadOnlyShellCommand(rawArguments) {
80
62
  (word) => word === "-u" || word === "--utc" || word === "--universal" || word.startsWith("+")
81
63
  );
82
64
  }
83
- if (executable === "rg") {
84
- return !words.slice(1).some(
85
- (word) => word === "--pre" || word.startsWith("--pre=") || word === "--hostname-bin" || word.startsWith("--hostname-bin=")
86
- );
87
- }
88
- if (executable === "find") {
89
- return !words.slice(1).some(
90
- (word) => ["-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint", "-fprint0", "-fprintf"].includes(word)
91
- );
92
- }
93
- if (executable === "tail") {
94
- return !words.slice(1).some(
95
- (word) => word === "-f" || word === "-F" || word === "--follow" || word.startsWith("--follow=")
96
- );
97
- }
98
- return true;
65
+ return words.length === 1;
99
66
  }
100
67
  function shellCommand(rawArguments) {
101
68
  if (typeof rawArguments === "object" && rawArguments !== null && !Array.isArray(rawArguments)) {
@@ -116,11 +83,7 @@ function shellCommand(rawArguments) {
116
83
  }
117
84
  }
118
85
  function isReadOnlyGitCommand(words) {
119
- let index = 0;
120
- while (words[index] === "-C") {
121
- if (!words[index + 1]) return false;
122
- index += 2;
123
- }
86
+ const index = 0;
124
87
  const subcommand = words[index];
125
88
  if (subcommand === "branch") {
126
89
  const flags = words.slice(index + 1);
@@ -134,7 +97,7 @@ function isReadOnlyGitCommand(words) {
134
97
  }
135
98
  if (!subcommand || !READ_ONLY_GIT_SUBCOMMANDS.has(subcommand)) return false;
136
99
  return !words.slice(index + 1).some(
137
- (word) => word === "-o" || word === "--output" || word.startsWith("--output=") || word === "--ext-diff" || word === "--textconv"
100
+ (word) => word === "-o" || word === "--output" || word.startsWith("--output=") || word === "--ext-diff" || word === "--textconv" || word === "--no-index" || word.startsWith("/") || word.startsWith("~") || /(^|\/)\.\.(\/|$)/u.test(word)
138
101
  );
139
102
  }
140
103
 
@@ -152,12 +115,14 @@ function apply(ctx, config = {}) {
152
115
  return (await requestBridgeApproval(config, request, next)).outcome;
153
116
  });
154
117
  approvalCtx.on("tools/pre-execute", async (execution, next) => {
155
- if (!isHighRiskTool(approvalCtx, execution)) return next();
118
+ const highRisk = isHighRiskTool(approvalCtx, execution);
119
+ if (!highRisk && !approvalEndpoint(config)) return next();
156
120
  const request = {
157
121
  ...execution.agent === void 0 ? {} : { agent: execution.agent },
158
122
  toolName: execution.name,
159
123
  reason: approvalReason(execution),
160
- toolInput: execution.arguments
124
+ toolInput: execution.arguments,
125
+ lowRisk: !highRisk
161
126
  };
162
127
  const decision = await requestBridgeApproval(config, request, async () => "unavailable");
163
128
  if (decision.outcome !== "allowed-once") {
@@ -181,7 +146,7 @@ function apply(ctx, config = {}) {
181
146
  });
182
147
  }
183
148
  async function requestBridgeApproval(config, request, next) {
184
- const endpoint = config.endpoint ?? process.env.DSH_LARK_APPROVAL_URL;
149
+ const endpoint = approvalEndpoint(config);
185
150
  const token = config.token ?? process.env.DSH_LARK_NOTIFY_TOKEN;
186
151
  if (!endpoint || !token) return { outcome: await next() };
187
152
  const sessionId = request.agent?.session?.id;
@@ -198,7 +163,8 @@ async function requestBridgeApproval(config, request, next) {
198
163
  toolName: request.toolName,
199
164
  ...request.callId === void 0 ? {} : { callId: String(request.callId) },
200
165
  ...typeof request.reason === "string" ? { reason: request.reason } : {},
201
- ...request.toolInput === void 0 ? {} : { toolInput: request.toolInput }
166
+ ...request.toolInput === void 0 ? {} : { toolInput: request.toolInput },
167
+ ..."lowRisk" in request && request.lowRisk === true ? { lowRisk: true } : {}
202
168
  }),
203
169
  ...request.signal === void 0 ? {} : { signal: request.signal }
204
170
  });
@@ -211,6 +177,9 @@ async function requestBridgeApproval(config, request, next) {
211
177
  return { outcome: request.signal?.aborted ? "cancelled" : "unavailable" };
212
178
  }
213
179
  }
180
+ function approvalEndpoint(config) {
181
+ return config.endpoint ?? process.env.DSH_LARK_APPROVAL_URL;
182
+ }
214
183
  function approvalReason(execution) {
215
184
  if (typeof execution.arguments === "object" && execution.arguments !== null && "description" in execution.arguments && typeof execution.arguments.description === "string") {
216
185
  return execution.arguments.description;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/policy/tool-policy.ts","../src/notify/approval-answerer.ts"],"sourcesContent":["import type { ToolPluginContext } from '../notify/raw-tool.js';\n\nexport type PolicyLayer = 'plan-gate' | 'permission-policy' | 'tool-approval' | 'file-sandbox';\n\nexport interface PolicyDenial {\n layer: PolicyLayer;\n reason: string;\n toChange: string;\n}\n\nexport interface PlanPolicyExecution {\n name: string;\n arguments: unknown;\n agent?: object;\n}\n\nconst READ_ONLY_SHELL_TOOLS = new Set(['bash', 'shell']);\nconst READ_ONLY_COMMANDS = new Set([\n 'basename', 'cat', 'date', 'df', 'dirname', 'du', 'find', 'grep', 'head', 'id',\n 'jq', 'ls', 'pgrep', 'ps', 'pwd', 'readlink', 'realpath', 'rg', 'stat', 'tail',\n 'uname', 'wc', 'whoami',\n]);\nconst READ_ONLY_GIT_SUBCOMMANDS = new Set([\n 'diff', 'ls-files', 'ls-tree', 'log', 'merge-base', 'rev-parse', 'show', 'status',\n]);\nconst SHELL_CONTROL_SYNTAX = /[\\n\\r;&|<>`]|\\$\\(|\\$\\{/u;\n\nexport function policyDenialText(denial: PolicyDenial): string {\n return `[policy-denial layer=${denial.layer}] denied by ${denial.layer}: ${denial.reason}\\n` +\n `to change: ${denial.toChange}`;\n}\n\nexport function planGateDenial(): PolicyDenial {\n return {\n layer: 'plan-gate',\n reason: 'this substantial or high-risk action has no plan approval for the current turn',\n toChange: 'call lark_request_plan_approval and wait for approval; /permission does not change the plan gate',\n };\n}\n\nexport function permissionPolicyDenial(scope: string, toolName: string): PolicyDenial {\n return {\n layer: 'permission-policy',\n reason: `tool ${toolName} is blocked by scope ${scope}'s deny policy`,\n toChange: `an admin can run /permission ask ${scope} or /permission allow ${scope}`,\n };\n}\n\nexport function toolApprovalDenial(toolName: string): PolicyDenial {\n return {\n layer: 'tool-approval',\n reason: `the user rejected the one-shot approval for tool ${toolName}`,\n toChange: 'choose a safer alternative or ask the user before requesting approval again',\n };\n}\n\n/** Persona text generated from the same command vocabulary used for enforcement. */\nexport function renderToolPolicyPersona(): string[] {\n return [\n `The bridge policy treats one uncomposed shell call as read-only only when its executable is one of: ${[...READ_ONLY_COMMANDS].join(', ')}; read-only git subcommands are ${[...READ_ONLY_GIT_SUBCOMMANDS].join(', ')} plus listing branch/remote forms.`,\n 'Read-only calls must not use chaining, redirects, command substitution, background execution, escalation, or mutating flags; run qualifying inspections directly.',\n 'Before modifying files, installing packages, running scripts, pushing, deleting, or taking another substantial or high-risk action, use lark_request_plan_approval and wait for approval.',\n 'The plan-gate confirms the intended plan; /permission separately controls per-tool approval and never bypasses the plan-gate or the Harness file-sandbox.',\n 'Policy refusals use [policy-denial layer=...] with a reason and to-change instruction. Harness errors beginning [sandbox: ...] are file-sandbox refusals. Report the named layer and exact error; never invent a different restriction.',\n ];\n}\n\nexport function isHighRiskTool(ctx: ToolPluginContext, execution: PlanPolicyExecution): boolean {\n if (execution.name === 'lark_request_plan_approval') return false;\n if (execution.name === 'run_code') return true;\n const normalized = execution.name.toLowerCase().replaceAll('-', '_');\n if (READ_ONLY_SHELL_TOOLS.has(normalized)) {\n return !isSimpleReadOnlyShellCommand(execution.arguments);\n }\n try {\n const view = ctx.tools.get?.(execution.name, execution.agent)?.presentCall?.(\n execution.arguments,\n ) as { card?: string; kind?: string } | undefined;\n if (view?.card === 'terminal' || view?.card === 'diff') return true;\n const kind = view?.kind;\n if (kind && ['edit', 'delete', 'move', 'execute'].includes(kind)) return true;\n } catch {\n // Fall through to the conservative name classifier.\n }\n return /(^|_)(bash|shell|exec|execute|run|write|edit|patch|delete|remove|move|rename)(_|$)/u\n .test(normalized);\n}\n\nfunction isSimpleReadOnlyShellCommand(rawArguments: unknown): boolean {\n const command = shellCommand(rawArguments)?.trim();\n if (!command || SHELL_CONTROL_SYNTAX.test(command)) return false;\n const words = command.split(/\\s+/u);\n const executablePath = words[0];\n if (\n !executablePath ||\n (executablePath.includes('/') &&\n !executablePath.startsWith('/bin/') &&\n !executablePath.startsWith('/usr/bin/'))\n ) return false;\n const executable = executablePath.split('/').at(-1);\n if (!executable) return false;\n if (executable === 'git') return isReadOnlyGitCommand(words.slice(1));\n if (!READ_ONLY_COMMANDS.has(executable)) return false;\n if (executable === 'date') {\n return words.slice(1).every((word) =>\n word === '-u' || word === '--utc' || word === '--universal' || word.startsWith('+')\n );\n }\n if (executable === 'rg') {\n return !words.slice(1).some((word) =>\n word === '--pre' || word.startsWith('--pre=') ||\n word === '--hostname-bin' || word.startsWith('--hostname-bin=')\n );\n }\n if (executable === 'find') {\n return !words.slice(1).some((word) =>\n ['-delete', '-exec', '-execdir', '-ok', '-okdir', '-fprint', '-fprint0', '-fprintf'].includes(word)\n );\n }\n if (executable === 'tail') {\n return !words.slice(1).some((word) =>\n word === '-f' || word === '-F' || word === '--follow' || word.startsWith('--follow=')\n );\n }\n return true;\n}\n\nfunction shellCommand(rawArguments: unknown): string | undefined {\n if (typeof rawArguments === 'object' && rawArguments !== null && !Array.isArray(rawArguments)) {\n const record = rawArguments as Record<string, unknown>;\n const allowedKeys = new Set(['command', 'description', 'workdir', 'run_in_background']);\n if (Object.keys(record).some((key) => !allowedKeys.has(key))) return undefined;\n if (record.description !== undefined && typeof record.description !== 'string') return undefined;\n if (record.workdir !== undefined && typeof record.workdir !== 'string') return undefined;\n if (record.run_in_background !== undefined && record.run_in_background !== false) return undefined;\n const command = record.command;\n return typeof command === 'string' ? command : undefined;\n }\n if (typeof rawArguments !== 'string') return undefined;\n try {\n return shellCommand(JSON.parse(rawArguments));\n } catch {\n return rawArguments;\n }\n}\n\nfunction isReadOnlyGitCommand(words: readonly string[]): boolean {\n let index = 0;\n while (words[index] === '-C') {\n if (!words[index + 1]) return false;\n index += 2;\n }\n const subcommand = words[index];\n if (subcommand === 'branch') {\n const flags = words.slice(index + 1);\n return flags.length === 0 || flags.every((word) =>\n ['--show-current', '--list', '--all', '-a', '--remotes', '-r', '-v', '-vv'].includes(word)\n );\n }\n if (subcommand === 'remote') {\n const args = words.slice(index + 1);\n return args.length === 0 ||\n args.every((word) => word === '-v' || word === '--verbose') ||\n args[0] === 'get-url';\n }\n if (!subcommand || !READ_ONLY_GIT_SUBCOMMANDS.has(subcommand)) return false;\n return !words.slice(index + 1).some((word) =>\n word === '-o' || word === '--output' || word.startsWith('--output=') ||\n word === '--ext-diff' || word === '--textconv'\n );\n}\n","import type { Context } from '@deepseek-ai/cordis';\nimport {\n isHighRiskTool,\n policyDenialText,\n toolApprovalDenial,\n type PlanPolicyExecution,\n type PolicyDenial,\n} from '../policy/tool-policy.js';\nimport type { ToolPluginContext } from './raw-tool.js';\n\nexport const name = 'lark-approval-answerer';\nexport const inject = ['approval', 'tools'];\n\nexport interface Config {\n endpoint?: string;\n token?: string;\n}\n\ntype Outcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';\n\ninterface ApprovalDecision {\n outcome: Outcome;\n denial?: PolicyDenial;\n}\n\ninterface RawApprovalRequest {\n agent?: { session?: { id?: unknown } };\n toolName?: unknown;\n callId?: unknown;\n reason?: unknown;\n toolInput?: unknown;\n signal?: AbortSignal;\n}\n\ntype ApprovalContext = Context & {\n on(\n event: 'approval/request',\n listener: (request: RawApprovalRequest, next: () => Promise<Outcome>) => Promise<Outcome>,\n ): unknown;\n on(\n event: 'tools/pre-execute',\n listener: (\n execution: PlanPolicyExecution,\n next: () => Promise<unknown>,\n ) => Promise<unknown>,\n ): unknown;\n};\n\n/** Terminal rc.8 approval answerer for the nested SDK and host runtimes. */\nexport function apply(ctx: Context, config: Config = {}): void {\n const approvalCtx = ctx as ApprovalContext & ToolPluginContext;\n const inFlightGrant = new WeakMap<object, string>();\n\n approvalCtx.on('approval/request', async (request, next) => {\n if (request.agent && inFlightGrant.get(request.agent) === request.toolName) {\n inFlightGrant.delete(request.agent);\n return 'allowed-once';\n }\n return (await requestBridgeApproval(config, request, next)).outcome;\n });\n\n approvalCtx.on('tools/pre-execute', async (execution, next) => {\n if (!isHighRiskTool(approvalCtx, execution)) return next();\n const request: RawApprovalRequest = {\n ...(execution.agent === undefined\n ? {}\n : { agent: execution.agent as NonNullable<RawApprovalRequest['agent']> }),\n toolName: execution.name,\n reason: approvalReason(execution),\n toolInput: execution.arguments,\n };\n const decision = await requestBridgeApproval(config, request, async () => 'unavailable');\n if (decision.outcome !== 'allowed-once') {\n return {\n kind: 'deny',\n reason: policyDenialText(\n decision.denial ?? (\n decision.outcome === 'rejected'\n ? toolApprovalDenial(execution.name)\n : {\n layer: 'tool-approval',\n reason: `tool ${execution.name} did not receive approval (${decision.outcome})`,\n toChange: 'retry only after approval is available, or choose a safer alternative',\n }\n ),\n ),\n };\n }\n if (execution.agent) inFlightGrant.set(execution.agent, execution.name);\n try {\n return await next();\n } finally {\n if (execution.agent) inFlightGrant.delete(execution.agent);\n }\n });\n}\n\nasync function requestBridgeApproval(\n config: Config,\n request: RawApprovalRequest,\n next: () => Promise<Outcome>,\n): Promise<ApprovalDecision> {\n const endpoint = config.endpoint ?? process.env.DSH_LARK_APPROVAL_URL;\n const token = config.token ?? process.env.DSH_LARK_NOTIFY_TOKEN;\n if (!endpoint || !token) return { outcome: await next() };\n const sessionId = request.agent?.session?.id;\n if (sessionId === undefined || typeof request.toolName !== 'string') {\n return { outcome: 'unavailable' };\n }\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n token,\n sessionId: String(sessionId),\n toolName: request.toolName,\n ...(request.callId === undefined ? {} : { callId: String(request.callId) }),\n ...(typeof request.reason === 'string' ? { reason: request.reason } : {}),\n ...(request.toolInput === undefined ? {} : { toolInput: request.toolInput }),\n }),\n ...(request.signal === undefined ? {} : { signal: request.signal }),\n });\n const body = await response.json() as {\n ok?: boolean;\n outcome?: unknown;\n denial?: unknown;\n };\n return response.ok && body.ok === true && isOutcome(body.outcome)\n ? {\n outcome: body.outcome,\n ...(isPolicyDenial(body.denial) ? { denial: body.denial } : {}),\n }\n : { outcome: 'unavailable' };\n } catch {\n return { outcome: request.signal?.aborted ? 'cancelled' : 'unavailable' };\n }\n}\n\nfunction approvalReason(execution: PlanPolicyExecution): string {\n if (\n typeof execution.arguments === 'object' && execution.arguments !== null &&\n 'description' in execution.arguments &&\n typeof (execution.arguments as { description?: unknown }).description === 'string'\n ) {\n return (execution.arguments as { description: string }).description;\n }\n return `Execute high-risk tool ${execution.name}`;\n}\n\nfunction isOutcome(value: unknown): value is Outcome {\n return value === 'allowed-once' || value === 'rejected' ||\n value === 'cancelled' || value === 'unavailable';\n}\n\nfunction isPolicyDenial(value: unknown): value is PolicyDenial {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const denial = value as Partial<PolicyDenial>;\n return (\n denial.layer === 'plan-gate' || denial.layer === 'permission-policy' ||\n denial.layer === 'tool-approval' || denial.layer === 'file-sandbox'\n ) && typeof denial.reason === 'string' && typeof denial.toChange === 'string';\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/policy/tool-policy.ts","../src/notify/approval-answerer.ts"],"sourcesContent":["import type { ToolPluginContext } from '../notify/raw-tool.js';\n\nexport type PolicyLayer = 'plan-gate' | 'permission-policy' | 'tool-approval' | 'file-sandbox';\n\nexport interface PolicyDenial {\n layer: PolicyLayer;\n reason: string;\n toChange: string;\n}\n\nexport interface PlanPolicyExecution {\n name: string;\n arguments: unknown;\n agent?: object;\n}\n\nconst READ_ONLY_SHELL_TOOLS = new Set(['bash', 'shell']);\nconst READ_ONLY_COMMANDS = new Set([\n 'date', 'id', 'pwd', 'uname', 'whoami',\n]);\nconst READ_ONLY_GIT_SUBCOMMANDS = new Set([\n 'diff', 'ls-files', 'ls-tree', 'log', 'merge-base', 'rev-parse', 'show', 'status',\n]);\nconst SHELL_CONTROL_SYNTAX = /[\\n\\r;&|<>`]|\\$\\(|\\$\\{/u;\n\nexport function policyDenialText(denial: PolicyDenial): string {\n return `[policy-denial layer=${denial.layer}] denied by ${denial.layer}: ${denial.reason}\\n` +\n `to change: ${denial.toChange}`;\n}\n\nexport function planGateDenial(): PolicyDenial {\n return {\n layer: 'plan-gate',\n reason: 'this substantial or high-risk action has no plan approval for the current turn',\n toChange: 'call lark_request_plan_approval and wait for approval; /permission does not change the plan gate',\n };\n}\n\nexport function permissionPolicyDenial(scope: string, toolName: string): PolicyDenial {\n return {\n layer: 'permission-policy',\n reason: `tool ${toolName} is blocked by scope ${scope}'s deny policy`,\n toChange: `an admin can run /permission ask ${scope} or /permission allow ${scope}`,\n };\n}\n\nexport function toolApprovalDenial(toolName: string): PolicyDenial {\n return {\n layer: 'tool-approval',\n reason: `the user rejected the one-shot approval for tool ${toolName}`,\n toChange: 'choose a safer alternative or ask the user before requesting approval again',\n };\n}\n\n/** Persona text generated from the same command vocabulary used for enforcement. */\nexport function renderToolPolicyPersona(): string[] {\n return [\n `The bridge policy treats one uncomposed shell call as read-only only when its executable is one of: ${[...READ_ONLY_COMMANDS].join(', ')}; read-only git subcommands are ${[...READ_ONLY_GIT_SUBCOMMANDS].join(', ')} plus listing branch/remote forms.`,\n 'Read-only calls must not use chaining, redirects, command substitution, background execution, escalation, or mutating flags; run qualifying inspections directly.',\n 'Before modifying files, installing packages, running scripts, pushing, deleting, or taking another substantial or high-risk action, use lark_request_plan_approval and wait for approval.',\n 'The plan-gate confirms the intended plan; /permission separately controls per-tool approval and never bypasses the plan-gate or the Harness file-sandbox.',\n 'Policy refusals use [policy-denial layer=...] with a reason and to-change instruction. Harness errors beginning [sandbox: ...] are file-sandbox refusals. Report the named layer and exact error; never invent a different restriction.',\n 'After any policy or sandbox refusal, do not try an equivalent command, tool, or path to obtain the same result. Stop, report the refusal accurately, and let the user decide whether to change the policy or approve another approach.',\n ];\n}\n\nexport function isHighRiskTool(ctx: ToolPluginContext, execution: PlanPolicyExecution): boolean {\n if (execution.name === 'lark_request_plan_approval') return false;\n if (execution.name === 'run_code') return true;\n const normalized = execution.name.toLowerCase().replaceAll('-', '_');\n if (READ_ONLY_SHELL_TOOLS.has(normalized)) {\n return !isSimpleReadOnlyShellCommand(execution.arguments);\n }\n try {\n const view = ctx.tools.get?.(execution.name, execution.agent)?.presentCall?.(\n execution.arguments,\n ) as { card?: string; kind?: string } | undefined;\n if (view?.card === 'terminal' || view?.card === 'diff') return true;\n const kind = view?.kind;\n if (kind && ['edit', 'delete', 'move', 'execute'].includes(kind)) return true;\n } catch {\n // Fall through to the conservative name classifier.\n }\n return /(^|_)(bash|shell|exec|execute|run|write|edit|patch|delete|remove|move|rename)(_|$)/u\n .test(normalized);\n}\n\nfunction isSimpleReadOnlyShellCommand(rawArguments: unknown): boolean {\n const command = shellCommand(rawArguments)?.trim();\n if (!command || SHELL_CONTROL_SYNTAX.test(command)) return false;\n const words = command.split(/\\s+/u);\n const executablePath = words[0];\n if (\n !executablePath ||\n (executablePath.includes('/') &&\n !executablePath.startsWith('/bin/') &&\n !executablePath.startsWith('/usr/bin/'))\n ) return false;\n const executable = executablePath.split('/').at(-1);\n if (!executable) return false;\n if (executable === 'git') return isReadOnlyGitCommand(words.slice(1));\n if (!READ_ONLY_COMMANDS.has(executable)) return false;\n if (executable === 'date') {\n return words.slice(1).every((word) =>\n word === '-u' || word === '--utc' || word === '--universal' || word.startsWith('+')\n );\n }\n return words.length === 1;\n}\n\nfunction shellCommand(rawArguments: unknown): string | undefined {\n if (typeof rawArguments === 'object' && rawArguments !== null && !Array.isArray(rawArguments)) {\n const record = rawArguments as Record<string, unknown>;\n const allowedKeys = new Set(['command', 'description', 'workdir', 'run_in_background']);\n if (Object.keys(record).some((key) => !allowedKeys.has(key))) return undefined;\n if (record.description !== undefined && typeof record.description !== 'string') return undefined;\n if (record.workdir !== undefined && typeof record.workdir !== 'string') return undefined;\n if (record.run_in_background !== undefined && record.run_in_background !== false) return undefined;\n const command = record.command;\n return typeof command === 'string' ? command : undefined;\n }\n if (typeof rawArguments !== 'string') return undefined;\n try {\n return shellCommand(JSON.parse(rawArguments));\n } catch {\n return rawArguments;\n }\n}\n\nfunction isReadOnlyGitCommand(words: readonly string[]): boolean {\n const index = 0;\n const subcommand = words[index];\n if (subcommand === 'branch') {\n const flags = words.slice(index + 1);\n return flags.length === 0 || flags.every((word) =>\n ['--show-current', '--list', '--all', '-a', '--remotes', '-r', '-v', '-vv'].includes(word)\n );\n }\n if (subcommand === 'remote') {\n const args = words.slice(index + 1);\n return args.length === 0 ||\n args.every((word) => word === '-v' || word === '--verbose') ||\n args[0] === 'get-url';\n }\n if (!subcommand || !READ_ONLY_GIT_SUBCOMMANDS.has(subcommand)) return false;\n return !words.slice(index + 1).some((word) =>\n word === '-o' || word === '--output' || word.startsWith('--output=') ||\n word === '--ext-diff' || word === '--textconv' || word === '--no-index' ||\n word.startsWith('/') || word.startsWith('~') || /(^|\\/)\\.\\.(\\/|$)/u.test(word)\n );\n}\n","import type { Context } from '@deepseek-ai/cordis';\nimport {\n isHighRiskTool,\n policyDenialText,\n toolApprovalDenial,\n type PlanPolicyExecution,\n type PolicyDenial,\n} from '../policy/tool-policy.js';\nimport type { ToolPluginContext } from './raw-tool.js';\n\nexport const name = 'lark-approval-answerer';\nexport const inject = ['approval', 'tools'];\n\nexport interface Config {\n endpoint?: string;\n token?: string;\n}\n\ntype Outcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';\n\ninterface ApprovalDecision {\n outcome: Outcome;\n denial?: PolicyDenial;\n}\n\ninterface RawApprovalRequest {\n agent?: { session?: { id?: unknown } };\n toolName?: unknown;\n callId?: unknown;\n reason?: unknown;\n toolInput?: unknown;\n signal?: AbortSignal;\n lowRisk?: boolean;\n}\n\ntype ApprovalContext = Context & {\n on(\n event: 'approval/request',\n listener: (request: RawApprovalRequest, next: () => Promise<Outcome>) => Promise<Outcome>,\n ): unknown;\n on(\n event: 'tools/pre-execute',\n listener: (\n execution: PlanPolicyExecution,\n next: () => Promise<unknown>,\n ) => Promise<unknown>,\n ): unknown;\n};\n\n/** Terminal rc.8 approval answerer for the nested SDK and host runtimes. */\nexport function apply(ctx: Context, config: Config = {}): void {\n const approvalCtx = ctx as ApprovalContext & ToolPluginContext;\n const inFlightGrant = new WeakMap<object, string>();\n\n approvalCtx.on('approval/request', async (request, next) => {\n if (request.agent && inFlightGrant.get(request.agent) === request.toolName) {\n inFlightGrant.delete(request.agent);\n return 'allowed-once';\n }\n return (await requestBridgeApproval(config, request, next)).outcome;\n });\n\n approvalCtx.on('tools/pre-execute', async (execution, next) => {\n const highRisk = isHighRiskTool(approvalCtx, execution);\n if (!highRisk && !approvalEndpoint(config)) return next();\n const request: RawApprovalRequest = {\n ...(execution.agent === undefined\n ? {}\n : { agent: execution.agent as NonNullable<RawApprovalRequest['agent']> }),\n toolName: execution.name,\n reason: approvalReason(execution),\n toolInput: execution.arguments,\n lowRisk: !highRisk,\n };\n const decision = await requestBridgeApproval(config, request, async () => 'unavailable');\n if (decision.outcome !== 'allowed-once') {\n return {\n kind: 'deny',\n reason: policyDenialText(\n decision.denial ?? (\n decision.outcome === 'rejected'\n ? toolApprovalDenial(execution.name)\n : {\n layer: 'tool-approval',\n reason: `tool ${execution.name} did not receive approval (${decision.outcome})`,\n toChange: 'retry only after approval is available, or choose a safer alternative',\n }\n ),\n ),\n };\n }\n if (execution.agent) inFlightGrant.set(execution.agent, execution.name);\n try {\n return await next();\n } finally {\n if (execution.agent) inFlightGrant.delete(execution.agent);\n }\n });\n}\n\nasync function requestBridgeApproval(\n config: Config,\n request: RawApprovalRequest,\n next: () => Promise<Outcome>,\n): Promise<ApprovalDecision> {\n const endpoint = approvalEndpoint(config);\n const token = config.token ?? process.env.DSH_LARK_NOTIFY_TOKEN;\n if (!endpoint || !token) return { outcome: await next() };\n const sessionId = request.agent?.session?.id;\n if (sessionId === undefined || typeof request.toolName !== 'string') {\n return { outcome: 'unavailable' };\n }\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n token,\n sessionId: String(sessionId),\n toolName: request.toolName,\n ...(request.callId === undefined ? {} : { callId: String(request.callId) }),\n ...(typeof request.reason === 'string' ? { reason: request.reason } : {}),\n ...(request.toolInput === undefined ? {} : { toolInput: request.toolInput }),\n ...('lowRisk' in request && request.lowRisk === true ? { lowRisk: true } : {}),\n }),\n ...(request.signal === undefined ? {} : { signal: request.signal }),\n });\n const body = await response.json() as {\n ok?: boolean;\n outcome?: unknown;\n denial?: unknown;\n };\n return response.ok && body.ok === true && isOutcome(body.outcome)\n ? {\n outcome: body.outcome,\n ...(isPolicyDenial(body.denial) ? { denial: body.denial } : {}),\n }\n : { outcome: 'unavailable' };\n } catch {\n return { outcome: request.signal?.aborted ? 'cancelled' : 'unavailable' };\n }\n}\n\nfunction approvalEndpoint(config: Config): string | undefined {\n return config.endpoint ?? process.env.DSH_LARK_APPROVAL_URL;\n}\n\nfunction approvalReason(execution: PlanPolicyExecution): string {\n if (\n typeof execution.arguments === 'object' && execution.arguments !== null &&\n 'description' in execution.arguments &&\n typeof (execution.arguments as { description?: unknown }).description === 'string'\n ) {\n return (execution.arguments as { description: string }).description;\n }\n return `Execute high-risk tool ${execution.name}`;\n}\n\nfunction isOutcome(value: unknown): value is Outcome {\n return value === 'allowed-once' || value === 'rejected' ||\n value === 'cancelled' || value === 'unavailable';\n}\n\nfunction isPolicyDenial(value: unknown): value is PolicyDenial {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const denial = value as Partial<PolicyDenial>;\n return (\n denial.layer === 'plan-gate' || denial.layer === 'permission-policy' ||\n denial.layer === 'tool-approval' || denial.layer === 'file-sandbox'\n ) && typeof denial.reason === 'string' && typeof denial.toChange === 'string';\n}\n"],"mappings":"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","names":[]}