thincoder 0.12.18 → 0.12.19
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/package.json +1 -1
- package/src/acp/bridge.mjs +8 -1
- package/src/tools/shared.mjs +38 -18
- package/src/tools/system.mjs +5 -12
- package/src/tui/interaction.mjs +8 -1
package/package.json
CHANGED
package/src/acp/bridge.mjs
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* `{ stopReason: "end_turn" }` (kimi session.ts parity).
|
|
21
21
|
*/
|
|
22
22
|
import { join } from "node:path"
|
|
23
|
+
import { detectDanger } from "../tools/shared.mjs"
|
|
23
24
|
|
|
24
25
|
/** ACP ToolKind inference (schema v1 enum) — best-effort, clients render by kind. */
|
|
25
26
|
function inferToolKind(name) {
|
|
@@ -99,10 +100,16 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {} }
|
|
|
99
100
|
* client. Any transport failure → reject (safety-first, kimi parity).
|
|
100
101
|
*/
|
|
101
102
|
onPermissionRequest: async (name, args) => {
|
|
103
|
+
// 危险命令标注(只提示不拦截):kimi 同款模式,帮助编辑器端用户审批
|
|
104
|
+
const base = name.includes("/") ? name.split("/").pop() : name
|
|
105
|
+
const danger = base === "bash" ? detectDanger(args?.command ?? "") : undefined
|
|
106
|
+
const content = [contentBlock(`Requesting approval to run ${name}`)]
|
|
107
|
+
if (danger) content.push(contentBlock(`⚠️ Dangerous: ${danger}`))
|
|
108
|
+
content.push(contentBlock(JSON.stringify(args ?? {})))
|
|
102
109
|
const toolCall = {
|
|
103
110
|
toolCallId: toolIds.get(name) ?? toolCallId(),
|
|
104
111
|
title: name,
|
|
105
|
-
content
|
|
112
|
+
content,
|
|
106
113
|
}
|
|
107
114
|
try {
|
|
108
115
|
const response = await request("session/request_permission", {
|
package/src/tools/shared.mjs
CHANGED
|
@@ -241,27 +241,47 @@ export function hasFileRedirection(command) {
|
|
|
241
241
|
return /(^|[\s;&|0-9])>{1,2}\s*\S/.test(bare) || /(^|[\s;&|0-9])<\s*\S/.test(bare)
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
/**
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (/\brd\b/i.test(s) && /\/s\b/i.test(s)) return true
|
|
255
|
-
// format called as a command (exclude --format= option false positives)
|
|
256
|
-
if (/\bformat\b\s+\S/i.test(s) && !/--format\b/i.test(s)) return true
|
|
257
|
-
if (/\bshred\b/i.test(s)) return true
|
|
258
|
-
if (/\bdd\b/.test(s) && /\bof=/i.test(s)) return true
|
|
259
|
-
if (/\bDROP\s+TABLE\b/i.test(s)) return true
|
|
260
|
-
if (/\bDELETE\s+FROM\b/i.test(s)) return true
|
|
261
|
-
if (/\bTRUNCATE\b/i.test(s)) return true
|
|
244
|
+
/**
|
|
245
|
+
* Whether a single command segment is destructive — ALWAYS FALSE (deliberate).
|
|
246
|
+
*
|
|
247
|
+
* 决策(2026-08):文本拦截对恶意模型是安全剧场——空白变体/heredoc/node -e/写脚本执行
|
|
248
|
+
* 都能绕过,拦住的只有正常操作(如清理临时目录、rm node_modules 重装)。
|
|
249
|
+
* 真实防线在工具审批层(autoApprove)与快照兜底(gitGuardSnapshot / checkpoint auto-snapshot),
|
|
250
|
+
* 与 env 过滤、git 破坏操作"快照后放行、永不拦截"同一哲学。
|
|
251
|
+
* 项目工具自带确认门(如 thin5 scripts/db.mjs --write/--danger)不应被双重拦截。
|
|
252
|
+
*/
|
|
253
|
+
export function isDestructiveCommand() {
|
|
262
254
|
return false
|
|
263
255
|
}
|
|
264
256
|
|
|
257
|
+
/**
|
|
258
|
+
* 危险命令识别(只标注、不拦截)——参考 kimi-code apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts
|
|
259
|
+
* DANGER_PATTERNS。定位:给审批中的人打红色警告标签,提升决策信息,不是机器防线。
|
|
260
|
+
* 拦截无用(可绕过),标注有用(人看到了才知道该多看一眼)。
|
|
261
|
+
*/
|
|
262
|
+
const DANGER_PATTERNS = [
|
|
263
|
+
{ pattern: /\brm\s+(-[a-zA-Z]*[rRfF][a-zA-Z]*|--recursive|--force)/i, label: "recursive delete" },
|
|
264
|
+
{ pattern: /\bsudo\b/i, label: "sudo" },
|
|
265
|
+
{ pattern: /\b(curl|wget)\b[^|]*\|\s*(sh|bash|zsh)\b/i, label: "pipe to shell" },
|
|
266
|
+
{ pattern: /\bdd\b[^|]*\bof=/i, label: "dd write" },
|
|
267
|
+
{ pattern: /\bmkfs\b/i, label: "mkfs" },
|
|
268
|
+
{ pattern: />\s*\/dev\/(sd|nvme|disk|hd)/i, label: "write to raw device" },
|
|
269
|
+
{ pattern: /\bchmod\s+(?:-[rR]\s+)?777\b/i, label: "chmod 777" },
|
|
270
|
+
{ pattern: /:\(\)\s*\{\s*:\|:&\s*\}/i, label: "fork bomb" },
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
/** 返回危险标注 label(如 "recursive delete");无危险返回 undefined。
|
|
274
|
+
* 引号感知:引号(单/双)内的内容清空后再检测——commit message、echo 文本等
|
|
275
|
+
* 纯文本不误标;危险命令(rm -rf "$dir")的参数在引号外,仍命中。
|
|
276
|
+
* 反引号内容保留(命令替换会执行)。 */
|
|
277
|
+
export function detectDanger(command) {
|
|
278
|
+
const s = blankQuoted(String(command ?? ""))
|
|
279
|
+
for (const { pattern, label } of DANGER_PATTERNS) {
|
|
280
|
+
if (pattern.test(s)) return label
|
|
281
|
+
}
|
|
282
|
+
return undefined
|
|
283
|
+
}
|
|
284
|
+
|
|
265
285
|
|
|
266
286
|
/** Convert glob pattern to regex */
|
|
267
287
|
export function globToRegex(pattern) {
|
package/src/tools/system.mjs
CHANGED
|
@@ -6,8 +6,6 @@ import {
|
|
|
6
6
|
BASH_TIMEOUT_MS,
|
|
7
7
|
IGNORED_DIRS,
|
|
8
8
|
resolveInCwd,
|
|
9
|
-
shellSegments,
|
|
10
|
-
isDestructiveCommand,
|
|
11
9
|
hasFileRedirection,
|
|
12
10
|
globToRegex,
|
|
13
11
|
normalizeEOL,
|
|
@@ -25,21 +23,16 @@ const MAX_STREAM_BUF = 2_000_000
|
|
|
25
23
|
|
|
26
24
|
/**
|
|
27
25
|
* Pre-execution safety checks for bash commands.
|
|
28
|
-
* Layers: file redirection
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* Layers: file redirection (guides toward structured tools, not a security gate).
|
|
27
|
+
* Destructive commands (rm -rf, DROP TABLE, ...) are deliberately NOT rejected:
|
|
28
|
+
* a determined model bypasses text matching anyway — real security is at the
|
|
29
|
+
* tool approval layer plus snapshot backups (gitGuardSnapshot / checkpoint).
|
|
30
|
+
* Git destructive ops: snapshot-then-proceed, never block.
|
|
32
31
|
*/
|
|
33
32
|
function checkBashSafety(command, cwd) {
|
|
34
33
|
if (hasFileRedirection(command)) {
|
|
35
34
|
throw new Error("File redirection via bash is not allowed — use the write/edit/insert_after tools instead")
|
|
36
35
|
}
|
|
37
|
-
if (shellSegments(command).some(isDestructiveCommand)) {
|
|
38
|
-
throw new Error(
|
|
39
|
-
"Destructive command blocked — use specific tools or confirm with the user first. " +
|
|
40
|
-
"(If work was already destroyed, recover from auto-snapshot: checkpoint action=list then action=rewind.)"
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
/**
|
package/src/tui/interaction.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ansi, C } from "./ansi.mjs"
|
|
2
|
+
import { detectDanger } from "../tools/shared.mjs"
|
|
2
3
|
|
|
3
4
|
/** Interaction primitives: permission approval + question input.
|
|
4
5
|
* Extracted from index.mjs, receives closure dependencies via createInteraction(ctx).
|
|
@@ -10,7 +11,13 @@ export function createInteraction(ctx) {
|
|
|
10
11
|
function formatPermission(name, args) {
|
|
11
12
|
const cap = (s, n = 3000) => (s.length > n ? `${s.slice(0, n)}…(${s.length} chars total)` : s)
|
|
12
13
|
const base = name.includes("/") ? name.split("/").pop() : name
|
|
13
|
-
if (base === "bash")
|
|
14
|
+
if (base === "bash") {
|
|
15
|
+
// 危险命令标注(只提示不拦截):给人看的红色警告,帮审批决策
|
|
16
|
+
const danger = detectDanger(args.command ?? "")
|
|
17
|
+
const lines = []
|
|
18
|
+
if (danger) lines.push(`${C.error}⚠️ Dangerous: ${danger}${ansi.reset}`)
|
|
19
|
+
return [...lines, ...cap(args.command ?? "").split("\n")]
|
|
20
|
+
}
|
|
14
21
|
if (base === "write") {
|
|
15
22
|
// approving file writes must show what's being written: path + content preview
|
|
16
23
|
return [`${args.path} (write ${(args.content ?? "").length} chars)`, ...cap(args.content ?? "", 3000).split("\n")]
|