Files
my-pi/pi-permission-system/src/synthesize.ts
T

93 lines
2.6 KiB
TypeScript

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 regular config
* rules. Their exact surface is selected before the universal fallback when the
* value-pattern specificity is tied.
*/
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];
}