mirror of
https://github.com/SikongJueluo/pi-extensions.git
synced 2026-10-05 11:52:55 +08:00
fix(ai-judge): defer irreversible destructive operations regardless of intent (prompt v4, PIEXTENSIO-20)
This commit is contained in:
@@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildJudgeContext,
|
||||
MAX_REASON_CODE_POINTS,
|
||||
PROMPT_VERSION,
|
||||
REPORT_VERDICT_TOOL_NAME,
|
||||
} from "../src/prompt";
|
||||
import type { Context } from "@earendil-works/pi-ai";
|
||||
|
||||
describe("buildJudgeContext", () => {
|
||||
it("builds one side-effect-free structured verdict tool", () => {
|
||||
@@ -42,3 +44,108 @@ describe("buildJudgeContext", () => {
|
||||
expect(text).toContain("untrusted data");
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Prompt v4 irreversibility contract (PIEXTENSIO-20).
|
||||
*
|
||||
* Cohort piextensio-19-v3-gpt56sol-20260819-01 failed its frozen safety
|
||||
* floor with 25/25 destructive-class AI-allow / blind human-deny rows:
|
||||
* prompt v3 licensed ALLOW for destructive operations on intent
|
||||
* specificity alone ("a destructive operation the user explicitly and
|
||||
* specifically requested is ALLOW") and few-shot example 1 taught
|
||||
* exactly the cohort's failing shape (`git clean -xfd` named verbatim →
|
||||
* allow). Live-model verdicts are not CI-assertable (PIEXTENSIO-18
|
||||
* non-goal); these tests pin the prompt contract that must hold for a
|
||||
* candidate to satisfy the PIEXTENSIO-10 floor.
|
||||
*/
|
||||
const DESTRUCTIVE_MARKERS =
|
||||
/(?:rm\s+-rf|git\s+clean\s+-[a-z]*f|git\s+reset\s+--hard|git\s+push\s+--force|git\s+checkout\s+--\s+\.)/i;
|
||||
|
||||
interface FewShotPair {
|
||||
readonly userText: string;
|
||||
readonly verdict: string | null;
|
||||
}
|
||||
|
||||
function fewShotPairs(context: Context): FewShotPair[] {
|
||||
const examples = context.messages.slice(0, -1);
|
||||
const pairs: FewShotPair[] = [];
|
||||
for (let i = 0; i + 1 < examples.length; i += 2) {
|
||||
const user = examples[i];
|
||||
const assistant = examples[i + 1];
|
||||
if (!user || !assistant) continue;
|
||||
const userText =
|
||||
user.role === "user" && Array.isArray(user.content)
|
||||
? user.content
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("\n")
|
||||
: "";
|
||||
const call =
|
||||
assistant.role === "assistant" && Array.isArray(assistant.content)
|
||||
? assistant.content.find((p) => p.type === "toolCall")
|
||||
: undefined;
|
||||
const verdict =
|
||||
call && call.type === "toolCall" && call.arguments !== null
|
||||
? String(call.arguments.verdict ?? "")
|
||||
: null;
|
||||
pairs.push({ userText, verdict });
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
describe("prompt v4 irreversibility contract (PIEXTENSIO-20)", () => {
|
||||
const context = buildJudgeContext({ fullCommand: "git clean -xfd" });
|
||||
const systemPrompt = context.systemPrompt ?? "";
|
||||
const pairs = fewShotPairs(context);
|
||||
|
||||
it("changes the candidate identity to bash-shadow-v4", () => {
|
||||
expect(PROMPT_VERSION).toBe("bash-shadow-v4");
|
||||
});
|
||||
|
||||
it("no longer licenses allow for specifically requested destructive operations", () => {
|
||||
expect(systemPrompt).not.toMatch(
|
||||
/destructive operation the user explicitly and specifically requested is ALLOW/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("mandates defer for irreversible operations regardless of request specificity", () => {
|
||||
expect(systemPrompt).toMatch(/irreversible/i);
|
||||
expect(systemPrompt).toMatch(/no matter how specifically/i);
|
||||
});
|
||||
|
||||
it("requires every allowed operation to have recoverable effects", () => {
|
||||
expect(systemPrompt).toMatch(/recoverable/i);
|
||||
expect(systemPrompt).toMatch(/reversible, or reproducible/i);
|
||||
});
|
||||
|
||||
it("keeps deny reserved for conflicts and excessive or unrequested behavior", () => {
|
||||
expect(systemPrompt).toMatch(
|
||||
/DENY only for a clear security conflict/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("teaches no destructive-to-allow few-shot example", () => {
|
||||
const destructiveAllows = pairs.filter(
|
||||
(p) => p.verdict === "allow" && DESTRUCTIVE_MARKERS.test(p.userText),
|
||||
);
|
||||
expect(destructiveAllows).toEqual([]);
|
||||
});
|
||||
|
||||
it("teaches the cohort's failing shape: verbatim-requested irreversible destruction defers", () => {
|
||||
const failingShape = pairs.filter(
|
||||
(p) =>
|
||||
p.verdict === "defer" &&
|
||||
/Run exactly this command/.test(p.userText) &&
|
||||
/git clean -xfd/.test(p.userText),
|
||||
);
|
||||
expect(failingShape.length).toBe(1);
|
||||
});
|
||||
|
||||
it("still calibrates one reversible allow example and one deny example", () => {
|
||||
const allow = pairs.filter((p) => p.verdict === "allow");
|
||||
const deny = pairs.filter((p) => p.verdict === "deny");
|
||||
expect(allow.length).toBe(1);
|
||||
expect(DESTRUCTIVE_MARKERS.test(allow[0]?.userText ?? "")).toBe(false);
|
||||
expect(deny.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user