mirror of
https://github.com/SikongJueluo/pi-extensions.git
synced 2026-10-05 11:52:55 +08:00
feat(pi-permission-inner-cmd): authorize inner commands behind timeout wrappers
- recover the full bash command from the session by tool-call id - add recognizer for the strict timeout wrapper grammar - add authorizer mapping inner allow/ask/deny and forwarding agent name - defer fail-closed on session mismatch, nested wrappers, and errors - add unit tests for recovery, recognizer, authorizer, and lifecycle - document the decision in ADR 0001
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
||||
import { recoverNativeBashCommand } from "../src/recovery";
|
||||
|
||||
/** Build a minimal assistant message entry carrying the given content blocks. */
|
||||
function assistantEntry(content: unknown[]): SessionEntry {
|
||||
return {
|
||||
type: "message",
|
||||
id: "entry-1",
|
||||
parentId: null,
|
||||
timestamp: "2026-08-08T00:00:00.000Z",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content,
|
||||
},
|
||||
} as unknown as SessionEntry;
|
||||
}
|
||||
|
||||
/** A user message entry, to confirm non-assistant entries are ignored. */
|
||||
function userEntry(): SessionEntry {
|
||||
return {
|
||||
type: "message",
|
||||
id: "entry-user",
|
||||
parentId: null,
|
||||
timestamp: "2026-08-08T00:00:00.000Z",
|
||||
message: { role: "user", content: "hello" },
|
||||
} as unknown as SessionEntry;
|
||||
}
|
||||
|
||||
/** A tool-result message entry, ignored by recovery. */
|
||||
function toolResultEntry(): SessionEntry {
|
||||
return {
|
||||
type: "message",
|
||||
id: "entry-tool-result",
|
||||
parentId: null,
|
||||
timestamp: "2026-08-08T00:00:00.000Z",
|
||||
message: {
|
||||
role: "toolResult",
|
||||
toolCallId: "call_1",
|
||||
toolName: "bash",
|
||||
content: [],
|
||||
isError: false,
|
||||
timestamp: 0,
|
||||
},
|
||||
} as unknown as SessionEntry;
|
||||
}
|
||||
|
||||
/** A non-message entry (compaction), ignored by recovery. */
|
||||
function compactionEntry(): SessionEntry {
|
||||
return {
|
||||
type: "compaction",
|
||||
id: "entry-compaction",
|
||||
parentId: null,
|
||||
timestamp: "2026-08-08T00:00:00.000Z",
|
||||
summary: "...",
|
||||
firstKeptEntryId: "x",
|
||||
tokensBefore: 0,
|
||||
} as unknown as SessionEntry;
|
||||
}
|
||||
|
||||
function toolCall(
|
||||
id: string,
|
||||
name: string,
|
||||
args: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
return { type: "toolCall", id, name, arguments: args };
|
||||
}
|
||||
|
||||
function bashToolCall(id: string, command: unknown): Record<string, unknown> {
|
||||
return { type: "toolCall", id, name: "bash", arguments: { command } };
|
||||
}
|
||||
|
||||
describe("recoverNativeBashCommand", () => {
|
||||
it("returns the command for a single native Bash tool call", () => {
|
||||
const entries = [
|
||||
userEntry(),
|
||||
assistantEntry([
|
||||
{ type: "text", text: "running tests" },
|
||||
bashToolCall("call_1", "timeout 30s pnpm test"),
|
||||
]),
|
||||
];
|
||||
expect(recoverNativeBashCommand(entries, "call_1")).toBe(
|
||||
"timeout 30s pnpm test",
|
||||
);
|
||||
});
|
||||
|
||||
it("finds the matching tool call among several with different ids", () => {
|
||||
const entries = [
|
||||
assistantEntry([
|
||||
bashToolCall("call_a", "pnpm build"),
|
||||
bashToolCall("call_b", "timeout 30s pnpm test"),
|
||||
]),
|
||||
];
|
||||
expect(recoverNativeBashCommand(entries, "call_b")).toBe(
|
||||
"timeout 30s pnpm test",
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores user, tool-result, and non-message entries", () => {
|
||||
const entries = [
|
||||
compactionEntry(),
|
||||
userEntry(),
|
||||
toolResultEntry(),
|
||||
assistantEntry([bashToolCall("call_1", "echo hi")]),
|
||||
];
|
||||
expect(recoverNativeBashCommand(entries, "call_1")).toBe("echo hi");
|
||||
});
|
||||
|
||||
it("returns undefined when no tool call matches the id", () => {
|
||||
const entries = [assistantEntry([bashToolCall("call_1", "echo hi")])];
|
||||
expect(recoverNativeBashCommand(entries, "call_missing")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined on a duplicate id (cannot prove authority)", () => {
|
||||
const entries = [
|
||||
assistantEntry([
|
||||
bashToolCall("call_1", "timeout 30s pnpm test"),
|
||||
bashToolCall("call_1", "timeout 30s rm -rf /"),
|
||||
]),
|
||||
];
|
||||
expect(recoverNativeBashCommand(entries, "call_1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when the only match is a non-Bash tool", () => {
|
||||
const entries = [
|
||||
assistantEntry([
|
||||
toolCall("call_1", "read", { path: "/etc/passwd" }),
|
||||
]),
|
||||
];
|
||||
expect(recoverNativeBashCommand(entries, "call_1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when arguments.command is not a string", () => {
|
||||
const entries = [
|
||||
assistantEntry([bashToolCall("call_1", 12345)]),
|
||||
];
|
||||
expect(recoverNativeBashCommand(entries, "call_1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when arguments.command is missing", () => {
|
||||
const entries = [
|
||||
assistantEntry([
|
||||
toolCall("call_1", "bash", { timeout: 30 }),
|
||||
]),
|
||||
];
|
||||
expect(recoverNativeBashCommand(entries, "call_1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for a duplicate id even when the first match is invalid", () => {
|
||||
const entries = [
|
||||
assistantEntry([
|
||||
toolCall("call_1", "read", { path: "/a" }),
|
||||
bashToolCall("call_1", "timeout 30s pnpm test"),
|
||||
]),
|
||||
];
|
||||
expect(recoverNativeBashCommand(entries, "call_1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("tolerates a malformed content block that is not a tool call", () => {
|
||||
const entries = [
|
||||
assistantEntry([
|
||||
{ type: "text", text: "thinking..." },
|
||||
null,
|
||||
{ type: "thinking", thinking: "..." },
|
||||
bashToolCall("call_1", "timeout 30s pnpm test"),
|
||||
]),
|
||||
];
|
||||
expect(recoverNativeBashCommand(entries, "call_1")).toBe(
|
||||
"timeout 30s pnpm test",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user