dsh-lark-bot 0.16.0 → 0.17.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
@@ -40,7 +40,7 @@
40
40
 
41
41
  **你的 DeepSeek Harness 只能“贴身”用?** dsh 跑在本机,每次看进度、改任务都得回到电脑前;离开工位后任务卡住、跑偏甚至 dsh 崩了,你都收不到任何消息——回来才发现白等半天。
42
42
 
43
- **dsh-lark-bot 把遥控器装进你的飞书**:在私聊、群聊、话题里直接指挥本机 dsh coding agent,流式卡片的飞书原生折叠面板实时展示思考与工具调用,最终回答单独成消息;任务完成还能主动推送到你所在的任何群并 @ 你;即使 dsh 崩溃下线,飞书里依然叫得应——发 `/safemode` 进入仅核心安全模式,直接在聊天里定位问题、重启引擎。**这是唯一“dsh 挂了你不会失联”的桥接方案。**
43
+ **dsh-lark-bot 把遥控器装进你的飞书**:在私聊、群聊、话题里直接指挥本机 dsh coding agent,流式卡片的飞书原生折叠面板实时展示阶段、耗时以及工具名称与状态,最终回答单独成消息;任务完成还能主动推送到你所在的任何群并 @ 你;即使 dsh 崩溃下线,飞书里依然叫得应——发 `/safemode` 进入仅核心安全模式,直接在聊天里定位问题、重启引擎。**这是唯一“dsh 挂了你不会失联”的桥接方案。**
44
44
 
45
45
  **适合谁**:在飞书 / Lark(私聊、群聊、话题)里指挥本机 dsh coding agent 的开发者与团队,尤其是需要多项目隔离、角色分工、并行任务与会话归档的协作场景。
46
46
 
@@ -49,7 +49,7 @@
49
49
  **基础能力**:
50
50
 
51
51
  - 私聊、群聊、话题(thread)里指挥本机 dsh coding agent,图片 / 文本文件直接发给 bot 即可;
52
- - 流式过程卡以飞书原生折叠面板实时展示思考、工具调用与结果,完成后最终回答单独发送,支持交互按钮(停止 / 计划门禁 / 审批 / 问答卡);
52
+ - 流式过程卡以飞书原生折叠面板实时展示阶段、耗时以及工具名称与状态,完成后最终回答单独发送,支持交互按钮(停止 / 计划门禁 / 审批 / 问答卡);原始推理、工具输入输出与底层错误不会进入卡片;卡片更新失败会有限重试并降级为普通提示,Agent 与最终回答继续,不会拖垮 bridge 进程;
53
53
  - Git 仓库内为每个会话自动创建隔离 worktree 项目工作区,多项目互不干扰。
54
54
 
55
55
  **十一项全网独有组合**:
@@ -85,6 +85,11 @@ dsh --profile dsh-lark
85
85
 
86
86
  ③ 首次启动终端打印二维码 → 飞书 / Lark App 扫码创建或选择 PersonalAgent 应用 → 绑定后私聊直接发消息;群聊 / 话题默认 `@bot`,也可显式开启受白名单保护的无 @ 模式。
87
87
 
88
+ 卡片按钮按 Card JSON 2.0 的 `behaviors.callback` 协议发送;扫码向导会显式申请
89
+ `card.action.trigger` 回调能力。这两项都是计划、审批、问答等交互卡片按钮正常工作的
90
+ 必要条件。已有应用若是在旧版本向导中创建,请在飞书开放平台的“事件与回调 → 回调配置”中启用
91
+ 卡片回调后重新发布应用,否则消息收发正常但按钮点击不会送达 bot。
92
+
88
93
  `setup` 自动完成:定位本机 dsh → 预批准 pnpm 构建策略 → 标准 `dsh plugin add` → 默认安装「安全网守护」系统服务,一条命令完成全部安装。
89
94
 
90
95
  > **无需公网 IP / 域名 / 服务器 / 内网穿透**(飞书 WebSocket 出站长连接),Linux / macOS / Windows 通用。
@@ -99,8 +104,8 @@ dsh --profile dsh-lark
99
104
 
100
105
  bot 自带的命令帮助、状态、错误提示与交互卡片均提供中文 / English。Card JSON 2.0 在各文本组件使用飞书原生
101
106
  `i18n_content`,同一张群卡会按每位读者的客户端语言显示;无法取得读者语言的普通
102
- Markdown、toast 与旧客户端降级路径同时显示中英文。agent 生成的回答、推理、工具输入输出和用户原文
103
- 保持原样,不自动翻译。
107
+ Markdown、toast 与旧客户端降级路径同时显示中英文。agent 最终回答和用户原文保持原样,不自动翻译;
108
+ 原始推理、工具输入输出和底层错误只保留在本机运行边界内,不进入飞书过程卡。
104
109
 
105
110
  | 命令 | 作用 |
106
111
  | --- | --- |
@@ -228,14 +233,18 @@ guardian 仍只救援其配置的主实例。
228
233
 
229
234
  **关键任务计划门禁**:SDK / ACP / Web agent 在修改文件、运行脚本等较大或高风险动作前使用
230
235
  `lark_request_plan_approval`;同一 turn 未获批准时,runtime pre-execute 策略会拒绝写入、删除、
231
- 移动、命令执行与 `run_code`。bridge 先把完整 Markdown 计划作为普通消息发出,再弹出“批准,开始执行 /
236
+ 移动、非只读 shell 命令与 `run_code`。一次计划批准只放行随后一次高风险调用,计划外的后续调用必须
237
+ 重新确认。`date`、`pwd`、`ls`、`find`、`rg`、`git status/log/diff` 等单条
238
+ 只读检查直接放行;包含串联、重定向、命令替换或未知程序的 shell 调用仍保守地走计划门禁。
239
+ bridge 先把完整 Markdown 计划作为普通消息发出,再弹出“批准,开始执行 /
232
240
  继续规划”决策卡;卡内可填写修改意见。工具在等待期间阻塞且暂停空闲超时,批准后原任务自动继续;
233
241
  继续规划时 agent 会收到意见、修订计划并再次请求确认。门禁无固定十分钟截止,跟随所属 run 的取消
234
- 信号;停止任务会精确取消该 session 的 pending 卡并撤回。legacy headless adapter 不具备工具回调能力。
242
+ 信号;停止任务会精确取消该 session 的 pending 卡并撤回。可信部署可设置
243
+ `DSH_LARK_PLAN_GATE=off` 关闭这层独立门禁(逐工具审批仍按原策略执行);legacy headless adapter 不具备工具回调能力。
235
244
 
236
245
  **任务中向你提问(问答卡)**:agent 需要你拍板、确认或补充信息时,通过 `lark_ask_user` 工具弹**问答卡**(单选 / 多选 / 自由文本)。可提交卡片,也可直接回复该卡片输入任意文字;单选/多选没有合适项时,回复文字就是补充答案。系统按被回复的 card messageId 精确匹配 pending 问题,回答后任务自动继续,等待期间运行超时看门狗暂停。(与 `/ask` 的“你主动提问”方向相反。)
237
246
 
238
- 计划、审批与问答卡提交后会立即显示成功提示、发送一条终态确认并撤回原卡,避免按钮仍停留在聊天中造成“未生效”的误解;确认或撤回失败不会影响已经提交给 agent 的决策、审批结果或答案。
247
+ 计划、审批与问答卡提交后会立即显示成功提示、发送一条终态确认并撤回原卡,避免按钮仍停留在聊天中造成“未生效”的误解;失效卡会返回明确错误提示,入站点击与失效原因写入结构化日志。确认或撤回失败不会影响已经提交给 agent 的决策、审批结果或答案。本地人机决策回调会以 JSON 空白流保活,避免 Node HTTP 客户端在等待 5 分钟后切断仍有效的卡片。
239
248
 
240
249
  **安全网守护**:独立于 dsh 进程、系统级常驻的最小守护进程(systemd / LaunchAgent / Windows 启动项),默认随 `setup` 安装。dsh 正常时静默;dsh 下线或无法 boot(如第三方插件破坏 profile 组合)时自动接管飞书通道,无需命令行即可自救:
241
250
 
@@ -521,7 +530,9 @@ SDK 模式下 dsh 原生 session 续跑,headless 模式则把历史注入下
521
530
  后续问答卡作为 reply 正确发回原话题。
522
531
  - **本地回调**:运行 `lark_notify`、`lark_send_file`、`lark_ask_user`、`lark_request_plan_approval` 或逐工具审批时,dsh
523
532
  runtime 子进程通过 `127.0.0.1` 随机端口 + 每启动随机 token 回调 bridge 进程(仅本机回环,
524
- 不监听公网);计划内容、待执行工具的理由/参数与决策卡会发送到当前飞书 / Lark 会话。群聊中的审批内容对群成员可见。
533
+ 不监听公网);等待人工回答的回调会立即发送响应头,并用 JSON 空白心跳维持连接,避免 Node 默认的
534
+ 5 分钟 HTTP 空闲边界中断审批。计划内容、待执行工具的理由/参数与决策卡会发送到当前飞书 / Lark 会话。
535
+ 群聊中的审批内容对群成员可见。
525
536
  - **进程**:spawn 本机 `dsh` runtime 子进程(`dsh-sdk-jsonrpc-server` / `dsh-acp` profile)执行 agent 任务。
526
537
  - **dsh 配置**:`/model` `/providers` `/provider` `/key` 命令按 dsh 官方存储协议读写
527
538
  `~/.dsh/settings.yaml` 与 `~/.dsh/.credentials.yaml`(仅管理员可写;settings 只存 `apiKeyEnv`
package/README_EN.md CHANGED
@@ -40,18 +40,18 @@ Turn **DeepSeek Harness (`dsh`)** into a member of your Feishu / Lark workspace
40
40
 
41
41
  Tired of being chained to your desk to drive DeepSeek Harness? dsh runs on your local machine, so checking progress and adjusting tasks means going back to your computer; once you leave your desk, a run can stall, drift, or dsh itself can crash without you ever hearing about it — until you come back and find you wasted hours.
42
42
 
43
- dsh-lark-bot puts the remote control in your Feishu: drive your local dsh coding agent from DMs, group chats and topics, with a native collapsible panel showing reasoning and tool calls in real time and the final answer delivered as a separate message; get proactive notifications pushed to any chat you're in with @mentions when tasks finish; and even when dsh crashes, Feishu still answers — send `/safemode` to enter core-only safe mode and locate the problem and restart the engine right from the chat. **It is the only bridge where you never lose contact when dsh goes down.**
43
+ dsh-lark-bot puts the remote control in your Feishu: drive your local dsh coding agent from DMs, group chats and topics, with a native collapsible panel showing phase, elapsed time, and tool names/statuses in real time and the final answer delivered as a separate message; get proactive notifications pushed to any chat you're in with @mentions when tasks finish; and even when dsh crashes, Feishu still answers — send `/safemode` to enter core-only safe mode and locate the problem and restart the engine right from the chat. **It is the only bridge where you never lose contact when dsh goes down.**
44
44
 
45
45
  **Who it is for**: developers and teams who drive a local dsh coding agent from Feishu / Lark (DMs, groups, topics) — especially those needing multi-project isolation, role-based collaboration, parallel tasks and session archival.
46
46
 
47
- Bot-owned command help, status/error messages and interactive cards are available in Chinese and English. Card JSON 2.0 uses native component-level `i18n_content`, so members of the same group see one shared card in their own client language. Plain Markdown, toast messages and legacy fallbacks cannot detect each viewer's locale and therefore show both languages. Agent answers, reasoning, tool input/output and user-authored text are never translated.
47
+ Bot-owned command help, status/error messages and interactive cards are available in Chinese and English. Card JSON 2.0 uses native component-level `i18n_content`, so members of the same group see one shared card in their own client language. Plain Markdown, toast messages and legacy fallbacks cannot detect each viewer's locale and therefore show both languages. Agent answers and user-authored text are never translated; raw reasoning and tool payloads remain local and are not rendered into process cards.
48
48
 
49
49
  ## What you get
50
50
 
51
51
  **Core**:
52
52
 
53
53
  - Drive your local dsh coding agent from private chats, group chats and threads; images / text files can be sent straight to the bot;
54
- - A streaming process card with a native collapsible panel for reasoning, tool calls and results; the final answer arrives separately, with interactive buttons for stop / plan gate / approval / questions;
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
57
  **Eleven exclusive capabilities**:
@@ -87,6 +87,12 @@ dsh --profile dsh-lark
87
87
 
88
88
  ③ On first boot the terminal prints a QR code → scan it with the Feishu / Lark app to create or choose a PersonalAgent app → after binding, DM the bot directly; groups/topics use `@bot` by default, with an explicitly enabled allowlist-protected no-@ mode available.
89
89
 
90
+ Buttons use Card JSON 2.0 `behaviors.callback`, and the QR flow explicitly requests the
91
+ `card.action.trigger` callback capability required by plan, approval, and
92
+ question-card buttons. If an existing app was created by an older flow, enable card callbacks under Developer
93
+ Console → Events & Callbacks → Callback Configuration and publish the app again. Without it, messages still work
94
+ while card clicks never reach the bot.
95
+
90
96
  `setup` automatically: locates your local dsh → pre-approves pnpm's build policy (protobufjs) → runs the standard `dsh plugin --profile dsh-lark add dsh-lark-bot@<version>` (pinned to the running package) → installs the safety-net guardian system service. One command installs everything.
91
97
 
92
98
  > **No public IP / domain / server / tunneling required** (Feishu outbound WebSocket long connection); works on Linux / macOS / Windows.
@@ -231,14 +237,20 @@ reject `web`, because a shared Web agent broadcast stream cannot isolate session
231
237
 
232
238
  **Plan gate for substantial tasks**: SDK / ACP / Web agents use `lark_request_plan_approval` before file
233
239
  changes, scripts, or other substantial/high-risk actions. A runtime pre-execute policy denies writes, deletes,
234
- moves, command execution and `run_code` in that turn until a plan is approved. The bridge sends the complete Markdown plan as a normal
240
+ moves, non-read-only shell commands and `run_code` until a plan is approved. Each approval grants only the next
241
+ high-risk call; later unplanned calls require approval again. Single read-only inspections such as `date`, `pwd`,
242
+ `ls`, `find`, `rg`, and `git status/log/diff` run directly; shell chaining,
243
+ redirection, command substitution, and unknown executables remain behind the conservative plan gate. The bridge sends the complete Markdown plan as a normal
235
244
  message, then a card with **Approve and execute** / **Continue planning** plus optional feedback. The tool blocks
236
245
  and pauses the idle watchdog; approval resumes the original turn, while revision returns the feedback and requires
237
246
  another plan. There is no fixed ten-minute deadline: the gate follows the owning run's cancellation signal, and
238
- stopping it cancels and recalls only that session's pending card. The legacy headless adapter cannot use callback tools.
247
+ stopping it cancels and recalls only that session's pending card. Trusted deployments may set
248
+ `DSH_LARK_PLAN_GATE=off` to disable this separate gate (ordinary per-tool approval still applies). The legacy headless adapter cannot use callback tools.
239
249
 
240
250
  **Mid-task questions (question cards)**: when the agent needs a decision, confirmation, or missing information, it sends a **question card** via the `lark_ask_user` tool (single choice / multi choice / free text). Submit the form or reply directly to that card with any text—even when none of the listed choices fits. The replied card message id selects the exact pending question, the agent resumes automatically, and the run-timeout watchdog pauses while it waits. (The opposite direction of `/ask`, where you ask the agent.)
241
251
 
252
+ Plan, approval and question-card submissions immediately show a native toast, post a terminal confirmation and recall the original card. Stale cards return an explicit error toast, while received actions and stale reasons are written to structured logs. Confirmation/recall failures do not change the decision already delivered to the agent. Local human-decision callbacks stream insignificant JSON whitespace while waiting so Node's HTTP client cannot invalidate a live card after five minutes.
253
+
242
254
  **Safety-net guardian**: a minimal system-level resident process (systemd / LaunchAgent / Windows startup), independent of the dsh process and installed **by default with `setup`**. Silent while dsh runs, it takes over the Feishu channel when dsh goes down or fails to boot (e.g. a third-party plugin breaks the profile composition), so you can self-heal without touching the command line:
243
255
 
244
256
  - `/safemode`: enter **core-only safe mode** (only the official `dsh-base` + `dsh-headless` bundles, **no third-party plugins**) — prefers the SDK streaming engine, falls back to headless, and lets you locate / fix / disable the offending plugin right from the chat;
@@ -493,7 +505,9 @@ This tool runs **locally**; before installing, be aware that it accesses:
493
505
  - **Scope routing**: `scopes.json` stores the chat/thread and latest inbound message id; that id is used only as
494
506
  the reply anchor that places later agent question cards back in the original topic.
495
507
  - **Local callback**: `lark_notify`, `lark_send_file`, `lark_ask_user`, `lark_request_plan_approval`, and per-tool approval call the bridge over a
496
- random 127.0.0.1 port with a per-boot token (loopback only); plan text and its decision card are sent to the
508
+ random 127.0.0.1 port with a per-boot token (loopback only). Human-wait callbacks send response headers immediately
509
+ and JSON-whitespace heartbeats while pending, so Node's default five-minute HTTP idle boundary cannot cancel a
510
+ legitimate approval wait. Plan text and its decision card are sent to the
497
511
  current Feishu / Lark conversation. Approval reasons/arguments are visible to members of a shared group.
498
512
  - **Processes**: spawns local `dsh` runtime subprocesses (`dsh-sdk-jsonrpc-server` / `dsh-acp` profiles) to run agent tasks.
499
513
  - **dsh configuration**: `/model` `/providers` `/provider` `/key` read / write `~/.dsh/settings.yaml` and `~/.dsh/.credentials.yaml` using the official dsh storage protocol (admin-only writes; settings keep only `apiKeyEnv` references; credentials file mode 0600, directory 0700; literal keys never enter settings or chat history).
package/cordis.patch.yml CHANGED
@@ -28,7 +28,7 @@
28
28
  workspace: !!js process.env.DSH_LARK_WORKSPACE
29
29
  adapter: !!js process.env.DSH_LARK_ADAPTER
30
30
  model: !!js process.env.DSH_LARK_MODEL
31
- scopeConcurrency: !!js process.env.DSH_LARK_SCOPE_CONCURRENCY ? Number(process.env.DSH_LARK_SCOPE_CONCURRENCY) : undefined
31
+ scopeConcurrency: !!js 'process.env.DSH_LARK_SCOPE_CONCURRENCY ? Number(process.env.DSH_LARK_SCOPE_CONCURRENCY) : undefined'
32
32
  notificationDefault: !!js process.env.DSH_LARK_NOTIFICATION_DEFAULT
33
33
  disabled: !!js process.env.DSH_LARK_DISABLED === '1'
34
34
 
@@ -49,6 +49,7 @@
49
49
  config:
50
50
  endpoint: !!js process.env.DSH_LARK_PLAN_URL
51
51
  token: !!js process.env.DSH_LARK_NOTIFY_TOKEN
52
+ mode: !!js process.env.DSH_LARK_PLAN_GATE
52
53
 
53
54
  - id: lark-approval-answerer
54
55
  name: 'dsh-lark-bot/approval'
package/dist/approval.js CHANGED
@@ -1,7 +1,48 @@
1
1
  // src/notify/plan-tool.ts
2
+ var READ_ONLY_SHELL_TOOLS = /* @__PURE__ */ new Set(["bash", "shell"]);
3
+ var READ_ONLY_COMMANDS = /* @__PURE__ */ new Set([
4
+ "basename",
5
+ "cat",
6
+ "date",
7
+ "df",
8
+ "dirname",
9
+ "du",
10
+ "find",
11
+ "grep",
12
+ "head",
13
+ "id",
14
+ "jq",
15
+ "ls",
16
+ "pgrep",
17
+ "ps",
18
+ "pwd",
19
+ "readlink",
20
+ "realpath",
21
+ "rg",
22
+ "stat",
23
+ "tail",
24
+ "uname",
25
+ "wc",
26
+ "whoami"
27
+ ]);
28
+ var READ_ONLY_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set([
29
+ "diff",
30
+ "ls-files",
31
+ "ls-tree",
32
+ "log",
33
+ "merge-base",
34
+ "rev-parse",
35
+ "show",
36
+ "status"
37
+ ]);
38
+ var SHELL_CONTROL_SYNTAX = /[\n\r;&|<>`]|\$\(|\$\{/u;
2
39
  function isHighRiskTool(ctx, execution) {
3
40
  if (execution.name === "lark_request_plan_approval") return false;
4
41
  if (execution.name === "run_code") return true;
42
+ const normalized = execution.name.toLowerCase().replaceAll("-", "_");
43
+ if (READ_ONLY_SHELL_TOOLS.has(normalized)) {
44
+ return !isSimpleReadOnlyShellCommand(execution.arguments);
45
+ }
5
46
  try {
6
47
  const view = ctx.tools.get?.(execution.name, execution.agent)?.presentCall?.(
7
48
  execution.arguments
@@ -11,9 +52,76 @@ function isHighRiskTool(ctx, execution) {
11
52
  if (kind && ["edit", "delete", "move", "execute"].includes(kind)) return true;
12
53
  } catch {
13
54
  }
14
- const normalized = execution.name.toLowerCase().replaceAll("-", "_");
15
55
  return /(^|_)(bash|shell|exec|execute|run|write|edit|patch|delete|remove|move|rename)(_|$)/u.test(normalized);
16
56
  }
57
+ function isSimpleReadOnlyShellCommand(rawArguments) {
58
+ const command = shellCommand(rawArguments)?.trim();
59
+ if (!command || SHELL_CONTROL_SYNTAX.test(command)) return false;
60
+ const words = command.split(/\s+/u);
61
+ const executablePath = words[0];
62
+ if (!executablePath || executablePath.includes("/") && !executablePath.startsWith("/bin/") && !executablePath.startsWith("/usr/bin/")) return false;
63
+ const executable = executablePath.split("/").at(-1);
64
+ if (!executable) return false;
65
+ if (executable === "git") return isReadOnlyGitCommand(words.slice(1));
66
+ if (!READ_ONLY_COMMANDS.has(executable)) return false;
67
+ if (executable === "date") {
68
+ return words.slice(1).every(
69
+ (word) => word === "-u" || word === "--utc" || word === "--universal" || word.startsWith("+")
70
+ );
71
+ }
72
+ if (executable === "rg") {
73
+ return !words.slice(1).some(
74
+ (word) => word === "--pre" || word.startsWith("--pre=") || word === "--hostname-bin" || word.startsWith("--hostname-bin=")
75
+ );
76
+ }
77
+ if (executable === "find") {
78
+ return !words.slice(1).some(
79
+ (word) => ["-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint", "-fprint0", "-fprintf"].includes(word)
80
+ );
81
+ }
82
+ if (executable === "tail") {
83
+ return !words.slice(1).some(
84
+ (word) => word === "-f" || word === "-F" || word === "--follow" || word.startsWith("--follow=")
85
+ );
86
+ }
87
+ return true;
88
+ }
89
+ function shellCommand(rawArguments) {
90
+ if (typeof rawArguments === "object" && rawArguments !== null && !Array.isArray(rawArguments)) {
91
+ const entries = Object.entries(rawArguments);
92
+ if (entries.length !== 1 || entries[0]?.[0] !== "command") return void 0;
93
+ const command = entries[0][1];
94
+ return typeof command === "string" ? command : void 0;
95
+ }
96
+ if (typeof rawArguments !== "string") return void 0;
97
+ try {
98
+ return shellCommand(JSON.parse(rawArguments));
99
+ } catch {
100
+ return rawArguments;
101
+ }
102
+ }
103
+ function isReadOnlyGitCommand(words) {
104
+ let index = 0;
105
+ while (words[index] === "-C") {
106
+ if (!words[index + 1]) return false;
107
+ index += 2;
108
+ }
109
+ const subcommand = words[index];
110
+ if (subcommand === "branch") {
111
+ const flags = words.slice(index + 1);
112
+ return flags.length === 0 || flags.every(
113
+ (word) => ["--show-current", "--list", "--all", "-a", "--remotes", "-r", "-v", "-vv"].includes(word)
114
+ );
115
+ }
116
+ if (subcommand === "remote") {
117
+ const args = words.slice(index + 1);
118
+ return args.length === 0 || args.every((word) => word === "-v" || word === "--verbose") || args[0] === "get-url";
119
+ }
120
+ if (!subcommand || !READ_ONLY_GIT_SUBCOMMANDS.has(subcommand)) return false;
121
+ return !words.slice(index + 1).some(
122
+ (word) => word === "-o" || word === "--output" || word.startsWith("--output=") || word === "--ext-diff" || word === "--textconv"
123
+ );
124
+ }
17
125
 
18
126
  // src/notify/approval-answerer.ts
19
127
  var name = "lark-approval-answerer";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/notify/plan-tool.ts","../src/notify/approval-answerer.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis';\nimport {\n objectArgs,\n requiredString,\n type RawToolExecution,\n type ToolPluginContext,\n} from './raw-tool.js';\n\nexport const name = 'lark-plan-approval';\nexport const inject = ['tools'];\n\nexport interface Config {\n endpoint?: string;\n token?: string;\n}\n\nexport interface PlanPolicyExecution {\n name: string;\n arguments: unknown;\n agent?: object;\n}\n\ntype PlanPolicyContext = ToolPluginContext & {\n on(\n event: 'agent/pre-step',\n listener: (\n payload: { agent: object; turn: number },\n next: () => Promise<unknown>,\n ) => Promise<unknown>,\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/** Human plan gate for substantial or high-risk repository actions. */\nexport function apply(ctx: Context, config: Config = {}) {\n const policyCtx = ctx as PlanPolicyContext;\n const currentTurns = new WeakMap<object, number>();\n const approvedTurns = new WeakMap<object, number>();\n\n policyCtx.on('agent/pre-step', async (payload, next) => {\n currentTurns.set(payload.agent, payload.turn);\n return next();\n });\n policyCtx.on('tools/pre-execute', async (execution, next) => {\n if (!isHighRiskTool(policyCtx, execution)) return next();\n const agent = execution.agent;\n const turn = agent ? currentTurns.get(agent) : undefined;\n if (agent && turn !== undefined && approvedTurns.get(agent) === turn) return next();\n return {\n kind: 'deny',\n reason:\n 'This action is blocked until the current turn calls lark_request_plan_approval and the user approves the plan.',\n };\n });\n\n policyCtx.tools.register({\n name: 'lark_request_plan_approval',\n description:\n 'Before modifying files, running scripts, or taking another substantial/high-risk action, draft a complete plan and call this tool before execution. It sends the full plan as a normal Feishu/Lark message, then waits for Approve or Continue planning plus optional feedback. Do not execute until approved. If revision is requested, revise the plan and call this tool again.',\n parameters: {\n type: 'object',\n additionalProperties: false,\n required: ['plan'],\n properties: {\n plan: { type: 'string', minLength: 1, description: 'Complete readable plan in Markdown.' },\n },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n required: ['resolved'],\n properties: {\n resolved: { type: 'boolean' },\n decision: { type: 'string', enum: ['approved', 'revise'] },\n feedback: { type: 'string' },\n error: { type: 'string' },\n },\n },\n render: (_args, rawValue) => {\n const value = rawValue as {\n resolved: boolean;\n decision?: string;\n feedback?: string;\n error?: string;\n };\n return [{\n type: 'text',\n text: value.resolved\n ? `Plan decision: ${value.decision}${value.feedback ? `; feedback: ${value.feedback}` : ''}`\n : `Plan approval failed: ${value.error ?? 'cancelled'}`,\n }];\n },\n },\n async execute(rawArgs, exec: RawToolExecution | undefined) {\n const args = objectArgs(rawArgs, 'lark_request_plan_approval');\n const plan = requiredString(args, 'plan', 'lark_request_plan_approval');\n const endpoint = config.endpoint ?? process.env.DSH_LARK_PLAN_URL;\n const token = config.token ?? process.env.DSH_LARK_NOTIFY_TOKEN;\n if (!endpoint || !token) {\n throw new Error('lark_request_plan_approval is not configured (endpoint/token missing)');\n }\n const sessionId = exec?.agent?.session === undefined\n ? undefined\n : String(exec.agent.session.id);\n if (!sessionId) throw new Error('lark_request_plan_approval needs an active session');\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ token, sessionId, plan }),\n ...(exec?.signal === undefined ? {} : { signal: exec.signal }),\n });\n const body = await response.json() as {\n ok?: boolean;\n decision?: 'approved' | 'revise';\n feedback?: string;\n error?: string;\n };\n if (!response.ok || body.ok !== true || !body.decision) {\n return { resolved: false, ...(body.error ? { error: body.error } : {}) };\n }\n if (body.decision === 'approved' && exec?.agent) {\n const turn = currentTurns.get(exec.agent);\n if (turn !== undefined) approvedTurns.set(exec.agent, turn);\n }\n return {\n resolved: true,\n decision: body.decision,\n ...(body.feedback ? { feedback: body.feedback } : {}),\n };\n },\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 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 const normalized = execution.name.toLowerCase().replaceAll('-', '_');\n return /(^|_)(bash|shell|exec|execute|run|write|edit|patch|delete|remove|move|rename)(_|$)/u\n .test(normalized);\n}\n","import type { Context } from '@deepseek-ai/cordis';\nimport { isHighRiskTool, type PlanPolicyExecution } from './plan-tool.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 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 requestBridgeApproval(config, request, next);\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 outcome = await requestBridgeApproval(config, request, async () => 'unavailable');\n if (outcome !== 'allowed-once') {\n return {\n kind: 'deny',\n reason: outcome === 'rejected'\n ? 'The user rejected this one-shot tool execution. Continue with a safer alternative.'\n : 'This tool execution was not approved and remains blocked.',\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<Outcome> {\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 next();\n const sessionId = request.agent?.session?.id;\n if (sessionId === undefined || typeof request.toolName !== 'string') return 'unavailable';\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 { ok?: boolean; outcome?: unknown };\n return response.ok && body.ok === true && isOutcome(body.outcome)\n ? body.outcome\n : 'unavailable';\n } catch {\n return 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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/notify/plan-tool.ts","../src/notify/approval-answerer.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis';\nimport {\n objectArgs,\n requiredString,\n type RawToolExecution,\n type ToolPluginContext,\n} from './raw-tool.js';\n\nexport const name = 'lark-plan-approval';\nexport const inject = ['tools'];\n\nexport interface Config {\n endpoint?: string;\n token?: string;\n mode?: 'strict' | 'off';\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',\n 'cat',\n 'date',\n 'df',\n 'dirname',\n 'du',\n 'find',\n 'grep',\n 'head',\n 'id',\n 'jq',\n 'ls',\n 'pgrep',\n 'ps',\n 'pwd',\n 'readlink',\n 'realpath',\n 'rg',\n 'stat',\n 'tail',\n 'uname',\n 'wc',\n 'whoami',\n]);\nconst READ_ONLY_GIT_SUBCOMMANDS = new Set([\n 'diff',\n 'ls-files',\n 'ls-tree',\n 'log',\n 'merge-base',\n 'rev-parse',\n 'show',\n 'status',\n]);\nconst SHELL_CONTROL_SYNTAX = /[\\n\\r;&|<>`]|\\$\\(|\\$\\{/u;\n\ntype PlanPolicyContext = ToolPluginContext & {\n on(\n event: 'agent/pre-step',\n listener: (\n payload: { agent: object; turn: number },\n next: () => Promise<unknown>,\n ) => Promise<unknown>,\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/** Human plan gate for substantial or high-risk repository actions. */\nexport function apply(ctx: Context, config: Config = {}) {\n const policyCtx = ctx as PlanPolicyContext;\n const currentTurns = new WeakMap<object, number>();\n const approvedCalls = new WeakMap<object, number>();\n const gateDisabled = (config.mode ?? process.env.DSH_LARK_PLAN_GATE) === 'off';\n\n policyCtx.on('agent/pre-step', async (payload, next) => {\n currentTurns.set(payload.agent, payload.turn);\n return next();\n });\n policyCtx.on('tools/pre-execute', async (execution, next) => {\n if (gateDisabled) return next();\n if (!isHighRiskTool(policyCtx, execution)) return next();\n const agent = execution.agent;\n const turn = agent ? currentTurns.get(agent) : undefined;\n if (agent && turn !== undefined && approvedCalls.get(agent) === turn) {\n approvedCalls.delete(agent);\n return next();\n }\n return {\n kind: 'deny',\n reason:\n 'This action is blocked until the current turn calls lark_request_plan_approval and the user approves the plan.',\n };\n });\n\n policyCtx.tools.register({\n name: 'lark_request_plan_approval',\n description:\n 'Before modifying files, running scripts, or taking another substantial/high-risk action, draft a complete plan and call this tool before execution. It sends the full plan as a normal Feishu/Lark message, then waits for Approve or Continue planning plus optional feedback. Do not execute until approved. If revision is requested, revise the plan and call this tool again.',\n parameters: {\n type: 'object',\n additionalProperties: false,\n required: ['plan'],\n properties: {\n plan: { type: 'string', minLength: 1, description: 'Complete readable plan in Markdown.' },\n },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n required: ['resolved'],\n properties: {\n resolved: { type: 'boolean' },\n decision: { type: 'string', enum: ['approved', 'revise'] },\n feedback: { type: 'string' },\n error: { type: 'string' },\n },\n },\n render: (_args, rawValue) => {\n const value = rawValue as {\n resolved: boolean;\n decision?: string;\n feedback?: string;\n error?: string;\n };\n return [{\n type: 'text',\n text: value.resolved\n ? `Plan decision: ${value.decision}${value.feedback ? `; feedback: ${value.feedback}` : ''}`\n : `Plan approval failed: ${value.error ?? 'cancelled'}`,\n }];\n },\n },\n async execute(rawArgs, exec: RawToolExecution | undefined) {\n const args = objectArgs(rawArgs, 'lark_request_plan_approval');\n const plan = requiredString(args, 'plan', 'lark_request_plan_approval');\n const endpoint = config.endpoint ?? process.env.DSH_LARK_PLAN_URL;\n const token = config.token ?? process.env.DSH_LARK_NOTIFY_TOKEN;\n if (!endpoint || !token) {\n throw new Error('lark_request_plan_approval is not configured (endpoint/token missing)');\n }\n const sessionId = exec?.agent?.session === undefined\n ? undefined\n : String(exec.agent.session.id);\n if (!sessionId) throw new Error('lark_request_plan_approval needs an active session');\n let response: Response;\n try {\n response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ token, sessionId, plan }),\n ...(exec?.signal === undefined ? {} : { signal: exec.signal }),\n });\n } catch (error) {\n if (exec?.signal?.aborted) return { resolved: false, error: 'cancelled' };\n throw error;\n }\n const body = await response.json() as {\n ok?: boolean;\n decision?: 'approved' | 'revise';\n feedback?: string;\n error?: string;\n };\n if (!response.ok || body.ok !== true || !body.decision) {\n if (body.error?.toLowerCase().includes('cancel')) {\n return { resolved: false, error: body.error };\n }\n throw new Error(body.error ?? `plan approval delivery failed (${response.status})`);\n }\n if (body.decision === 'approved' && exec?.agent) {\n const turn = currentTurns.get(exec.agent);\n if (turn !== undefined) approvedCalls.set(exec.agent, turn);\n }\n return {\n resolved: true,\n decision: body.decision,\n ...(body.feedback ? { feedback: body.feedback } : {}),\n };\n },\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\n/**\n * Read-only shell calls bypass both the plan gate and one-shot approval.\n * Keep this deliberately narrow: one command only, no shell composition, and\n * an allowlisted executable/subcommand. Unknown syntax remains high risk.\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' ||\n word.startsWith('--pre=') ||\n word === '--hostname-bin' ||\n 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 entries = Object.entries(rawArguments);\n if (entries.length !== 1 || entries[0]?.[0] !== 'command') return undefined;\n const command = entries[0][1];\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' ||\n word === '--output' ||\n word.startsWith('--output=') ||\n word === '--ext-diff' ||\n word === '--textconv'\n );\n}\n","import type { Context } from '@deepseek-ai/cordis';\nimport { isHighRiskTool, type PlanPolicyExecution } from './plan-tool.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 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 requestBridgeApproval(config, request, next);\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 outcome = await requestBridgeApproval(config, request, async () => 'unavailable');\n if (outcome !== 'allowed-once') {\n return {\n kind: 'deny',\n reason: outcome === 'rejected'\n ? 'The user rejected this one-shot tool execution. Continue with a safer alternative.'\n : 'This tool execution was not approved and remains blocked.',\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<Outcome> {\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 next();\n const sessionId = request.agent?.session?.id;\n if (sessionId === undefined || typeof request.toolName !== 'string') return 'unavailable';\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 { ok?: boolean; outcome?: unknown };\n return response.ok && body.ok === true && isOutcome(body.outcome)\n ? body.outcome\n : 'unavailable';\n } catch {\n return 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"],"mappings":"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","names":[]}