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
@@ -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";
@@ -1,88 +0,0 @@
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
/** Name of Pi's native Bash tool, as recorded in a tool-call block. */
export const NATIVE_BASH_TOOL_NAME = "bash";
/** A structurally-validated tool-call content block. */
interface ToolCallBlock {
readonly id: string;
readonly name: unknown;
readonly arguments: unknown;
}
function isToolCallBlock(block: unknown): block is ToolCallBlock {
return (
block !== null &&
typeof block === "object" &&
(block as { type?: unknown }).type === "toolCall" &&
typeof (block as { id?: unknown }).id === "string"
);
}
/** Read the native Bash command off a single validated tool-call block. */
function extractBashCommand(block: ToolCallBlock): string | undefined {
if (block.name !== NATIVE_BASH_TOOL_NAME) {
return undefined;
}
const command = (
block.arguments as { command?: unknown } | null | undefined
)?.command;
return typeof command === "string" ? command : undefined;
}
/**
* Recover the complete native Bash command for one tool call.
*
* The tool call being authorized is always the most recent one, so entries are
* 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
* `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.
*
* @returns the complete Bash command, or `undefined`.
*/
export function recoverNativeBashCommand(
entries: ReadonlyArray<SessionEntry>,
toolCallId: string,
): string | undefined {
for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i];
if (entry.type !== "message") {
continue;
}
const message = entry.message;
if (message.role !== "assistant") {
continue;
}
const matches: ToolCallBlock[] = [];
for (const block of message.content) {
if (isToolCallBlock(block) && block.id === toolCallId) {
matches.push(block);
}
}
if (matches.length === 0) {
continue;
}
// Latest message containing the id. Uniqueness only has to hold within
// this one message (see above); a cross-message reuse resolves to the
// latest, which is the call currently being authorized.
return matches.length === 1
? extractBashCommand(matches[0])
: undefined;
}
return undefined;
}