diff --git a/apps/buddy/electron/main/config/LexoraConfigStore.ts b/apps/buddy/electron/main/config/LexoraConfigStore.ts index 0babe06d..4edddcaf 100644 --- a/apps/buddy/electron/main/config/LexoraConfigStore.ts +++ b/apps/buddy/electron/main/config/LexoraConfigStore.ts @@ -12,6 +12,7 @@ import { Emitter } from '../../../shared/events/Emitter' import { copyEventSnapshot } from '../../../shared/events/eventSnapshot' import { extensionAuthorSchema } from '../../../shared/extensions/extensionIdentity' import { DEFAULT_PROXY_SETTINGS, proxySettingsSchema } from '../../../shared/network/proxySettings' +import { BUDDY_PERMISSION_MODES } from '../../../shared/permissions/permissionMode' import { DEFAULT_RUNTIME_PREFERENCES, runtimePreferencesSchema } from '../../../shared/runtime/runtimePreferences' import { keybindingsSchema } from '../../../shared/shortcuts/keybindingSchema' import { DEFAULT_DESKTOP_CHAT_PREFERENCES, DESKTOP_CHAT_OUTLINE_POSITIONS, DESKTOP_CHAT_WELCOME_VARIANT_IDS, DESKTOP_PROFILE_AVATAR_MAX_DATA_URL_LENGTH, DESKTOP_TASK_SIDEBAR_SECTIONS } from '../../shared/desktopApi' @@ -43,6 +44,7 @@ const desktopConfigSchema = z.object({ background_close_notice_shown: z.boolean().default(false), chat: z.object({ outline_position: z.enum(DESKTOP_CHAT_OUTLINE_POSITIONS).default(DEFAULT_DESKTOP_CHAT_PREFERENCES.outlinePosition), + permission_mode: z.enum(BUDDY_PERMISSION_MODES).default(DEFAULT_DESKTOP_CHAT_PREFERENCES.permissionMode), welcome: z.enum(['none', 'random', ...DESKTOP_CHAT_WELCOME_VARIANT_IDS]).default(DEFAULT_DESKTOP_CHAT_PREFERENCES.welcome), }).passthrough().prefault({}), context_panel_mode: z.enum(['task', 'independent']).default('task'), @@ -74,6 +76,7 @@ const desktopConfigSchema = z.object({ plugin_author: '', chat: { outline_position: DEFAULT_DESKTOP_CHAT_PREFERENCES.outlinePosition, + permission_mode: DEFAULT_DESKTOP_CHAT_PREFERENCES.permissionMode, welcome: DEFAULT_DESKTOP_CHAT_PREFERENCES.welcome, }, context_panel_mode: 'task', @@ -288,6 +291,7 @@ function decodeConfig(value: unknown): LexoraConfig { pluginAuthor: config.desktop.plugin_author, chat: { outlinePosition: config.desktop.chat.outline_position, + permissionMode: config.desktop.chat.permission_mode, welcome: config.desktop.chat.welcome, }, contextPanelMode: config.desktop.context_panel_mode, @@ -337,6 +341,7 @@ function encodeConfig(config: LexoraConfig) { plugin_author: config.desktop.pluginAuthor, chat: { outline_position: config.desktop.chat.outlinePosition, + permission_mode: config.desktop.chat.permissionMode, welcome: config.desktop.chat.welcome, }, context_panel_mode: config.desktop.contextPanelMode, @@ -382,6 +387,9 @@ function mergeConfig(current: LexoraConfig, patch: LexoraConfigPatch): LexoraCon chat: { ...current.desktop.chat, ...patch.desktop?.chat, + permissionMode: z.enum(BUDDY_PERMISSION_MODES).parse( + patch.desktop?.chat?.permissionMode ?? current.desktop.chat.permissionMode, + ), }, keybindings: keybindingsSchema.parse(patch.desktop?.keybindings ?? current.desktop.keybindings), taskSidebar: { diff --git a/apps/buddy/electron/main/config/__tests__/LexoraConfigStore.spec.ts b/apps/buddy/electron/main/config/__tests__/LexoraConfigStore.spec.ts index a5ee839b..558af4b4 100644 --- a/apps/buddy/electron/main/config/__tests__/LexoraConfigStore.spec.ts +++ b/apps/buddy/electron/main/config/__tests__/LexoraConfigStore.spec.ts @@ -39,19 +39,37 @@ describe('lexoraConfigStore', () => { await mkdir(dirname(configPath), { recursive: true }) await writeFile(configPath, '[desktop]\ntheme = "dark"\nwelcome_variant = "writing"\n[custom]\nkeep = true\n') const original = await store.read() - expect(original.desktop.chat).toEqual({ outlinePosition: 'top-right', welcome: 'random' }) + expect(original.desktop.chat).toEqual({ outlinePosition: 'top-right', permissionMode: 'policy_approval', welcome: 'random' }) await store.update({ desktop: { chat: { welcome: 'none' } } }) for (const outlinePosition of DESKTOP_CHAT_OUTLINE_POSITIONS) { await store.update({ desktop: { chat: { outlinePosition } } }) const restored = await new LexoraConfigStore({ configPath }).read() expect(restored).toEqual({ ...original, - desktop: { ...original.desktop, chat: { outlinePosition, welcome: 'none' } }, + desktop: { ...original.desktop, chat: { outlinePosition, permissionMode: 'policy_approval', welcome: 'none' } }, }) expect(await readFile(configPath, 'utf8')).toContain('keep = true') } }) + it('persists permission mode, defaults legacy profiles, and rejects invalid values without a partial write', async () => { + const { configPath, store } = await createConfigStore() + await mkdir(dirname(configPath), { recursive: true }) + await writeFile(configPath, '[desktop.chat]\nwelcome = "random"\n') + expect((await store.read()).desktop.chat.permissionMode).toBe('policy_approval') + + await store.update({ desktop: { chat: { permissionMode: 'full_access' } } }) + expect(await readFile(configPath, 'utf8')).toContain('permission_mode = "full_access"') + expect((await new LexoraConfigStore({ configPath }).read()).desktop.chat.permissionMode).toBe('full_access') + + const saved = await readFile(configPath, 'utf8') + await expect(store.update({ desktop: { chat: { permissionMode: 'invalid' as never } } })).rejects.toThrow() + expect(await readFile(configPath, 'utf8')).toBe(saved) + + await writeFile(configPath, '[desktop.chat]\npermission_mode = "invalid"\n') + await expect(store.read()).rejects.toThrow() + }) + it('migrates browser preferences with defaults and preserves unrelated settings across updates', async () => { const { configPath, store } = await createConfigStore() await mkdir(dirname(configPath), { recursive: true }) @@ -139,7 +157,7 @@ describe('lexoraConfigStore', () => { it('updates only requested settings and writes a private TOML file atomically', async () => { const { configPath, store } = await createConfigStore() await expect(store.read()).resolves.toMatchObject({ - desktop: { theme: 'system', chat: { outlinePosition: 'top-right', welcome: 'random' } }, + desktop: { theme: 'system', chat: { outlinePosition: 'top-right', permissionMode: 'policy_approval', welcome: 'random' } }, pet: { alwaysOnTop: true, enabled: true, rememberPosition: true }, }) @@ -157,7 +175,7 @@ describe('lexoraConfigStore', () => { keybindings: {}, backgroundCloseNoticeShown: false, pluginAuthor: '', - chat: { outlinePosition: 'top-right', welcome: 'writing' }, + chat: { outlinePosition: 'top-right', permissionMode: 'policy_approval', welcome: 'writing' }, taskSidebarPinnedItems: [], taskSidebar: { collapsed: false, @@ -300,7 +318,7 @@ describe('lexoraConfigStore', () => { await writeFile(configPath, '[desktop.chat]\nwelcome = "none"\nfuture = true\n') await expect(store.read()).resolves.toMatchObject({ - desktop: { chat: { outlinePosition: 'top-right', welcome: 'none' } }, + desktop: { chat: { outlinePosition: 'top-right', permissionMode: 'policy_approval', welcome: 'none' } }, }) await store.update({ desktop: { theme: 'dark' } }) diff --git a/apps/buddy/electron/shared/__tests__/desktopApi.spec.ts b/apps/buddy/electron/shared/__tests__/desktopApi.spec.ts index 48ac7c7e..860f1d04 100644 --- a/apps/buddy/electron/shared/__tests__/desktopApi.spec.ts +++ b/apps/buddy/electron/shared/__tests__/desktopApi.spec.ts @@ -5,8 +5,9 @@ import { feedbackIssueInputSchema, lexoraConfigPatchSchema, releasePageInputSche describe('desktop Preload API contract', () => { it('rejects unknown outline positions before updating the application configuration', () => { expect(() => lexoraConfigPatchSchema.parse({ desktop: { chat: { outlinePosition: 'center' } } })).toThrow() - expect(lexoraConfigPatchSchema.parse({ desktop: { chat: { outlinePosition: 'bottom-left', welcome: 'none' } } })) - .toEqual({ desktop: { chat: { outlinePosition: 'bottom-left', welcome: 'none' } } }) + expect(lexoraConfigPatchSchema.parse({ desktop: { chat: { outlinePosition: 'bottom-left', permissionMode: 'full_access', welcome: 'none' } } })) + .toEqual({ desktop: { chat: { outlinePosition: 'bottom-left', permissionMode: 'full_access', welcome: 'none' } } }) + expect(() => lexoraConfigPatchSchema.parse({ desktop: { chat: { permissionMode: 'invalid' } } })).toThrow() }) it('accepts release pages from the canonical repository', () => { diff --git a/apps/buddy/electron/shared/desktopApi.ts b/apps/buddy/electron/shared/desktopApi.ts index 84cc6d27..fc5153e4 100644 --- a/apps/buddy/electron/shared/desktopApi.ts +++ b/apps/buddy/electron/shared/desktopApi.ts @@ -8,6 +8,7 @@ import type { BuddyCapabilities } from '../../shared/platform' import type { BuddyRuntimeProfile } from '../../shared/runtime/profile' import type { DesktopCommandId, DesktopPlatform } from './desktopCommands' import type { LocalChatApi } from './localChatApi' +import { BUDDY_DEFAULT_PERMISSION_MODE } from '../../shared/permissions/permissionMode' export const DESKTOP_IPC_CHANNELS = { workbenchRead: 'lexora:workbench:read', @@ -148,11 +149,13 @@ export type DesktopChatOutlinePosition = typeof DESKTOP_CHAT_OUTLINE_POSITIONS[n export interface DesktopChatPreferences { outlinePosition: DesktopChatOutlinePosition + permissionMode: import('../../shared/permissions/permissionMode').BuddyPermissionMode welcome: DesktopChatWelcomePreference } export const DEFAULT_DESKTOP_CHAT_PREFERENCES: Readonly = { outlinePosition: 'top-right', + permissionMode: BUDDY_DEFAULT_PERMISSION_MODE, welcome: 'random', } diff --git a/apps/buddy/electron/shared/desktopApiSchemas.ts b/apps/buddy/electron/shared/desktopApiSchemas.ts index 14bb8be9..25556ca8 100644 --- a/apps/buddy/electron/shared/desktopApiSchemas.ts +++ b/apps/buddy/electron/shared/desktopApiSchemas.ts @@ -3,6 +3,7 @@ import { z } from 'zod' import { browserPreferencesSchema } from '../../shared/browser/browserPreferences' import { extensionAuthorSchema } from '../../shared/extensions/extensionIdentity' import { proxySettingsSchema } from '../../shared/network/proxySettings' +import { BUDDY_PERMISSION_MODES } from '../../shared/permissions/permissionMode' import { runtimePreferencesSchema } from '../../shared/runtime/runtimePreferences' import { keybindingsSchema } from '../../shared/shortcuts/keybindingSchema' import { @@ -65,6 +66,7 @@ export const lexoraConfigPatchSchema: z.ZodType = z.object({ backgroundCloseNoticeShown: z.boolean().optional(), chat: z.object({ outlinePosition: z.enum(DESKTOP_CHAT_OUTLINE_POSITIONS).optional(), + permissionMode: z.enum(BUDDY_PERMISSION_MODES).optional(), welcome: z.enum(['none', 'random', ...DESKTOP_CHAT_WELCOME_VARIANT_IDS]).optional(), }).strict().optional(), contextPanelMode: z.enum(['task', 'independent']).optional(), diff --git a/apps/buddy/shared/permissions/permissionMode.ts b/apps/buddy/shared/permissions/permissionMode.ts index e5e958dc..732afeb4 100644 --- a/apps/buddy/shared/permissions/permissionMode.ts +++ b/apps/buddy/shared/permissions/permissionMode.ts @@ -10,6 +10,8 @@ export const BUDDY_PERMISSION_MODES = [ export type BuddyPermissionMode = typeof BUDDY_PERMISSION_MODES[number] +export const BUDDY_DEFAULT_PERMISSION_MODE: BuddyPermissionMode = 'policy_approval' + export interface BuddyPermissionSettings { approvalPolicy: BuddyApprovalPolicy executionProfile: BuddyExecutionProfile diff --git a/apps/buddy/src/i18n/locales/en-US/settings.ts b/apps/buddy/src/i18n/locales/en-US/settings.ts index 2cdbcff8..16d547c0 100644 --- a/apps/buddy/src/i18n/locales/en-US/settings.ts +++ b/apps/buddy/src/i18n/locales/en-US/settings.ts @@ -2,7 +2,14 @@ import type zhCN from '../zh-CN/settings' export default { 'desktop.settings.category.runtime': 'Runtime', - 'desktop.settings.categoryDescription.runtime': 'Manage context and model request behavior.', + 'desktop.settings.categoryDescription.runtime': 'Manage permissions for new tasks, context, and model request behavior.', + 'desktop.settings.runtime.taskPermissions': 'Task permissions', + 'desktop.settings.runtime.defaultPermissionMode': 'New task permissions', + 'desktop.settings.runtime.defaultPermissionModeDescription': 'New tasks start with the selected permissions. Existing tasks, including unsent drafts, keep their current settings.', + 'desktop.settings.runtime.fullAccessDefaultTitle': 'Use full access by default for new tasks?', + 'desktop.settings.runtime.fullAccessDefaultDescription': 'New tasks will start with local access to your files, credentials, and network. The sandbox cannot restrict what arbitrary commands do; some sensitive operations will still ask for confirmation.', + 'desktop.settings.runtime.fullAccessDefaultAcknowledgement': 'I understand that full access will apply to every new task by default', + 'desktop.settings.runtime.fullAccessDefaultConfirm': 'Use as default', 'desktop.settings.runtime.context': 'Context', 'desktop.settings.runtime.cacheWarming': 'Cache warming', 'desktop.settings.runtime.cacheWarmingDescription': 'Maintain prompt caches during active tasks when the model supports it and the expected benefit covers the cost. Additional usage is included in usage reports. Stops when the task ends.', diff --git a/apps/buddy/src/i18n/locales/zh-CN/settings.ts b/apps/buddy/src/i18n/locales/zh-CN/settings.ts index d2f3ac4b..3baeea11 100644 --- a/apps/buddy/src/i18n/locales/zh-CN/settings.ts +++ b/apps/buddy/src/i18n/locales/zh-CN/settings.ts @@ -1,6 +1,13 @@ export default { 'desktop.settings.category.runtime': '运行时', - 'desktop.settings.categoryDescription.runtime': '管理上下文与模型调用策略。', + 'desktop.settings.categoryDescription.runtime': '管理新任务权限、上下文与模型调用策略。', + 'desktop.settings.runtime.taskPermissions': '任务权限', + 'desktop.settings.runtime.defaultPermissionMode': '新任务权限', + 'desktop.settings.runtime.defaultPermissionModeDescription': '新建任务时使用所选权限;已有任务(包括未发送的草稿)保持当前设置。', + 'desktop.settings.runtime.fullAccessDefaultTitle': '将完全访问设为新任务默认权限?', + 'desktop.settings.runtime.fullAccessDefaultDescription': '之后新建的任务将默认以你的本机权限访问文件、凭据和网络。任意命令的实际行为无法由沙盒限制;部分敏感操作仍会请求确认。', + 'desktop.settings.runtime.fullAccessDefaultAcknowledgement': '我已了解完全访问会默认应用于每个新任务', + 'desktop.settings.runtime.fullAccessDefaultConfirm': '设为默认权限', 'desktop.settings.runtime.context': '上下文', 'desktop.settings.runtime.cacheWarming': '缓存预热', 'desktop.settings.runtime.cacheWarmingDescription': '任务执行期间按需维护模型的提示词缓存,仅在支持的模型上且预计有收益时执行。会产生额外用量,计入使用情况;任务结束后停止。', diff --git a/apps/buddy/src/modules/prompt-input/components/DesktopFullAccessConfirmationDialog.vue b/apps/buddy/src/modules/prompt-input/components/DesktopFullAccessConfirmationDialog.vue index a2a78553..2601fe78 100644 --- a/apps/buddy/src/modules/prompt-input/components/DesktopFullAccessConfirmationDialog.vue +++ b/apps/buddy/src/modules/prompt-input/components/DesktopFullAccessConfirmationDialog.vue @@ -1,63 +1,66 @@