mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
feat(permission): integrate auto-review and hard guards
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
normalizeFlatPermissionValue,
|
||||
stripJsonComments,
|
||||
} from "./config-loader";
|
||||
import type { PolicyLoader, ResolvedPolicyPaths } from "./policy-loader";
|
||||
import type { FlatPermissionConfig, PatternValue, ScopeConfig } from "./types";
|
||||
|
||||
interface HardGuardDocument {
|
||||
version?: number;
|
||||
permission?: unknown;
|
||||
}
|
||||
|
||||
const EMPTY_SCOPE: ScopeConfig = {};
|
||||
const DENY_ALL: ScopeConfig = { permission: { "*": "deny" } };
|
||||
|
||||
function isDenyValue(value: PatternValue): boolean {
|
||||
return value === "deny" ||
|
||||
(typeof value === "object" && value !== null && "action" in value && value.action === "deny");
|
||||
}
|
||||
|
||||
function assertHardGuardValues(permission: FlatPermissionConfig): void {
|
||||
const valid = (value: PatternValue): boolean => value === "allow" || isDenyValue(value);
|
||||
for (const [surface, value] of Object.entries(permission)) {
|
||||
if (typeof value === "string" || (typeof value === "object" && value !== null && "action" in value)) {
|
||||
if (!valid(value as PatternValue)) {
|
||||
throw new Error(`hard guard '${surface}' must be allow or deny`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (const [pattern, action] of Object.entries(value)) {
|
||||
if (!valid(action)) {
|
||||
throw new Error(`hard guard '${surface}.${pattern}' must be allow or deny`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readHardGuards(path: string): ScopeConfig {
|
||||
const parsed = JSON.parse(stripJsonComments(readFileSync(path, "utf8"))) as HardGuardDocument;
|
||||
if (parsed.version !== undefined && parsed.version !== 1) {
|
||||
throw new Error(`unsupported hard guard version: ${String(parsed.version)}`);
|
||||
}
|
||||
const normalized = normalizeFlatPermissionValue(parsed.permission ?? {});
|
||||
if (normalized === undefined) {
|
||||
throw new Error("hard guard permission must be an object");
|
||||
}
|
||||
assertHardGuardValues(normalized);
|
||||
return { permission: { "*": "allow", ...normalized } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Global-only hard boundary: deny rules plus same-file allow exceptions. It keeps the last valid snapshot and
|
||||
* falls back to the bundled defaults; if both are invalid it denies all.
|
||||
*/
|
||||
export class HardGuardPolicyLoader implements PolicyLoader {
|
||||
private lastStamp = "";
|
||||
private lastGood: ScopeConfig | undefined;
|
||||
private issues: string[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly userPath: string,
|
||||
private readonly fallbackPath = fileURLToPath(
|
||||
new URL("../config/hard-guards.default.jsonc", import.meta.url),
|
||||
),
|
||||
) {}
|
||||
|
||||
loadGlobalConfig(): ScopeConfig {
|
||||
const stamp = this.getCacheStamp();
|
||||
if (stamp === this.lastStamp && this.lastGood !== undefined) return this.lastGood;
|
||||
this.lastStamp = stamp;
|
||||
this.issues = [];
|
||||
|
||||
for (const candidate of [this.userPath, this.fallbackPath]) {
|
||||
if (!existsSync(candidate)) continue;
|
||||
try {
|
||||
const loaded = readHardGuards(candidate);
|
||||
this.lastGood = loaded;
|
||||
return loaded;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.issues.push(`Failed to load hard guards '${candidate}': ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.lastGood !== undefined) return this.lastGood;
|
||||
this.issues.push("No valid hard guard configuration was available; denying all requests.");
|
||||
return DENY_ALL;
|
||||
}
|
||||
|
||||
loadProjectConfig(): ScopeConfig { return EMPTY_SCOPE; }
|
||||
loadAgentConfig(): ScopeConfig { return EMPTY_SCOPE; }
|
||||
loadProjectAgentConfig(): ScopeConfig { return EMPTY_SCOPE; }
|
||||
getConfiguredMcpServerNames(): readonly string[] { return []; }
|
||||
|
||||
getCacheStamp(): string {
|
||||
return [this.userPath, this.fallbackPath]
|
||||
.map((path) => {
|
||||
try {
|
||||
const stat = statSync(path);
|
||||
return `${path}:${stat.mtimeMs}:${stat.size}`;
|
||||
} catch {
|
||||
return `${path}:missing`;
|
||||
}
|
||||
})
|
||||
.join("|");
|
||||
}
|
||||
|
||||
getConfigIssues(): string[] { return [...this.issues]; }
|
||||
|
||||
getResolvedPolicyPaths(): ResolvedPolicyPaths {
|
||||
return {
|
||||
globalConfigPath: this.userPath,
|
||||
globalConfigExists: existsSync(this.userPath),
|
||||
projectConfigPath: null,
|
||||
projectConfigExists: false,
|
||||
agentsDir: dirname(this.userPath),
|
||||
agentsDirExists: existsSync(dirname(this.userPath)),
|
||||
projectAgentsDir: null,
|
||||
projectAgentsDirExists: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user