feat: 项目代码逻辑与安全分析

Co-authored-by: traeagent <traeagent@users.noreply.github.com>
This commit is contained in:
jeo-ch 2026-06-17 06:19:18 +00:00
parent f2b4356b8f
commit 55a879f118
11 changed files with 83 additions and 2 deletions

View File

@ -461,6 +461,8 @@ export const en = {
'settings.providers.toolSearchEnabled': 'Enable Tool Search',
'settings.providers.toolSearchDesc': 'Load MCP and deferred tools on demand to reduce initial tool schema tokens. Disable it for weak models or providers that reject tool references.',
'settings.providers.toolSearchUnsupported': 'Only Anthropic Messages providers support Tool Search here. OpenAI proxy formats keep full tool schemas available.',
'settings.providers.availableModels': 'Available models — click to select',
'settings.providers.clickToSelectModel': 'Click to use this model as the main model',
// Settings > Permissions
'settings.permissions.title': 'Permission Mode',

View File

@ -463,6 +463,8 @@ export const jp: Record<TranslationKey, string> = {
'settings.providers.toolSearchEnabled': 'Tool Search を有効にする',
'settings.providers.toolSearchDesc': 'MCP と遅延ツールを必要に応じて読み込み、初回のツール schema トークンを減らします。弱いモデルや tool_reference を拒否するプロバイダーでは無効にできます。',
'settings.providers.toolSearchUnsupported': 'ここでは Anthropic Messages 形式のプロバイダーのみ Tool Search をサポートします。OpenAI プロキシ形式では完全なツール schema を維持します。',
'settings.providers.availableModels': '利用可能なモデル — クリックして選択',
'settings.providers.clickToSelectModel': 'クリックしてこのモデルを使用',
// Settings > Permissions
'settings.permissions.title': '権限モード',

View File

@ -463,6 +463,8 @@ export const kr: Record<TranslationKey, string> = {
'settings.providers.toolSearchEnabled': 'Tool Search 사용',
'settings.providers.toolSearchDesc': 'MCP와 지연 도구를 필요할 때 로드해 초기 도구 schema 토큰을 줄입니다. 약한 모델이나 tool_reference를 거부하는 공급자에서는 끌 수 있습니다.',
'settings.providers.toolSearchUnsupported': '여기서는 Anthropic Messages 형식 공급자만 Tool Search를 지원합니다. OpenAI 프록시 형식은 전체 도구 schema를 유지합니다.',
'settings.providers.availableModels': '사용 가능한 모델 — 클릭하여 선택',
'settings.providers.clickToSelectModel': '클릭하여 이 모델을 주 모델로 사용',
// Settings > Permissions
'settings.permissions.title': '권한 모드',

View File

@ -463,6 +463,8 @@ export const zh: Record<TranslationKey, string> = {
'settings.providers.toolSearchEnabled': '啟用 Tool Search',
'settings.providers.toolSearchDesc': '按需載入 MCP 和延遲工具,減少首輪工具 schema token。弱模型或不支援 tool_reference 的服務商可以關閉。',
'settings.providers.toolSearchUnsupported': '目前僅 Anthropic Messages 格式支援 Tool SearchOpenAI 代理格式會保留完整工具 schema。',
'settings.providers.availableModels': '可用模型 — 點擊選擇',
'settings.providers.clickToSelectModel': '點擊使用此模型作為主模型',
// Settings > Permissions
'settings.permissions.title': '許可權模式',

View File

@ -463,6 +463,8 @@ export const zh: Record<TranslationKey, string> = {
'settings.providers.toolSearchEnabled': '启用 Tool Search',
'settings.providers.toolSearchDesc': '按需加载 MCP 和延迟工具,减少首轮工具 schema token。弱模型或不支持 tool_reference 的服务商可以关闭。',
'settings.providers.toolSearchUnsupported': '当前仅 Anthropic Messages 格式支持 Tool SearchOpenAI 代理格式会保留完整工具 schema。',
'settings.providers.availableModels': '可用模型 — 点击选择',
'settings.providers.clickToSelectModel': '点击使用此模型作为主模型',
// Settings > Permissions
'settings.permissions.title': '权限模式',

View File

@ -1794,6 +1794,33 @@ function ProviderFormModal({ open, onClose, mode, provider, presets }: ProviderF
)}
</div>
{/* Available models — auto-fill main model when a chip is clicked */}
{testResult?.availableModels && testResult.availableModels.length > 0 && (
<div>
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-1.5 block">
{t('settings.providers.availableModels') || 'Available models'}
</label>
<div className="flex flex-wrap gap-1.5">
{testResult.availableModels.slice(0, 20).map((model) => (
<button
key={model}
type="button"
onClick={() => handleModelChange('main', model)}
title={t('settings.providers.clickToSelectModel') || 'Click to select'}
className="max-w-[200px] truncate rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-2 py-1 text-xs text-[var(--color-text-primary)] transition-colors hover:border-[var(--color-border-focus)] hover:bg-[var(--color-surface-hover)] focus:outline-none focus:shadow-[var(--shadow-focus-ring)]"
>
{model}
</button>
))}
{testResult.availableModels.length > 20 && (
<span className="px-2 py-1 text-xs text-[var(--color-text-tertiary)]">
+{testResult.availableModels.length - 20} more
</span>
)}
</div>
</div>
)}
{/* Settings JSON — editable, shown for all presets including official */}
<div>
<label className="text-sm font-medium text-[var(--color-text-primary)] mb-2 block">{t('settings.providers.settingsJson')}</label>

View File

@ -322,8 +322,12 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
},
setLocale: (locale) => {
const prev = get().locale
set({ locale })
try { localStorage.setItem(LOCALE_STORAGE_KEY, locale) } catch { /* noop */ }
settingsApi.updateUser({ locale }).catch(() => {
set({ locale: prev })
})
},
setTheme: async (theme) => {

View File

@ -21,7 +21,8 @@ import { sessionService } from './services/sessionService.js'
import { conversationService } from './services/conversationService.js'
import { OPENAI_CODEX_REDIRECT_PATH } from '../services/openaiAuth/client.js'
import { ensureDesktopCliLauncherInstalled } from './services/desktopCliLauncherService.js'
import { enableConfigs } from '../utils/config.js'
import { enableConfigs, getGlobalConfig } from '../utils/config.js'
import { setLocale } from '../utils/i18n/index.js'
import { diagnosticsService } from './services/diagnosticsService.js'
import { ensurePersistentStorageUpgraded } from './services/persistentStorageMigrations.js'
import { handleStaticH5Request } from './staticH5.js'
@ -126,6 +127,9 @@ function originFromUrl(value: string | null): string | null {
export function startServer(port = PORT, host = HOST) {
enableConfigs()
// Apply persisted locale from ~/.claude/settings.json (set by desktop UI language switcher)
const { locale } = getGlobalConfig()
if (locale) setLocale(locale)
// Warm the synchronous disconnect-grace cache from managed settings so the
// first client disconnect honors the configured value (issue #764).
void refreshDisconnectGraceMs()

View File

@ -9,7 +9,8 @@ import {
} from '../../utils/bash/commands.js'
import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
import { getDirectoryForPath } from '../../utils/path.js'
import { allWorkingDirectories } from '../../utils/permissions/filesystem.js'
import { getOriginalCwd } from '../../bootstrap/state.js'
import { allWorkingDirectories, checkSymlinkPathSafety } from '../../utils/permissions/filesystem.js'
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
import { createReadRuleSuggestion } from '../../utils/permissions/PermissionUpdate.js'
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
@ -691,6 +692,23 @@ function validateCommandPaths(
decisionReason,
}
}
// Symlink TOCTOU check for write/create operations:
// Block commands where the resolved path crosses a symlink outside the session.
// Read-only operations (ls, cat, grep, etc.) are exempt since they don't modify files.
if (operationType === 'write' || operationType === 'create') {
const symlinkResult = checkSymlinkPathSafety(resolvedPath, [getOriginalCwd()])
if (!symlinkResult.safe) {
return {
behavior: 'deny',
message: `'${command}' to '${resolvedPath}' is blocked: contains a symlink pointing outside the working directory.`,
decisionReason: {
type: 'safetyCheck',
reason: symlinkResult.reason ?? 'Symlink points outside the session working directory',
},
}
}
}
}
// All paths are valid - return passthrough
@ -994,6 +1012,21 @@ function validateOutputRedirections(
],
}
}
// Symlink TOCTOU check: ensure the resolved path doesn't cross a symlink
// that points outside the session working directory.
// This is defense-in-depth on top of the normal permission flow.
const symlinkResult = checkSymlinkPathSafety(resolvedPath, [getOriginalCwd()])
if (!symlinkResult.safe) {
return {
behavior: 'deny',
message: `Output redirection to '${resolvedPath}' is blocked: contains a symlink pointing outside the working directory.`,
decisionReason: {
type: 'safetyCheck',
reason: symlinkResult.reason ?? 'Symlink points outside the session working directory',
},
}
}
}
return {

View File

@ -195,6 +195,7 @@ export type GlobalConfig = {
doctorShownAtSession?: number
userID?: string
theme: ThemeSetting
locale?: string // cc-haha i18n locale (en | zh-CN | zh-TW), synced from desktop UI
hasCompletedOnboarding?: boolean
// Tracks the last version that reset onboarding, used with MIN_VERSION_REQUIRING_ONBOARDING_RESET
lastOnboardingVersion?: string

View File

@ -54,6 +54,8 @@ export function setLocale(l: Locale | string): void {
if (lower === 'zh-tw' || lower === 'zh_hant') currentLocale = 'zh-TW'
else if (lower.startsWith('zh')) currentLocale = 'zh-CN'
else currentLocale = 'en'
// Sync to env so child processes and the CLI startup path pick it up
process.env.CC_HAHA_LOCALE = currentLocale
}
export function getLocale(): Locale {