dsh-lark-bot 0.17.2 → 0.17.3

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
@@ -141,14 +141,21 @@ Markdown、toast 与旧客户端降级路径同时显示中英文。agent 最终
141
141
  | `/model`、`/providers`、`/provider`、`/key` | 打开交互式管理卡片(模型直接点选/恢复默认;管理写操作走多轮向导)|
142
142
  | `/model use <provider/model>` | 热切换当前会话模型(也兼容唯一模型 ID;下一轮生效,无需重启)|
143
143
  | `/model default <id>` | 写入 dsh 默认模型 `agent-default-model`(管理员)|
144
- | `/model add\|remove <provider> <modelId>` | 添加 / 删除 provider 的模型(管理员)|
144
+ | `/model add\|remove <provider> <modelId> [--input-modalities text,image]` | 添加 / 删除 provider 的模型,可声明视觉输入能力(管理员)|
145
145
  | `/provider add\|update\|remove <id>` | 管理 provider(管理员;deepseek-official 与自定义 pi-ai)|
146
146
  | `/key set\|remove\|list <引用名>` | 管理 dsh 凭据(set / remove 需管理员)|
147
147
  | `/ask <问题>` | 发送问答卡,回答写入会话上下文|
148
148
  | `/invite user\|admin\|group <id>`、`/invite list`、`/invite remove user\|group <id>` | 管理访问白名单(写操作需管理员)|
149
149
  | `/help` | 查看帮助|
150
150
 
151
- 飞书消息中的图片会下载到本地 media 目录并传给 dsh;文本类文件会读取内容并注入任务上下文。
151
+ 飞书消息中的图片会按文件内容识别 PNG/JPEG/WebP/GIF 并补全安全扩展名;默认 SDK 会经 dsh
152
+ 附件存储校验后发送原生 image block,而不是把路径当作图片。无法读取或模型不支持视觉时会明确
153
+ 失败,agent 被要求不得用工作区内其他图片替代。文本类文件会读取内容并注入任务上下文。
154
+ `/model` 卡片会把 dsh 默认模型并入可切换目录(即使 provider 的显式列表尚未包含它),
155
+ 并用去除公共前缀后的短标签、每行最多两个按钮适配移动端。provider 名称、模型、输入模态和
156
+ 推理档位从 models.dev 运行时目录发现(15 分钟内存缓存);网络失败时只使用 dsh settings 中的
157
+ 显式配置与默认选择,不回退到代码内置名单。通过命令或向导新增视觉模型时可声明 `text,image`,
158
+ 能力字段会原样写入 settings。目录地址可用 `DSH_LARK_MODEL_CATALOG_URL` 替换。
152
159
 
153
160
  **DSH session 消息级同步(`web` adapter)**:发送 `/session` 只会列出当前 canonical workspace
154
161
  的非 subagent session 元数据,不显示正文;选择后确认卡会列明标题、ID、workspace、更新时间、
@@ -235,13 +242,21 @@ guardian 仍只救援其配置的主实例。
235
242
  `lark_request_plan_approval`;同一 turn 未获批准时,runtime pre-execute 策略会拒绝写入、删除、
236
243
  移动、非只读 shell 命令与 `run_code`。一次计划批准只放行随后一次高风险调用,计划外的后续调用必须
237
244
  重新确认。`date`、`pwd`、`ls`、`find`、`rg`、`git status/log/diff` 等单条
238
- 只读检查直接放行;包含串联、重定向、命令替换或未知程序的 shell 调用仍保守地走计划门禁。
245
+ 只读检查直接放行;SDK `bash` 自动附带的 `description`、`workdir` 与
246
+ `run_in_background:false` 经无副作用校验后不会改变只读判定。包含未知参数、串联、重定向、
247
+ 命令替换或未知程序的 shell 调用仍保守地走计划门禁。
239
248
  bridge 先把完整 Markdown 计划作为普通消息发出,再弹出“批准,开始执行 /
240
249
  继续规划”决策卡;卡内可填写修改意见。工具在等待期间阻塞且暂停空闲超时,批准后原任务自动继续;
241
250
  继续规划时 agent 会收到意见、修订计划并再次请求确认。门禁无固定十分钟截止,跟随所属 run 的取消
242
251
  信号;停止任务会精确取消该 session 的 pending 卡并撤回。可信部署可设置
243
252
  `DSH_LARK_PLAN_GATE=off` 关闭这层独立门禁(逐工具审批仍按原策略执行);legacy headless adapter 不具备工具回调能力。
244
253
 
254
+ 插件可控的拒绝统一为 `[policy-denial layer=<plan-gate|permission-policy|tool-approval>]`,随后给出
255
+ `reason` 与 `to change`;Harness 自己的 `[sandbox: ...]` 明确归类为 `file-sandbox`。高风险分类器、
256
+ persona 中的只读说明和拒绝文本由 `src/policy/tool-policy.ts` 同一来源生成,因此策略调整不会只改
257
+ 提示词或只改执行钩子。计划门与逐工具审批仍保留不同语义,`/permission allow` 不扩大文件沙箱,
258
+ 也不替代计划确认。
259
+
245
260
  **任务中向你提问(问答卡)**:agent 需要你拍板、确认或补充信息时,通过 `lark_ask_user` 工具弹**问答卡**(单选 / 多选 / 自由文本)。可提交卡片,也可直接回复该卡片输入任意文字;单选/多选没有合适项时,回复文字就是补充答案。系统按被回复的 card messageId 精确匹配 pending 问题,回答后任务自动继续,等待期间运行超时看门狗暂停。(与 `/ask` 的“你主动提问”方向相反。)
246
261
 
247
262
  计划、审批与问答卡提交后会立即显示成功提示、发送一条终态确认并撤回原卡,避免按钮仍停留在聊天中造成“未生效”的误解;失效卡会返回明确错误提示,入站点击与失效原因写入结构化日志。确认或撤回失败不会影响已经提交给 agent 的决策、审批结果或答案。本地人机决策回调会以 JSON 空白流保活,避免 Node HTTP 客户端在等待 5 分钟后切断仍有效的卡片。
@@ -401,8 +416,8 @@ dsh plugin --profile dsh-lark remove dsh-lark-bot
401
416
  自动/人工验证边界见 [`docs/DSH_RC8_AUDIT.md`](docs/DSH_RC8_AUDIT.md)。
402
417
  - **运行时**:Node.js ≥ 22.19(见 `package.json` engines)。
403
418
  - **平台**:Linux / macOS / Windows(飞书 WebSocket 出站长连接,免公网服务器 / 域名 / 内网穿透)。
404
- - 默认 adapter 为官方 **`@deepseek-ai/dsh-sdk-client`**(SDK JSON-RPC runtime,原生 session 续跑 +
405
- token 级流式事件);`DSH_LARK_ADAPTER=acp` 切到官方 **ACP server**(审批卡);`headless` 保留旧版
419
+ - 默认 adapter 为官方 **`@deepseek-ai/dsh-sdk-client`**(SDK JSON-RPC runtime,原生 session 续跑、
420
+ token 级流式事件与 dsh attachment store 原生图片块);`DSH_LARK_ADAPTER=acp` 切到官方 **ACP server**(审批卡);`headless` 保留旧版
406
421
  子进程 fallback;`DSH_LARK_ADAPTER=web` 驱动**本地 dsh web agent**(`session.prompt` +
407
422
  `/api/events.mux`,网页端成为唯一写者,从根上消除多写者会话损坏)。首次启动自动在
408
423
  `~/.dsh/profiles/dsh-lark-sdk`(或 `dsh-lark-acp`)创建 runtime profile。
@@ -458,8 +473,9 @@ SDK 模式下 dsh 原生 session 续跑,headless 模式则把历史注入下
458
473
  | `DSH_LARK_DSH_COMMAND` | `自动发现` | dsh 启动命令;通常无需设置|
459
474
  | `DSH_LARK_DSH_ARGS` | `自动发现` | dsh 启动参数,逗号分隔;通常无需设置|
460
475
  | `DSH_LARK_ADAPTER` | `sdk` | `sdk`(默认,approval answerer)/ `acp`(协议原生审批)/ `headless`(legacy)/ `web`(本地 dsh web agent,单写者)|
461
- | `DSH_LARK_PROVIDER` | `deepseek-official` | 模型 provider|
462
- | `DSH_LARK_MODEL` | `deepseek-v4-flash` | 默认模型|
476
+ | `DSH_LARK_PROVIDER` | 未设置 | 模型 provider;可由对象形式的 dsh 默认模型提供|
477
+ | `DSH_LARK_MODEL` | 未设置 | 默认模型;可由 dsh `agent-default-model` 提供|
478
+ | `DSH_LARK_MODEL_CATALOG_URL` | `https://models.dev/api.json` | provider / 模型能力实时目录或兼容镜像|
463
479
  | `DSH_LARK_MAX_TOKENS` | 未设置 | SDK agent 每请求输出 token 上限|
464
480
  | `DSH_LARK_WEB_URL` | `http://127.0.0.1:3080` | `web` 适配器:本地 dsh web agent 的 base URL|
465
481
  | `DSH_LARK_SESSION_PROJECTION` | `true` | `web` 适配器:启用用户显式绑定后的历史/实时消息投影;绝不自动切换(`0` 关闭)|
package/README_EN.md CHANGED
@@ -139,14 +139,23 @@ Send a normal message to the bot in Feishu to get started. Common commands:
139
139
  | `/model`, `/providers`, `/provider`, `/key` | Open the interactive hub (tap a model or restore the default; management writes use a multi-turn wizard) |
140
140
  | `/model use <provider/model>` | Hot-switch the current session model (a unique bare model ID also works; effective next message, no restart) |
141
141
  | `/model default <id>` | Write the dsh default model `agent-default-model` (admin) |
142
- | `/model add\|remove <provider> <modelId>` | Add / remove a provider model (admin) |
142
+ | `/model add\|remove <provider> <modelId> [--input-modalities text,image]` | Add / remove a provider model and declare vision input (admin) |
143
143
  | `/provider add\|update\|remove <id>` | Manage providers (admin; deepseek-official and custom pi-ai) |
144
144
  | `/key set\|remove\|list <ref>` | Manage dsh credentials (set / remove require admin) |
145
145
  | `/ask <question>` | Send a Q&A card; the answer is written back to session context |
146
146
  | `/invite user\|admin\|group <id>`、`/invite list`、`/invite remove user\|group <id>` | Manage the access allowlist (mutating commands require admin) |
147
147
  | `/help` | Show help |
148
148
 
149
- Images in Feishu messages are downloaded to the local media directory and passed to dsh; text files are read and their content is injected into the task context.
149
+ Feishu images are detected by content as PNG/JPEG/WebP/GIF and receive a safe extension. The default
150
+ SDK validates them through dsh's attachment store and sends native image blocks instead of path text.
151
+ Unreadable or unsupported images fail explicitly, and the agent is instructed never to substitute another
152
+ workspace image. Text files are read and injected into the task context.
153
+ The `/model` card merges the dsh default into its switchable catalogue even when a provider's explicit
154
+ list omits it, and uses compact distinguishing labels with at most two buttons per mobile row. Provider
155
+ names, models, input modalities, and reasoning-effort options are discovered from the models.dev runtime
156
+ catalogue and cached in memory for 15 minutes. If it is unavailable, only explicit dsh settings and the
157
+ configured default are shown—there is no hardcoded fallback list. Override the feed with
158
+ `DSH_LARK_MODEL_CATALOG_URL`; model commands and the wizard preserve `inputModalities`.
150
159
 
151
160
  **Message-level DSH session sync (`web` adapter)**: `/session` lists metadata only for non-subagent sessions
152
161
  in the current canonical workspace—never message bodies. The confirmation card freezes the title, ID,
@@ -239,7 +248,8 @@ reject `web`, because a shared Web agent broadcast stream cannot isolate session
239
248
  changes, scripts, or other substantial/high-risk actions. A runtime pre-execute policy denies writes, deletes,
240
249
  moves, non-read-only shell commands and `run_code` until a plan is approved. Each approval grants only the next
241
250
  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,
251
+ `ls`, `find`, `rg`, and `git status/log/diff` run directly. Inert SDK `bash` metadata (`description`,
252
+ `workdir`, and `run_in_background:false`) does not change that decision; unknown metadata, shell chaining,
243
253
  redirection, command substitution, and unknown executables remain behind the conservative plan gate. The bridge sends the complete Markdown plan as a normal
244
254
  message, then a card with **Approve and execute** / **Continue planning** plus optional feedback. The tool blocks
245
255
  and pauses the idle watchdog; approval resumes the original turn, while revision returns the feedback and requires
@@ -247,6 +257,13 @@ another plan. There is no fixed ten-minute deadline: the gate follows the owning
247
257
  stopping it cancels and recalls only that session's pending card. Trusted deployments may set
248
258
  `DSH_LARK_PLAN_GATE=off` to disable this separate gate (ordinary per-tool approval still applies). The legacy headless adapter cannot use callback tools.
249
259
 
260
+ Plugin-controlled refusals now use `[policy-denial layer=<plan-gate|permission-policy|tool-approval>]`
261
+ followed by `reason` and `to change`; Harness `[sandbox: ...]` errors are identified as the
262
+ `file-sandbox` layer. The high-risk classifier, persona read-only guidance, and denial text share
263
+ `src/policy/tool-policy.ts`, so policy changes cannot update only the prompt or only enforcement.
264
+ The plan gate and per-tool approval retain distinct semantics: `/permission allow` neither expands
265
+ the file sandbox nor replaces plan confirmation.
266
+
250
267
  **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.)
251
268
 
252
269
  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.
@@ -394,7 +411,7 @@ See [`docs/QUICK_START.md`](docs/QUICK_START.md) for installation details, state
394
411
  - **DeepSeek Harness (`dsh`)**: verified against **dsh 0.1.0-rc.8** (last verified 2026-08-22: clean temporary install, SDK JSON-RPC / ACP initialize, tool/approval, live-session resume, and restart-collision probes), connected through the official `@deepseek-ai/dsh-sdk-client` / `@deepseek-ai/dsh-acp`; see [`docs/COMPATIBILITY.md`](docs/COMPATIBILITY.md) for exact pins and probing, [`docs/adapter-notes.md`](docs/adapter-notes.md) for adapter details, and [`docs/DSH_RC8_AUDIT.md`](docs/DSH_RC8_AUDIT.md) for rc.8 risks and verification boundaries.
395
412
  - **Runtime**: Node.js ≥ 22.19 (see `engines` in `package.json`).
396
413
  - **Platform**: Linux / macOS / Windows (Feishu outbound WebSocket long connection; no public server, domain or tunneling required).
397
- - The default adapter is the official **`@deepseek-ai/dsh-sdk-client`** (SDK JSON-RPC runtime with native continuation, streaming events, and the rc.8 approval answerer); `DSH_LARK_ADAPTER=acp` switches to the official **ACP server** with protocol-native approval; `headless` keeps the legacy subprocess fallback; `DSH_LARK_ADAPTER=web` drives the **local dsh web agent** (`session.prompt` + `/api/events.mux` — the web agent becomes the single writer, eliminating multi-writer session-log corruption at the root). On first start the bot creates the runtime profile at `~/.dsh/profiles/dsh-lark-sdk` (or `dsh-lark-acp`).
414
+ - The default adapter is the official **`@deepseek-ai/dsh-sdk-client`** (SDK JSON-RPC runtime with native continuation, streaming events, dsh attachment-store image blocks, and the rc.8 approval answerer); `DSH_LARK_ADAPTER=acp` switches to the official **ACP server** with protocol-native approval; `headless` keeps the legacy subprocess fallback; `DSH_LARK_ADAPTER=web` drives the **local dsh web agent** (`session.prompt` + `/api/events.mux` — the web agent becomes the single writer, eliminating multi-writer session-log corruption at the root). On first start the bot creates the runtime profile at `~/.dsh/profiles/dsh-lark-sdk` (or `dsh-lark-acp`).
398
415
 
399
416
  ## Known limitations
400
417
 
@@ -429,8 +446,9 @@ Core environment variables:
429
446
  | `DSH_LARK_DSH_COMMAND` | auto-discovered | dsh launch command; usually not needed |
430
447
  | `DSH_LARK_DSH_ARGS` | auto-discovered | dsh launch args, comma-separated; usually not needed |
431
448
  | `DSH_LARK_ADAPTER` | `sdk` | `sdk` (default, approval answerer) / `acp` (protocol-native approval) / `headless` (legacy) / `web` (local dsh web agent, single writer) |
432
- | `DSH_LARK_PROVIDER` | `deepseek-official` | Model provider |
433
- | `DSH_LARK_MODEL` | `deepseek-v4-flash` | Default model |
449
+ | `DSH_LARK_PROVIDER` | unset | Model provider; may come from an object-form dsh default selection |
450
+ | `DSH_LARK_MODEL` | unset | Default model; may come from dsh `agent-default-model` |
451
+ | `DSH_LARK_MODEL_CATALOG_URL` | `https://models.dev/api.json` | Live provider/model capability feed or compatible mirror |
434
452
  | `DSH_LARK_MAX_TOKENS` | unset | Per-request output token cap for SDK agents |
435
453
  | `DSH_LARK_WEB_URL` | `http://127.0.0.1:3080` | `web` adapter: base URL of the local dsh web agent |
436
454
  | `DSH_LARK_SESSION_PROJECTION` | `true` | `web` adapter: enable history/live message projection after explicit user binding; never auto-switches (`0` disables) |
package/dist/approval.js CHANGED
@@ -1,4 +1,4 @@
1
- // src/notify/plan-tool.ts
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
4
  "basename",
@@ -36,6 +36,17 @@ var READ_ONLY_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set([
36
36
  "status"
37
37
  ]);
38
38
  var SHELL_CONTROL_SYNTAX = /[\n\r;&|<>`]|\$\(|\$\{/u;
39
+ function policyDenialText(denial) {
40
+ return `[policy-denial layer=${denial.layer}] denied by ${denial.layer}: ${denial.reason}
41
+ to change: ${denial.toChange}`;
42
+ }
43
+ function toolApprovalDenial(toolName) {
44
+ return {
45
+ layer: "tool-approval",
46
+ reason: `the user rejected the one-shot approval for tool ${toolName}`,
47
+ toChange: "choose a safer alternative or ask the user before requesting approval again"
48
+ };
49
+ }
39
50
  function isHighRiskTool(ctx, execution) {
40
51
  if (execution.name === "lark_request_plan_approval") return false;
41
52
  if (execution.name === "run_code") return true;
@@ -88,9 +99,13 @@ function isSimpleReadOnlyShellCommand(rawArguments) {
88
99
  }
89
100
  function shellCommand(rawArguments) {
90
101
  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];
102
+ const record = rawArguments;
103
+ const allowedKeys = /* @__PURE__ */ new Set(["command", "description", "workdir", "run_in_background"]);
104
+ if (Object.keys(record).some((key) => !allowedKeys.has(key))) return void 0;
105
+ if (record.description !== void 0 && typeof record.description !== "string") return void 0;
106
+ if (record.workdir !== void 0 && typeof record.workdir !== "string") return void 0;
107
+ if (record.run_in_background !== void 0 && record.run_in_background !== false) return void 0;
108
+ const command = record.command;
94
109
  return typeof command === "string" ? command : void 0;
95
110
  }
96
111
  if (typeof rawArguments !== "string") return void 0;
@@ -134,7 +149,7 @@ function apply(ctx, config = {}) {
134
149
  inFlightGrant.delete(request.agent);
135
150
  return "allowed-once";
136
151
  }
137
- return requestBridgeApproval(config, request, next);
152
+ return (await requestBridgeApproval(config, request, next)).outcome;
138
153
  });
139
154
  approvalCtx.on("tools/pre-execute", async (execution, next) => {
140
155
  if (!isHighRiskTool(approvalCtx, execution)) return next();
@@ -144,11 +159,17 @@ function apply(ctx, config = {}) {
144
159
  reason: approvalReason(execution),
145
160
  toolInput: execution.arguments
146
161
  };
147
- const outcome = await requestBridgeApproval(config, request, async () => "unavailable");
148
- if (outcome !== "allowed-once") {
162
+ const decision = await requestBridgeApproval(config, request, async () => "unavailable");
163
+ if (decision.outcome !== "allowed-once") {
149
164
  return {
150
165
  kind: "deny",
151
- reason: outcome === "rejected" ? "The user rejected this one-shot tool execution. Continue with a safer alternative." : "This tool execution was not approved and remains blocked."
166
+ reason: policyDenialText(
167
+ decision.denial ?? (decision.outcome === "rejected" ? toolApprovalDenial(execution.name) : {
168
+ layer: "tool-approval",
169
+ reason: `tool ${execution.name} did not receive approval (${decision.outcome})`,
170
+ toChange: "retry only after approval is available, or choose a safer alternative"
171
+ })
172
+ )
152
173
  };
153
174
  }
154
175
  if (execution.agent) inFlightGrant.set(execution.agent, execution.name);
@@ -162,9 +183,11 @@ function apply(ctx, config = {}) {
162
183
  async function requestBridgeApproval(config, request, next) {
163
184
  const endpoint = config.endpoint ?? process.env.DSH_LARK_APPROVAL_URL;
164
185
  const token = config.token ?? process.env.DSH_LARK_NOTIFY_TOKEN;
165
- if (!endpoint || !token) return next();
186
+ if (!endpoint || !token) return { outcome: await next() };
166
187
  const sessionId = request.agent?.session?.id;
167
- if (sessionId === void 0 || typeof request.toolName !== "string") return "unavailable";
188
+ if (sessionId === void 0 || typeof request.toolName !== "string") {
189
+ return { outcome: "unavailable" };
190
+ }
168
191
  try {
169
192
  const response = await fetch(endpoint, {
170
193
  method: "POST",
@@ -180,9 +203,12 @@ async function requestBridgeApproval(config, request, next) {
180
203
  ...request.signal === void 0 ? {} : { signal: request.signal }
181
204
  });
182
205
  const body = await response.json();
183
- return response.ok && body.ok === true && isOutcome(body.outcome) ? body.outcome : "unavailable";
206
+ return response.ok && body.ok === true && isOutcome(body.outcome) ? {
207
+ outcome: body.outcome,
208
+ ...isPolicyDenial(body.denial) ? { denial: body.denial } : {}
209
+ } : { outcome: "unavailable" };
184
210
  } catch {
185
- return request.signal?.aborted ? "cancelled" : "unavailable";
211
+ return { outcome: request.signal?.aborted ? "cancelled" : "unavailable" };
186
212
  }
187
213
  }
188
214
  function approvalReason(execution) {
@@ -194,6 +220,11 @@ function approvalReason(execution) {
194
220
  function isOutcome(value) {
195
221
  return value === "allowed-once" || value === "rejected" || value === "cancelled" || value === "unavailable";
196
222
  }
223
+ function isPolicyDenial(value) {
224
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
225
+ const denial = value;
226
+ return (denial.layer === "plan-gate" || denial.layer === "permission-policy" || denial.layer === "tool-approval" || denial.layer === "file-sandbox") && typeof denial.reason === "string" && typeof denial.toChange === "string";
227
+ }
197
228
  export {
198
229
  apply,
199
230
  inject,
@@ -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 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":[]}
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":[]}