mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
AutoReview: - 审查器回到 Codex Guardian 风格的对话式审查,默认跟随会话当前主模型 (每次审查实时读取 ctx.model);provider/model 必须同时设置才固定审查 模型,只设其一视为配置错误并转人工。 - 结论只通过 approve / reject 两个工具给出,参数仅 reason;不强制 toolChoice,正文文本(即使是合法 JSON)一律忽略。 - approve 自动放行;reject 转人工并在弹窗显示理由(标注模型生成)。 超时、Provider 错误、截断、未调用工具、多次调用、参数无效等异常同样 转人工,并附固定的系统说明;用户按 Esc 中止时直接拒绝。 - 模型调用改为 modelRegistry.streamSimple(...).result():Pi 1.3.0 面向 扩展的 ModelRegistry 没有 completeSimple,原写法在真实环境中每次都会 转人工。 - 对话记录保留可信度标注;工具结果在有更短的 pi-compaction 摘要时替换 为摘要;token 估算改为非 ASCII 字符按 1 token 计;预算可配置。 - 同一条用户消息内完全相同的请求只缓存 approve 结论。 - 默认 reasoning low、总超时 30 秒、最多 2 次尝试、maxOutputTokens 4000。 删除: - 未发布过的 TypeSafe Jev 分类器实现、DenialCircuitBreaker。 - 整个 annotator 机制(model-explanation、注册表、registerAnnotator、 provider-timeout);annotators 配置键保留为已弃用并忽略,避免旧配置 被拒绝。 测试: - 新增 reviewer / transcript / 弹窗说明 / 默认配置测试;清理重复、恒真 或只测实现细节的旧测试。 - 使用 deepseek-v4.1-flash 实测 5 个场景各 5 次,25 次结论全部符合预期。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
import { mkdirSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
normalizeAutoReviewConfig,
|
|
type AutoReviewConfig,
|
|
} from "./auto-review/config.js";
|
|
import type {
|
|
ShellToolsConfig,
|
|
UnifiedPermissionConfig,
|
|
} from "./config-loader";
|
|
import {
|
|
OWNER_ONLY_DIRECTORY_MODE,
|
|
restrictExistingPathToOwner,
|
|
} from "./log-file-permissions";
|
|
|
|
export const EXTENSION_ID = "pi-permission-system";
|
|
|
|
export interface PermissionSystemExtensionConfig {
|
|
debugLog: boolean;
|
|
permissionReviewLog: boolean;
|
|
yoloMode: boolean;
|
|
/** Require a confirming second press of a decision hotkey in the inline TUI dialog. Defaults to true. */
|
|
doublePressToConfirm: boolean;
|
|
/** How long a subagent waits for the parent's answer to a forwarded ask, in ms. Defaults to 600000. */
|
|
forwardingTimeoutMs?: number;
|
|
/** Max rows a permission prompt renders before eliding its evidence. Defaults to 24. */
|
|
promptMaxRows?: number;
|
|
/** Max characters of any one field shown in a permission prompt. Defaults to 400. */
|
|
promptFieldMaxWidth?: number;
|
|
/** Max characters of any one value written to the permission review log. Defaults to 1000. */
|
|
reviewLogFieldMaxWidth?: number;
|
|
/** Non-bash tools that carry shell semantics, keyed by tool name. */
|
|
shellTools?: ShellToolsConfig;
|
|
authorizerChain?: string[];
|
|
/** Built-in model reviewer configuration. */
|
|
autoReview?: AutoReviewConfig;
|
|
}
|
|
|
|
export const DEFAULT_EXTENSION_CONFIG: PermissionSystemExtensionConfig = {
|
|
debugLog: false,
|
|
permissionReviewLog: true,
|
|
yoloMode: false,
|
|
doublePressToConfirm: true,
|
|
};
|
|
|
|
function resolveExtensionRoot(moduleUrl = import.meta.url): string {
|
|
return join(dirname(fileURLToPath(moduleUrl)), "..");
|
|
}
|
|
|
|
export const EXTENSION_ROOT = resolveExtensionRoot();
|
|
|
|
const PERMISSION_POLICY_KEYS: ReadonlySet<string> = new Set([
|
|
"defaultPolicy",
|
|
"tools",
|
|
"bash",
|
|
"mcp",
|
|
"skills",
|
|
"special",
|
|
]);
|
|
|
|
export function detectMisplacedPermissionKeys(
|
|
raw: Record<string, unknown>,
|
|
): string[] {
|
|
return Object.keys(raw).filter((key) => PERMISSION_POLICY_KEYS.has(key));
|
|
}
|
|
|
|
export function normalizePermissionSystemConfig(
|
|
raw: UnifiedPermissionConfig,
|
|
): PermissionSystemExtensionConfig {
|
|
const result: PermissionSystemExtensionConfig = {
|
|
debugLog: raw.debugLog === true,
|
|
permissionReviewLog: raw.permissionReviewLog !== false,
|
|
yoloMode: raw.yoloMode === true,
|
|
doublePressToConfirm: raw.doublePressToConfirm !== false,
|
|
};
|
|
if (raw.autoReview !== undefined) {
|
|
result.autoReview = normalizeAutoReviewConfig(raw.autoReview);
|
|
}
|
|
if (raw.forwardingTimeoutMs !== undefined) {
|
|
result.forwardingTimeoutMs = raw.forwardingTimeoutMs;
|
|
}
|
|
if (raw.promptMaxRows !== undefined) {
|
|
result.promptMaxRows = raw.promptMaxRows;
|
|
}
|
|
if (raw.promptFieldMaxWidth !== undefined) {
|
|
result.promptFieldMaxWidth = raw.promptFieldMaxWidth;
|
|
}
|
|
if (raw.reviewLogFieldMaxWidth !== undefined) {
|
|
result.reviewLogFieldMaxWidth = raw.reviewLogFieldMaxWidth;
|
|
}
|
|
// `toolInputPreviewMaxLength` / `toolTextSummaryMaxLength` / `annotators`
|
|
// are deliberately absent: the schema and the merge still accept them so a
|
|
// deployed config keeps loading, but no runtime consumer may read one
|
|
// (ADR 0011 §5, #745).
|
|
if (raw.shellTools !== undefined) {
|
|
result.shellTools = raw.shellTools;
|
|
}
|
|
if (raw.authorizerChain !== undefined) {
|
|
result.authorizerChain = raw.authorizerChain;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function isYoloModeEnabled(
|
|
config: PermissionSystemExtensionConfig,
|
|
): boolean {
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion -- typed as boolean but may be undefined at runtime (untyped callers); Boolean() guards against that
|
|
return Boolean(config.yoloMode);
|
|
}
|
|
|
|
export function ensurePermissionSystemLogsDirectory(
|
|
logsDir: string,
|
|
): string | undefined {
|
|
try {
|
|
// `recursive` applies the mode to every directory this creates, so a fresh
|
|
// install also gets an owner-only extension config dir. Directories that
|
|
// already exist are untouched by `mkdirSync`, hence the explicit tighten.
|
|
mkdirSync(logsDir, { recursive: true, mode: OWNER_ONLY_DIRECTORY_MODE });
|
|
restrictExistingPathToOwner(logsDir, OWNER_ONLY_DIRECTORY_MODE);
|
|
return undefined;
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
return `Failed to create permission-system log directory '${logsDir}': ${message}`;
|
|
}
|
|
}
|