mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
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>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
dec48dab6d
commit
7e6a8faae1
@@ -2,6 +2,7 @@ import type { DecisionSource } from "#src/authority/decision-source";
|
||||
import type { AuthorizerLog, PermissionQuery } from "#src/service";
|
||||
import type {
|
||||
AuthorizerVerdict,
|
||||
DeferNote,
|
||||
NamedAuthorizer,
|
||||
TerminalAuthorizer,
|
||||
} from "./authorizer";
|
||||
@@ -38,15 +39,25 @@ export function composeAuthorizerChain(
|
||||
}
|
||||
return {
|
||||
async authorize(details) {
|
||||
const notes: DeferNote[] = [];
|
||||
for (const link of links) {
|
||||
const verdict = await link.authorize(details, query, log);
|
||||
const decision = decideFromVerdict(link.name, verdict);
|
||||
if (decision) {
|
||||
return decision;
|
||||
}
|
||||
// `defer` \u2014 try the next link.
|
||||
// `defer` — keep the link's note and try the next link.
|
||||
if (verdict.kind === "defer" && verdict.note) {
|
||||
notes.push(verdict.note);
|
||||
}
|
||||
}
|
||||
return terminal.authorize(details);
|
||||
// Notes ride to the terminal on the details so the human prompt can
|
||||
// show why auto-approval did not apply. They do not cross the wire: a
|
||||
// relaying terminal (ParentAuthorizer) does not forward them, so the
|
||||
// serving node regenerates its own notes from its own chain.
|
||||
return terminal.authorize(
|
||||
notes.length > 0 ? { ...details, deferNotes: notes } : details,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,10 +20,23 @@ import type { SubagentDetector } from "./subagent-detection";
|
||||
* pass the ask on to the next link (`defer`). A `deny` carries an optional
|
||||
* teaching `reason` the invoking model sees, so it can self-correct.
|
||||
*/
|
||||
/**
|
||||
* A note a deferring link attaches to its refusal, for the human prompt.
|
||||
*
|
||||
* `origin` says who wrote the text: `model` is the reviewer's own verdict
|
||||
* reason (marked as model-generated when rendered), `system` is a
|
||||
* deterministic, code-authored sentence explaining why no verdict was
|
||||
* produced.
|
||||
*/
|
||||
export interface DeferNote {
|
||||
readonly origin: "model" | "system";
|
||||
readonly text: string;
|
||||
}
|
||||
|
||||
export type AuthorizerVerdict =
|
||||
| { kind: "allow" }
|
||||
| { kind: "deny"; reason?: string }
|
||||
| { kind: "defer" };
|
||||
| { kind: "defer"; note?: DeferNote };
|
||||
|
||||
/**
|
||||
* A non-terminal link in the live-authority chain: reviews an `ask` and may
|
||||
|
||||
@@ -44,7 +44,7 @@ export interface LocalUserAuthorizerDeps {
|
||||
export class LocalUserAuthorizer implements TerminalAuthorizer {
|
||||
constructor(private readonly deps: LocalUserAuthorizerDeps) {}
|
||||
|
||||
authorize(
|
||||
async authorize(
|
||||
details: PromptPermissionDetails,
|
||||
): Promise<PermissionPromptDecision> {
|
||||
const uiPrompt = buildUiPrompt(details);
|
||||
@@ -73,6 +73,9 @@ function buildRequestOptions(
|
||||
details: PromptPermissionDetails,
|
||||
): RequestPermissionOptions | undefined {
|
||||
const pattern = details.sessionApproval?.patterns[0];
|
||||
const notes = details.deferNotes;
|
||||
const withNotes =
|
||||
notes && notes.length > 0 ? { deferNotes: notes } : undefined;
|
||||
if (details.forwarding && details.sessionApproval && pattern) {
|
||||
return {
|
||||
sessionScope: buildForwardedScopeLabels(
|
||||
@@ -80,9 +83,11 @@ function buildRequestOptions(
|
||||
details.sessionApproval.surface,
|
||||
pattern,
|
||||
),
|
||||
...withNotes,
|
||||
};
|
||||
}
|
||||
return details.sessionLabel
|
||||
? { sessionLabel: details.sessionLabel }
|
||||
: undefined;
|
||||
if (details.sessionLabel) {
|
||||
return { sessionLabel: details.sessionLabel, ...withNotes };
|
||||
}
|
||||
return withNotes;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DecisionSource } from "#src/authority/decision-source";
|
||||
import type { PromptDecisionNote } from "#src/presentation/prompt-payload";
|
||||
|
||||
export type PermissionDecisionState =
|
||||
| "approved"
|
||||
@@ -110,6 +111,14 @@ export interface RequestPermissionOptions {
|
||||
subagentLabel: string;
|
||||
servingSessionLabel: string;
|
||||
};
|
||||
/**
|
||||
* Why automatic review declined to approve, shown ahead of the ask facts.
|
||||
*
|
||||
* `origin` marks who wrote each note: a `model` reason is the reviewer's own
|
||||
* verdict text and renders marked as model-generated; a `system` note is a
|
||||
* deterministic, code-authored sentence.
|
||||
*/
|
||||
deferNotes?: readonly PromptDecisionNote[];
|
||||
}
|
||||
|
||||
export async function requestPermissionDecisionFromUi(
|
||||
|
||||
@@ -30,7 +30,10 @@ import {
|
||||
renderPromptDialog,
|
||||
} from "#src/presentation/dialog-renderer";
|
||||
import { fitLinesToWidth } from "#src/presentation/line-fitting";
|
||||
import type { PromptPayload } from "#src/presentation/prompt-payload";
|
||||
import type {
|
||||
PromptDecisionNote,
|
||||
PromptPayload,
|
||||
} from "#src/presentation/prompt-payload";
|
||||
|
||||
/**
|
||||
* Inline `ctx.ui.custom` permission dialog for TUI sessions.
|
||||
@@ -91,10 +94,15 @@ export async function requestPermissionDecision(
|
||||
// The fallback renders once and cannot re-render, so it neither paints nor
|
||||
// offers an expansion; it substitutes a nominal width for the terminal size
|
||||
// it is never told, and the host's own select wraps from there.
|
||||
const rendered = renderPromptDialog(payload, {
|
||||
...view.budget,
|
||||
width: FALLBACK_RENDER_WIDTH,
|
||||
});
|
||||
const rendered = renderPromptDialog(
|
||||
payload,
|
||||
{
|
||||
...view.budget,
|
||||
width: FALLBACK_RENDER_WIDTH,
|
||||
},
|
||||
undefined,
|
||||
options?.deferNotes,
|
||||
);
|
||||
return attributeToHuman(
|
||||
await requestPermissionDecisionFromUi(
|
||||
view.ui,
|
||||
@@ -157,6 +165,7 @@ export function presentInlinePermissionPrompt(
|
||||
tui.requestRender();
|
||||
},
|
||||
done,
|
||||
options?.deferNotes,
|
||||
),
|
||||
{ overlay: false },
|
||||
);
|
||||
@@ -203,6 +212,7 @@ class PermissionPromptComponent implements Component {
|
||||
private readonly handleAppAction: (data: string) => boolean,
|
||||
private readonly requestRender: () => void,
|
||||
private readonly done: (decision: UnattributedDecision) => void,
|
||||
private readonly notes: readonly PromptDecisionNote[] = [],
|
||||
) {
|
||||
this.state = initialPromptState(config);
|
||||
this.reason = this.createReasonEditor();
|
||||
@@ -259,6 +269,7 @@ class PermissionPromptComponent implements Component {
|
||||
this.payload,
|
||||
this.expanded ? completeViewBudget(width) : { ...this.budget, width },
|
||||
(text) => this.theme.fg("warning", text),
|
||||
this.notes,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
import type { PromptPayload } from "#src/presentation/prompt-payload";
|
||||
import { renderReviewLogFacts } from "#src/presentation/review-log-renderer";
|
||||
import type { ReviewLogger } from "#src/session-logger";
|
||||
import type { TerminalAuthorizer } from "./authorizer";
|
||||
import type { DeferNote, TerminalAuthorizer } from "./authorizer";
|
||||
|
||||
export type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
|
||||
|
||||
@@ -70,6 +70,15 @@ export interface PromptPermissionDetails {
|
||||
* no intent leaves this absent.
|
||||
*/
|
||||
accessIntent?: ForwardedAccessFacts;
|
||||
/**
|
||||
* Notes the live-authority chain attached to its refusal, in chain order.
|
||||
*
|
||||
* Set by `composeAuthorizerChain` when a link defers, so the human prompt can
|
||||
* show *why* the ask was not auto-approved. `origin` says who wrote each
|
||||
* note: `model` is the reviewer's own verdict reason (rendered marked as
|
||||
* model-generated), `system` is a code-authored sentence.
|
||||
*/
|
||||
deferNotes?: readonly DeferNote[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user