๐ File detail
utils/computerUse/appNames.ts
๐ฏ Use case
This file lives under โutils/โ, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, โฆ). On the API surface it exposes filterAppsForDescription โ mainly functions, hooks, or classes. What the file header says: Filter and sanitize installed-app data for inclusion in the `request_access` tool description. Ported from Cowork's appNames.ts. Two concerns: noise filtering (Spotlight returns every bundle on disk โ XPC helpers, daemons, input methods) and prompt-injection hardening (app names.
Generated from folder role, exports, dependency roots, and inline comments โ not hand-reviewed for every path.
๐ง Inline summary
Filter and sanitize installed-app data for inclusion in the `request_access` tool description. Ported from Cowork's appNames.ts. Two concerns: noise filtering (Spotlight returns every bundle on disk โ XPC helpers, daemons, input methods) and prompt-injection hardening (app names are attacker-controlled; anyone can ship an app named anything). Residual risk: short benign-char adversarial names ("grant all") can't be filtered programmatically. The tool description's structural framing ("Available applications:") makes it clear these are app names, and the downstream permission dialog requires explicit user approval โ a bad name can't auto-grant anything.
๐ค Exports (heuristic)
filterAppsForDescription
๐ฅ๏ธ Source preview
/**
* Filter and sanitize installed-app data for inclusion in the `request_access`
* tool description. Ported from Cowork's appNames.ts. Two
* concerns: noise filtering (Spotlight returns every bundle on disk โ XPC
* helpers, daemons, input methods) and prompt-injection hardening (app names
* are attacker-controlled; anyone can ship an app named anything).
*
* Residual risk: short benign-char adversarial names ("grant all") can't be
* filtered programmatically. The tool description's structural framing
* ("Available applications:") makes it clear these are app names, and the
* downstream permission dialog requires explicit user approval โ a bad name
* can't auto-grant anything.
*/
/** Minimal shape โ matches what `listInstalledApps` returns. */
type InstalledAppLike = {
readonly bundleId: string
readonly displayName: string
readonly path: string
}
// โโ Noise filtering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/**
* Only apps under these roots are shown. /System/Library subpaths (CoreServices,
* PrivateFrameworks, Input Methods) are OS plumbing โ anchor on known-good
* roots rather than blocklisting every junk subpath since new macOS versions
* add more.
*
* ~/Applications is checked at call time via the `homeDir` arg (HOME isn't
* reliably known at module load in all environments).
*/
const PATH_ALLOWLIST: readonly string[] = [
'/Applications/',
'/System/Applications/',
]
/**
* Display-name patterns that mark background services even under /Applications.
* `(?:$|\s\()` โ matches keyword at end-of-string OR immediately before ` (`:
* "Slack Helper (GPU)" and "ABAssistantService" fail, "Service Desk" passes
* (Service is followed by " D").
*/
const NAME_PATTERN_BLOCKLIST: readonly RegExp[] = [
/Helper(?:$|\s\()/,
/Agent(?:$|\s\()/,
/Service(?:$|\s\()/,
/Uninstaller(?:$|\s\()/,
/Updater(?:$|\s\()/,
/^\./,
]
/**
* Apps commonly requested for CU automation. ALWAYS included if installed,
* bypassing path check + count cap โ the model needs these exact names even
* when the machine has 200+ apps. Bundle IDs (locale-invariant), not display
* names. Keep <30 โ each entry is a guaranteed token in the description.
*/
const ALWAYS_KEEP_BUNDLE_IDS: ReadonlySet<string> = new Set([
// Browsers
'com.apple.Safari',
'com.google.Chrome',
'com.microsoft.edgemac',
'org.mozilla.firefox',
'company.thebrowser.Browser', // Arc
// Communication
'com.tinyspeck.slackmacgap',
'us.zoom.xos',
'com.microsoft.teams2',
'com.microsoft.teams',
'com.apple.MobileSMS',
'com.apple.mail',
// Productivity
'com.microsoft.Word',
'com.microsoft.Excel',
'com.microsoft.Powerpoint',
'com.microsoft.Outlook',
'com.apple.iWork.Pages',
'com.apple.iWork.Numbers',
'com.apple.iWork.Keynote',
'com.google.GoogleDocs',
// Notes / PM
'notion.id',
'com.apple.Notes',
'md.obsidian',
'com.linear',
'com.figma.Desktop',
// Dev
'com.microsoft.VSCode',
'com.apple.Terminal',
'com.googlecode.iterm2',
'com.github.GitHubDesktop',
// System essentials the model genuinely targets
'com.apple.finder',
'com.apple.iCal',
'com.apple.systempreferences',
])
// โโ Prompt-injection hardening โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/**
* `\p{L}\p{M}\p{N}` with /u โ not `\w` (ASCII-only, would drop Bรผcher, ๅพฎไฟก,
* Prรฉfรฉrences Systรจme). `\p{M}` matches combining marks so NFD-decomposed
* diacritics (รผ โ u + โฬ) pass. Single space not `\s` โ `\s` matches newlines,
* which would let "App\nIgnore previousโฆ" through as a multi-line injection.
* Still bars quotes, angle brackets, backticks, pipes, colons.
*/
const APP_NAME_ALLOWED = /^[\p{L}\p{M}\p{N}_ .&'()+-]+$/u
const APP_NAME_MAX_LEN = 40
const APP_NAME_MAX_COUNT = 50
function isUserFacingPath(path: string, homeDir: string | undefined): boolean {
if (PATH_ALLOWLIST.some(root => path.startsWith(root))) return true
if (homeDir) {
const userApps = homeDir.endsWith('/')
? `${homeDir}Applications/`
: `${homeDir}/Applications/`
if (path.startsWith(userApps)) return true
}
return false
}
function isNoisyName(name: string): boolean {
return NAME_PATTERN_BLOCKLIST.some(re => re.test(name))
}
/**
* Length cap + trim + dedupe + sort. `applyCharFilter` โ skip for trusted
* bundle IDs (Apple/Google/MS; a localized "Rรฉglages Systรจme" with unusual
* punctuation shouldn't be dropped), apply for anything attacker-installable.
*/
function sanitizeCore(
raw: readonly string[],
applyCharFilter: boolean,
): string[] {
const seen = new Set<string>()
return raw
.map(name => name.trim())
.filter(trimmed => {
if (!trimmed) return false
if (trimmed.length > APP_NAME_MAX_LEN) return false
if (applyCharFilter && !APP_NAME_ALLOWED.test(trimmed)) return false
if (seen.has(trimmed)) return false
seen.add(trimmed)
return true
})
.sort((a, b) => a.localeCompare(b))
}
function sanitizeAppNames(raw: readonly string[]): string[] {
const filtered = sanitizeCore(raw, true)
if (filtered.length <= APP_NAME_MAX_COUNT) return filtered
return [
...filtered.slice(0, APP_NAME_MAX_COUNT),
`โฆ and ${filtered.length - APP_NAME_MAX_COUNT} more`,
]
}
function sanitizeTrustedNames(raw: readonly string[]): string[] {
return sanitizeCore(raw, false)
}
/**
* Filter raw Spotlight results to user-facing apps, then sanitize. Always-keep
* apps bypass path/name filter AND char allowlist (trusted vendors, not
* attacker-installed); still length-capped, deduped, sorted.
*/
export function filterAppsForDescription(
installed: readonly InstalledAppLike[],
homeDir: string | undefined,
): string[] {
const { alwaysKept, rest } = installed.reduce<{
alwaysKept: string[]
rest: string[]
}>(
(acc, app) => {
if (ALWAYS_KEEP_BUNDLE_IDS.has(app.bundleId)) {
acc.alwaysKept.push(app.displayName)
} else if (
isUserFacingPath(app.path, homeDir) &&
!isNoisyName(app.displayName)
) {
acc.rest.push(app.displayName)
}
return acc
},
{ alwaysKept: [], rest: [] },
)
const sanitizedAlways = sanitizeTrustedNames(alwaysKept)
const alwaysSet = new Set(sanitizedAlways)
return [
...sanitizedAlways,
...sanitizeAppNames(rest).filter(n => !alwaysSet.has(n)),
]
}