thincoder 0.12.17 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.12.17",
3
+ "version": "0.12.19",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -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: [contentBlock(`Requesting approval to run ${name}`), contentBlock(JSON.stringify(args ?? {}))],
112
+ content,
106
113
  }
107
114
  try {
108
115
  const response = await request("session/request_permission", {
@@ -83,7 +83,10 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
83
83
  body.temperature = t
84
84
  }
85
85
  if (provider.thinking) body.thinking = provider.thinking
86
- if (provider.reasoningEffort && provider.format !== "anthropic" && provider.format !== "google") {
86
+ // reasoning_effort is a provider-native parameter routers/proxies (model ID with "/"
87
+ // prefix like kimi/kimi-k3) may misinterpret it, causing empty responses or 400s.
88
+ const isRouter = provider.model.includes("/")
89
+ if (provider.reasoningEffort && !isRouter && provider.format !== "anthropic" && provider.format !== "google") {
87
90
  if (spec.reasoningEffortEnum && !spec.reasoningEffortEnum.includes(provider.reasoningEffort)) {
88
91
  throw new Error(
89
92
  `reasoning_effort "${provider.reasoningEffort}" not supported by model "${provider.model}"; ` +
@@ -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
- /** Whether a single command segment is a destructive non-git command (conservative: prefer false positives) */
245
- export function isDestructiveCommand(seg) {
246
- const s = seg
247
- // rm with recursive (-r/-R/--recursive): destructive WITH or WITHOUT -f
248
- // (recursive delete removes trees non-interactively in many setups; -rf is
249
- // the classic case). Conservative: prefer blocking. The \s before the flag
250
- // requires a separator "rm-rf" is not a valid command (no such program).
251
- if (/\brm\b/.test(s) && (/\s-\S*r/i.test(s) || /\s--recursive\b/i.test(s))) return true
252
- if (/\brmdir\b/i.test(s)) return true
253
- if (/\bdel\b/i.test(s) && /\/f\b/i.test(s)) return true
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) {
@@ -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 destructive commands (rm -rf etc.).
29
- * Git destructive ops are deliberately NOT rejected — the model would just find a
30
- * way around the rejection; instead gitGuardSnapshot copies every uncommitted file
31
- * and the command is ALLOWED (snapshot-then-proceed, never block).
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
  /**
@@ -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") return cap(args.command ?? "").split("\n")
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")]