mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
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;
|
|
}
|
|
|
|
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),
|
|
agentsDir: dirname(this.userPath),
|
|
agentsDirExists: existsSync(dirname(this.userPath)),
|
|
projectAgentsDir: null,
|
|
projectAgentsDirExists: false,
|
|
};
|
|
}
|
|
}
|