* feat: 第一版大重构 * fix: 修复类型问题 * chore: 更新版本到 1.3.2 * Add brave as alternative WebSearchTool * fix: 修正顺序 * fix: 修复对穷鬼模式的 auto dream 和 session memory 越过 * feat: 穷鬼模式去除 session-summary * feat: 创建 builtin-tools 包,搬运所有工具实现 将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/, 内部导入路径已更新为 src/ alias 模式。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/ - src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/ - 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock - tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射 - 新增 packages/builtin-tools/src 至 include Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀 所有包名及 import 路径统一添加 @claude-code-best/ 前缀: - builtin-tools → @claude-code-best/builtin-tools - mcp-client → @claude-code-best/mcp-client - agent-tools → @claude-code-best/agent-tools Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复 node 环境没有 bun 的问题 --------- Co-authored-by: Eric-Guo <eric.guocz@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
235 lines
7.9 KiB
TypeScript
235 lines
7.9 KiB
TypeScript
import type { ToolPermissionContext } from '../../Tool.js'
|
|
import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/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
|
|
}
|