@@ -20,6 +20,9 @@ import javax.inject.Singleton
2020 * 5) 否则 → ASK,可记忆前缀为各段的「程序名」或「程序名+子命令」(对 git/npm 等子命令分发器,
2121 * 记 `git pull` 而非 `git`,使不同子命令各自独立授权)。
2222 * 非 shell 的 ASK 工具按整工具(pattern=`*`)匹配。
23+ *
24+ * [SHIZUKU_TOOL](Shizuku)按高危处理:无论内置白名单或已记忆规则,一律 ASK 且不可记忆;
25+ * AUTO 模式也不自动放行(除非已开启「禁用安全拦截」)。
2326 */
2427@Singleton
2528class ToolPermissionPolicyEngine @Inject constructor(
@@ -28,7 +31,13 @@ class ToolPermissionPolicyEngine @Inject constructor(
2831) {
2932 private companion object {
3033 /* * 以 `command` 参数承载 shell 命令、按命令前缀做指令级匹配的工具。 */
31- val SHELL_TOOLS = setOf (" Bash" )
34+ val SHELL_TOOLS = setOf (" Bash" , SHIZUKU_TOOL )
35+
36+ /* * Shizuku 工具:以 adb shell 身份直接操作宿主 Android 系统,按高危处理(一律弹窗、不可记忆)。 */
37+ const val SHIZUKU_TOOL = " Shizuku"
38+
39+ const val REASON_SHIZUKU =
40+ " Shizuku 直接以 adb shell 身份操作宿主 Android 系统,权限高危,仅支持单次放行,不可记忆"
3241
3342 /* *
3443 * 合并后的终端会话工具:其 `start` 动作承载 shell 命令,需走指令级前缀匹配;
@@ -115,7 +124,8 @@ class ToolPermissionPolicyEngine @Inject constructor(
115124 if (mode == com.aicode.feature.agent.domain.model.AgentMode .AUTO ) {
116125 // AUTO 模式放行所有权限;灾难性 rm 防护(根目录/系统目录删除)默认保留,
117126 // 可在「工具授权」设置中关闭(禁用安全拦截)。遭遇拦截时仍可凭 elevate 参数提权重试。
118- if (! toolSafetySettings.isSafetyInterceptionDisabled() && isShellTool(toolName, args)) {
127+ val safetyDisabled = toolSafetySettings.isSafetyInterceptionDisabled()
128+ if (! safetyDisabled && isShellTool(toolName, args)) {
119129 val command = ((args[" command" ] ? : args[" input" ]) as ? JsonPrimitive )?.content
120130 if (command != null ) {
121131 val analysis = ShellCommandParser .analyze(command)
@@ -127,6 +137,10 @@ class ToolPermissionPolicyEngine @Inject constructor(
127137 }
128138 }
129139 }
140+ // Shizuku 直接操作宿主系统,AUTO 下也不自动放行,仍需逐次确认(除非已开启「禁用安全拦截」)。
141+ if (toolName == SHIZUKU_TOOL && ! safetyDisabled) {
142+ return EvalResult (Verdict .ASK , emptyList(), rememberDisabledReason = REASON_SHIZUKU )
143+ }
130144 return EvalResult (Verdict .ALLOW , emptyList())
131145 }
132146
@@ -148,7 +162,11 @@ class ToolPermissionPolicyEngine @Inject constructor(
148162 }
149163
150164 val rules = rulesRepo.loadEffectiveForCurrentProject().filter { it.toolName == toolName }
151- return if (isShellTool(toolName, args)) evaluateShell(rules, args) else evaluateGeneric(rules, capabilities)
165+ return if (isShellTool(toolName, args)) {
166+ evaluateShell(rules, args, forceAsk = toolName == SHIZUKU_TOOL )
167+ } else {
168+ evaluateGeneric(rules, capabilities)
169+ }
152170 }
153171
154172 private fun isDangerousTool (toolName : String , args : Map <String , JsonElement >, capabilities : Set <ToolCapability >): Boolean {
@@ -301,7 +319,11 @@ class ToolPermissionPolicyEngine @Inject constructor(
301319 }
302320 }
303321
304- private fun evaluateShell (rules : List <PermissionRule >, args : Map <String , JsonElement >): EvalResult {
322+ private fun evaluateShell (
323+ rules : List <PermissionRule >,
324+ args : Map <String , JsonElement >,
325+ forceAsk : Boolean = false
326+ ): EvalResult {
305327 val command = ((args[" command" ] ? : args[" input" ]) as ? JsonPrimitive )?.content
306328 ? : return EvalResult (Verdict .ASK , emptyList())
307329
@@ -321,17 +343,22 @@ class ToolPermissionPolicyEngine @Inject constructor(
321343 return EvalResult (Verdict .DENY , emptyList(), denyReason = " 该命令被项目权限规则策略禁止执行" )
322344 }
323345
324- // 2) 不可静态判定 → 必须弹窗、不可记忆(内置白名单也不适用)。
346+ // 2) Shizuku 高危工具:无论内置白名单或已记忆规则,一律弹窗且不可记忆。
347+ if (forceAsk) {
348+ return EvalResult (Verdict .ASK , emptyList(), rememberDisabledReason = REASON_SHIZUKU )
349+ }
350+
351+ // 3) 不可静态判定 → 必须弹窗、不可记忆(内置白名单也不适用)。
325352 if (! analysis.analyzable) return EvalResult (Verdict .ASK , emptyList())
326353
327- // 3 ) 内置安全白名单:每段都命中安全前缀(ls/git status 等)→ 自动放行,不弹窗。
354+ // 4 ) 内置安全白名单:每段都命中安全前缀(ls/git status 等)→ 自动放行,不弹窗。
328355 if (analysis.segments.isNotEmpty() &&
329356 analysis.segments.all { BuiltInSafeCommands .isSafe(it) }
330357 ) {
331358 return EvalResult (Verdict .ALLOW , emptyList())
332359 }
333360
334- // 4 ) 已记忆的 ALLOW:每段都命中 → 放行。
361+ // 5 ) 已记忆的 ALLOW:每段都命中 → 放行。
335362 // 对 rm 命令进行精细化校验:避免存量或宽泛的 "rm"/"rm -rf" 无目标规则放行高风险删除
336363 val allAllowed = analysis.segments.isNotEmpty() &&
337364 analysis.segments.all { seg ->
@@ -349,7 +376,7 @@ class ToolPermissionPolicyEngine @Inject constructor(
349376 }
350377 if (allAllowed) return EvalResult (Verdict .ALLOW , emptyList())
351378
352- // 5 ) 否则弹窗,可记忆前缀(子命令分发器记 程序名+子命令)。
379+ // 6 ) 否则弹窗,可记忆前缀(子命令分发器记 程序名+子命令)。
353380 val hasRmUnrememberable = analysis.segments.any { seg ->
354381 val rmInfo = ShellCommandParser .parseRmInfo(seg)
355382 rmInfo.isRm && (rmInfo.targetPaths.isEmpty() || rmInfo.isRecursive || rmInfo.isWildcard)
0 commit comments