Files
my-pi/pi-permission-system/test/access-intent/bash/wrapper-analysis.test.ts
T
云服务部-叶林立andDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 7e6a8faae1 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>
2026-10-19 01:54:17 -07:00

189 lines
5.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
type CommandWord,
classifyWrapperWords,
executedUnitOf,
} from "#src/access-intent/bash/wrapper-analysis";
/**
* Split a command unit into words the way the AST walk does: whitespace
* separated, but a quoted span is one word carrying its quotes — tree-sitter
* emits a `string`/`raw_string` argument as a single named child.
*
* `sync-commands.test.ts` pins the real node adapter end to end (wrapperKind
* and executedUnit over a warm tree-sitter parse); this stands in for it so
* the extraction rules can be exercised without a parse.
*/
function words(unitText: string): CommandWord[] {
const out: CommandWord[] = [];
const pattern = /"[^"]*"|'[^']*'|\S+/g;
let match = pattern.exec(unitText);
while (match !== null) {
out.push({ text: match[0], offset: match.index });
match = pattern.exec(unitText);
}
return out;
}
describe("classifyWrapperWords", () => {
describe("opaque payloads", () => {
it.each([
"eval rm",
"bash -c rm",
"sh -c rm",
"dash -c rm",
"zsh -c rm",
"ksh -c rm",
"bash -ec rm",
"bash -xc rm",
"/bin/bash -c rm",
])("flags %s", (unit) => {
expect(classifyWrapperWords(words(unit))).toBe("opaque-payload");
});
it("does not flag a shell running a script file", () => {
expect(classifyWrapperWords(words("bash script.sh"))).toBeUndefined();
});
it("does not flag a -c cluster after the end-of-options marker", () => {
expect(classifyWrapperWords(words("bash -- -c"))).toBeUndefined();
});
});
describe("indirection wrappers", () => {
it.each([
"sudo aws s3 ls",
"env FOO=bar aws",
"xargs grep foo",
"timeout 10 grep foo",
"nice -n 5 make",
"doas ls",
"flock /tmp/lock ls",
])("flags %s", (unit) => {
expect(classifyWrapperWords(words(unit))).toBe("indirection");
});
it.each([
"find . -exec grep foo {} ;",
"find . -execdir rm {} ;",
"fd -x rm",
"fd --exec-batch rm",
])("flags the exec-conditional %s", (unit) => {
expect(classifyWrapperWords(words(unit))).toBe("indirection");
});
it("does not flag a bare search", () => {
expect(classifyWrapperWords(words("find . -name x"))).toBeUndefined();
});
});
describe("ordinary commands", () => {
it.each([
"ls -la",
"grep -c foo file",
"git status",
])("does not flag %s", (unit) => {
expect(classifyWrapperWords(words(unit))).toBeUndefined();
});
it("does not flag an empty word list", () => {
expect(classifyWrapperWords([])).toBeUndefined();
});
});
});
describe("executedUnitOf", () => {
/** Extract from a unit spelled as plain whitespace-separated words. */
function executedUnit(unitText: string): string | null {
return executedUnitOf(unitText, words(unitText));
}
describe("opaque payloads", () => {
it.each([
['bash -c "rm -rf /"', "rm -rf /"],
["bash -c 'rm -rf /'", "rm -rf /"],
['sh -ec "make build"', "make build"],
['/bin/bash -c "ls"', "ls"],
['eval "rm x"', "rm x"],
])("names the inner program of %s", (unit, expected) => {
expect(executedUnit(unit)).toBe(expected);
});
it("returns null when the payload argument is missing", () => {
expect(executedUnit("bash -c")).toBeNull();
});
});
describe("indirection wrappers", () => {
it.each([
["sudo aws s3 rm", "aws s3 rm"],
["sudo -u root aws s3 rm", "aws s3 rm"],
["sudo -- ls -la", "ls -la"],
["xargs grep foo", "grep foo"],
["xargs -0 -n1 grep foo", "grep foo"],
["xargs -I{} rm {}", "rm {}"],
["timeout 10 grep foo", "grep foo"],
["timeout -s KILL 10 grep foo", "grep foo"],
["nice -n 5 make build", "make build"],
["env FOO=bar grep foo", "grep foo"],
["flock /tmp/lock aws s3 ls", "aws s3 ls"],
["watch -n 2 ls", "ls"],
])("names the inner command of %s", (unit, expected) => {
expect(executedUnit(unit)).toBe(expected);
});
it("preserves the inner command's original spacing and quoting", () => {
expect(executedUnit("sudo grep 'a b' x")).toBe("grep 'a b' x");
});
it.each([
"xargs",
"sudo",
"sudo -u root",
"timeout 10",
])("returns null when %s names no inner command", (unit) => {
expect(executedUnit(unit)).toBeNull();
});
it("returns null rather than guessing past an unknown trailing option", () => {
expect(executedUnit("xargs --unknown-opt")).toBeNull();
});
});
describe("exec-conditional wrappers", () => {
it.each([
["find . -name x -exec grep foo {} ;", "grep foo {}"],
["find . -exec rm {} +", "rm {}"],
["find . -execdir grep foo {} ;", "grep foo {}"],
["fd -x rm", "rm"],
["fd --exec-batch rm -f", "rm -f"],
])("names the per-result command of %s", (unit, expected) => {
expect(executedUnit(unit)).toBe(expected);
});
it("returns null when the exec flag ends the command", () => {
expect(executedUnit("find . -exec")).toBeNull();
});
});
describe("nested wrappers", () => {
it.each([
["sudo timeout 5 xargs grep foo", "grep foo"],
["sudo bash -c 'rm x'", "rm x"],
["timeout 10 sudo -u root aws s3 rm", "aws s3 rm"],
])("unwraps %s to its innermost command", (unit, expected) => {
expect(executedUnit(unit)).toBe(expected);
});
});
describe("nothing to add", () => {
it("returns null for an ordinary command", () => {
expect(executedUnit("grep foo")).toBeNull();
});
it("returns null for an empty word list", () => {
expect(executedUnitOf("", [])).toBeNull();
});
});
});