Download src/utils/permissions/shadowedRuleDetection.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 8.05 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/permissions/shadowedRuleDetection.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/permissions/shadowedRuleDetection.ts
-
curl -L -o shadowedRuleDetection.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/permissions/shadowedRuleDetection.ts
8.05 kB
| import type { ToolPermissionContext } from '../../Tool.js' | |
| import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js' | |
| import type { PermissionRule, PermissionRuleSource } from './PermissionRule.js' | |
| import { | |
| getAllowRules, | |
| getAskRules, | |
| getDenyRules, | |
| permissionRuleSourceDisplayString, | |
| } from './permissions.js' | |
| /** | |
| * Type of shadowing that makes a rule unreachable | |
| */ | |
| export type ShadowType = 'ask' | 'deny' | |
| /** | |
| * Represents an unreachable permission rule with explanation | |
| */ | |
| export type UnreachableRule = { | |
| rule: PermissionRule | |
| reason: string | |
| shadowedBy: PermissionRule | |
| shadowType: ShadowType | |
| fix: string | |
| } | |
| /** | |
| * Options for detecting unreachable rules | |
| */ | |
| export type DetectUnreachableRulesOptions = { | |
| /** | |
| * Whether sandbox auto-allow is enabled for Bash commands. | |
| * When true, tool-wide Bash ask rules from personal settings don't block | |
| * specific Bash allow rules because sandboxed commands are auto-allowed. | |
| */ | |
| sandboxAutoAllowEnabled: boolean | |
| } | |
| /** | |
| * Result of checking if a rule is shadowed. | |
| * Uses discriminated union for type safety. | |
| */ | |
| type ShadowResult = | |
| | { shadowed: false } | |
| | { shadowed: true; shadowedBy: PermissionRule; shadowType: ShadowType } | |
| /** | |
| * Check if a permission rule source is shared (visible to other users). | |
| * Shared settings include: | |
| * - projectSettings: Committed to git, shared with team | |
| * - policySettings: Enterprise-managed, pushed to all users | |
| * - command: From slash command frontmatter, potentially shared | |
| * | |
| * Personal settings include: | |
| * - userSettings: User's global ~/.claude settings | |
| * - localSettings: Gitignored per-project settings | |
| * - cliArg: Runtime CLI arguments | |
| * - session: In-memory session rules | |
| * - flagSettings: From --settings flag (runtime) | |
| */ | |
| export function isSharedSettingSource(source: PermissionRuleSource): boolean { | |
| return ( | |
| source === 'projectSettings' || | |
| source === 'policySettings' || | |
| source === 'command' | |
| ) | |
| } | |
| /** | |
| * Format a rule source for display in warning messages. | |
| */ | |
| function formatSource(source: PermissionRuleSource): string { | |
| return permissionRuleSourceDisplayString(source) | |
| } | |
| /** | |
| * Generate a fix suggestion based on the shadow type. | |
| */ | |
| function generateFixSuggestion( | |
| shadowType: ShadowType, | |
| shadowingRule: PermissionRule, | |
| shadowedRule: PermissionRule, | |
| ): string { | |
| const shadowingSource = formatSource(shadowingRule.source) | |
| const shadowedSource = formatSource(shadowedRule.source) | |
| const toolName = shadowingRule.ruleValue.toolName | |
| if (shadowType === 'deny') { | |
| return `Remove the "${toolName}" deny rule from ${shadowingSource}, or remove the specific allow rule from ${shadowedSource}` | |
| } | |
| return `Remove the "${toolName}" ask rule from ${shadowingSource}, or remove the specific allow rule from ${shadowedSource}` | |
| } | |
| /** | |
| * Check if a specific allow rule is shadowed (unreachable) by an ask rule. | |
| * | |
| * An allow rule is unreachable when: | |
| * 1. There's a tool-wide ask rule (e.g., "Bash" in ask list) | |
| * 2. And a specific allow rule (e.g., "Bash(ls:*)" in allow list) | |
| * | |
| * The ask rule takes precedence, making the specific allow rule unreachable | |
| * because the user will always be prompted first. | |
| * | |
| * Exception: For Bash with sandbox auto-allow enabled, tool-wide ask rules | |
| * from PERSONAL settings don't shadow specific allow rules because: | |
| * - Sandboxed commands are auto-allowed regardless of ask rules | |
| * - This only applies to personal settings (userSettings, localSettings, etc.) | |
| * - Shared settings (projectSettings, policySettings) always warn because | |
| * other team members may not have sandbox enabled | |
| */ | |
| function isAllowRuleShadowedByAskRule( | |
| allowRule: PermissionRule, | |
| askRules: PermissionRule[], | |
| options: DetectUnreachableRulesOptions, | |
| ): ShadowResult { | |
| const { toolName, ruleContent } = allowRule.ruleValue | |
| // Only check allow rules that have specific content (e.g., "Bash(ls:*)") | |
| // Tool-wide allow rules cannot be shadowed by ask rules | |
| if (ruleContent === undefined) { | |
| return { shadowed: false } | |
| } | |
| // Find any tool-wide ask rule for the same tool | |
| const shadowingAskRule = askRules.find( | |
| askRule => | |
| askRule.ruleValue.toolName === toolName && | |
| askRule.ruleValue.ruleContent === undefined, | |
| ) | |
| if (!shadowingAskRule) { | |
| return { shadowed: false } | |
| } | |
| // Special case: Bash with sandbox auto-allow from personal settings | |
| // The sandbox exception is based on the ASK rule's source, not the allow rule's source. | |
| // If the ask rule is from personal settings, the user's own sandbox will auto-allow. | |
| // If the ask rule is from shared settings, other team members may not have sandbox enabled. | |
| if (toolName === BASH_TOOL_NAME && options.sandboxAutoAllowEnabled) { | |
| if (!isSharedSettingSource(shadowingAskRule.source)) { | |
| return { shadowed: false } | |
| } | |
| // Fall through to mark as shadowed - shared settings should always warn | |
| } | |
| return { shadowed: true, shadowedBy: shadowingAskRule, shadowType: 'ask' } | |
| } | |
| /** | |
| * Check if an allow rule is shadowed (completely blocked) by a deny rule. | |
| * | |
| * An allow rule is unreachable when: | |
| * 1. There's a tool-wide deny rule (e.g., "Bash" in deny list) | |
| * 2. And a specific allow rule (e.g., "Bash(ls:*)" in allow list) | |
| * | |
| * Deny rules are checked first in the permission evaluation order, | |
| * so the allow rule will never be reached - the tool is always denied. | |
| * This is more severe than ask-shadowing because the rule is truly blocked. | |
| */ | |
| function isAllowRuleShadowedByDenyRule( | |
| allowRule: PermissionRule, | |
| denyRules: PermissionRule[], | |
| ): ShadowResult { | |
| const { toolName, ruleContent } = allowRule.ruleValue | |
| // Only check allow rules that have specific content (e.g., "Bash(ls:*)") | |
| // Tool-wide allow rules conflict with tool-wide deny rules but are not "shadowed" | |
| if (ruleContent === undefined) { | |
| return { shadowed: false } | |
| } | |
| // Find any tool-wide deny rule for the same tool | |
| const shadowingDenyRule = denyRules.find( | |
| denyRule => | |
| denyRule.ruleValue.toolName === toolName && | |
| denyRule.ruleValue.ruleContent === undefined, | |
| ) | |
| if (!shadowingDenyRule) { | |
| return { shadowed: false } | |
| } | |
| return { shadowed: true, shadowedBy: shadowingDenyRule, shadowType: 'deny' } | |
| } | |
| /** | |
| * Detect all unreachable permission rules in the given context. | |
| * | |
| * Currently detects: | |
| * - Allow rules shadowed by tool-wide deny rules (more severe - completely blocked) | |
| * - Allow rules shadowed by tool-wide ask rules (will always prompt) | |
| */ | |
| export function detectUnreachableRules( | |
| context: ToolPermissionContext, | |
| options: DetectUnreachableRulesOptions, | |
| ): UnreachableRule[] { | |
| const unreachable: UnreachableRule[] = [] | |
| const allowRules = getAllowRules(context) | |
| const askRules = getAskRules(context) | |
| const denyRules = getDenyRules(context) | |
| // Check each allow rule for shadowing | |
| for (const allowRule of allowRules) { | |
| // Check deny shadowing first (more severe) | |
| const denyResult = isAllowRuleShadowedByDenyRule(allowRule, denyRules) | |
| if (denyResult.shadowed) { | |
| const shadowSource = formatSource(denyResult.shadowedBy.source) | |
| unreachable.push({ | |
| rule: allowRule, | |
| reason: `Blocked by "${denyResult.shadowedBy.ruleValue.toolName}" deny rule (from ${shadowSource})`, | |
| shadowedBy: denyResult.shadowedBy, | |
| shadowType: 'deny', | |
| fix: generateFixSuggestion('deny', denyResult.shadowedBy, allowRule), | |
| }) | |
| continue // Don't also report ask-shadowing if deny-shadowed | |
| } | |
| // Check ask shadowing | |
| const askResult = isAllowRuleShadowedByAskRule(allowRule, askRules, options) | |
| if (askResult.shadowed) { | |
| const shadowSource = formatSource(askResult.shadowedBy.source) | |
| unreachable.push({ | |
| rule: allowRule, | |
| reason: `Shadowed by "${askResult.shadowedBy.ruleValue.toolName}" ask rule (from ${shadowSource})`, | |
| shadowedBy: askResult.shadowedBy, | |
| shadowType: 'ask', | |
| fix: generateFixSuggestion('ask', askResult.shadowedBy, allowRule), | |
| }) | |
| } | |
| } | |
| return unreachable | |
| } | |