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, }; } }