Files
my-pi/pi-permission-system/src/extension-config.ts
T
云服务部-叶林立andDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 7e6a8faae1 feat(pi-permission-system): AutoReview 改为跟随主模型的对话式工具调用审查
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>
2026-10-19 01:54:17 -07:00

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}`;
}
}