pi-web-ui 0.94.1 → 0.96.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/CHANGELOG.md +91 -2
- package/README.md +5 -6
- package/README.zh-CN.md +4 -5
- package/bin/pi-web-ui.mjs +910 -237
- package/dist/server/agent-service.js +2655 -231
- package/dist/server/approval-rules.js +596 -0
- package/dist/server/attachment-store.js +129 -0
- package/dist/server/attachments.js +92 -174
- package/dist/server/bg-servers.js +45 -7
- package/dist/server/claim-files-tool.js +4 -8
- package/dist/server/claim-store.js +4 -2
- package/dist/server/client-state.js +67 -14
- package/dist/server/compact-context-tool.js +126 -0
- package/dist/server/composer-drafts.js +9 -0
- package/dist/server/context-budget.js +317 -0
- package/dist/server/control-socket.js +57 -28
- package/dist/server/conversation-read-tool.js +7 -17
- package/dist/server/dangling-tools.js +229 -0
- package/dist/server/delegate-task.js +25 -28
- package/dist/server/dsh/dsh-agent-service.js +69 -66
- package/dist/server/edit-soft-tool.js +84 -16
- package/dist/server/eval-tool.js +588 -0
- package/dist/server/file-archives.js +16 -5
- package/dist/server/files-service.js +65 -18
- package/dist/server/goal-service.js +371 -77
- package/dist/server/hashline-engine.js +703 -0
- package/dist/server/host-guard.js +94 -0
- package/dist/server/host-metrics.js +26 -2
- package/dist/server/i18n.js +3 -3
- package/dist/server/index.js +456 -80
- package/dist/server/lsp-tool.js +1371 -0
- package/dist/server/mcp-bridge.js +47 -3
- package/dist/server/model-admin.js +112 -32
- package/dist/server/office-parse.js +375 -0
- package/dist/server/patch-tool.js +85 -0
- package/dist/server/permission-preset.js +25 -0
- package/dist/server/plan-manager.js +114 -0
- package/dist/server/plugin-api-catalog.js +296 -0
- package/dist/server/plugin-catalog-sync.js +36 -19
- package/dist/server/plugin-catalog.js +11 -4
- package/dist/server/plugin-facilities.js +92 -21
- package/dist/server/plugin-install-spec.js +196 -0
- package/dist/server/plugin-installer.js +73 -0
- package/dist/server/plugin-llm.js +71 -65
- package/dist/server/plugin-manifest-validate.js +305 -0
- package/dist/server/plugin-project.js +117 -13
- package/dist/server/plugin-tool-guard.js +120 -0
- package/dist/server/plugin-updater.js +115 -12
- package/dist/server/plugins.js +950 -205
- package/dist/server/present-files-tool.js +9 -12
- package/dist/server/process-utils.js +16 -6
- package/dist/server/prompt-composer.js +9 -0
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/read-tool.js +69 -30
- package/dist/server/resolve-global-sdk.js +30 -16
- package/dist/server/schedule-agent-tool.js +12 -15
- package/dist/server/scheduler-tasks.js +6 -0
- package/dist/server/sdk-origin.js +18 -2
- package/dist/server/serialize.js +111 -14
- package/dist/server/settings-service.js +112 -3
- package/dist/server/skill-tool.js +5 -7
- package/dist/server/subagent-templates.js +51 -0
- package/dist/server/subagents.js +83 -66
- package/dist/server/terminals.js +248 -81
- package/dist/server/tool-approval.js +84 -0
- package/dist/server/tool-manager.js +237 -12
- package/dist/server/tool-overrides.js +59 -0
- package/dist/server/update-check.js +28 -1
- package/dist/server/uploads.js +17 -2
- package/dist/server/wait-subscription-scan.js +18 -21
- package/dist/server/workspace-snapshot.js +113 -0
- package/dist/server/ws-client-id.js +29 -0
- package/dist/server/ws-pending-queue.js +60 -0
- package/extensions/webui.ts +60 -2
- package/package.json +5 -3
- package/plugin-sdk/README.md +25 -0
- package/plugin-sdk/index.d.ts +31 -38
- package/plugin-sdk/index.mjs +35 -19
- package/plugins/catalog.json +40 -0
- package/themes/aetheris.css +457 -0
- package/themes/ayu-light.css +6 -6
- package/themes/catppuccin-latte.css +6 -6
- package/themes/claude-code-dark.css +144 -0
- package/themes/codex.css +6 -6
- package/themes/everforest-light.css +6 -6
- package/themes/geist.css +6 -6
- package/themes/gruvbox-light.css +6 -6
- package/themes/kanagawa-lotus.css +6 -6
- package/themes/rose-pine-dawn.css +6 -6
- package/themes/solarized-light.css +6 -6
- package/themes/vs-code-dark.css +146 -0
- package/themes/zhupi-dark.css +658 -0
- package/themes/zhupi.css +711 -0
- package/web/dist/assets/{TerminalPanel-MVoxpJOA.js → TerminalPanel-DDChcYOh.js} +1 -1
- package/web/dist/assets/index-Do9RgJC3.js +366 -0
- package/web/dist/assets/index-DryOsILO.css +1 -0
- package/web/dist/assets/{markdown-eUQn_o9D.js → markdown-DXwnfD9T.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/dist/assets/index-B3S9MxnN.css +0 -1
- package/web/dist/assets/index-DVLrHI2E.js +0 -364
package/dist/server/terminals.js
CHANGED
|
@@ -24,7 +24,7 @@ import "./patch-node-pty.js";
|
|
|
24
24
|
import { spawn } from "node-pty";
|
|
25
25
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
26
26
|
import { Type } from "typebox";
|
|
27
|
-
import {
|
|
27
|
+
import { pick } from "./i18n.js";
|
|
28
28
|
/** Location of the command list for a project: <workspaceRoot>/.pi/commands.json */
|
|
29
29
|
export function commandsFilePath(workspaceRoot) {
|
|
30
30
|
return join(workspaceRoot, ".pi", "commands.json");
|
|
@@ -147,8 +147,10 @@ export function terminalIdleNotifyLines() {
|
|
|
147
147
|
/** 哨兵行:命令执行完后由 shell 打印,携带真实退出码。正则只匹配数字,
|
|
148
148
|
* 因此不会误匹配回显里的 printf 格式串 `[pi-exit:%s]`。 */
|
|
149
149
|
const BASH_SENTINEL_RE = /\[pi-exit:(\d+)\]/g;
|
|
150
|
+
/** bash 阻塞循环 collected 的累积上限:超限丢最旧一半(见 bash 工具执行循环)。 */
|
|
151
|
+
const BASH_COLLECT_MAX = 2 * 1024 * 1024;
|
|
150
152
|
/**
|
|
151
|
-
*
|
|
153
|
+
* 把任意命令(含多行脚本)构造成一行交互 shell 命令:执行 + 捕获退出码。
|
|
152
154
|
*
|
|
153
155
|
* 单行很关键:整行先被 shell 完整解析再执行,命令中途读 stdin 也不会吃掉
|
|
154
156
|
* 后续哨兵;也避开交互 shell 的 bracketed-paste 对多行输入的特殊处理。
|
|
@@ -156,15 +158,6 @@ const BASH_SENTINEL_RE = /\[pi-exit:(\d+)\]/g;
|
|
|
156
158
|
*/
|
|
157
159
|
export function buildTerminalBashLine(command, tailFile) {
|
|
158
160
|
const trimmed = command.replace(/\s+$/, "");
|
|
159
|
-
let body = trimmed;
|
|
160
|
-
if (trimmed.includes("\n")) {
|
|
161
|
-
body = `eval $'${trimmed
|
|
162
|
-
.replace(/\\/g, "\\\\")
|
|
163
|
-
.replace(/'/g, "\\'")
|
|
164
|
-
.replace(/\r/g, "\\r")
|
|
165
|
-
.replace(/\n/g, "\\n")
|
|
166
|
-
.replace(/\t/g, "\\t")}'`;
|
|
167
|
-
}
|
|
168
161
|
// 退出码取【第一个】命令(真正干活的那个)而非管道末尾命令:`head`/`grep`/`tail`
|
|
169
162
|
// 在管道末尾会把退出码吞成自己的(head 恒 0、grep 无命恒 1)。`${PIPESTATUS:-$?}`
|
|
170
163
|
// 在 bash 里取 PIPESTATUS[0](首命令),busybox ash/dash 无 PIPESTATUS 时退化为 `$?`。
|
|
@@ -172,12 +165,84 @@ export function buildTerminalBashLine(command, tailFile) {
|
|
|
172
165
|
// 留下的 141 不是真失败,而是“按要求截断=成功”。只有恰好 141 才转 0,真失败照报。
|
|
173
166
|
// tailFile:`cmd > log 2>&1 | tail -N`——拆掉 tail 后 stdout 进文件、终端为空;
|
|
174
167
|
// 在哨兵前补一个 `tail -N log` 让模型看到日志尾部,退出码仍是底层命令的。
|
|
175
|
-
// eslint-disable-next-line no-useless-escape -- \$? must reach the shell literally
|
|
176
|
-
const rcGuard = `__pi_rc=\${PIPESTATUS:-\$?}; [ "$__pi_rc" -eq 141 ] && __pi_rc=0`;
|
|
177
168
|
const tailPart = tailFile
|
|
178
169
|
? `; tail -n ${Math.max(1, Math.floor(tailFile.lines))} -- '${tailFile.file.replace(/'/g, `'\\''`)}'`
|
|
179
170
|
: "";
|
|
180
|
-
|
|
171
|
+
// eslint-disable-next-line no-useless-escape -- \$? must reach the shell literally
|
|
172
|
+
const sentinel = `__pi_rc=\${PIPESTATUS:-\$?}; [ "$__pi_rc" -eq 141 ] && __pi_rc=0${tailPart}; printf '\\n[pi-exit:%s]\\n' "$__pi_rc"`;
|
|
173
|
+
if (trimmed.includes("\n")) {
|
|
174
|
+
// 多行路径(行为保持):$'...' 转义后是一行物理输入,引号/换行全部字面化,
|
|
175
|
+
// 以 `'` 收尾——`; ` 同行拼接哨兵不会被尾注释/尾管道/续行吞掉。
|
|
176
|
+
const body = `eval $'${trimmed
|
|
177
|
+
.replace(/\\/g, "\\\\")
|
|
178
|
+
.replace(/'/g, "\\'")
|
|
179
|
+
.replace(/\r/g, "\\r")
|
|
180
|
+
.replace(/\n/g, "\\n")
|
|
181
|
+
.replace(/\t/g, "\\t")}'`;
|
|
182
|
+
return `${body}; ${sentinel}`;
|
|
183
|
+
}
|
|
184
|
+
// 单行路径:哨兵独占一行追加。行内 `; ` 拼接会被常见形态吞掉——
|
|
185
|
+
// 尾注释(`cmd # note; printf…` 整段进注释)、尾管道(`cmd |; …` 语法错误)。
|
|
186
|
+
// 独占一行后这两类安全;尾随续行符/未闭合引号仍会把哨兵行并入命令或吞进
|
|
187
|
+
// 字符串——这类形态无法安全续行,直接拒注入(调用方按无退出码模式执行并注明)。
|
|
188
|
+
if (sentinelUnsafeReason(trimmed))
|
|
189
|
+
return trimmed;
|
|
190
|
+
return `${trimmed}\n${sentinel}`;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 判定命令能否安全追加「独占一行的退出码哨兵」;返回拒注入原因或 null(可注入)。
|
|
194
|
+
*
|
|
195
|
+
* - 尾随续行符 `\`:下一行被并入本命令,哨兵行成为命令的一部分。
|
|
196
|
+
* - 未闭合引号:哨兵行被吞进字符串字面量,永远不会作为代码执行。
|
|
197
|
+
* - 多行命令:经 `$'...'` 转义单行化后引号/换行全部字面化,总是安全 → null。
|
|
198
|
+
*
|
|
199
|
+
* 扫描按 POSIX 引号语义跟踪 `'`/`"`/`` ` `` 三种引号与转义;`$'...'`(ANSI-C
|
|
200
|
+
* 引号)内 `\'` 不闭合引号,也一并识别。
|
|
201
|
+
*/
|
|
202
|
+
export function sentinelUnsafeReason(command) {
|
|
203
|
+
const trimmed = command.replace(/\s+$/, "");
|
|
204
|
+
if (trimmed.includes("\n"))
|
|
205
|
+
return null;
|
|
206
|
+
let quote = null;
|
|
207
|
+
// 当前单引号是否 $' 开头(ANSI-C 引号,内含转义语义)
|
|
208
|
+
let ansiC = false;
|
|
209
|
+
let escaped = false;
|
|
210
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
211
|
+
const ch = trimmed[i];
|
|
212
|
+
if (quote) {
|
|
213
|
+
if (escaped) {
|
|
214
|
+
escaped = false;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
// POSIX 单引号内 `\` 是普通字符;ANSI-C/双引号/反引号内 `\` 转义下一字符
|
|
218
|
+
if (ch === "\\" && (quote !== "'" || ansiC)) {
|
|
219
|
+
escaped = true;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (ch === quote)
|
|
223
|
+
quote = null;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (escaped) {
|
|
227
|
+
// 引号外的转义:被转义字符原样跳过(`\"` 不开引号等)
|
|
228
|
+
escaped = false;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (ch === "\\") {
|
|
232
|
+
escaped = true;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
236
|
+
quote = ch;
|
|
237
|
+
ansiC = ch === "'" && i > 0 && trimmed[i - 1] === "$";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (quote !== null)
|
|
241
|
+
return "unclosed_quote";
|
|
242
|
+
// 扫描结束仍在转义态 = 最后一个字符是引号外的 `\`(尾随续行符)
|
|
243
|
+
if (escaped)
|
|
244
|
+
return "trailing_backslash";
|
|
245
|
+
return null;
|
|
181
246
|
}
|
|
182
247
|
/** 顶层(引号/反引号/转义外)按 `|` 拆分的管道元素。 */
|
|
183
248
|
export function splitTopLevelPipes(cmd) {
|
|
@@ -230,6 +295,11 @@ function parseTailLines(rest) {
|
|
|
230
295
|
* - `tail -f` / `tail --follow`(长驻观察)也不拆。
|
|
231
296
|
*/
|
|
232
297
|
export function detectTrailingLimiter(command) {
|
|
298
|
+
// 含命令替换($() / 反引号)的命令保守跳过拆管:splitTopLevelPipes 只跟踪引号
|
|
299
|
+
// 与顶层 `|`,不解析括号嵌套——`cmd $(x | y)` 里替换内的管道会被误判为顶层
|
|
300
|
+
// 管道段,拆掉后直接破坏命令语义。保守正确优先,宁可少优化。
|
|
301
|
+
if (/\$\(|`/.test(command))
|
|
302
|
+
return null;
|
|
233
303
|
const parts = splitTopLevelPipes(command);
|
|
234
304
|
if (parts.length < 2)
|
|
235
305
|
return null;
|
|
@@ -1368,34 +1438,50 @@ export class TerminalManager {
|
|
|
1368
1438
|
* Kill the native PTY (issue #215:Windows ConPTY 关机死锁;issue #269:MSYS2 控制台耗尽死锁)。
|
|
1369
1439
|
*
|
|
1370
1440
|
* Windows 下:
|
|
1371
|
-
* 1. 若进程已经退出(entry.exited
|
|
1372
|
-
*
|
|
1373
|
-
* 2. 若进程尚未退出(!entry.exited
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1441
|
+
* 1. 若进程已经退出(entry.exited):直接调 pty.kill() 释放 HPCON 句柄
|
|
1442
|
+
* (子进程已死、管道见 EOF,绝不会死锁);关机路径跳过,由 OS 回收。
|
|
1443
|
+
* 2. 若进程尚未退出(!entry.exited):先向 PTY 写入 \x03exit\r 优雅关闭(让
|
|
1444
|
+
* bash 正常触发清理钩子),300ms 后仍活着再调 pty.kill() 兜底——node-pty 在
|
|
1445
|
+
* Windows 上走 console-process-list 做**全树**清理。不再使用裸
|
|
1446
|
+
* process.kill(pid)(TerminateProcess):它只杀 ConPTY 附着的单一进程,
|
|
1447
|
+
* 且从拿到 pid 到调用之间隔着一个 PID 复用误杀窗口。
|
|
1448
|
+
* 3. 关机(shutdown=true):只做优雅写,跳过 pty.kill() 与 300ms 兜底,由 OS
|
|
1449
|
+
* 回收(保持 50ms 内退出的关机预算)。
|
|
1450
|
+
* 非 Windows 原样直调 pty.kill()(POSIX 下 shell 是直接子进程,pty.kill 足够,
|
|
1451
|
+
* 保持既有行为不做额外延迟)。
|
|
1378
1452
|
*/
|
|
1379
1453
|
killNative(entry, shutdown = false) {
|
|
1380
1454
|
if (process.platform === "win32") {
|
|
1381
|
-
if (
|
|
1382
|
-
|
|
1383
|
-
if (typeof pid === "number") {
|
|
1384
|
-
try {
|
|
1385
|
-
// 优雅关闭:先发 Ctrl+C,再发 exit\r
|
|
1386
|
-
entry.pty.write("\x03exit\r");
|
|
1387
|
-
}
|
|
1388
|
-
catch { }
|
|
1455
|
+
if (entry.exited) {
|
|
1456
|
+
if (!shutdown) {
|
|
1389
1457
|
try {
|
|
1390
|
-
|
|
1458
|
+
entry.pty.kill();
|
|
1391
1459
|
}
|
|
1392
1460
|
catch {
|
|
1393
1461
|
// already dead
|
|
1394
1462
|
}
|
|
1395
1463
|
}
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
// 优雅关闭:先发 Ctrl+C,再发 exit\r
|
|
1467
|
+
try {
|
|
1468
|
+
entry.pty.write("\x03exit\r");
|
|
1396
1469
|
}
|
|
1470
|
+
catch { }
|
|
1397
1471
|
if (shutdown)
|
|
1398
1472
|
return;
|
|
1473
|
+
const t = setTimeout(() => {
|
|
1474
|
+
if (entry.exited)
|
|
1475
|
+
return;
|
|
1476
|
+
try {
|
|
1477
|
+
entry.pty.kill();
|
|
1478
|
+
}
|
|
1479
|
+
catch {
|
|
1480
|
+
// already dead
|
|
1481
|
+
}
|
|
1482
|
+
}, 300);
|
|
1483
|
+
t.unref?.();
|
|
1484
|
+
return;
|
|
1399
1485
|
}
|
|
1400
1486
|
try {
|
|
1401
1487
|
entry.pty.kill();
|
|
@@ -1530,29 +1616,29 @@ export function makeTerminalBashTool(terminals, opts) {
|
|
|
1530
1616
|
return defineTool({
|
|
1531
1617
|
name: "bash",
|
|
1532
1618
|
label: "Run bash command",
|
|
1533
|
-
description:
|
|
1534
|
-
"persist=false (default
|
|
1535
|
-
"persist=true:
|
|
1536
|
-
"
|
|
1537
|
-
"
|
|
1538
|
-
"
|
|
1539
|
-
|
|
1540
|
-
promptSnippet: bilingual("run shell commands (persist=true keeps the terminal alive across calls)", "运行 shell 命令(persist=true 让终端跨调用保持存活)"),
|
|
1619
|
+
description: "Run a shell command in a visible terminal; returns full output and exit code. " +
|
|
1620
|
+
"persist=false (default): a fresh one-shot terminal per call; the shell exits after the command but its output stays viewable. " +
|
|
1621
|
+
"persist=true: runs in the persistent 'ai-bash' terminal — shell state (cd/venv/ssh) is retained across calls; " +
|
|
1622
|
+
"terminal_read/terminal_input/terminal_key observe or interact; terminal_wait blocks on backgrounded commands. " +
|
|
1623
|
+
"Run the bare command — never pipe through head/tail/more/less (output is returned complete; pipes hide live progress); use the head/tail params instead. " +
|
|
1624
|
+
"For interactive commands (REPLs, y/n prompts) set persist=true and drive them with terminal_input / terminal_key.",
|
|
1625
|
+
promptSnippet: "run shell commands (persist=true keeps the terminal alive across calls)",
|
|
1541
1626
|
parameters: Type.Object({
|
|
1542
|
-
command: Type.String({ description:
|
|
1543
|
-
timeout: Type.Optional(Type.Number({ description:
|
|
1627
|
+
command: Type.String({ description: "The shell command to run" }),
|
|
1628
|
+
timeout: Type.Optional(Type.Number({ description: "Optional timeout in seconds" })),
|
|
1544
1629
|
persist: Type.Optional(Type.Boolean({
|
|
1545
|
-
description:
|
|
1630
|
+
description: "true → run in the persistent 'ai-bash' terminal: shell state (cd/venv/ssh) is retained across calls. " +
|
|
1631
|
+
"false (default) → one-shot terminal that exits when the command finishes; output stays viewable.",
|
|
1546
1632
|
})),
|
|
1547
1633
|
head: Type.Optional(Type.Integer({
|
|
1548
1634
|
minimum: 1,
|
|
1549
1635
|
maximum: 5000,
|
|
1550
|
-
description:
|
|
1636
|
+
description: "Only return the FIRST N lines of output (like `| head -N`); prefer this over piping through head.",
|
|
1551
1637
|
})),
|
|
1552
1638
|
tail: Type.Optional(Type.Integer({
|
|
1553
1639
|
minimum: 1,
|
|
1554
1640
|
maximum: 5000,
|
|
1555
|
-
description:
|
|
1641
|
+
description: "Only return the LAST N lines of output (like `| tail -N`); prefer this over piping through tail.",
|
|
1556
1642
|
})),
|
|
1557
1643
|
}),
|
|
1558
1644
|
execute: async (_id, p, signal) => {
|
|
@@ -1583,6 +1669,8 @@ export function makeTerminalBashTool(terminals, opts) {
|
|
|
1583
1669
|
void terminals.inputChecked(termId, "exit\r");
|
|
1584
1670
|
};
|
|
1585
1671
|
const idleMs = Math.max(0, opts.idleMs());
|
|
1672
|
+
const maxForegroundMs = Math.max(0, opts.maxForegroundMs?.() ?? 0);
|
|
1673
|
+
const startTime = Date.now();
|
|
1586
1674
|
const deadline = p.timeout && p.timeout > 0 ? Date.now() + p.timeout * 1000 : null;
|
|
1587
1675
|
// 拆掉模型常写的尾部输出限制/过滤管道(`| tail -N` / `| less` / `| more` / `| cat`):
|
|
1588
1676
|
// 这类管道在终端里 ①缓冲输出——可见终端全程哑火、无法感知实时进度;②吞掉真实退出码——
|
|
@@ -1612,8 +1700,16 @@ export function makeTerminalBashTool(terminals, opts) {
|
|
|
1612
1700
|
let collected = "";
|
|
1613
1701
|
let cursor = start;
|
|
1614
1702
|
let lastDataAt = Date.now();
|
|
1703
|
+
// 尾随续行符/未闭合引号的命令无法安全注入哨兵(哨兵行会被并入命令或
|
|
1704
|
+
// 吞进字符串):按无退出码模式执行,靠静默/超时/总时长解阻收尾并注明。
|
|
1705
|
+
const sentinelUnsafe = sentinelUnsafeReason(runCommand);
|
|
1706
|
+
const sentinelNote = sentinelUnsafe
|
|
1707
|
+
? pick(lang, `\n[注:命令以${sentinelUnsafe === "trailing_backslash" ? "续行符 \\" : "未闭合引号"}结尾,无法注入退出码哨兵——已按无退出码模式执行,拿不到真实 exit code。建议拆分成更简单的单行命令重试。]`, `\n[Note: the command ends with ${sentinelUnsafe === "trailing_backslash" ? "a line-continuation backslash" : "an unclosed quote"}; the exit-code sentinel could not be injected — it ran without exit-code detection. Prefer splitting it into simpler single-line commands.]`, "terminals.bash.nosentinel.note")
|
|
1708
|
+
: "";
|
|
1615
1709
|
// 标记「有哨兵命令在跑」:terminal_wait 据此区分等待与空闲。
|
|
1616
|
-
|
|
1710
|
+
// 无哨兵命令没有可等待的哨兵,标记反而误导 terminal_wait。
|
|
1711
|
+
if (!sentinelUnsafe)
|
|
1712
|
+
terminals.setSentinelPending(termId, true);
|
|
1617
1713
|
const inputErr = terminals.inputChecked(termId, buildTerminalBashLine(runCommand, tailFile) + "\r");
|
|
1618
1714
|
if (inputErr)
|
|
1619
1715
|
throw new Error(inputErr);
|
|
@@ -1631,6 +1727,12 @@ export function makeTerminalBashTool(terminals, opts) {
|
|
|
1631
1727
|
collected += read.data;
|
|
1632
1728
|
cursor = read.cursor;
|
|
1633
1729
|
lastDataAt = Date.now();
|
|
1730
|
+
// 长跑/刷屏命令的累积无上限会拖爆服务进程内存。哨兵判定只看尾部
|
|
1731
|
+
// 8KB(lastSentinel),超限丢最旧一半、保留近期输出即可;
|
|
1732
|
+
// 命令结束后的 cleanBashOutput 语义不受影响。
|
|
1733
|
+
if (collected.length > BASH_COLLECT_MAX) {
|
|
1734
|
+
collected = collected.slice(collected.length - BASH_COLLECT_MAX / 2);
|
|
1735
|
+
}
|
|
1634
1736
|
}
|
|
1635
1737
|
const m = lastSentinel(collected);
|
|
1636
1738
|
if (m) {
|
|
@@ -1652,11 +1754,15 @@ export function makeTerminalBashTool(terminals, opts) {
|
|
|
1652
1754
|
terminals.inputChecked(termId, "\x03");
|
|
1653
1755
|
closeOneShot();
|
|
1654
1756
|
const timeoutPartial = truncateMiddle(stripAnsi(collected), 4000);
|
|
1655
|
-
throw new Error(pick(lang, `Command timed out after ${p.timeout}s(已发 Ctrl+C;已有输出:${timeoutPartial}
|
|
1757
|
+
throw new Error(pick(lang, `Command timed out after ${p.timeout}s(已发 Ctrl+C;已有输出:${timeoutPartial})${sentinelNote}`, `Command timed out after ${p.timeout}s (sent Ctrl+C; partial output: ${timeoutPartial})${sentinelNote}`, "terminals.bash.timeout", { "p.timeout": p.timeout, timeoutPartial }));
|
|
1656
1758
|
}
|
|
1657
1759
|
// 静默解阻(仅持久终端):转后台 + 注册完成观察器,立即把控制权还给模型。
|
|
1658
1760
|
if (persist && idleMs > 0 && Date.now() - lastDataAt >= idleMs) {
|
|
1659
|
-
return backgroundResult(terminals, opts, runCommand, applyHeadTail(cleanBashOutput(collected), p.head, effectiveTail, lang), Math.round((Date.now() - lastDataAt) / 1000), lang);
|
|
1761
|
+
return backgroundResult(terminals, opts, runCommand, applyHeadTail(cleanBashOutput(collected), p.head, effectiveTail, lang) + sentinelNote, Math.round((Date.now() - lastDataAt) / 1000), lang, termId, persist, "idle");
|
|
1762
|
+
}
|
|
1763
|
+
// 总时长解阻(持久与一次性终端):前台运行达到上限,转入后台继续执行,避免卡死。
|
|
1764
|
+
if (maxForegroundMs > 0 && Date.now() - startTime >= maxForegroundMs) {
|
|
1765
|
+
return backgroundResult(terminals, opts, runCommand, applyHeadTail(cleanBashOutput(collected), p.head, effectiveTail, lang) + sentinelNote, Math.round((Date.now() - startTime) / 1000), lang, termId, persist, "elapsed");
|
|
1660
1766
|
}
|
|
1661
1767
|
}
|
|
1662
1768
|
}
|
|
@@ -1666,36 +1772,85 @@ export function makeTerminalBashTool(terminals, opts) {
|
|
|
1666
1772
|
},
|
|
1667
1773
|
});
|
|
1668
1774
|
}
|
|
1669
|
-
/**
|
|
1670
|
-
function backgroundResult(terminals, opts, command, partialText,
|
|
1671
|
-
terminals.watchOutput(
|
|
1775
|
+
/** 静默 / 总时长解阻路径:注册完成观察器后立即返回「仍在后台运行」。 */
|
|
1776
|
+
function backgroundResult(terminals, opts, command, partialText, durationSeconds, lang, termId = "ai-bash", persist = true, reason = "idle") {
|
|
1777
|
+
terminals.watchOutput(termId, BASH_SENTINEL_RE, (m) => {
|
|
1672
1778
|
// 后台命令最终结束(或终端被关)→ 清除待决标记,terminal_wait 不再适用。
|
|
1673
|
-
terminals.setSentinelPending(
|
|
1779
|
+
terminals.setSentinelPending(termId, false);
|
|
1674
1780
|
opts.notifyBackgroundDone({
|
|
1675
|
-
terminalId:
|
|
1781
|
+
terminalId: termId,
|
|
1676
1782
|
command,
|
|
1677
1783
|
exitCode: m ? Number(m[1]) : null,
|
|
1678
1784
|
});
|
|
1785
|
+
if (!persist) {
|
|
1786
|
+
void terminals.inputChecked(termId, "exit\r");
|
|
1787
|
+
}
|
|
1679
1788
|
});
|
|
1680
1789
|
// partialText 已在调用方做过 cleanBashOutput + applyTail。
|
|
1681
1790
|
const partial = truncateMiddle(partialText, 6000);
|
|
1682
1791
|
// 空输出占位按语言预渲染(issue #91 v2:vars 只收干净标识)。
|
|
1683
1792
|
const partialZh = partial || "(暂无输出)";
|
|
1684
1793
|
const partialEn = partial || "(no output yet)";
|
|
1794
|
+
let descZh = "";
|
|
1795
|
+
let descEn = "";
|
|
1796
|
+
if (reason === "elapsed") {
|
|
1797
|
+
descZh =
|
|
1798
|
+
`命令仍在终端 ${termId} 中运行(前台执行已达 ${durationSeconds} 秒上限,自动转入后台)。` +
|
|
1799
|
+
`本次调用不阻塞——命令继续在后台执行,结束时你会收到自动通知。\n` +
|
|
1800
|
+
`已有输出:\n${partialZh}\n`;
|
|
1801
|
+
descEn =
|
|
1802
|
+
`Command still running in terminal ${termId} (foreground execution reached ${durationSeconds}s limit, moved to background). ` +
|
|
1803
|
+
`This call does not block — the command keeps running in the background and you will be notified automatically when it finishes.\n` +
|
|
1804
|
+
`Partial output:\n${partialEn}\n`;
|
|
1805
|
+
}
|
|
1806
|
+
else {
|
|
1807
|
+
descZh =
|
|
1808
|
+
`命令仍在持久终端 ${termId} 中运行(已连续 ${durationSeconds} 秒无输出,未结束)。` +
|
|
1809
|
+
`本次调用不阻塞——命令继续在后台执行,结束时你会收到自动通知。\n` +
|
|
1810
|
+
`已有输出:\n${partialZh}\n`;
|
|
1811
|
+
descEn =
|
|
1812
|
+
`Command still running in the persistent terminal ${termId} (no output for ${durationSeconds}s, not finished). ` +
|
|
1813
|
+
`This call does not block — the command keeps running in the background and you will be notified automatically when it finishes.\n` +
|
|
1814
|
+
`Partial output:\n${partialEn}\n`;
|
|
1815
|
+
}
|
|
1816
|
+
if (persist) {
|
|
1817
|
+
descZh += `要重新阻塞等它结束就用 terminal_wait(terminalId="${termId}")(无需反复轮询);需要交互用 terminal_input / terminal_key(Ctrl+C 可终止)。`;
|
|
1818
|
+
descEn += `To block until it finishes, use terminal_wait(terminalId="${termId}") (no polling needed); use terminal_input / terminal_key to interact (Ctrl+C aborts).`;
|
|
1819
|
+
}
|
|
1820
|
+
else {
|
|
1821
|
+
descZh += `命令完成后该终端将自动退出并发送通知;如需查看输出可用 terminal_read(terminalId="${termId}")。`;
|
|
1822
|
+
descEn += `When completed, this terminal will exit automatically and notify; to inspect output, use terminal_read(terminalId="${termId}").`;
|
|
1823
|
+
}
|
|
1824
|
+
if (reason === "elapsed") {
|
|
1825
|
+
return {
|
|
1826
|
+
content: [
|
|
1827
|
+
{
|
|
1828
|
+
type: "text",
|
|
1829
|
+
text: pick(lang, descZh, descEn, "terminals.bash.background.elapsed", {
|
|
1830
|
+
durationSeconds,
|
|
1831
|
+
partialZh,
|
|
1832
|
+
partialEn,
|
|
1833
|
+
termId,
|
|
1834
|
+
}),
|
|
1835
|
+
},
|
|
1836
|
+
],
|
|
1837
|
+
details: { running: true, terminalId: termId, persist, durationSeconds, reason },
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1685
1840
|
return {
|
|
1686
1841
|
content: [
|
|
1687
1842
|
{
|
|
1688
1843
|
type: "text",
|
|
1689
|
-
text: pick(lang,
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1844
|
+
text: pick(lang, descZh, descEn, "terminals.bash.background.running", {
|
|
1845
|
+
silentSeconds: durationSeconds,
|
|
1846
|
+
durationSeconds,
|
|
1847
|
+
partialZh,
|
|
1848
|
+
partialEn,
|
|
1849
|
+
termId,
|
|
1850
|
+
}),
|
|
1696
1851
|
},
|
|
1697
1852
|
],
|
|
1698
|
-
details: { running: true, terminalId:
|
|
1853
|
+
details: { running: true, terminalId: termId, persist, durationSeconds, reason },
|
|
1699
1854
|
};
|
|
1700
1855
|
}
|
|
1701
1856
|
/** Agent 持久终端工具名(唯一登记见 tool-manager.ts;此处 re-export 保兼容)。 */
|
|
@@ -1703,15 +1858,15 @@ export { TERMINAL_TOOL_NAMES } from "./tool-manager.js";
|
|
|
1703
1858
|
/** System-prompt guidance teaching the model WHEN to prefer the terminal tools
|
|
1704
1859
|
* over one-shot bash. Without it models almost never pick them — bash returns
|
|
1705
1860
|
* complete output in a single call, so it always wins on convenience. */
|
|
1706
|
-
export const TERMINAL_TOOLS_GUIDANCE = `Persistent interactive terminal tools are available (terminal_create / terminal_list / terminal_close / terminal_input / terminal_key / terminal_read / terminal_wait). The bash tool stays the DEFAULT for ordinary commands
|
|
1707
|
-
- The program is interactive or TUI-based (REPLs
|
|
1708
|
-
- You start a long-running server or watcher and want to
|
|
1709
|
-
- The user
|
|
1710
|
-
Liveness watchdog: terminals you touched
|
|
1861
|
+
export const TERMINAL_TOOLS_GUIDANCE = `Persistent interactive terminal tools are available (terminal_create / terminal_list / terminal_close / terminal_input / terminal_key / terminal_read / terminal_wait). The bash tool stays the DEFAULT for ordinary commands. Switch to bash({persist:true}) or the terminal tools when:
|
|
1862
|
+
- The program is interactive or TUI-based (REPLs, vim/htop, y/n installers, anything waiting on stdin): prefer bash({persist:true}), which runs it in the persistent 'ai-bash' terminal and returns immediately; then drive it with terminal_input / terminal_key / terminal_read on terminalId='ai-bash'.
|
|
1863
|
+
- You start a long-running server or watcher and want to watch its output (terminal_read with waitMs), send keys later (e.g. Ctrl+c via terminal_key), or block until a backgrounded command finishes without polling (terminal_wait).
|
|
1864
|
+
- The user asks you to work in the visible terminal panel.
|
|
1865
|
+
Liveness watchdog: terminals you touched are monitored — if one goes silent while you are working (default 15s), a system reminder prompts you to check it (terminal_read), answer its input prompt, or close it (terminal_close) if no longer needed.`;
|
|
1711
1866
|
/** Build the agent-facing persistent terminal tools for one conversation. */
|
|
1712
1867
|
export function makePersistentTerminalTools(terminals, cwd,
|
|
1713
|
-
/** per-call 返回文本的服务端语言(默认英文);工具 definition
|
|
1714
|
-
lang) {
|
|
1868
|
+
/** per-call 返回文本的服务端语言(默认英文);工具 definition 为纯英文。 */
|
|
1869
|
+
lang, options) {
|
|
1715
1870
|
const getLang = lang ?? (() => "en");
|
|
1716
1871
|
const result = (text, details = {}) => ({ content: [{ type: "text", text }], details });
|
|
1717
1872
|
const failIf = (error) => {
|
|
@@ -1722,11 +1877,13 @@ lang) {
|
|
|
1722
1877
|
defineTool({
|
|
1723
1878
|
name: "terminal_create",
|
|
1724
1879
|
label: "Create terminal",
|
|
1725
|
-
description:
|
|
1726
|
-
|
|
1880
|
+
description: "Create a named persistent interactive PTY. Interact via terminal_input / terminal_key, inspect output via terminal_read. " +
|
|
1881
|
+
"Prefer bash for one-shot commands; use this (or bash persist=true) for interactive/TUI programs (REPLs, vim/htop, y/n prompts) or long-running servers to observe or interrupt.",
|
|
1882
|
+
promptSnippet: "run interactive programs or long-running servers in a persistent visible PTY (multi-step: " +
|
|
1883
|
+
"create → input/key → read)",
|
|
1727
1884
|
parameters: Type.Object({
|
|
1728
|
-
terminalId: Type.String({ description:
|
|
1729
|
-
cwd: Type.Optional(Type.String({ description:
|
|
1885
|
+
terminalId: Type.String({ description: "Stable terminal name" }),
|
|
1886
|
+
cwd: Type.Optional(Type.String({ description: "Workspace-relative directory" })),
|
|
1730
1887
|
cols: Type.Optional(Type.Integer({ minimum: 2, maximum: 500 })),
|
|
1731
1888
|
rows: Type.Optional(Type.Integer({ minimum: 2, maximum: 200 })),
|
|
1732
1889
|
}),
|
|
@@ -1744,15 +1901,15 @@ lang) {
|
|
|
1744
1901
|
defineTool({
|
|
1745
1902
|
name: "terminal_list",
|
|
1746
1903
|
label: "List terminals",
|
|
1747
|
-
description:
|
|
1748
|
-
promptSnippet:
|
|
1904
|
+
description: "List all persistent PTY terminals owned by this conversation.",
|
|
1905
|
+
promptSnippet: "list persistent terminals",
|
|
1749
1906
|
parameters: Type.Object({}),
|
|
1750
1907
|
execute: async () => result(JSON.stringify(terminals.list()), terminals.list()),
|
|
1751
1908
|
}),
|
|
1752
1909
|
defineTool({
|
|
1753
1910
|
name: "terminal_close",
|
|
1754
1911
|
label: "Close terminal",
|
|
1755
|
-
description:
|
|
1912
|
+
description: "Close a persistent PTY and terminate its process tree.",
|
|
1756
1913
|
parameters: Type.Object({ terminalId: Type.String() }),
|
|
1757
1914
|
execute: async (_id, p) => {
|
|
1758
1915
|
const lang = getLang();
|
|
@@ -1767,10 +1924,16 @@ lang) {
|
|
|
1767
1924
|
defineTool({
|
|
1768
1925
|
name: "terminal_input",
|
|
1769
1926
|
label: "Send terminal input",
|
|
1770
|
-
description:
|
|
1927
|
+
description: "Send arbitrary text to a persistent PTY. Include newline when a command should be submitted.",
|
|
1771
1928
|
parameters: Type.Object({ terminalId: Type.String(), data: Type.String() }),
|
|
1772
1929
|
execute: async (_id, p) => {
|
|
1773
1930
|
const lang = getLang();
|
|
1931
|
+
if (options?.checkSafety && (p.data.includes("\n") || p.data.includes("\r"))) {
|
|
1932
|
+
const safety = options.checkSafety(p.data.trim());
|
|
1933
|
+
if (safety.blocked) {
|
|
1934
|
+
throw new Error(pick(lang, `【终端输入命令被安全规则阻断】${safety.reason ? ` 原因:${safety.reason}` : ""}`, `[Terminal command blocked by safety rule]${safety.reason ? ` Reason: ${safety.reason}` : ""}`, "terminals.command.blocked", { reason: safety.reason ?? "" }));
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1774
1937
|
failIf(terminals.inputChecked(p.terminalId, p.data));
|
|
1775
1938
|
// AI 发了输入 = 在等结果,重开一个静默纪元。
|
|
1776
1939
|
terminals.noteAgentActivity(p.terminalId);
|
|
@@ -1780,11 +1943,11 @@ lang) {
|
|
|
1780
1943
|
defineTool({
|
|
1781
1944
|
name: "terminal_key",
|
|
1782
1945
|
label: "Send terminal key",
|
|
1783
|
-
description:
|
|
1946
|
+
description: "Send Enter, Tab, arrows, function keys, or Ctrl/Alt combinations to a persistent PTY.",
|
|
1784
1947
|
parameters: Type.Object({
|
|
1785
1948
|
terminalId: Type.String(),
|
|
1786
1949
|
key: Type.String({
|
|
1787
|
-
description:
|
|
1950
|
+
description: "Enter, Tab, ArrowUp, c, etc.",
|
|
1788
1951
|
}),
|
|
1789
1952
|
modifiers: Type.Optional(Type.Object({
|
|
1790
1953
|
ctrl: Type.Optional(Type.Boolean()),
|
|
@@ -1803,7 +1966,9 @@ lang) {
|
|
|
1803
1966
|
defineTool({
|
|
1804
1967
|
name: "terminal_read",
|
|
1805
1968
|
label: "Read terminal output",
|
|
1806
|
-
description:
|
|
1969
|
+
description: "Read incremental output from a persistent PTY. " +
|
|
1970
|
+
"Keep the returned cursor and pass it on the next read; " +
|
|
1971
|
+
"optionally wait for new output or process exit.",
|
|
1807
1972
|
parameters: Type.Object({
|
|
1808
1973
|
terminalId: Type.String(),
|
|
1809
1974
|
cursor: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
@@ -1824,18 +1989,20 @@ lang) {
|
|
|
1824
1989
|
defineTool({
|
|
1825
1990
|
name: "terminal_wait",
|
|
1826
1991
|
label: "Wait for terminal command",
|
|
1827
|
-
description:
|
|
1828
|
-
|
|
1992
|
+
description: "Block until a command started through the BASH TOOL finishes (exit marker appears) or timeout — no polling. " +
|
|
1993
|
+
"Only applies to bash-tool commands; commands sent via terminal_input have no completion marker — use terminal_read(waitMs=…) for those. " +
|
|
1994
|
+
"Returns {finished, exitCode} plus output produced while waiting; finished=false means still running (call again).",
|
|
1995
|
+
promptSnippet: "block until a terminal's current command finishes (no polling)",
|
|
1829
1996
|
parameters: Type.Object({
|
|
1830
1997
|
terminalId: Type.String(),
|
|
1831
1998
|
cursor: Type.Optional(Type.Integer({
|
|
1832
1999
|
minimum: 0,
|
|
1833
|
-
description:
|
|
2000
|
+
description: "Ignore exit markers before this absolute offset (default: now)",
|
|
1834
2001
|
})),
|
|
1835
2002
|
maxWaitMs: Type.Optional(Type.Integer({
|
|
1836
2003
|
minimum: 100,
|
|
1837
2004
|
maximum: 600000,
|
|
1838
|
-
description:
|
|
2005
|
+
description: "Max wait in ms (default 300000)",
|
|
1839
2006
|
})),
|
|
1840
2007
|
}),
|
|
1841
2008
|
execute: async (_id, p, signal) => {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/tool-approval.ts
|
|
3
|
+
*
|
|
4
|
+
* 人机协同拦截与「改写执行」(Human-in-the-Loop: Edit & Run)。
|
|
5
|
+
*
|
|
6
|
+
* 功能:
|
|
7
|
+
* 1. 高危操作识别与规则裁决(Dangerous Tool Call & Rule Evaluation):
|
|
8
|
+
* - 委托 server/approval-rules.ts 的规则引擎评估
|
|
9
|
+
* - 支持三种动作:ask(弹窗审批)、deny(直接拒绝报错)、allow(白名单放行)
|
|
10
|
+
* 2. 审批事件挂起与答复处理(askApproval / resolveApproval):
|
|
11
|
+
* - 用户可「批准」(Approve)
|
|
12
|
+
* - 用户可「拒绝」(Deny)
|
|
13
|
+
* - 用户可就地修改参数后「修改并放行」(Edit & Run)
|
|
14
|
+
* 3. 三档放行策略(省去反复弹窗):
|
|
15
|
+
* - 全局关(设置 →「工具」页的「工具执行审批」总开关)→ 一律不弹;
|
|
16
|
+
* - 本对话「全部允许」→ 该对话内一律不弹;
|
|
17
|
+
* - 本对话「允许同类」→ 该对话内同一规则档位(category)不再弹。
|
|
18
|
+
* 策略只活在内存里(挂在 Conversation 上,过户随对话搬走),重启/新对话即恢复询问。
|
|
19
|
+
*/
|
|
20
|
+
import { DEFAULT_APPROVAL_RULES, evaluateApprovalRules } from "./approval-rules.js";
|
|
21
|
+
/** 策略为空(既没全部允许也没记同类):调用方据此不挂空策略、面板据此不显示撤销区。 */
|
|
22
|
+
export function isApprovalPolicyEmpty(policy) {
|
|
23
|
+
return !policy || (!policy.allowAll && policy.categories.size === 0);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 三档放行的纯判定:返回 null = 需要弹审批,否则返回放行的原因。
|
|
27
|
+
* - `disabled`:全局开关关(设置 →「工具」页)→ 一切审批都不弹(插件显式 ask 也放行);
|
|
28
|
+
* - `allow-all`:本对话已「全部允许」;
|
|
29
|
+
* - `category`:本对话已记住该同类档位。
|
|
30
|
+
*/
|
|
31
|
+
export function approvalSuppressionReason(policy, enabled, categoryId) {
|
|
32
|
+
if (!enabled)
|
|
33
|
+
return "disabled";
|
|
34
|
+
if (!policy)
|
|
35
|
+
return null;
|
|
36
|
+
if (policy.allowAll)
|
|
37
|
+
return "allow-all";
|
|
38
|
+
if (categoryId && policy.categories.has(categoryId))
|
|
39
|
+
return "category";
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/** 插件显式要求确认(pre guard 的 ask)对应的档位:按插件 id 分档,可「允许同类」。 */
|
|
43
|
+
export function pluginApprovalCategory(pluginId) {
|
|
44
|
+
return {
|
|
45
|
+
id: `plugin:${pluginId}`,
|
|
46
|
+
label: `插件 ${pluginId} 要求确认`,
|
|
47
|
+
labelEn: `Plugin ${pluginId} requested confirmation`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 检测某个工具调用是否属于高危操作,或命中用户自定义规则。
|
|
52
|
+
* 可选接收 rules 规则集;缺省时以内置默认高危规则集判定。
|
|
53
|
+
*/
|
|
54
|
+
export function checkDangerousToolCall(toolName, params, cwd, workspaceRoots = [], rules) {
|
|
55
|
+
if (!params || typeof params !== "object") {
|
|
56
|
+
return { dangerous: false };
|
|
57
|
+
}
|
|
58
|
+
const res = evaluateApprovalRules(rules ?? DEFAULT_APPROVAL_RULES, toolName, params, cwd, workspaceRoots);
|
|
59
|
+
if (res.action === "deny") {
|
|
60
|
+
return {
|
|
61
|
+
dangerous: true,
|
|
62
|
+
denied: true,
|
|
63
|
+
reason: res.reason,
|
|
64
|
+
reasonEn: res.reasonEn,
|
|
65
|
+
category: res.category,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (res.action === "allow") {
|
|
69
|
+
return {
|
|
70
|
+
dangerous: false,
|
|
71
|
+
allowed: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (res.action === "ask") {
|
|
75
|
+
return {
|
|
76
|
+
dangerous: true,
|
|
77
|
+
denied: false,
|
|
78
|
+
reason: res.reason,
|
|
79
|
+
reasonEn: res.reasonEn,
|
|
80
|
+
category: res.category,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return { dangerous: false };
|
|
84
|
+
}
|