feat(permission): integrate auto-review and hard guards

This commit is contained in:
云服务部-叶林立
2026-09-02 15:34:29 +08:00
parent a4807dde67
commit 180da95ab7
79 changed files with 4263 additions and 9203 deletions
@@ -0,0 +1,141 @@
import { mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
DEFAULT_AUTO_REVIEW_ANALYSIS_CONFIG,
DEFAULT_AUTO_REVIEW_CONFIG,
} from "#src/auto-review/config";
import { AutoReviewLedger } from "#src/auto-review/ledger";
import type { PromptPermissionDetails } from "#src/service";
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});
function details(requestId: string): PromptPermissionDetails {
return {
requestId,
source: "tool",
surface: "bash",
toolName: "bash",
command: "npm install example",
agentName: "main",
} as unknown as PromptPermissionDetails;
}
describe("AutoReviewLedger", () => {
it("marks analysis pending only after the configured number of completed verdicts", () => {
const root = mkdtempSync(join(tmpdir(), "auto-review-ledger-"));
roots.push(root);
const ledger = new AutoReviewLedger(root);
const analysis = { ...DEFAULT_AUTO_REVIEW_ANALYSIS_CONFIG, threshold: 2 };
const assessment = {
outcome: "allow" as const,
riskLevel: "low" as const,
userAuthorization: "high" as const,
rationale: "directly authorized",
};
ledger.recordDecision({
details: details("request-1"),
assessment,
policyRevision: "policy-a",
config: DEFAULT_AUTO_REVIEW_CONFIG,
analysis,
});
expect(ledger.hasPendingAnalysis()).toBe(false);
expect(ledger.prepareBatch(analysis)).toBeUndefined();
ledger.recordDecision({
details: details("request-2"),
assessment,
policyRevision: "policy-a",
config: DEFAULT_AUTO_REVIEW_CONFIG,
analysis,
});
expect(ledger.hasPendingAnalysis()).toBe(true);
const batch = ledger.prepareBatch(analysis);
expect(batch?.fromSequence).toBe(1);
expect(batch?.toSequence).toBe(2);
expect(batch?.records).toHaveLength(2);
expect(batch?.records[0]).not.toHaveProperty("rationale");
expect(batch?.records[0].normalizedPattern).toBe("npm install *");
});
it("persists owner-only state and resumes a pending batch", () => {
const root = mkdtempSync(join(tmpdir(), "auto-review-ledger-"));
roots.push(root);
const analysis = { ...DEFAULT_AUTO_REVIEW_ANALYSIS_CONFIG, threshold: 1 };
const ledger = new AutoReviewLedger(root);
ledger.recordDecision({
details: details("request-1"),
assessment: {
outcome: "deny",
riskLevel: "high",
userAuthorization: "unknown",
rationale: "unsafe",
},
policyRevision: "policy-b",
config: DEFAULT_AUTO_REVIEW_CONFIG,
analysis,
});
const resumed = new AutoReviewLedger(root);
expect(resumed.hasPendingAnalysis()).toBe(true);
expect(resumed.prepareBatch(analysis)?.records[0].outcome).toBe("deny");
expect(statSync(join(root, "state.json")).mode & 0o777).toBe(0o600);
expect(readFileSync(join(root, "ledger", "decisions.jsonl"), "utf8")).not.toContain("unsafe");
});
it("does not retain command arguments, secret path segments, or rationale", () => {
const root = mkdtempSync(join(tmpdir(), "auto-review-ledger-"));
roots.push(root);
const ledger = new AutoReviewLedger(root);
const analysis = { ...DEFAULT_AUTO_REVIEW_ANALYSIS_CONFIG, threshold: 10 };
const assessment = {
outcome: "allow" as const,
riskLevel: "medium" as const,
userAuthorization: "high" as const,
rationale: "secret-rationale",
};
const commandRecord = ledger.recordDecision({
details: {
requestId: "request-command",
source: "tool",
agentName: "main",
surface: "bash",
toolName: "bash",
command: "curl https://token.example/?api_key=secret-value",
} as unknown as PromptPermissionDetails,
assessment,
policyRevision: "policy-c",
config: DEFAULT_AUTO_REVIEW_CONFIG,
analysis,
});
const pathRecord = ledger.recordDecision({
details: {
requestId: "request-path",
source: "tool",
agentName: "main",
surface: "path",
toolName: "read",
path: "clients/acme-secret/token.txt",
} as unknown as PromptPermissionDetails,
assessment,
policyRevision: "policy-c",
config: DEFAULT_AUTO_REVIEW_CONFIG,
analysis,
});
expect(commandRecord.normalizedPattern).toBe("curl *");
expect(pathRecord.normalizedPattern).toBe("<relative>/*");
const persisted = readFileSync(join(root, "ledger", "decisions.jsonl"), "utf8");
expect(persisted).not.toContain("secret-value");
expect(persisted).not.toContain("acme-secret");
expect(persisted).not.toContain("secret-rationale");
});
});