mirror of
https://github.com/SikongJueluo/pi-extensions.git
synced 2026-10-05 11:52:55 +08:00
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:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
});
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@gotgenes/pi-permission-system": ">=24.0.0"
|
||||
"@gotgenes/pi-permission-system": ">=24.0.0",
|
||||
"@sikongjueluo/pi-permission-shared": "workspace:*"
|
||||
},
|
||||
"bundledDependencies": [
|
||||
"@gotgenes/pi-permission-system"
|
||||
"@gotgenes/pi-permission-system",
|
||||
"@sikongjueluo/pi-permission-shared"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@earendil-works/pi-ai": "*",
|
||||
|
||||
@@ -6,7 +6,10 @@ import type {
|
||||
PromptPermissionDetails,
|
||||
} from "@gotgenes/pi-permission-system";
|
||||
import { classifyWrapper, isRecognizedWrapper } from "./recognizer";
|
||||
import { NATIVE_BASH_TOOL_NAME, recoverNativeBashCommand } from "./recovery";
|
||||
import {
|
||||
NATIVE_BASH_TOOL_NAME,
|
||||
recoverNativeBashCommand,
|
||||
} from "@sikongjueluo/pi-permission-shared";
|
||||
|
||||
/** Bash permission surface queried when re-evaluating the inner command. */
|
||||
const BASH_SURFACE = "bash";
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@sikongjueluo/pi-permission-shared",
|
||||
"version": "0.0.1",
|
||||
"description": "Shared session-recovery utilities for the pi-permission extensions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"private": true,
|
||||
"peerDependencies": {
|
||||
"@earendil-works/pi-coding-agent": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@earendil-works/pi-coding-agent": "*",
|
||||
"@types/node": "^26.0.0",
|
||||
"typescript": "^5",
|
||||
"vitest": "^3"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./recovery";
|
||||
+9
-8
@@ -37,17 +37,18 @@ function extractBashCommand(block: ToolCallBlock): string | undefined {
|
||||
* walked in reverse and the search stops at the first (latest) assistant
|
||||
* message that contains a `toolCall` block whose `id` equals `toolCallId`.
|
||||
*
|
||||
* Per ADR 0001, the id must match exactly one block *within that single
|
||||
* message*. An earlier message reusing the same id is a stale, already-resolved
|
||||
* call and is irrelevant to the current authorization; but two matching blocks
|
||||
* inside one message cannot be disambiguated (we cannot tell which one
|
||||
* `details.toolCallId` refers to), so that case stays fail-closed. The matched
|
||||
* block must then name the native Bash tool and carry a string
|
||||
* The id must match exactly one block *within that single message*. An earlier
|
||||
* message reusing the same id is a stale, already-resolved call and is
|
||||
* irrelevant to the current authorization; but two matching blocks inside one
|
||||
* message cannot be disambiguated (we cannot tell which one the caller's
|
||||
* `toolCallId` refers to), so that case returns `undefined` (fail-closed). The
|
||||
* matched block must then name the native Bash tool and carry a string
|
||||
* `arguments.command`.
|
||||
*
|
||||
* Any other outcome — no match, a within-message duplicate id, a non-Bash tool
|
||||
* call, a non-string command, or malformed entries — returns `undefined` so the
|
||||
* caller defers fail-closed.
|
||||
* call, a non-string command, or malformed entries — returns `undefined`.
|
||||
*
|
||||
* See ADR 0001 for the underlying permission/evidence boundaries.
|
||||
*
|
||||
* @returns the complete Bash command, or `undefined`.
|
||||
*/
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"test"
|
||||
]
|
||||
}
|
||||
Generated
+25
@@ -212,6 +212,10 @@ importers:
|
||||
version: 24.0.0(@earendil-works/pi-coding-agent@0.84.1)(@earendil-works/pi-tui@0.84.1)
|
||||
|
||||
packages/pi-permission-ai-judge:
|
||||
dependencies:
|
||||
'@sikongjueluo/pi-permission-shared':
|
||||
specifier: workspace:*
|
||||
version: link:../pi-permission-shared
|
||||
devDependencies:
|
||||
'@earendil-works/pi-ai':
|
||||
specifier: '*'
|
||||
@@ -222,6 +226,9 @@ importers:
|
||||
'@gotgenes/pi-permission-system':
|
||||
specifier: '>=20.10.0'
|
||||
version: 24.0.0(@earendil-works/pi-coding-agent@0.84.1)(@earendil-works/pi-tui@0.84.1)
|
||||
'@types/node':
|
||||
specifier: ^26.0.0
|
||||
version: 26.1.2
|
||||
typescript:
|
||||
specifier: ^5
|
||||
version: 5.9.3
|
||||
@@ -234,6 +241,9 @@ importers:
|
||||
'@gotgenes/pi-permission-system':
|
||||
specifier: '>=24.0.0'
|
||||
version: 24.0.0(@earendil-works/pi-coding-agent@0.84.1)(@earendil-works/pi-tui@0.84.1)
|
||||
'@sikongjueluo/pi-permission-shared':
|
||||
specifier: workspace:*
|
||||
version: link:../pi-permission-shared
|
||||
devDependencies:
|
||||
'@earendil-works/pi-ai':
|
||||
specifier: '*'
|
||||
@@ -251,6 +261,21 @@ importers:
|
||||
specifier: ^3
|
||||
version: 3.2.7(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)
|
||||
|
||||
packages/pi-permission-shared:
|
||||
devDependencies:
|
||||
'@earendil-works/pi-coding-agent':
|
||||
specifier: '*'
|
||||
version: 0.84.1(ws@8.21.2)(zod@4.4.3)
|
||||
'@types/node':
|
||||
specifier: ^26.0.0
|
||||
version: 26.1.2
|
||||
typescript:
|
||||
specifier: ^5
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3
|
||||
version: 3.2.7(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)
|
||||
|
||||
packages:
|
||||
|
||||
'@anthropic-ai/sdk@0.91.1':
|
||||
|
||||
Reference in New Issue
Block a user