import type { Rule, RuleOrigin, Ruleset } from "./rule"; import type { PermissionState } from "./types"; /** * Synthesize a single universal catch-all rule from the universal default. * * Produces one rule: * `{ surface: "*", pattern: "*", action: universalDefault, layer: "default" }` * * Per-surface catch-alls (`bash["*"]`, `mcp["*"]`, etc.) are expressed as * regular config rules from `normalizeFlatConfig()` and sit at higher indices * in the composed array, so they override this default via last-match-wins. */ export function synthesizeDefaults( universalDefault: PermissionState, origin: RuleOrigin = "builtin", ): Ruleset { return [ { surface: "*", pattern: "*", action: universalDefault, layer: "default", origin, }, ]; } /** * MCP metadata operation targets that are auto-allowed when any explicit MCP * allow rule exists in the config layer. */ const MCP_BASELINE_TARGETS: readonly string[] = [ "mcp_status", "mcp_list", "mcp_search", "mcp_describe", "mcp_connect", ]; /** * Conditionally synthesize MCP baseline auto-allow rules. * * Emits allow rules for the 5 MCP metadata targets only when `configRules` * contains at least one `surface: "mcp", action: "allow"` rule. This replicates * the `hasAnyMcpAllowRule` heuristic as actual rules. * * When `permission["mcp"]` is `"allow"` (or `mcp["*"]` is `"allow"`), the * synthesized config catch-all already covers all MCP targets — no separate * baseline rules are needed (and this function is not called in that case). * * Baseline rules are placed BEFORE config rules in the composed array so * that explicit config deny rules can still override them. * * All rules carry `layer: "baseline"`. */ export function synthesizeBaseline(configRules: Ruleset): Ruleset { const hasAnyMcpAllow = configRules.some( (r) => r.surface === "mcp" && r.action === "allow", ); if (!hasAnyMcpAllow) { return []; } return MCP_BASELINE_TARGETS.map( (target): Rule => ({ surface: "mcp", pattern: target, action: "allow", layer: "baseline", origin: "baseline", }), ); } /** * Concatenate all rule layers into a single flat ruleset. * * Priority order (lowest → highest, i.e. earlier index → later index): * defaults → baseline → config * * Session rules are NOT included here — they are appended at call-time inside * `checkPermission()` so that the cached composed ruleset remains session-agnostic. * * `evaluate()` scans from the end, so later layers override earlier ones. */ export function composeRuleset( defaults: Ruleset, baseline: Ruleset, config: Ruleset, ): Ruleset { return [...defaults, ...baseline, ...config]; }