Skip to content

Commit f96062d

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents a06e630 + bac1613 commit f96062d

25 files changed

Lines changed: 822 additions & 28 deletions

File tree

‎app/build.gradle.kts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ android {
204204

205205
buildFeatures {
206206
compose = true
207+
// UserService 走 AIDL 跨进程接口(app/src/main/aidl),显式开启以免依赖 AGP 默认值。
208+
aidl = true
207209
}
208210

209211

@@ -380,6 +382,11 @@ dependencies {
380382
implementation("androidx.work:work-runtime-ktx:2.10.0")
381383
implementation("androidx.hilt:hilt-work:1.2.0")
382384

385+
// Shizuku:以 adb shell(uid 2000)身份执行命令。api 提供 Shizuku 类与 UserService 绑定,
386+
// provider 注册 ShizukuProvider(见 AndroidManifest)以跨进程获取 binder。
387+
implementation("dev.rikka.shizuku:api:13.1.5")
388+
implementation("dev.rikka.shizuku:provider:13.1.5")
389+
383390
// Core Android
384391
implementation("androidx.core:core:1.16.0")
385392
// AppCompat — 提供 AppCompatDelegate.setApplicationLocales 实现 per-app 语言切换

‎app/proguard-rules.pro‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@
9292
# ---- Compose ----
9393
-dontwarn androidx.compose.**
9494

95+
# ---- Shizuku(UserService 以 adb shell 身份执行命令)----
96+
# UserService 实现类由 Shizuku 在 shell 进程中按类名反射加载,类名与构造器不可被混淆/裁剪。
97+
-keep class com.aicode.feature.agent.domain.shizuku.ShizukuShellService { *; }
98+
# AIDL 接口与 Stub 跨进程传递,保持不混淆。
99+
-keep interface com.aicode.feature.agent.domain.shizuku.IShizukuShellService { *; }
100+
-keep class com.aicode.feature.agent.domain.shizuku.IShizukuShellService$Stub { *; }
101+
-keep class rikka.shizuku.** { *; }
102+
-dontwarn rikka.shizuku.**
103+
95104
# ---- Termux terminal-emulator/view(native + JNI)----
96105
-keep class com.termux.terminal.** { *; }
97106
-keep class com.termux.view.** { *; }

‎app/src/main/AndroidManifest.xml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@
7272
android:enabled="true"
7373
android:exported="false"
7474
android:foregroundServiceType="dataSync" />
75+
76+
<!-- Shizuku:用于以 adb shell 身份执行命令(UserService)。
77+
permission 声明为 INTERACT_ACROSS_USERS_FULL 是官方要求,防止普通应用访问该 provider。 -->
78+
<provider
79+
android:name="rikka.shizuku.ShizukuProvider"
80+
android:authorities="${applicationId}.shizuku"
81+
android:multiprocess="false"
82+
android:enabled="true"
83+
android:exported="true"
84+
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
7585
</application>
7686

7787
</manifest>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Shizuku UserService 接口:运行在 shell(uid 2000)进程,代 App 执行 shell 命令。
2+
// 返回 Bundle:output(String) 命令输出、exitCode(int) 退出码(超时/异常为负值)。
3+
package com.aicode.feature.agent.domain.shizuku;
4+
5+
import android.os.Bundle;
6+
7+
interface IShizukuShellService {
8+
// Shizuku 保留的销毁入口:事务号固定(aidl 写 16777114,服务端实际按 16777115 调用),
9+
// unbindUserService(remove=true) 时触发,实现里应结束进程。不可改动。
10+
void destroy() = 16777114;
11+
12+
Bundle exec(String command, int timeoutMs) = 1;
13+
}

‎app/src/main/assets/prompts/60-tools-and-paths.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
## 命令与终端工具
1919
- `Bash`:执行一次性 shell 命令(列目录、搜索、构建、lint、格式化、git、装依赖等),同步等待命令结束并返回输出。默认超时 120 秒,上限 1800 秒;耗时命令(如安装依赖)可用 timeout 参数调大。
20+
- `Shizuku`:通过 Shizuku 以 adb shell(uid 2000)身份在 Android 系统上执行 Shell 命令,等价于 `adb shell`。适用于需要 shell 权限的系统操作:`pm`/`am`/`cmd` 等系统命令、读写 `/sdcard`、查询系统状态等。与 `Bash`(在本地容器或远程 SSH 中执行)不同,它直接作用于宿主 Android 系统本身。使用前用户需已安装 Shizuku 并在本应用中授权(设置 → 软件权限 → Shizuku),未就绪时会返回错误提示。参数:`command`(必填)、`timeout`(秒,可选,默认 120、上限 1800)。**注意:调用 `Shizuku` 一律会弹出授权确认且不支持「始终允许」,每次都需要用户逐次放行;AUTO 模式下也不例外(除非用户已开启「禁用安全拦截」)。**
2021
- 环境已内置常用开发工具:`git`、`rg`(ripgrep)、`py`/`python`、`node`。需要时优先直接通过 `Bash` 调用,不要先询问是否安装。
2122
- `terminal`:管理常驻后台终端会话,用 `action` 参数选操作:
2223
- **优先复用 AI 自己创建的终端**:启动新常驻进程或执行交互式命令前,先用 `action="read"`(不传 tab_id)列出现有终端。若有 AI 之前创建的活跃标签,直接用 `action="send"` 复用,切忌反复 `start` 开一堆新窗口。

‎app/src/main/assets/prompts/agent/auto-mode.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
## 行为规则
77

8-
- 所有工具调用(写文件、执行命令、终端操作、网络请求等)将自动放行,不会弹出权限确认窗(唯一例外:灾难性删除提权确认,见下)。请像平时一样正常调用工具完成任务,不要因为「无需授权」就改变工作方式或省略自验步骤。
8+
- 所有工具调用(写文件、执行命令、终端操作、网络请求等)将自动放行,不会弹出权限确认窗(例外:灾难性删除提权确认,以及 `Shizuku` 工具——它直接操作宿主 Android 系统,AUTO 下仍会逐次弹窗且不支持「始终允许」;若用户已在「工具授权」中开启「禁用安全拦截」,则连这层也不再生效,见下)。请像平时一样正常调用工具完成任务,不要因为「无需授权」就改变工作方式或省略自验步骤。
99
- **默认仍会拦截灾难性删除与无法判定的破坏性命令**:`rm` 删除根目录、系统关键目录(`/etc`、`/usr`、`/bin` 等)、工作区根目录 `~/workspace` 整体、用户主目录整体等高危操作会被安全防护拒绝。另外,含命令替换、子 shell、绝对路径重定向等无法静态判定、且疑似破坏性的命令(如 `rm -rf $(...)`、`cat > /etc/xxx`)也会被拦截——因为无法确认其删除目标。这是防误删的底线,不属于「权限」范畴。如确有必要执行,可在调用时加 `elevate: true` 重试——届时会弹窗请用户一次性授权(AUTO 下也会弹,且不可记忆),用户同意才执行。若用户已在「工具授权」中开启「禁用安全拦截」,则连这层拦截也不再生效,删除命令将直接执行,务必加倍谨慎。如需清理大量文件,请精确指定目标路径,不要用通配符删除整个目录树。
1010
- 仍需遵守通用安全规则:不引入命令注入、XSS、SQL 注入等漏洞;不执行破坏性且难以撤销的操作(`git push --force`、`rm -rf` 未核实路径、覆盖未提交改动等)前先确认。
1111

‎app/src/main/assets/prompts/agent/plan-mode.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
## 绝对约束(覆盖其它所有指令)
77

8-
- 禁止任何写操作:`writeFile`、`editFile`、`Bash`、`terminal` 的 start/send/key/close 等写工具调用会被拦截并返回错误,不要尝试调用。
8+
- 禁止任何写操作:`writeFile`、`editFile`、`Bash`、`Shizuku`、`terminal` 的 start/send/key/close 等写工具调用会被拦截并返回错误,不要尝试调用。
99
- 除只读探索与输出方案外,不对系统做任何更改(不提交、不装包、不改配置、不动文件)。
1010
- 用户尚未批准执行——以上约束优先于任何其它指令,包括用户直接要求编辑的请求。你只能观察、分析、规划。
1111

‎app/src/main/java/com/aicode/di/AgentModule.kt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import com.aicode.feature.agent.domain.tool.container.ExecuteCommandTool
3737
import com.aicode.feature.agent.domain.tool.container.TerminalSessionTool
3838
import com.aicode.feature.agent.domain.tool.explorer.ListFilesTool
3939
import com.aicode.feature.agent.domain.tool.explorer.SearchCodeTool
40+
import com.aicode.feature.agent.domain.tool.shizuku.ShizukuTool
4041
import com.aicode.feature.agent.domain.tool.skill.LoadSkillTool
4142
import com.aicode.feature.agent.domain.tool.question.AskUserQuestionTool
4243
import com.aicode.feature.agent.domain.tool.todo.TodoTool
@@ -271,6 +272,7 @@ object AgentModule {
271272
editFileTool: EditFileTool,
272273
generateImageTool: GenerateImageTool,
273274
executeCommandTool: ExecuteCommandTool,
275+
shizukuTool: ShizukuTool,
274276
terminalSessionTool: TerminalSessionTool,
275277
listFilesTool: ListFilesTool,
276278
searchCodeTool: SearchCodeTool,
@@ -293,6 +295,7 @@ object AgentModule {
293295
register("editFile", editFileTool)
294296
register("generateImage", generateImageTool)
295297
register("Bash", executeCommandTool)
298+
register("Shizuku", shizukuTool)
296299
register("terminal", terminalSessionTool)
297300
register("list", listFilesTool)
298301
register("search", searchCodeTool)

‎app/src/main/java/com/aicode/feature/agent/domain/permission/ToolPermissionPolicyEngine.kt‎

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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
2528
class 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

Comments
 (0)