refactor(permission): consume structured bash payload

- require @gotgenes/pi-permission-system >=25.3.0 and read the complete local bash command from PromptPermissionDetails.payload instead of session-walking recovery
- remove the @sikongjueluo/pi-permission-shared package
- pass the triggering command unit to handlers via HandlerContext.unit in place of details.command
- add shadow-only AI judge modules for evidence projection, structured verdict requests, and prompt building, with vitest coverage
- record ADR 0004 and mark the ADR 0001 recovery mechanism superseded
- exclude pi-permission-system 25.3.0 from the pnpm minimumReleaseAge guard
This commit is contained in:
2026-08-16 23:08:45 +08:00
parent 6008c9e817
commit 1afcbd3118
29 changed files with 1540 additions and 587 deletions
@@ -0,0 +1,62 @@
import type { PromptPermissionDetails } from "@gotgenes/pi-permission-system";
const NATIVE_BASH_TOOL_NAME = "bash";
const FULL_COMMAND_LABEL = "full command";
export interface BashJudgmentEvidence {
readonly fullCommand: string;
readonly triggeringUnit?: string;
}
function isNonBlank(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
/**
* Project a local native-Bash ask from permission-system's complete payload.
*
* A `full command` evidence entry is emitted only when the original tool input
* differs from `request.value`; otherwise the value itself is complete. Any
* forwarded, aliased, inconsistent, or ambiguous payload defers upstream.
*/
export function buildBashJudgmentEvidence(
details: PromptPermissionDetails,
): BashJudgmentEvidence | undefined {
const payload = details.payload;
const request = payload?.request;
if (
request === undefined ||
!Array.isArray(payload.evidence) ||
details.forwarding !== undefined ||
payload.kind !== "bash" ||
request.requester?.forwarded !== false ||
details.toolName !== NATIVE_BASH_TOOL_NAME ||
request.toolName !== NATIVE_BASH_TOOL_NAME ||
request.invokedToolName !== null ||
request.surface !== NATIVE_BASH_TOOL_NAME ||
!isNonBlank(request.value) ||
(details.command !== undefined && details.command !== request.value)
) {
return undefined;
}
const fullCommands = payload.evidence.filter(
(entry) => entry.label === FULL_COMMAND_LABEL,
);
if (fullCommands.length > 1) {
return undefined;
}
const fullCommand =
fullCommands.length === 0 ? request.value : fullCommands[0]?.text;
if (!isNonBlank(fullCommand)) {
return undefined;
}
return {
fullCommand,
triggeringUnit:
request.value === fullCommand ? undefined : request.value,
};
}