refactor(pi-permission): extract shared bash-recovery package

- add @sikongjueluo/pi-permission-shared with recoverNativeBashCommand and its tests
- move the recovery module out of pi-permission-inner-cmd and import it from the shared package
- wire pi-permission-ai-judge to capture the UI-root session and recover the full bash command
- gate pi-permission-ai-judge registration on a UI-present root session
- add @types/node to pi-permission-ai-judge and allow its test script to pass with no tests
This commit is contained in:
2026-08-11 21:53:46 +08:00
parent 06b21a28a6
commit 2014e7f793
10 changed files with 139 additions and 22 deletions
+8 -1
View File
@@ -16,15 +16,22 @@
"@earendil-works/pi-coding-agent": "*",
"@gotgenes/pi-permission-system": ">=20.10.0"
},
"dependencies": {
"@sikongjueluo/pi-permission-shared": "workspace:*"
},
"bundledDependencies": [
"@sikongjueluo/pi-permission-shared"
],
"devDependencies": {
"@earendil-works/pi-ai": "*",
"@earendil-works/pi-coding-agent": "*",
"@gotgenes/pi-permission-system": ">=20.10.0",
"@types/node": "^26.0.0",
"typescript": "^5",
"vitest": "^3"
},
"scripts": {
"check": "tsc --noEmit",
"test": "vitest run"
"test": "vitest run --passWithNoTests"
}
}
+50 -10
View File
@@ -1,13 +1,25 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import type {
ExtensionAPI,
SessionEntry,
} from "@earendil-works/pi-coding-agent";
import {
getPermissionsService,
PERMISSIONS_READY_CHANNEL,
} from "@gotgenes/pi-permission-system";
import {
NATIVE_BASH_TOOL_NAME,
recoverNativeBashCommand,
} from "@sikongjueluo/pi-permission-shared";
const LINK_NAME = "ai-bash-judge";
/** 捕获的 UI-root 会话读取入口,用于还原完整命令。 */
interface CapturedSession {
getEntries(): ReadonlyArray<SessionEntry>;
}
export default function permissionAiJudge(pi: ExtensionAPI): void {
let sessionStarted = false;
let session: CapturedSession | undefined;
let disposeAuthorizer: (() => void) | undefined;
/**
@@ -21,7 +33,7 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
* 谁后满足条件,谁完成注册。
*/
function tryRegister(): void {
if (!sessionStarted || disposeAuthorizer) {
if (!session || disposeAuthorizer) {
return;
}
@@ -34,6 +46,9 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
return;
}
// 捕获此刻的会话引用:回调触发时读取最新 entries。
const captured = session;
disposeAuthorizer = service.registerAuthorizer(
LINK_NAME,
@@ -43,11 +58,29 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
details.surface ??
undefined;
/**
* 还原完整的 bash 命令。
*
* details.command 可能只是聚合 ask 里的某个命令单元(见
* ADR 0001),AI 判定需要完整输入。只有原生 bash 工具调用
* 才能从会话里还原;否则回退到 details.command。
*/
const command =
details.toolName === NATIVE_BASH_TOOL_NAME &&
details.toolCallId !== undefined
? recoverNativeBashCommand(
captured.getEntries(),
details.toolCallId,
)
: undefined;
const effectiveCommand = command ?? details.command;
console.error(`[${LINK_NAME}] permission ask received`, {
requestId: details.requestId,
surface,
toolName: details.toolName,
command: details.command,
command: effectiveCommand ?? null,
path: details.path,
value: details.value,
agentName: details.agentName,
@@ -60,10 +93,10 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
* 它只是询问 pi-permission-system 的确定性规则:
* “如果检查这个 bash command,规则本身会怎么判?”
*/
if (surface === "bash" && details.command) {
if (surface === "bash" && effectiveCommand) {
const result = query.checkPermission(
"bash",
details.command,
effectiveCommand,
details.agentName ?? undefined,
);
@@ -81,7 +114,7 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
log.review("ai_bash_judge.test", {
requestId: details.requestId,
surface,
command: details.command ?? null,
command: effectiveCommand ?? null,
verdict: "defer",
});
@@ -103,8 +136,15 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
console.error(`[${LINK_NAME}] registered`);
}
pi.on("session_start", () => {
sessionStarted = true;
pi.on("session_start", (_event, ctx) => {
// 仅从 proven UI-present root 注册:headless / 进程内 subagent child
// 能解析到父进程的 service,但不能用 child 捕获的上下文注册,
// 否则还原出的命令会来自错误的会话。
if (!ctx.hasUI) {
return;
}
session = ctx.sessionManager;
tryRegister();
});
@@ -116,7 +156,7 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
disposeAuthorizer?.();
disposeAuthorizer = undefined;
sessionStarted = false;
session = undefined;
console.error(`[${LINK_NAME}] unregistered`);
});