fix(agent): track rewritten xdev approval tiers

This commit is contained in:
can1357
2026-08-02 20:56:06 +02:00
parent 721c12382c
commit f7480294b4
5 changed files with 96 additions and 1 deletions
@@ -239,6 +239,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
// short-circuit above.
const resolvedArgs = approvalArgs(effectiveParams, context);
const resolved = resolveApproval(this.tool, resolvedArgs, approvalMode, userPolicies);
context?.xdevTierResolved?.(resolved.tier);
if (resolved.policy === "deny") {
throw new Error(
`Tool "${this.tool.name}" is blocked by user policy.\n` +
@@ -13,6 +13,9 @@ declare module "@oh-my-pi/pi-agent-core" {
* wrapper must not re-prompt for the same action (explicit per-tool
* policies and overrides still apply). */
xdevApproved?: boolean;
/** Reports the approval tier resolved after an extension rewrites an
* xd:// device call, so dispatch metadata describes the input that ran. */
xdevTierResolved?(tier: "read" | "write" | "exec"): void;
/** Set only after an interactive prompt approves provider computer safety checks. */
providerSafetyApproved?: boolean;
}
+9 -1
View File
@@ -444,7 +444,15 @@ export async function dispatchXdevTool(
})
: undefined;
const executable = state.decorateExecution?.(canonical) ?? canonical;
const result = await executable.execute(toolCallId, validated as never, signal, innerOnUpdate, context);
const executionContext = context
? {
...context,
xdevTierResolved: (effectiveTier: ToolTier) => {
xdev = { ...xdev, tier: effectiveTier };
},
}
: undefined;
const result = await executable.execute(toolCallId, validated as never, signal, innerOnUpdate, executionContext);
return { result, xdev: { ...xdev, inner: result.details } };
} catch (error) {
if (
@@ -2542,6 +2542,50 @@ describe("ExtensionRunner", () => {
expect(fs.existsSync(recordPath)).toBe(false); // tool never executed
});
it("reports the effective tier after a tool_call handler revises xd:// input", async () => {
const recordPath = path.join(tempDir.path(), "xdev-effective-tier.jsonl");
const extCode = `
export default function(pi) {
pi.on("tool_call", async (event) => {
if (event.toolName !== "bash") return;
return { input: { command: "echo revised" } };
});
}
`;
fs.writeFileSync(path.join(extensionsDir, "tool-call-xdev-tier.ts"), extCode);
const result = await loadTestExtensions();
const runner = new ExtensionRunner(
result.extensions,
result.runtime,
tempDir.path(),
sessionManager,
modelRegistry,
);
const tool = createRecordingTool(recordPath);
tool.approval = args =>
args &&
typeof args === "object" &&
"command" in args &&
typeof args.command === "string" &&
args.command.includes("revised")
? "write"
: "read";
const wrapped = new ExtensionToolWrapper(tool, runner);
let effectiveTier: string | undefined;
const xdevContext = {
settings: { get: (key: string) => (key === "tools.approvalMode" ? "yolo" : {}) },
xdevApproved: true,
xdevTierResolved: (tier: string) => {
effectiveTier = tier;
},
} as never;
await wrapped.execute("xdev-tier-id", { command: "echo original" }, undefined, undefined, xdevContext);
expect(JSON.parse(fs.readFileSync(recordPath, "utf8"))).toEqual({ command: "echo revised" });
expect(effectiveTier).toBe("write");
});
it("emits tool_call before the approval prompt so approval sees the final input", async () => {
const order: string[] = [];
const extCode = `
@@ -131,6 +131,45 @@ describe("read and write route xd:// device URLs", () => {
expect(result.details?.xdev).toMatchObject({ tool: "peek", mode: "execute", tier: "read" });
});
it("records the effective tier reported after an execution decorator rewrites device args", async () => {
let executedQuery: string | undefined;
const device: AgentTool = {
name: "peek",
label: "Peek",
description: "Argument-dependent device",
parameters: type({ q: "string" }),
approval: args =>
args && typeof args === "object" && "q" in args && args.q === "mutate" ? "write" : "read",
async execute(_id, args) {
if (!args || typeof args !== "object" || !("q" in args) || typeof args.q !== "string") {
throw new Error("Expected a string query");
}
executedQuery = args.q;
return { content: [{ type: "text", text: "done" }] };
},
};
const xdev = createTestXdevState([device]);
xdev.decorateExecution = canonical => ({
...canonical,
async execute(id, _args, signal, onUpdate, context) {
const revised = { q: "mutate" };
context?.xdevTierResolved?.("write");
return canonical.execute(id, revised as never, signal, onUpdate, context);
},
});
const write = new WriteTool(xdevSession(process.cwd(), { xdev }));
const result = await write.execute(
"write-xdev-revised",
{ path: "xd://peek", content: JSON.stringify({ q: "inspect" }) },
undefined,
undefined,
{} as never,
);
expect(executedQuery).toBe("mutate");
expect(result.details?.xdev?.tier).toBe("write");
});
it("rejects near-miss xd addresses before filesystem fallback", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "write-xdev-near-miss-"));
try {