mirror of
https://github.com/SikongJueluo/pi-extensions.git
synced 2026-10-05 11:52:55 +08:00
feat(pi-permission-inner-cmd): recognize bare time wrapper as transparent
- add time handler that unwraps the bare reserved-word form time <command> and re-evaluates the full de-wrapped compound like timeout (ADR 0009) - defer fail-closed on dash-leading modifiers, bare time, and nested wrappers in both directions - generalize isRecognizedWrapper to timeout and time, and classifyWrapper to recognized/unsupported/other with a wrapper name - rename defer events to inner_cmd.nested_wrapper and inner_cmd.unsupported_wrapper_syntax with a wrapper field - extract stripWrapperUnit into handlers/strip.ts for shared use
This commit is contained in:
@@ -326,8 +326,11 @@ describe("authorizeInnerCommand — fail-closed deferrals", () => {
|
||||
expect(log).toEqual([
|
||||
{
|
||||
level: "debug",
|
||||
event: "inner_cmd.unsupported_timeout_syntax",
|
||||
details: { command: "timeout -k 5s 30s pnpm test" },
|
||||
event: "inner_cmd.unsupported_wrapper_syntax",
|
||||
details: {
|
||||
command: "timeout -k 5s 30s pnpm test",
|
||||
wrapper: "timeout",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -342,10 +345,11 @@ describe("authorizeInnerCommand — fail-closed deferrals", () => {
|
||||
expect(log).toEqual([
|
||||
{
|
||||
level: "debug",
|
||||
event: "inner_cmd.nested_timeout",
|
||||
event: "inner_cmd.nested_wrapper",
|
||||
details: {
|
||||
command: "timeout 30s timeout 10s pnpm test",
|
||||
innerCommand: "timeout 10s pnpm test",
|
||||
wrapper: "timeout",
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -602,6 +606,176 @@ describe("authorizeInnerCommand — fail-closed deferrals", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("authorizeInnerCommand — time wrapper", () => {
|
||||
it("maps an inner allow to allow and records a review", async () => {
|
||||
const { verdict, log, check } = await run({
|
||||
recoveredCommand: "time pnpm test",
|
||||
states: { "pnpm test": "allow" },
|
||||
});
|
||||
expect(verdict.kind).toBe("allow");
|
||||
expect(log).toEqual([
|
||||
{
|
||||
level: "review",
|
||||
event: "inner_cmd.allow",
|
||||
details: {
|
||||
requestId: "req-1",
|
||||
command: "time pnpm test",
|
||||
innerCommand: "pnpm test",
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(check).toEqual([
|
||||
{ surface: "bash", value: "pnpm test", agentName: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
it("maps an inner deny to deny and an inner ask to defer", async () => {
|
||||
const denied = await run({
|
||||
recoveredCommand: "time rm -rf /",
|
||||
states: { "rm -rf /": "deny" },
|
||||
});
|
||||
expect(denied.verdict.kind).toBe("deny");
|
||||
expect(denied.log[0]?.event).toBe("inner_cmd.deny");
|
||||
|
||||
const asked = await run({
|
||||
recoveredCommand: "time git push",
|
||||
states: { "git push": "ask" },
|
||||
});
|
||||
expect(asked.verdict.kind).toBe("defer");
|
||||
expect(asked.log[0]?.event).toBe("inner_cmd.inner_ask");
|
||||
});
|
||||
|
||||
it("re-checks the complete inner program for compound input", async () => {
|
||||
const { verdict, check } = await run({
|
||||
recoveredCommand: "time pnpm test && git push",
|
||||
states: { "pnpm test && git push": "ask" },
|
||||
});
|
||||
expect(verdict.kind).toBe("defer");
|
||||
expect(check).toEqual([
|
||||
{
|
||||
surface: "bash",
|
||||
value: "pnpm test && git push",
|
||||
agentName: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("unwraps a time buried in a scaffold", async () => {
|
||||
const full = "cd /repo && time pnpm install 2>&1 | tail -5";
|
||||
const deWrapped = "cd /repo && pnpm install 2>&1 | tail -5";
|
||||
const { verdict, log, check } = await run({
|
||||
recoveredCommand: full,
|
||||
unitCommand: "time pnpm install",
|
||||
states: { [deWrapped]: "allow" },
|
||||
});
|
||||
expect(verdict.kind).toBe("allow");
|
||||
expect(check).toEqual([
|
||||
{ surface: "bash", value: deWrapped, agentName: undefined },
|
||||
]);
|
||||
expect(log).toEqual([
|
||||
{
|
||||
level: "review",
|
||||
event: "inner_cmd.allow",
|
||||
details: {
|
||||
requestId: "req-1",
|
||||
command: full,
|
||||
innerCommand: "pnpm install",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("defers on unsupported time syntax with a debug log", async () => {
|
||||
// `-p` (and any dash-leading modifier) makes the form unsupported:
|
||||
// `/usr/bin/time` flags can write files (`-o`), and the command
|
||||
// string cannot distinguish the reserved word from the binary.
|
||||
const { verdict, log, check } = await run({
|
||||
recoveredCommand: "time -p ls",
|
||||
states: { ls: "allow" },
|
||||
});
|
||||
expect(verdict.kind).toBe("defer");
|
||||
expect(check).toEqual([]);
|
||||
expect(log).toEqual([
|
||||
{
|
||||
level: "debug",
|
||||
event: "inner_cmd.unsupported_wrapper_syntax",
|
||||
details: { command: "time -p ls", wrapper: "time" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("defers on a bare time with no inner command", async () => {
|
||||
const { verdict, log } = await run({
|
||||
recoveredCommand: "time",
|
||||
states: {},
|
||||
});
|
||||
expect(verdict.kind).toBe("defer");
|
||||
expect(log).toEqual([
|
||||
{
|
||||
level: "debug",
|
||||
event: "inner_cmd.unsupported_wrapper_syntax",
|
||||
details: { command: "time", wrapper: "time" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("defers on a nested wrapper (time wrapping time or timeout)", async () => {
|
||||
const nested = await run({
|
||||
recoveredCommand: "time time pnpm test",
|
||||
states: { "pnpm test": "allow" },
|
||||
});
|
||||
expect(nested.verdict.kind).toBe("defer");
|
||||
expect(nested.check).toEqual([]);
|
||||
expect(nested.log).toEqual([
|
||||
{
|
||||
level: "debug",
|
||||
event: "inner_cmd.nested_wrapper",
|
||||
details: {
|
||||
command: "time time pnpm test",
|
||||
innerCommand: "time pnpm test",
|
||||
wrapper: "time",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const nestedTimeout = await run({
|
||||
recoveredCommand: "time timeout 10s pnpm test",
|
||||
states: { "timeout 10s pnpm test": "allow" },
|
||||
});
|
||||
expect(nestedTimeout.verdict.kind).toBe("defer");
|
||||
expect(nestedTimeout.log[0]?.event).toBe("inner_cmd.nested_wrapper");
|
||||
});
|
||||
|
||||
it("defers on timeout wrapping time (nested the other way)", async () => {
|
||||
const { verdict, log, check } = await run({
|
||||
recoveredCommand: "timeout 30s time pnpm test",
|
||||
states: { "time pnpm test": "allow" },
|
||||
});
|
||||
expect(verdict.kind).toBe("defer");
|
||||
expect(check).toEqual([]);
|
||||
expect(log).toEqual([
|
||||
{
|
||||
level: "debug",
|
||||
event: "inner_cmd.nested_wrapper",
|
||||
details: {
|
||||
command: "timeout 30s time pnpm test",
|
||||
innerCommand: "time pnpm test",
|
||||
wrapper: "timeout",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("defers silently on /usr/bin/time (full path is not claimed)", async () => {
|
||||
const { verdict, log } = await run({
|
||||
recoveredCommand: "/usr/bin/time ls",
|
||||
states: { ls: "allow" },
|
||||
});
|
||||
expect(verdict.kind).toBe("defer");
|
||||
expect(log).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authorizeInnerCommand — env wrapper", () => {
|
||||
it("defers on an env wrapper with a debug log (non-transparent)", async () => {
|
||||
const { verdict, log, check } = await run({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
classifyWrapper,
|
||||
isRecognizedWrapper,
|
||||
parseTimeWrapper,
|
||||
parseTimeoutWrapper,
|
||||
} from "../src/recognizer";
|
||||
|
||||
@@ -86,34 +87,101 @@ describe("parseTimeoutWrapper", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseTimeWrapper", () => {
|
||||
it("matches the bare reserved-word form", () => {
|
||||
expect(parseTimeWrapper("time pnpm test")).toEqual({
|
||||
innerCommand: "pnpm test",
|
||||
});
|
||||
expect(parseTimeWrapper("time\techo hi")).toEqual({
|
||||
innerCommand: "echo hi",
|
||||
});
|
||||
expect(parseTimeWrapper("time build")).toEqual({
|
||||
innerCommand: "build",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves compound inner programs as the inner command", () => {
|
||||
expect(parseTimeWrapper("time pnpm test && git push")).toEqual({
|
||||
innerCommand: "pnpm test && git push",
|
||||
});
|
||||
expect(parseTimeWrapper("time bash -c something")).toEqual({
|
||||
innerCommand: "bash -c something",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects modifier args regardless of separator width", () => {
|
||||
// A dash right after the separator means flags: not transparent.
|
||||
expect(parseTimeWrapper("time -p ls")).toBeUndefined();
|
||||
// Backtracking must not smuggle a leading space into the inner.
|
||||
expect(parseTimeWrapper("time -p ls")).toBeUndefined();
|
||||
expect(parseTimeWrapper("time\t-- ls")).toBeUndefined();
|
||||
expect(parseTimeWrapper("time -o out.txt ls")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects a bare time and non-time commands", () => {
|
||||
expect(parseTimeWrapper("time")).toBeUndefined();
|
||||
expect(parseTimeWrapper("timeout 10s ls")).toBeUndefined();
|
||||
expect(parseTimeWrapper("my-time ls")).toBeUndefined();
|
||||
expect(parseTimeWrapper("/usr/bin/time ls")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRecognizedWrapper", () => {
|
||||
it("is true for the strict form and false otherwise", () => {
|
||||
it("is true for the strict forms and false otherwise", () => {
|
||||
expect(isRecognizedWrapper("timeout 10s pnpm test")).toBe(true);
|
||||
expect(isRecognizedWrapper("timeout 10s timeout 5s pnpm test")).toBe(true);
|
||||
expect(isRecognizedWrapper("timeout 10s timeout 5s pnpm test")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isRecognizedWrapper("time pnpm test")).toBe(true);
|
||||
expect(isRecognizedWrapper("time timeout 5s pnpm test")).toBe(true);
|
||||
expect(isRecognizedWrapper("timeout 10s time pnpm test")).toBe(true);
|
||||
expect(isRecognizedWrapper("pnpm test")).toBe(false);
|
||||
expect(isRecognizedWrapper("timeout -k 5s 30s pnpm test")).toBe(false);
|
||||
expect(isRecognizedWrapper("time -p pnpm test")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("classifyWrapper", () => {
|
||||
it("classifies the recognized wrapper", () => {
|
||||
it("classifies recognized wrappers with their name", () => {
|
||||
expect(classifyWrapper("timeout 30s pnpm test")).toEqual({
|
||||
kind: "recognized",
|
||||
wrapper: "timeout",
|
||||
match: { duration: "30s", innerCommand: "pnpm test" },
|
||||
});
|
||||
expect(classifyWrapper("time pnpm test")).toEqual({
|
||||
kind: "recognized",
|
||||
wrapper: "time",
|
||||
match: { innerCommand: "pnpm test" },
|
||||
});
|
||||
});
|
||||
|
||||
it("classifies unsupported timeout syntax", () => {
|
||||
expect(classifyWrapper("timeout -k 5s 30s pnpm test").kind).toBe(
|
||||
"unsupportedTimeout",
|
||||
);
|
||||
expect(classifyWrapper("timeout 30s").kind).toBe("unsupportedTimeout");
|
||||
expect(classifyWrapper("timeout --help").kind).toBe("unsupportedTimeout");
|
||||
it("classifies unsupported wrapper syntax with its name", () => {
|
||||
expect(classifyWrapper("timeout -k 5s 30s pnpm test")).toEqual({
|
||||
kind: "unsupported",
|
||||
wrapper: "timeout",
|
||||
});
|
||||
expect(classifyWrapper("timeout 30s")).toEqual({
|
||||
kind: "unsupported",
|
||||
wrapper: "timeout",
|
||||
});
|
||||
expect(classifyWrapper("timeout --help")).toEqual({
|
||||
kind: "unsupported",
|
||||
wrapper: "timeout",
|
||||
});
|
||||
expect(classifyWrapper("time -p ls")).toEqual({
|
||||
kind: "unsupported",
|
||||
wrapper: "time",
|
||||
});
|
||||
expect(classifyWrapper("time")).toEqual({
|
||||
kind: "unsupported",
|
||||
wrapper: "time",
|
||||
});
|
||||
});
|
||||
|
||||
it("classifies ordinary commands as non-timeout", () => {
|
||||
expect(classifyWrapper("pnpm test").kind).toBe("nonTimeout");
|
||||
expect(classifyWrapper("rm -rf /").kind).toBe("nonTimeout");
|
||||
expect(classifyWrapper("git push").kind).toBe("nonTimeout");
|
||||
it("classifies ordinary commands as other", () => {
|
||||
expect(classifyWrapper("pnpm test").kind).toBe("other");
|
||||
expect(classifyWrapper("rm -rf /").kind).toBe("other");
|
||||
expect(classifyWrapper("git push").kind).toBe("other");
|
||||
expect(classifyWrapper("/usr/bin/time ls").kind).toBe("other");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user