This pass rewrites comment-only ANT-ONLY markers to neutral internal-only language across the source tree without changing runtime strings, flags, commands, or protocol identifiers. The goal is to lower obvious internal prose leakage while keeping the diff mechanically safe and easy to review. Constraint: Phase B is limited to comments/prose only; runtime strings and user-facing labels remain deferred Rejected: Broad search-and-replace across strings and command descriptions | too risky for a prose-only pass Confidence: high Scope-risk: narrow Reversibility: clean Directive: Remaining ANT-ONLY hits are mostly runtime/user-facing strings and should be handled separately from comment cleanup Tested: bun run build Tested: bun run smoke Tested: bun run verify:privacy Tested: bun run test:provider Tested: bun run test:provider-recommendation Not-tested: Full repo typecheck (upstream baseline remains noisy) Co-authored-by: anandh8x <test@example.com>
199 lines
7.1 KiB
TypeScript
199 lines
7.1 KiB
TypeScript
import { feature } from 'bun:bundle'
|
|
import { AGENT_TOOL_NAME } from '../../tools/AgentTool/constants.js'
|
|
import { TASK_OUTPUT_TOOL_NAME } from '../../tools/TaskOutputTool/constants.js'
|
|
import { TASK_STOP_TOOL_NAME } from '../../tools/TaskStopTool/prompt.js'
|
|
import type { PermissionRuleValue } from './PermissionRule.js'
|
|
|
|
// Dead code elimination: internal-only tool names are conditionally required so
|
|
// their strings don't leak into external builds. Static imports always bundle.
|
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
const BRIEF_TOOL_NAME: string | null =
|
|
feature('KAIROS') || feature('KAIROS_BRIEF')
|
|
? (
|
|
require('../../tools/BriefTool/prompt.js') as typeof import('../../tools/BriefTool/prompt.js')
|
|
).BRIEF_TOOL_NAME
|
|
: null
|
|
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
|
|
// Maps legacy tool names to their current canonical names.
|
|
// When a tool is renamed, add old → new here so permission rules,
|
|
// hooks, and persisted wire names resolve to the canonical name.
|
|
const LEGACY_TOOL_NAME_ALIASES: Record<string, string> = {
|
|
Task: AGENT_TOOL_NAME,
|
|
KillShell: TASK_STOP_TOOL_NAME,
|
|
AgentOutputTool: TASK_OUTPUT_TOOL_NAME,
|
|
BashOutputTool: TASK_OUTPUT_TOOL_NAME,
|
|
...((feature('KAIROS') || feature('KAIROS_BRIEF')) && BRIEF_TOOL_NAME
|
|
? { Brief: BRIEF_TOOL_NAME }
|
|
: {}),
|
|
}
|
|
|
|
export function normalizeLegacyToolName(name: string): string {
|
|
return LEGACY_TOOL_NAME_ALIASES[name] ?? name
|
|
}
|
|
|
|
export function getLegacyToolNames(canonicalName: string): string[] {
|
|
const result: string[] = []
|
|
for (const [legacy, canonical] of Object.entries(LEGACY_TOOL_NAME_ALIASES)) {
|
|
if (canonical === canonicalName) result.push(legacy)
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Escapes special characters in rule content for safe storage in permission rules.
|
|
* Permission rules use the format "Tool(content)", so parentheses in content must be escaped.
|
|
*
|
|
* Escaping order matters:
|
|
* 1. Escape existing backslashes first (\ -> \\)
|
|
* 2. Then escape parentheses (( -> \(, ) -> \))
|
|
*
|
|
* @example
|
|
* escapeRuleContent('psycopg2.connect()') // => 'psycopg2.connect\\(\\)'
|
|
* escapeRuleContent('echo "test\\nvalue"') // => 'echo "test\\\\nvalue"'
|
|
*/
|
|
export function escapeRuleContent(content: string): string {
|
|
return content
|
|
.replace(/\\/g, '\\\\') // Escape backslashes first
|
|
.replace(/\(/g, '\\(') // Escape opening parentheses
|
|
.replace(/\)/g, '\\)') // Escape closing parentheses
|
|
}
|
|
|
|
/**
|
|
* Unescapes special characters in rule content after parsing from permission rules.
|
|
* This reverses the escaping done by escapeRuleContent.
|
|
*
|
|
* Unescaping order matters (reverse of escaping):
|
|
* 1. Unescape parentheses first (\( -> (, \) -> ))
|
|
* 2. Then unescape backslashes (\\ -> \)
|
|
*
|
|
* @example
|
|
* unescapeRuleContent('psycopg2.connect\\(\\)') // => 'psycopg2.connect()'
|
|
* unescapeRuleContent('echo "test\\\\nvalue"') // => 'echo "test\\nvalue"'
|
|
*/
|
|
export function unescapeRuleContent(content: string): string {
|
|
return content
|
|
.replace(/\\\(/g, '(') // Unescape opening parentheses
|
|
.replace(/\\\)/g, ')') // Unescape closing parentheses
|
|
.replace(/\\\\/g, '\\') // Unescape backslashes last
|
|
}
|
|
|
|
/**
|
|
* Parses a permission rule string into its components.
|
|
* Handles escaped parentheses in the content portion.
|
|
*
|
|
* Format: "ToolName" or "ToolName(content)"
|
|
* Content may contain escaped parentheses: \( and \)
|
|
*
|
|
* @example
|
|
* permissionRuleValueFromString('Bash') // => { toolName: 'Bash' }
|
|
* permissionRuleValueFromString('Bash(npm install)') // => { toolName: 'Bash', ruleContent: 'npm install' }
|
|
* permissionRuleValueFromString('Bash(python -c "print\\(1\\)")') // => { toolName: 'Bash', ruleContent: 'python -c "print(1)"' }
|
|
*/
|
|
export function permissionRuleValueFromString(
|
|
ruleString: string,
|
|
): PermissionRuleValue {
|
|
// Find the first unescaped opening parenthesis
|
|
const openParenIndex = findFirstUnescapedChar(ruleString, '(')
|
|
if (openParenIndex === -1) {
|
|
// No parenthesis found - this is just a tool name
|
|
return { toolName: normalizeLegacyToolName(ruleString) }
|
|
}
|
|
|
|
// Find the last unescaped closing parenthesis
|
|
const closeParenIndex = findLastUnescapedChar(ruleString, ')')
|
|
if (closeParenIndex === -1 || closeParenIndex <= openParenIndex) {
|
|
// No matching closing paren or malformed - treat as tool name
|
|
return { toolName: normalizeLegacyToolName(ruleString) }
|
|
}
|
|
|
|
// Ensure the closing paren is at the end
|
|
if (closeParenIndex !== ruleString.length - 1) {
|
|
// Content after closing paren - treat as tool name
|
|
return { toolName: normalizeLegacyToolName(ruleString) }
|
|
}
|
|
|
|
const toolName = ruleString.substring(0, openParenIndex)
|
|
const rawContent = ruleString.substring(openParenIndex + 1, closeParenIndex)
|
|
|
|
// Missing toolName (e.g., "(foo)") is malformed - treat whole string as tool name
|
|
if (!toolName) {
|
|
return { toolName: normalizeLegacyToolName(ruleString) }
|
|
}
|
|
|
|
// Empty content (e.g., "Bash()") or standalone wildcard (e.g., "Bash(*)")
|
|
// should be treated as just the tool name (tool-wide rule)
|
|
if (rawContent === '' || rawContent === '*') {
|
|
return { toolName: normalizeLegacyToolName(toolName) }
|
|
}
|
|
|
|
// Unescape the content
|
|
const ruleContent = unescapeRuleContent(rawContent)
|
|
return { toolName: normalizeLegacyToolName(toolName), ruleContent }
|
|
}
|
|
|
|
/**
|
|
* Converts a permission rule value to its string representation.
|
|
* Escapes parentheses in the content to prevent parsing issues.
|
|
*
|
|
* @example
|
|
* permissionRuleValueToString({ toolName: 'Bash' }) // => 'Bash'
|
|
* permissionRuleValueToString({ toolName: 'Bash', ruleContent: 'npm install' }) // => 'Bash(npm install)'
|
|
* permissionRuleValueToString({ toolName: 'Bash', ruleContent: 'python -c "print(1)"' }) // => 'Bash(python -c "print\\(1\\)")'
|
|
*/
|
|
export function permissionRuleValueToString(
|
|
ruleValue: PermissionRuleValue,
|
|
): string {
|
|
if (!ruleValue.ruleContent) {
|
|
return ruleValue.toolName
|
|
}
|
|
const escapedContent = escapeRuleContent(ruleValue.ruleContent)
|
|
return `${ruleValue.toolName}(${escapedContent})`
|
|
}
|
|
|
|
/**
|
|
* Find the index of the first unescaped occurrence of a character.
|
|
* A character is escaped if preceded by an odd number of backslashes.
|
|
*/
|
|
function findFirstUnescapedChar(str: string, char: string): number {
|
|
for (let i = 0; i < str.length; i++) {
|
|
if (str[i] === char) {
|
|
// Count preceding backslashes
|
|
let backslashCount = 0
|
|
let j = i - 1
|
|
while (j >= 0 && str[j] === '\\') {
|
|
backslashCount++
|
|
j--
|
|
}
|
|
// If even number of backslashes, the char is unescaped
|
|
if (backslashCount % 2 === 0) {
|
|
return i
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
/**
|
|
* Find the index of the last unescaped occurrence of a character.
|
|
* A character is escaped if preceded by an odd number of backslashes.
|
|
*/
|
|
function findLastUnescapedChar(str: string, char: string): number {
|
|
for (let i = str.length - 1; i >= 0; i--) {
|
|
if (str[i] === char) {
|
|
// Count preceding backslashes
|
|
let backslashCount = 0
|
|
let j = i - 1
|
|
while (j >= 0 && str[j] === '\\') {
|
|
backslashCount++
|
|
j--
|
|
}
|
|
// If even number of backslashes, the char is unescaped
|
|
if (backslashCount % 2 === 0) {
|
|
return i
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|