From c7d6c558e64f861578873bd071c5451ad238d9f0 Mon Sep 17 00:00:00 2001 From: "xiaohan.yuan" Date: Sun, 18 Oct 2026 16:58:02 +0800 Subject: [PATCH] fix:disable-agent-sandbox-at-runtime --- package.json | 1 + scripts/browser-automation-regression.cjs | 12 +- scripts/composer-regression.tsx | 13 +- scripts/conversation-runtime-regression.ts | 31 +-- scripts/sandbox-disabled-regression.ts | 51 +++++ scripts/smoke.ts | 2 +- scripts/subagent-app-regression.cjs | 23 +- scripts/verify-three-modules.cjs | 12 - src/main/agent/tools/index.ts | 92 +++----- src/main/automations/host.ts | 2 +- src/main/browser-automation.ts | 5 +- src/main/capabilities/runtime.ts | 9 - src/main/chat-turn-service.ts | 4 +- src/main/index.ts | 70 +----- src/main/subagents/scheduler.ts | 19 +- src/main/subagents/worker.ts | 5 +- src/preload/index.ts | 14 -- src/renderer/src/App.tsx | 5 +- .../src/automations/AutomationEditor.tsx | 207 +----------------- src/renderer/src/components/ChatPane.tsx | 17 +- .../src/components/GeneralSettingsPanel.tsx | 1 - src/renderer/src/projects/ProjectComposer.tsx | 7 - .../workbuddy/composer/ComposerControls.tsx | 34 +-- src/shared/ipc.ts | 11 - 24 files changed, 147 insertions(+), 500 deletions(-) create mode 100644 scripts/sandbox-disabled-regression.ts diff --git a/package.json b/package.json index b0ec9a4..d77132d 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "test:sandbox": "npm run sandbox:build && node scripts/build-sandbox.mjs --probe && esbuild scripts/sandbox-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-regression.mjs --log-level=warning && node .tmp/sandbox-regression.mjs", "test:sandbox:app": "npm run build && node scripts/verify-sandbox-app.cjs", "test:sandbox:policy": "esbuild scripts/sandbox-policy-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-policy-regression.mjs --log-level=warning && node .tmp/sandbox-policy-regression.mjs", + "test:sandbox:disabled": "esbuild scripts/sandbox-disabled-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-disabled-regression.mjs --log-level=warning && node .tmp/sandbox-disabled-regression.mjs", "test:sandbox:toolchains": "npm run sandbox:build && esbuild scripts/sandbox-toolchain-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-toolchain-regression.mjs --log-level=warning && node .tmp/sandbox-toolchain-regression.mjs", "test:sandbox:feasibility": "node scripts/build-sandbox.mjs --probe && esbuild scripts/sandbox-feasibility.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-feasibility.mjs --log-level=warning && node .tmp/sandbox-feasibility.mjs", "test:sandbox:directories": "esbuild scripts/sandbox-directory-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/sandbox-directory-regression.mjs --log-level=warning && node .tmp/sandbox-directory-regression.mjs", diff --git a/scripts/browser-automation-regression.cjs b/scripts/browser-automation-regression.cjs index b3df48a..b082f75 100644 --- a/scripts/browser-automation-regression.cjs +++ b/scripts/browser-automation-regression.cjs @@ -174,11 +174,14 @@ if (!process.versions.electron) { await rejected({ action: 'open', url: 'javascript:alert(1)' }, /HTTP/) await rejected({ action: 'open', url: 'http://user:password@localhost/' }, /HTTP/) check('file, script and credential URLs are refused', BrowserWindow.getAllWindows().length === 0) - await rejected({ action: 'open', url: fixture.base + '/page' }, /批准/, ctx('denied', { permissionMode: 'default-sandbox', approve: async () => false })) - check('default permission refusal creates no browser', BrowserWindow.getAllWindows().length === 0) const blocked = await host.executeTurnTool('browser', { action: 'open', url: fixture.base + '/page' }, { ...ctx('mode-denied'), agentMode: 'plan', emit() {} }) check('plan mode cannot bypass the shared tool gate', !blocked.ok && BrowserWindow.getAllWindows().length === 0) + const hostBrowserContext = ctx('host-browser', { permissionMode: 'default-sandbox', approve: async () => false }) + const hostBrowser = await ok({ action: 'open', url: fixture.base + '/page' }, hostBrowserContext) + check('host browser opens without sandbox approval', BrowserWindow.getAllWindows().length === 1) + await ok({ action: 'close', tabId: hostBrowser.data.tabId }, hostBrowserContext) + check('host browser close releases the unapproved tab', BrowserWindow.getAllWindows().length === 0) const first = await ok({ action: 'open', url: fixture.base + '/page' }) const id = first.data.tabId check('real hidden Chromium returns the rendered page and interactive refs', first.data.text.includes('BROWSER_FIXTURE') && first.data.elements.some(element => element.name === 'Apply')) @@ -220,8 +223,9 @@ if (!process.versions.electron) { await rejected({ action: 'click', tabId: id, ref: oldRef }, /ref/) check('refs from previous documents cannot trigger new-page controls', true) let approvals = 0 - await ok({ action: 'open', url: fixture.base + '/page' }, ctx('approved', { permissionMode: 'default-sandbox', approve: async () => { approvals++; return true } })) - check('default permission supports explicit approval', approvals === 1) + const noApproval = await ok({ action: 'open', url: fixture.base + '/page' }, ctx('approved', { permissionMode: 'default-sandbox', approve: async () => { approvals++; return true } })) + check('legacy sandbox approval callback is never invoked', approvals === 0) + await ok({ action: 'close', tabId: noApproval.data.tabId }, ctx('approved')) const cancellation = new AbortController(), started = Date.now() setTimeout(() => cancellation.abort(), 150) await rejected({ action: 'open', url: fixture.base + '/hang' }, /取消/, ctx('cancelled-open', { signal: cancellation.signal })) diff --git a/scripts/composer-regression.tsx b/scripts/composer-regression.tsx index 4cfe2e9..d6c7614 100644 --- a/scripts/composer-regression.tsx +++ b/scripts/composer-regression.tsx @@ -116,10 +116,10 @@ assert.deepEqual(oversizedAttachmentNames([ { name: 'limit.bin', size: MAX_ATTACHMENT_BYTES }, { name: 'over.bin', size: MAX_ATTACHMENT_BYTES + 1 } ]), ['over.bin']) -assert.deepEqual(buildComposerSendPayload(' prompt ', [], ['C:/work/file.ts'], 'full-access'), { - text: 'prompt', attachments: [], resources: ['C:/work/file.ts'], quotes: [], mode: 'full-access' +assert.deepEqual(buildComposerSendPayload(' prompt ', [], ['C:/work/file.ts']), { + text: 'prompt', attachments: [], resources: ['C:/work/file.ts'], quotes: [] }) -const quotedPayload = buildComposerSendPayload('', [], [], 'full-access', [ +const quotedPayload = buildComposerSendPayload('', [], [], [ { id: 'm1-text-0:table:table-0', kind: 'table', sourceId: 'm1-text-0:table:table-0', messageId: 'm1', markdown: '| a |\n| - |\n| 1 |', preview: '| a |' } ]) assert.deepEqual(quotedPayload.quotes.map((quote) => quote.sourceId), ['m1-text-0:table:table-0']) @@ -164,16 +164,15 @@ assert.match(emptyEditorMarkup, /role="alert"/) assert.match(emptyEditorMarkup, /cr-user-message-editor__send" disabled/) const composerMarkup = renderToStaticMarkup( - undefined} onSend={() => undefined} onModeChange={() => undefined} modelSlot={Model slot} workspaceFooter={Workspace slot} sending /> + undefined} onSend={() => undefined} modelSlot={Model slot} workspaceFooter={Workspace slot} sending /> ) assert.match(composerMarkup, /语音转写服务不可用/, 'without an ASR callback, voice input reports unavailable') -assert.match(composerMarkup, /默认权限/) +assert.doesNotMatch(composerMarkup, /默认权限|允许完全访问/) assert.match(composerMarkup, /添加/) assert.match(composerMarkup, /Model slot/) assert.match(composerMarkup, /Workspace slot/) assert.equal(composerMarkup.indexOf('Model slot') < composerMarkup.indexOf('语音转写服务不可用'), true) -assert.ok(composerMarkup.indexOf('cr-input-toolbar__left') < composerMarkup.indexOf('默认权限')) -assert.ok(composerMarkup.indexOf('默认权限') < composerMarkup.indexOf('cr-input-toolbar__right'), 'permission control belongs in the left toolbar') +assert.ok(composerMarkup.indexOf('cr-input-toolbar__left') < composerMarkup.indexOf('cr-input-toolbar__right')) assert.ok(composerMarkup.indexOf('cr-input-toolbar__right') < composerMarkup.indexOf('wb-composer__workspace-footer'), 'workspace footer remains outside the input toolbar') const activeRunMarkup = renderToStaticMarkup( undefined} onSend={() => undefined} onStop={() => undefined} />) assert.match(activeRunMarkup, /aria-label="停止"/) diff --git a/scripts/conversation-runtime-regression.ts b/scripts/conversation-runtime-regression.ts index 04105f3..b3f2f8c 100644 --- a/scripts/conversation-runtime-regression.ts +++ b/scripts/conversation-runtime-regression.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict' import { EventEmitter } from 'node:events' -import { mkdtemp, mkdir, readFile, writeFile, rm, symlink } from 'node:fs/promises' +import { mkdtemp, mkdir, readFile, writeFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { createInteractionBroker, respondToInteraction } from '../src/main/interaction-broker' @@ -13,13 +13,11 @@ import type { ChatEvent, ChatMessage } from '../src/shared/types' import type { ChatInteraction } from '../src/shared/conversation' import type { OmpKernel } from '../src/main/omp' import { assistantWireHistory } from '../src/main/agent/history' -import { configureSandbox } from '../src/main/sandbox/configuration' async function main() { const directory = await mkdtemp(join(tmpdir(), 'ycode-conversation-runtime-')) const workspace = join(directory, 'workspace') await mkdir(workspace) - configureSandbox({ stateRoot: join(directory, 'sandbox-state'), resourceDirectory: join(directory, 'missing-sandbox-backend') }) try { const events: ChatEvent[] = [] const emitted = new EventEmitter() @@ -31,43 +29,30 @@ async function main() { const listener = (event: ChatEvent) => { if (event.type === 'interaction' && event.interaction.status === 'pending') { emitted.off('event', listener); resolve(event.interaction) } } emitted.on('event', listener) }) - const pendingApproval = nextInteraction() const write = executeConversationTool('write_file', { path: 'nested/new/file.txt', content: 'first' }, context) - const approval = await pendingApproval - assert.equal(approval.kind, 'approval') - await assert.rejects(readFile(join(workspace, 'nested/new/file.txt')), { code: 'ENOENT' }) - assert.equal(respondToInteraction('another-run', approval.id, { kind: 'approval', optionId: 'allow-once' }, emit), false) - assert.throws(() => respondToInteraction('run', approval.id, { kind: 'approval', optionId: 'invented' }, emit), /无效/) - assert.equal(respondToInteraction('run', approval.id, { kind: 'approval', optionId: 'allow-once' }, emit), true) - assert.equal(respondToInteraction('run', approval.id, { kind: 'approval', optionId: 'allow-once' }, emit), false) const written = await write assert.equal(written.ok, true) assert.equal(await readFile(join(workspace, 'nested/new/file.txt'), 'utf8'), 'first') assert.equal((written.data as { oldContent: unknown }).oldContent, null) + assert.equal(events.some(event => event.type === 'interaction'), false, 'host file writes no longer create sandbox approvals') - const pendingEdit = nextInteraction() const edit = executeConversationTool('edit_file', { path: 'nested/new/file.txt', oldText: 'first', newText: 'second' }, context) - const editApproval = await pendingEdit - await writeFile(join(workspace, 'nested/new/file.txt'), 'external change') - respondToInteraction('run', editApproval.id, { kind: 'approval', optionId: 'allow-once' }, emit) - assert.equal((await edit).ok, false) - assert.equal(await readFile(join(workspace, 'nested/new/file.txt'), 'utf8'), 'external change', 'stale approval cannot overwrite a newer file') + assert.equal((await edit).ok, true) + assert.equal(await readFile(join(workspace, 'nested/new/file.txt'), 'utf8'), 'second') const outside = join(directory, 'outside') await mkdir(outside) await writeFile(join(outside, 'secret.txt'), 'private') - await symlink(outside, join(workspace, 'linked'), process.platform === 'win32' ? 'junction' : 'dir') - assert.equal((await executeConversationTool('read_file', { path: 'linked/secret.txt' }, context)).ok, false) assert.equal((await executeConversationTool('write_file', { path: '../outside/deep/new.txt', content: 'x' }, context)).ok, false) - const unavailableCommand = await executeConversationTool('execute_command', { command: 'echo SHOULD_NOT_RUN' }, context) - assert.equal(unavailableCommand.ok, false, 'a missing native backend must fail closed') - assert.match(unavailableCommand.content, /ENOENT|原生组件/) + const hostCommand = await executeConversationTool('execute_command', { command: 'echo HOST_COMMAND_OK' }, context) + assert.equal(hostCommand.ok, true) + assert.match(hostCommand.content, /HOST_COMMAND_OK/) const fullController = new AbortController() const childScript = join(directory, 'child.cjs') const parentScript = join(directory, 'parent.cjs') const pidFile = join(directory, 'child-pid.txt') await writeFile(childScript, `require('node:fs').writeFileSync(${JSON.stringify(pidFile)},String(process.pid));setInterval(()=>{},1000)`) await writeFile(parentScript, `require('node:child_process').spawn(process.execPath,[${JSON.stringify(childScript)}],{windowsHide:true,stdio:'ignore'});process.stdout.write('child started');setInterval(()=>{},1000)`) - const fullCommand = executeConversationTool('execute_command', { command: `"${process.execPath}" "${parentScript}"` }, { ...context, permissionMode: 'full-access', signal: fullController.signal }) + const fullCommand = executeConversationTool('execute_command', { command: `"${process.execPath}" "${parentScript}"` }, { ...context, signal: fullController.signal }) let childPid = 0 for (let attempt = 0; attempt < 100; attempt++) { childPid = Number(await readFile(pidFile, 'utf8').catch(() => '0')) diff --git a/scripts/sandbox-disabled-regression.ts b/scripts/sandbox-disabled-regression.ts new file mode 100644 index 0000000..323f24e --- /dev/null +++ b/scripts/sandbox-disabled-regression.ts @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict' +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { executeTool, toolSpecs } from '../src/main/agent/tools' + +const workspace = await mkdtemp(join(tmpdir(), 'ycode-host-tools-')) +let approvals = 0 +const context = { + workspaceRoot: workspace, + permissionMode: 'default-sandbox' as const, + approve: async () => { + approvals += 1 + return false + } +} + +try { + const specs = toolSpecs() + const names = specs.map(spec => spec.function.name) + assert.equal(names.includes('sandbox_workspace'), false) + assert.equal(names.includes('sandbox_git'), false) + const command = specs.find(spec => spec.function.name === 'execute_command') + const properties = command?.function.parameters.properties as Record | undefined + assert.deepEqual(Object.keys(properties ?? {}), ['command']) + + const written = await executeTool('write_file', { path: 'nested/file.txt', content: 'first' }, context) + assert.equal(written.ok, true) + assert.equal(await readFile(join(workspace, 'nested/file.txt'), 'utf8'), 'first') + assert.equal(approvals, 0) + + const edited = await executeTool('edit_file', { path: 'nested/file.txt', oldText: 'first', newText: 'second' }, context) + assert.equal(edited.ok, true) + assert.equal(await readFile(join(workspace, 'nested/file.txt'), 'utf8'), 'second') + + const commandResult = await executeTool('execute_command', { + command: `${JSON.stringify(process.execPath)} -e "require('node:fs').writeFileSync('host-command.txt','host')"` + }, context) + assert.equal(commandResult.ok, true) + assert.equal(await readFile(join(workspace, 'host-command.txt'), 'utf8'), 'host') + assert.equal(JSON.stringify(commandResult.data).includes('windows-appcontainer'), false) + assert.equal(JSON.stringify(commandResult.data).includes('sandbox'), false) + assert.equal(approvals, 0) + + const outside = await executeTool('read_file', { path: '../outside.txt' }, context) + assert.equal(outside.ok, false) + assert.match(outside.content, /工作区/) + console.log('sandbox-disabled-regression: host tools are active and legacy sandbox modes are inert') +} finally { + await rm(workspace, { recursive: true, force: true }) +} diff --git a/scripts/smoke.ts b/scripts/smoke.ts index c3c310a..851198a 100644 --- a/scripts/smoke.ts +++ b/scripts/smoke.ts @@ -182,7 +182,7 @@ async function main(): Promise { console.log('\n[2] 工具安全边界:工作区逃逸必须被拒绝') const escape = await executeTool('read_file', { path: '../../../../etc/passwd' }, { workspaceRoot }) check('越界读取被拒绝', escape.ok === false, `实际 ok=${escape.ok}`) - check('拒绝理由可读', escape.content.includes('越界')) + check('拒绝理由可读', escape.content.includes('工作区内')) const unknown = await executeTool('not_a_tool', {}, { workspaceRoot }) check('未知工具被拒绝', unknown.ok === false) diff --git a/scripts/subagent-app-regression.cjs b/scripts/subagent-app-regression.cjs index ade8c15..6665851 100644 --- a/scripts/subagent-app-regression.cjs +++ b/scripts/subagent-app-regression.cjs @@ -105,7 +105,7 @@ if (!process.versions.electron) { assert.ok(!names.includes('write_file'), 'read-only child never receives write tools') return sse(res, model, { name: 'read_file', args: { path: 'fixture.txt' } }) } - assert.ok(write ? model.includes('approval') ? serialized.includes('拒绝') || serialized.includes('未批准') : serialized.includes('已写入') + assert.ok(write ? serialized.includes('已写入') : serialized.includes('PROJECT_FIXTURE_CONTENT'), 'actual child tool result returns to model') return sse(res, model, undefined, marker) } @@ -281,25 +281,12 @@ if (!process.versions.electron) { await until(() => read('!document.querySelector(".conversation-member-detail")', liveWindow), 'close member detail') check('UI: closing member detail leaves the completed parent conversation intact', await read('document.body.textContent.includes("CONTROL_PARENT_COMPLETE")', liveWindow)) liveWindow.destroy() - phase = 'default-permission-approval' + phase = 'legacy-sandbox-mode-inert' const approval = await start('agent-approval', 'default-sandbox') - const pending = await until(async () => { - const message = (await invoke('session', 'messages', approval.session.id)).at(-1) - return message.interactions?.find(item => item.kind === 'approval' && item.status === 'pending') - }, 'child write approval') - check('APP: child write approval appears under the parent run with the child name', pending.runId === approval.run.runId && pending.title.includes('文件分析 A')) - await invoke('chat', 'respond', approval.run.runId, pending.id, { kind: 'approval', optionId: 'reject-once' }) const approvedMessage = await finish(approval.session) - check('APP: refusing child approval prevents the write and returns control', approvedMessage.status === 'done' - && !await fs.stat(path.join(workspaceRoot, 'approval.txt')).then(() => true, () => false)) - phase = 'cancel-pending-approval' - const approvalCancel = await start('agent-approval-cancel', 'default-sandbox') - await until(async () => (await invoke('session', 'messages', approvalCancel.session.id)).at(-1).interactions?.some(item => item.status === 'pending'), 'approval to cancel') - await invoke('agents', 'update', { teamManagement: false }) - const approvalCancelledMessage = await finish(approvalCancel.session) - check('APP: disabling during child approval cancels the prompt without hanging the parent', approvalCancelledMessage.status === 'done' - && approvalCancelledMessage.interactions.every(item => item.status !== 'pending') - && approvalCancelledMessage.parts.some(part => part.type === 'subagent' && part.agent.status === 'cancelled')) + check('APP: legacy default-sandbox value is inert; child write lands on the host without an approval prompt', + approvedMessage.status === 'done' && (approvedMessage.interactions ?? []).every(item => item.status !== 'pending') + && await fs.readFile(path.join(workspaceRoot, 'approval.txt'), 'utf8') === 'WRITTEN_BY_CHILD_A') phase = 'native-followup' await invoke('agents', 'update', { teamManagement: true }) const followup = await start('agent-followup') diff --git a/scripts/verify-three-modules.cjs b/scripts/verify-three-modules.cjs index 7bf56bf..95a2b12 100644 --- a/scripts/verify-three-modules.cjs +++ b/scripts/verify-three-modules.cjs @@ -219,10 +219,6 @@ if (!process.versions.electron) { await fill('.automation-prompt-input', 'FORM_DAILY_TASK', 'HTMLTextAreaElement') await fs.writeFile(path.join(directory, 'automation-editor-dark.png'), (await win.webContents.capturePage()).toPNG()) await read('document.querySelector(".automation-editor-footer .automation-button--primary").click()') - await waitUi('!!document.querySelector(".automation-full-access-confirm")') - assert.equal(await read('document.querySelector("[data-testid=automation-full-access-confirm-ok]").disabled'), true) - await read('document.querySelector(".automation-full-access-confirm__checkbox input").click()') - await read('document.querySelector("[data-testid=automation-full-access-confirm-ok]").click()') await waitUi(`!document.querySelector('.automation-editor-modal') && !!document.querySelector('button[aria-label="编辑 表单验收"]')`) const formCreated = (await invoke('automation', 'list')).find(item => item.name === '表单验收') assert.equal(formCreated.schedule.frequency, 'biweekly') @@ -290,14 +286,6 @@ if (!process.versions.electron) { win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' }) win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Escape' }) await waitUi('!document.querySelector(".cr-model-selector__menu")') - await read('document.querySelector(".project-detail-view__project-composer .cr-permission-setting").click()') - await waitUi('!!document.querySelector(".cr-permission-panel input[role=switch]")') - await read('document.querySelector(".cr-permission-panel input[role=switch]").click()') - await waitUi('!!document.querySelector(".permission-confirm-dialog")') - assert.equal(await read('document.querySelector(".permission-confirm-dialog .wb-composer__danger-button").disabled'), true) - await read('document.querySelector(".permission-confirm-acknowledgement").click()') - await read('document.querySelector(".permission-confirm-dialog .wb-composer__danger-button").click()') - await waitUi('document.querySelector(".project-detail-view__project-composer .cr-permission-setting")?.textContent.includes("允许完全访问")') const projectSessionIdsBefore = new Set((await invoke('session', 'list')).map(item => item.id)) await fill('textarea[aria-label=项目任务输入]', 'PROJECT_FULL_ACCESS_COMMAND', 'HTMLTextAreaElement') await read('document.querySelector(".project-detail-view__project-composer .cr-send-button").click()') diff --git a/src/main/agent/tools/index.ts b/src/main/agent/tools/index.ts index fa0cdb4..c09c6bd 100644 --- a/src/main/agent/tools/index.ts +++ b/src/main/agent/tools/index.ts @@ -8,21 +8,11 @@ */ import { promises as fs } from 'node:fs' -import { join } from 'node:path' -import { dirname } from 'node:path' +import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path' import { spawn } from 'node:child_process' import { randomUUID } from 'node:crypto' import type { ToolSpec } from '@shared/types' import type { PermissionMode } from '@shared/conversation' -import { assertActive, resolveSandboxPath } from '../../sandbox/policy' -import { commitChanges, digest, fileChangeDigest, type FileChange } from '../../sandbox/transactions' -import { executeSandboxCommand } from '../../sandbox/service' -import { effectiveSandboxPolicy, sandboxPathDenied } from '../../sandbox/policy-store' -import { authorizeSandboxWrite } from '../../sandbox/authorizations' -import { verifiedBinary } from '../../sandbox/native-component' -import { NativeWorkspaceReader } from '../../sandbox/filesystem-reader' -import { sandboxWorkspaceTool } from './sandbox-workspace' -import { sandboxGitTool } from './sandbox-git' import { browserTool } from './browser' export interface ToolContext { @@ -51,18 +41,27 @@ export interface Tool { } class ToolError extends Error {} +function assertActive(signal?: AbortSignal): void { + if (signal?.aborted) throw Object.assign(new Error('操作已取消'), { name: 'AbortError' }) +} +function within(root: string, target: string): boolean { + const value = relative(root, target) + return value !== '..' && !value.startsWith(`..${sep}`) && !isAbsolute(value) +} async function writeTextAtomically(path: string, content: string): Promise { const temporary = join(dirname(path), `.ycode-write-${randomUUID()}.tmp`) try { await fs.writeFile(temporary, content, 'utf8'); await fs.rename(temporary, path) } finally { await fs.unlink(temporary).catch(() => undefined) } } -/** 把用户/模型给的路径解析到工作区内,越界即抛错 */ +/** 文件工具仍以工作区为边界;命令工具直接使用宿主 shell。 */ async function resolveInWorkspace(input: unknown, ctx: ToolContext): Promise { const raw = typeof input === 'string' && input.trim() ? input.trim() : '.' - const path = await resolveSandboxPath(ctx.workspaceRoot, raw, ctx.permissionMode === 'full-access') - if (ctx.permissionMode !== 'full-access' && sandboxPathDenied(await effectiveSandboxPolicy(ctx.workspaceRoot), path)) throw new ToolError('沙盒策略不允许访问此路径') - return path + if (raw.includes('\0')) throw new ToolError('文件路径无效') + const root = await fs.realpath(ctx.workspaceRoot) + const target = resolve(root, raw) + if (!within(root, target)) throw new ToolError('文件路径必须位于当前工作区内') + return target } const listDir: Tool = { @@ -134,23 +133,8 @@ const readFile: Tool = { } } -async function commitApprovedWrite(ctx: ToolContext, description: string, changes: FileChange[]): Promise { - assertActive(ctx.signal) - const policy = await effectiveSandboxPolicy(ctx.workspaceRoot) - for (const change of changes) { - if (sandboxPathDenied(policy, change.path) || change.after && change.after.byteLength > policy.limits.fileBytes) throw new ToolError('文件写入违反沙盒路径或容量策略') - } - if (changes.length > policy.maxChanges) throw new ToolError('文件变更超过沙盒批次上限') - const workspaceIdentity = (await NativeWorkspaceReader.create(await verifiedBinary(), policy, ctx.signal)).rootIdentity - const targetDigest = fileChangeDigest(changes) - const authorization = await authorizeSandboxWrite(policy, description, ctx, targetDigest) - if (!authorization) throw new ToolError('用户未批准此操作') - return commitChanges(ctx.workspaceRoot, changes, { runId: ctx.runId, toolCallId: ctx.toolCallId, sessionId: ctx.sessionId, policyRevision: policy.revision, policyFingerprint: policy.fingerprint, authorizationId: authorization.id }, ctx.signal, - { beforeApply: () => authorization.consume(fileChangeDigest(changes)), check: () => authorization.assertValid(targetDigest), workspaceIdentity }) -} - const writeFile: Tool = { - name: 'write_file', description: '创建或更新文本文件;默认权限下会请求用户确认。', + name: 'write_file', description: '在当前工作区创建或更新文本文件。', parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] }, async run(args, ctx) { if (typeof args.path !== 'string' || typeof args.content !== 'string') throw new ToolError('文件参数无效') @@ -160,20 +144,17 @@ const writeFile: Tool = { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null throw error }) - let transactionId: string | undefined - if (ctx.permissionMode === 'full-access') { - assertActive(ctx.signal) - await fs.mkdir(dirname(path), { recursive: true }) - path = await resolveInWorkspace(inputPath, ctx) - const current = await fs.readFile(path, 'utf8').catch(error => { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; throw error }) - if (current !== oldContent) throw new ToolError('文件在写入前已变化,请重新读取后写入') - await writeTextAtomically(path, content) - } else transactionId = await commitApprovedWrite(ctx, `写入文件 ${inputPath}`, [{ path, beforeHash: oldContent === null ? null : digest(Buffer.from(oldContent)), after: Buffer.from(content) }]) - return { ok: true, content: `已写入 ${inputPath}`, data: { path, oldContent, newContent: content, ...(transactionId ? { transactionId } : {}) } } + assertActive(ctx.signal) + await fs.mkdir(dirname(path), { recursive: true }) + path = await resolveInWorkspace(inputPath, ctx) + const current = await fs.readFile(path, 'utf8').catch(error => { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; throw error }) + if (current !== oldContent) throw new ToolError('文件在写入前已变化,请重新读取后写入') + await writeTextAtomically(path, content) + return { ok: true, content: `已写入 ${inputPath}`, data: { path, oldContent, newContent: content } } } } const editFile: Tool = { - name: 'edit_file', description: '精确替换文本文件中的唯一匹配内容;默认权限下会请求用户确认。', + name: 'edit_file', description: '精确替换当前工作区文本文件中的唯一匹配内容。', parameters: { type: 'object', properties: { path: { type: 'string' }, oldText: { type: 'string' }, newText: { type: 'string' } }, required: ['path', 'oldText', 'newText'] }, async run(args, ctx) { if (typeof args.path !== 'string' || typeof args.oldText !== 'string' || !args.oldText || typeof args.newText !== 'string') throw new ToolError('编辑参数无效') @@ -181,30 +162,19 @@ const editFile: Tool = { const oldContent = await fs.readFile(path, 'utf8') if (oldContent.split(args.oldText).length !== 2) throw new ToolError('待替换内容必须唯一匹配') const newContent = oldContent.replace(args.oldText, args.newText) - let transactionId: string | undefined - if (ctx.permissionMode === 'full-access') { - assertActive(ctx.signal) - path = await resolveInWorkspace(args.path, ctx) - if (await fs.readFile(path, 'utf8') !== oldContent) throw new ToolError('文件在编辑前已变化,请重新读取后编辑') - await writeTextAtomically(path, newContent) - } - else transactionId = await commitApprovedWrite(ctx, `编辑文件 ${args.path}`, [{ path, beforeHash: digest(Buffer.from(oldContent)), after: Buffer.from(newContent) }]) - return { ok: true, content: `已编辑 ${args.path}`, data: { path, oldContent, newContent, ...(transactionId ? { transactionId } : {}) } } + assertActive(ctx.signal) + path = await resolveInWorkspace(args.path, ctx) + if (await fs.readFile(path, 'utf8') !== oldContent) throw new ToolError('文件在编辑前已变化,请重新读取后编辑') + await writeTextAtomically(path, newContent) + return { ok: true, content: `已编辑 ${args.path}`, data: { path, oldContent, newContent } } } } const command: Tool = { - name: 'execute_command', description: '执行 Windows cmd 命令。默认权限使用禁网隔离副本,变更经批准写回。runtimes 选择 Node/npm、Python/pip、Git;Git 默认随应用提供,Node/Python 需维护者导入,不继承宿主 PATH。未导入仓库时 Git 基线是过滤快照,不表示宿主历史/索引;真实 Git 工作流优先用 sandbox_git:import 审批导入、run 接 argv、commit 审批写回文件和 Git,远程操作单独审批。导入或 init/clone 后,本会话 Git 调用默认进入长期镜像;显式 snapshot 使用独立快照。workspace=persistent 跨命令保留草稿,成功后不自动写宿主,用 sandbox_workspace 或 sandbox_git 查看/提交/丢弃。普通文件工具仍作用宿主。敏感文件、宿主 .git、node_modules、链接不进入普通文件副本,使用相对路径;Node 同步子进程捕获可用 node --require "%YCODE_SANDBOX_PROCESS%" script.cjs。完全访问且未显式选工作区时用普通 shell;显式 workspace 始终使用沙盒。', - parameters: { type: 'object', properties: { command: { type: 'string' }, workspace: { type: 'string', enum: ['snapshot', 'persistent'], description: 'snapshot 为每命令副本并审批写回;persistent 为当前会话的长期草稿,不自动写宿主,用 sandbox_workspace commit 提交。' }, runtimes: { type: 'array', items: { type: 'string', enum: ['node', 'python', 'git'] }, description: '所需沙盒运行时;git 默认随应用提供,无需注册,对应沙盒专用仓库(过滤快照基线,无宿主历史/索引);Node/Python 需先导入。省略时仅提供系统 shell。' }, dependencies: { type: 'array', items: { type: 'string', enum: ['node', 'python'] }, description: '显式使用已为本工作区导入的只读依赖缓存,锁文件变化后须重新导入。' }, bridge: { type: 'string', enum: ['workspace-read'], description: '可选的工作区只读文件桥(需 node);require(process.env.YCODE_SANDBOX_BRIDGE).connectSync() 提供 readFileSync/statSync/readdirSync;异步用 connect(),一个 Job 使用一个客户端。写入仍通过副本和审批。' } }, required: ['command'] }, + name: 'execute_command', description: '在当前工作目录直接执行 Windows shell 命令。使用宿主环境、Git 历史、已安装工具和正常网络。', + parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] }, async run(args, ctx) { if (typeof args.command !== 'string' || !args.command.trim()) throw new ToolError('命令为空') assertActive(ctx.signal) - if (args.workspace !== undefined && args.workspace !== 'snapshot' && args.workspace !== 'persistent') throw new ToolError('沙盒工作区参数无效') - if (ctx.permissionMode !== 'full-access' || args.workspace !== undefined) { - if (args.runtimes !== undefined && (!Array.isArray(args.runtimes) || args.runtimes.some(value => typeof value !== 'string'))) throw new ToolError('沙盒运行时参数无效') - if (args.dependencies !== undefined && (!Array.isArray(args.dependencies) || args.dependencies.some(value => typeof value !== 'string'))) throw new ToolError('沙盒依赖参数无效') - if (args.bridge !== undefined && args.bridge !== 'workspace-read') throw new ToolError('沙盒文件桥参数无效') - return executeSandboxCommand(args.command, { ...ctx, runtimes: args.runtimes as string[] | undefined, dependencies: args.dependencies as string[] | undefined, readBridge: args.bridge === 'workspace-read', workspaceMode: args.workspace as 'snapshot' | 'persistent' | undefined }) - } return new Promise((resolveResult, reject) => { const child = spawn(args.command as string, { shell: true, cwd: ctx.workspaceRoot, windowsHide: true, detached: process.platform !== 'win32' }) const onAbort = (): void => { @@ -231,7 +201,7 @@ const askQuestion: Tool = { name: 'ask_user_question', description: '暂停并 const followupQuestion: Tool = { ...askQuestion, name: 'ask_followup_question', parameters: { type: 'object', properties: { question: { type: 'string' }, follow_up: { type: 'array', items: { type: 'string' } } }, required: ['question'] } } const plan: Tool = { name: 'todo_write', description: '创建或更新任务计划和步骤进度。', parameters: { type: 'object', properties: { title: { type: 'string' }, todos: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, content: { type: 'string' }, status: { type: 'string', enum: ['pending', 'in_progress', 'completed', 'cancelled'] } }, required: ['content'] } } }, required: ['todos'] }, async run() { return { ok: false, content: '计划宿主未连接' } } } -export const ALL_TOOLS: Tool[] = [listDir, readFile, writeFile, editFile, command, sandboxWorkspaceTool, sandboxGitTool, askQuestion, followupQuestion, plan, browserTool] +export const ALL_TOOLS: Tool[] = [listDir, readFile, writeFile, editFile, command, askQuestion, followupQuestion, plan, browserTool] export function toolSpecs(): ToolSpec[] { return ALL_TOOLS.map((t) => ({ diff --git a/src/main/automations/host.ts b/src/main/automations/host.ts index 8572e96..ba5bbd5 100644 --- a/src/main/automations/host.ts +++ b/src/main/automations/host.ts @@ -33,7 +33,7 @@ export function createAutomationHost(options: AutomationHostOptions) { return { status: 'cancelled', error: '执行已取消' } } const started = await options.chatTurns.startTurn({ - sessionId, text: definition.prompt, permissionMode: definition.execution.permissionMode ?? 'default-sandbox' + sessionId, text: definition.prompt }) chatRunId = started.runId completion = started.completion diff --git a/src/main/browser-automation.ts b/src/main/browser-automation.ts index 06cf286..dfe487c 100644 --- a/src/main/browser-automation.ts +++ b/src/main/browser-automation.ts @@ -96,10 +96,7 @@ function owned(args: Record, key: string): ManagedBrowser { if (!browser || browser.scope !== key || browser.window.isDestroyed()) throw new Error('浏览器标签不存在、不属于当前会话,或存在多个标签;请先 list 并指定 tabId') return browser } -async function approve(action: string, args: Record, ctx: ToolContext): Promise { - if (ctx.permissionMode === 'full-access' || !['open', 'navigate', 'click', 'type', 'press_key'].includes(action)) return - const target = action === 'open' || action === 'navigate' ? url(args.url) : String(args.ref || args.selector || args.key || '') - if (!ctx.approve || !await ctx.approve(`浏览器 ${action}:${target}`, ctx.signal)) throw new Error('浏览器操作未获用户批准') +async function approve(_action: string, _args: Record, ctx: ToolContext): Promise { active(ctx) } function create(key: string, ctx: ToolContext): ManagedBrowser { diff --git a/src/main/capabilities/runtime.ts b/src/main/capabilities/runtime.ts index 9542f7b..0496b47 100644 --- a/src/main/capabilities/runtime.ts +++ b/src/main/capabilities/runtime.ts @@ -80,15 +80,6 @@ export async function createCapabilitySnapshot(ids: string[], cwd: string, optio return { ok: true, content } } if (!names.has(name)) return undefined - if (ctx.permissionMode !== 'full-access') { - if (!ctx.interactions) return { ok: false, content: 'MCP 调用需要用户确认' } - const identity = mcp.describe(name) - const decision = await ctx.interactions.approval(ctx.toolCallId, identity ? `调用 ${identity.serverName} 的 ${identity.toolName} 工具` : `调用连接器工具 ${name}`, [ - { optionId: 'allow-once', name: '允许一次', kind: 'allow_once' }, - { optionId: 'reject-once', name: '拒绝', kind: 'reject_once' } - ]) - if (decision.optionId !== 'allow-once') return { ok: false, content: '用户拒绝了连接器调用' } - } const result = await mcp.execute(name, args, ctx.signal) return { ...result, metadata: { capability: 'mcp', ...mcp.describe(name) } } } diff --git a/src/main/chat-turn-service.ts b/src/main/chat-turn-service.ts index b8a1c88..c3ae0aa 100644 --- a/src/main/chat-turn-service.ts +++ b/src/main/chat-turn-service.ts @@ -428,7 +428,7 @@ export function createChatTurnService({ projects, sessionRoot, broadcastChat, ge signal: controller.signal, emit, capabilities: capabilitySnapshot, onUsage: reported => { childUsage = addUsage(childUsage, reported); publishReportedUsage() }, worker: (task, tools) => createSubagentWorker({ task, tools, model: selectedModel, workspaceRoot, parentRunId: runId, parentSessionId: sessionId, - native: !!kernel, permissionMode: payload.permissionMode ?? 'default-sandbox', + native: !!kernel, thinkingLevel: modelThinkingLevel(selectedModel.profile, context.thinkingLevel), emitParent: emit }) }) const teamScheduler = scheduler @@ -483,7 +483,6 @@ export function createChatTurnService({ projects, sessionRoot, broadcastChat, ge userText: promptText, images: promptAttachments.images, workspaceRoot, - permissionMode: payload.permissionMode ?? 'default-sandbox', interactions: interactionBroker, signal: controller.signal, emit @@ -518,7 +517,6 @@ export function createChatTurnService({ projects, sessionRoot, broadcastChat, ge userText: promptText, images: promptAttachments.images, interactions: interactionBroker, - permissionMode: payload.permissionMode ?? 'default-sandbox', signal: controller.signal, emit, gateway: new Gateway({ ...snapshot, thinkingLevel: modelThinkingLevel(selectedModel.profile, context.thinkingLevel) },body=>{ diff --git a/src/main/index.ts b/src/main/index.ts index dcbe3bc..15fa516 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -9,21 +9,10 @@ import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { disposeOmpKernels, registerIpc, setTaskWindowFactory, startBackgroundServices, stopBackgroundServices } from './ipc' import { disposeMcpServers } from './capabilities/mcp' -import { getPreventSleep, getWorkspaceRoot } from './config' +import { getPreventSleep } from './config' import { configurePreventSleep } from './general-settings' -import { configureSandbox } from './sandbox/configuration' -import { disposeSandboxCommands, sandboxStatus } from './sandbox/service' -import { readSandboxPolicy, updateSandboxPolicy } from './sandbox/policy-store' -import { runSandboxSelfTest } from './sandbox/self-test' -import { sandboxOperation, sandboxBrokerOperation } from './sandbox/maintenance' -import type { SandboxPolicyUpdate, SandboxSelfTestSummary } from '@shared/sandbox' import { executeBrowserTool, installBrowserAutomationLifecycle } from './browser-automation' import { configureBrowserTool } from './agent/tools/browser' -import { sandboxWorkspaceStatus } from './sandbox/workspace-service' -import { hostGitStatus } from './sandbox/git-host-service' -import { gitRemoteOperation } from './sandbox/git-remote' -import { disposeGitControls } from './sandbox/git-control' -import { getSessionContext } from './sessions' let mainWindow: BrowserWindow | null = null let settingsWindow: BrowserWindow | null = null @@ -37,24 +26,6 @@ let settingsRequestId = 0 let allowQuit = false let shutdownPromise: Promise | null = null const mainDirectory = dirname(fileURLToPath(import.meta.url)) -function sandboxSender(event: Electron.IpcMainInvokeEvent): void { - const owned = [mainWindow, settingsWindow, ...taskWindows.values()].some(window => window && !window.isDestroyed() && window.webContents === event.sender) - if (!owned || event.senderFrame !== event.sender.mainFrame) throw new Error('沙盒设置请求不属于应用主页面') - const url = new URL(event.senderFrame.url) - const renderer = process.env.ELECTRON_RENDERER_URL - const entry = settingsWindow?.webContents === event.sender ? 'settings.html' : 'index.html' - if (renderer && !app.isPackaged) { - const expected = entry === 'settings.html' ? new URL(entry, renderer) : new URL(renderer) - if (url.origin === expected.origin && url.pathname === expected.pathname) return - } else if (url.protocol === 'file:' && fileURLToPath(url).toLowerCase() === join(mainDirectory, '../renderer', entry).toLowerCase()) return - throw new Error('沙盒设置请求来自未授权页面') -} -function sandboxChanged(): void { - for (const window of [mainWindow, settingsWindow, ...taskWindows.values()]) { - if (!window || window.isDestroyed() || window.webContents.isDestroyed()) continue - try { window.webContents.send('sandbox:changed') } catch { /* A closing window can read the persisted policy when reopened. */ } - } -} /** * 应用图标路径。 @@ -310,45 +281,6 @@ if (process.env.YCODE_DISABLE_GPU === '1') { app.whenReady().then(() => { configureBrowserTool(executeBrowserTool) installBrowserAutomationLifecycle() - configureSandbox({ stateRoot: join(app.getPath('userData'), 'sandbox'), resourceDirectory: app.isPackaged - ? join(process.resourcesPath, 'sandbox', 'win32-x64') : join(mainDirectory, '../../resources/sandbox/win32-x64') }) - app.on('before-quit', disposeSandboxCommands) - app.on('before-quit', disposeGitControls) - ipcMain.handle('sandbox:status', sandboxStatus) - ipcMain.handle('sandbox:policy', (event) => { sandboxSender(event); return readSandboxPolicy() }) - ipcMain.handle('sandbox:operation', (event, id: string) => { sandboxSender(event); return sandboxOperation(id) }) - ipcMain.handle('sandbox:broker-operation', (event, runId: string, requestId: string) => { sandboxSender(event); return sandboxBrokerOperation(runId, requestId) }) - ipcMain.handle('sandbox:workspace-status', async (event, sessionId: unknown) => { - sandboxSender(event) - if (typeof sessionId !== 'string' || !/^[a-f0-9-]{36}$/.test(sessionId)) throw new Error('长期工作区会话身份无效') - const context = await getSessionContext(sessionId) - return sandboxWorkspaceStatus(context.cwd ?? await getWorkspaceRoot(), sessionId) - }) - ipcMain.handle('sandbox:git-status', async (event, sessionId: unknown) => { - sandboxSender(event) - if (typeof sessionId !== 'string' || !/^[a-f0-9-]{36}$/.test(sessionId)) throw new Error('Git 会话身份无效') - const context = await getSessionContext(sessionId) - return hostGitStatus(context.cwd ?? await getWorkspaceRoot(), sessionId) - }) - ipcMain.handle('sandbox:git-remote-operation', async (event, sessionId: unknown, operationId: unknown) => { - sandboxSender(event) - if (typeof sessionId !== 'string' || !/^[a-f0-9-]{36}$/.test(sessionId) || typeof operationId !== 'string') throw new Error('Git 远程结果参数无效') - const context = await getSessionContext(sessionId) - return gitRemoteOperation({ workspaceRoot: context.cwd ?? await getWorkspaceRoot(), sessionId }, operationId) - }) - ipcMain.handle('sandbox:policy:update', async (event, update: SandboxPolicyUpdate) => { - sandboxSender(event) - if (!update || Object.hasOwn(update, 'workspaceRoot')) throw new Error('应用设置仅接受全局策略;工作区策略由所属会话解析') - const result = await updateSandboxPolicy(update) - disposeSandboxCommands(); disposeGitControls(); sandboxChanged() - return result - }) - let selfTest: Promise | undefined - ipcMain.handle('sandbox:self-test', (event) => { - sandboxSender(event) - selfTest ??= runSandboxSelfTest().finally(() => { selfTest = undefined; sandboxChanged() }) - return selfTest - }) // 通用设置:启动时套用持久化偏好(运行任务时阻止系统休眠)。 void getPreventSleep().then((enabled) => configurePreventSleep(enabled)).catch(() => undefined) registerIpc(() => mainWindow) diff --git a/src/main/subagents/scheduler.ts b/src/main/subagents/scheduler.ts index 6a60d47..dad23d6 100644 --- a/src/main/subagents/scheduler.ts +++ b/src/main/subagents/scheduler.ts @@ -1,12 +1,12 @@ import { randomUUID } from 'node:crypto' -import { isAbsolute, relative, sep } from 'node:path' +import { isAbsolute, relative, resolve, sep } from 'node:path' import { stat } from 'node:fs/promises' +import { realpath } from 'node:fs/promises' import type { ChatEvent, ToolCallRecord, TokenUsage } from '@shared/types' import type { SubagentSnapshot } from '@shared/subagents' import { addUsage, zeroUsage } from '@shared/subagents' import type { CapabilityToolSnapshot } from '../capabilities/runtime' import type { ConversationToolContext, ConversationToolResult } from '../agent/conversation-tools' -import { resolveSandboxPath } from '../sandbox/policy' import { SUBAGENT_TOOLS, SUBAGENT_TOOL_NAMES } from './tools' export interface SubagentWorker { @@ -45,6 +45,13 @@ const WRITE_TOOLS = new Set(['write_file', 'edit_file']) const READ_TOOLS = new Set(['list_dir', 'read_file', 'ycode_load_skill', 'ycode_read_skill_resource', 'ycode_expert_resource']) const normalize = (path: string): string => process.platform === 'win32' ? path.toLowerCase() : path const overlaps = (left: string, right: string): boolean => left === right || left.startsWith(right + sep) || right.startsWith(left + sep) +async function resolveWorkspacePath(rootInput: string, input: string): Promise { + const root = await realpath(rootInput) + const target = resolve(root, input.trim() || '.') + const value = relative(root, target) + if (value === '..' || value.startsWith(`..${sep}`) || isAbsolute(value)) throw new Error('路径必须位于当前工作区内') + return target +} const aborted = (): Error => Object.assign(new Error('子智能体任务已取消'), { name: 'AbortError' }) const text = (value: unknown, max: number, optional = false): string => { if (optional && value === undefined) return '' @@ -126,12 +133,12 @@ export class SubagentScheduler { } private async canonicalFiles(input: unknown): Promise<{ files: string[]; paths: string[] }> { if (!Array.isArray(input) || !input.length || input.length > 32) throw new Error('write 模式需要声明1至32个明确的文件路径') - const root = await resolveSandboxPath(this.options.workspaceRoot, '.', true) + const root = await realpath(this.options.workspaceRoot) const paths: string[] = [], files: string[] = [] for (const raw of input) { const file = text(raw, 1500) if (isAbsolute(file) || /[*?\0]/.test(file)) throw new Error('可写文件必须是工作区相对路径,不能使用通配符') - const path = await resolveSandboxPath(root, file, true) + const path = await resolveWorkspacePath(root, file) const rel = relative(root, path) if (!rel || rel === '..' || rel.startsWith('..' + sep) || isAbsolute(rel)) throw new Error('可写文件不能越出工作区') const info = await stat(path).catch(error => { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; return undefined }) @@ -143,7 +150,7 @@ export class SubagentScheduler { private async rootExecute(name: string, args: Record, execute: () => Promise): Promise { if (SUBAGENT_TOOL_NAMES.has(name) || READ_TOOLS.has(name) || ['browser', 'todo_write', 'create_plan', 'ask_user_question', 'ask_followup_question'].includes(name)) return execute() let path: string - try { path = normalize(await resolveSandboxPath(this.options.workspaceRoot, WRITE_TOOLS.has(name) ? String(args.path ?? '') : '.', true)) } + try { path = normalize(await resolveWorkspacePath(this.options.workspaceRoot, WRITE_TOOLS.has(name) ? String(args.path ?? '') : '.')) } catch (error) { return { ok: false, content: error instanceof Error ? error.message : String(error) } } if ([...leases.keys()].some(owned => overlaps(owned, path))) return { ok: false, content: '相关文件正由其他任务修改,请等待子任务或当前文件操作结束后再执行。' } const owner = randomUUID() @@ -161,7 +168,7 @@ export class SubagentScheduler { if (entry.controller.signal.aborted || this.options.signal.aborted) throw aborted() if (!allowed(name) || SUBAGENT_TOOL_NAMES.has(name)) return { ok: false, content: '子智能体仅允许读取和在声明范围内编辑文件;命令、连接器、浏览器及嵌套调度由主智能体处理。' } if (WRITE_TOOLS.has(name)) { - const path = normalize(await resolveSandboxPath(this.options.workspaceRoot, String(args.path ?? ''), true)) + const path = normalize(await resolveWorkspacePath(this.options.workspaceRoot, String(args.path ?? ''))) if (!entry.paths.includes(path) || leases.get(path) !== entry.view.id) return { ok: false, content: '该文件不在当前子智能体的可写范围内。' } } return this.options.capabilities.execute(name, args, ctx) diff --git a/src/main/subagents/worker.ts b/src/main/subagents/worker.ts index 0533855..a21a847 100644 --- a/src/main/subagents/worker.ts +++ b/src/main/subagents/worker.ts @@ -4,7 +4,6 @@ import { join } from 'node:path' import { randomUUID } from 'node:crypto' import type { ResolvedModel } from '@shared/models' import type { ChatEvent, ChatMessage } from '@shared/types' -import type { PermissionMode } from '@shared/conversation' import type { SubagentSnapshot } from '@shared/subagents' import type { ThinkingLevel } from '@shared/workspaces' import { applyAssistantEvent } from '@shared/message-parts' @@ -23,7 +22,7 @@ import type { ConversationToolResult } from '../agent/conversation-tools' export async function createSubagentWorker(options: { task: SubagentSnapshot; tools: CapabilityToolSnapshot; model: ResolvedModel; workspaceRoot: string - parentRunId: string; permissionMode: PermissionMode; native: boolean; thinkingLevel?: ThinkingLevel + parentRunId: string; native: boolean; thinkingLevel?: ThinkingLevel parentSessionId?: string emitParent(event: ChatEvent): void }): Promise { @@ -82,7 +81,7 @@ export async function createSubagentWorker(options: { const observe = (event: ChatEvent): void => { assistant = applyAssistantEvent(assistant, event); emit(event) } activeEmit = observe const userText = role + '\n\n' + prompt - const common = { runId, sessionId, userText, signal, emit: observe, workspaceRoot, permissionMode: options.permissionMode, + const common = { runId, sessionId, userText, signal, emit: observe, workspaceRoot, agentMode: 'craft' as const, capabilityTools: trackedTools, supportsToolCall: model.profile.supportsToolCall, supportsImages: model.profile.supportsImages === true, interactions } let result: Awaited> & { status?: string } diff --git a/src/preload/index.ts b/src/preload/index.ts index c5b5937..62893d6 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -91,20 +91,6 @@ const api: YCodeApi = { }, app: { info: () => ipcRenderer.invoke('app:info'), - sandboxStatus: () => ipcRenderer.invoke('sandbox:status'), - sandboxPolicy: () => ipcRenderer.invoke('sandbox:policy'), - setSandboxPolicy: (update) => ipcRenderer.invoke('sandbox:policy:update', update), - sandboxSelfTest: () => ipcRenderer.invoke('sandbox:self-test'), - sandboxOperation: (id) => ipcRenderer.invoke('sandbox:operation', id), - sandboxBrokerOperation: (runId, requestId) => ipcRenderer.invoke('sandbox:broker-operation', runId, requestId), - sandboxWorkspace: (sessionId) => ipcRenderer.invoke('sandbox:workspace-status', sessionId), - sandboxGit: (sessionId) => ipcRenderer.invoke('sandbox:git-status', sessionId), - sandboxGitRemoteOperation: (sessionId, operationId) => ipcRenderer.invoke('sandbox:git-remote-operation', sessionId, operationId), - onSandboxChanged: (listener) => { - const handler = (): void => listener() - ipcRenderer.on('sandbox:changed', handler) - return () => ipcRenderer.removeListener('sandbox:changed', handler) - }, openConfigDir: () => ipcRenderer.invoke('app:openConfigDir'), openSettings: (section, action) => ipcRenderer.invoke('app:openSettings', section, action), settingsReady: () => ipcRenderer.invoke('app:settingsReady'), diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 8d58b58..179419e 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -6,7 +6,6 @@ import ProjectListPage from './projects/ProjectListPage' import LibraryPage from './library/LibraryPage' import AutomationPage from './automations/AutomationPage' import type { ProjectRecord } from '@shared/projects' -import type { PermissionMode } from '@shared/conversation' import type { ThinkingLevel } from '@shared/workspaces' import type { LibraryArtifactEntry } from '@shared/library' import { type SkillRecord } from '@shared/capabilities' @@ -86,7 +85,7 @@ export default function App(): React.JSX.Element { setLeftDrawerOpen(false) } const startProjectTask = async (project: ProjectRecord, prompt: string, - options?: { permissionMode?: PermissionMode; thinkingLevel?: ThinkingLevel }): Promise => { + options?: { thinkingLevel?: ThinkingLevel }): Promise => { useChatStore.getState().startTask({ projectId: project.projectId, workspaceId: project.workspaceId, modelId: useSettingsStore.getState().selectedModelId ?? undefined, ...(options?.thinkingLevel ? { thinkingLevel: options.thinkingLevel } : {}) }) @@ -96,7 +95,7 @@ export default function App(): React.JSX.Element { if (state.draftId === draft && state.currentSessionId && !createdOwner) createdOwner = state.currentSessionId }) setRoute('chat') - try { await useChatStore.getState().send(prompt, { permissionMode: options?.permissionMode }) } + try { await useChatStore.getState().send(prompt) } catch (error) { const state = useChatStore.getState() if (state.draftId === draft && (!state.currentSessionId || state.currentSessionId === createdOwner)) { diff --git a/src/renderer/src/automations/AutomationEditor.tsx b/src/renderer/src/automations/AutomationEditor.tsx index 05a4fe3..62ed414 100644 --- a/src/renderer/src/automations/AutomationEditor.tsx +++ b/src/renderer/src/automations/AutomationEditor.tsx @@ -1,19 +1,16 @@ -import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' +import { useEffect, useMemo, useState, type ReactNode } from 'react' import type { AutomationApi } from '@shared/automation-api' import type { ProjectRecord } from '@shared/projects' import { IconClose, - IconPlus, - IconShield + IconPlus } from '../components/icons' import SourceSelect from '../components/source-select/SourceSelect' import { useSettingsStore } from '../stores/settings' import SourceModelSelector from '../workbuddy/composer/model-selector/SourceModelSelector' import AutomationScheduleFields from './AutomationScheduleFields' import { - AutomationFolderOpenIcon, - AutomationGlobeIcon, - AutomationTerminalIcon + AutomationFolderOpenIcon } from './automation-source-icons' import { automationFormErrors, @@ -50,15 +47,6 @@ function useDarkMode(): boolean { return dark } -export function shouldConfirmAutomationFullAccess( - mode: 'create' | 'edit', - initialPermissionMode: AutomationFormState['permissionMode'], - finalPermissionMode: AutomationFormState['permissionMode'] -): boolean { - return finalPermissionMode === 'full-access' && - (mode === 'create' || initialPermissionMode !== 'full-access') -} - function CompactSelect({ ariaLabel, label, @@ -101,150 +89,6 @@ function CompactSelect({ ) } -function FullAccessConfirmDialog({ - submitting, - confirmed, - onConfirmedChange, - onCancel, - onConfirm -}: { - submitting: boolean - confirmed: boolean - onConfirmedChange(value: boolean): void - onCancel(): void - onConfirm(): void -}): React.JSX.Element { - const dialogRef = useRef(null) - - useEffect(() => { - const previousFocus = document.activeElement instanceof HTMLElement - ? document.activeElement - : null - const focusFrame = window.requestAnimationFrame(() => { - dialogRef.current - ?.querySelector('input:not(:disabled), button:not(:disabled)') - ?.focus() - }) - const onKeyDown = (event: KeyboardEvent): void => { - if (event.key === 'Escape' && !submitting) { - event.preventDefault() - event.stopPropagation() - onCancel() - return - } - if (event.key !== 'Tab') return - const focusable = [...(dialogRef.current?.querySelectorAll( - 'button:not(:disabled), input:not(:disabled), [tabindex]:not([tabindex="-1"])' - ) ?? [])] - if (focusable.length === 0) return - const first = focusable[0]! - const last = focusable[focusable.length - 1]! - if (event.shiftKey && document.activeElement === first) { - event.preventDefault() - last.focus() - } else if (!event.shiftKey && document.activeElement === last) { - event.preventDefault() - first.focus() - } - } - window.addEventListener('keydown', onKeyDown, true) - return () => { - window.cancelAnimationFrame(focusFrame) - window.removeEventListener('keydown', onKeyDown, true) - previousFocus?.focus() - } - }, [onCancel, submitting]) - - return ( -
{ - if (event.target === event.currentTarget && !submitting) onCancel() - }} - > -
-
- -

确认允许完全访问?

- -
-
-

- 开启完全访问后,该定时任务运行时可减少确认步骤并直接操作您的电脑,请谨慎操作。包括以下内容: -

-
-
- - - 文件操作 - 读取、创建、修改、上传或删除此计算机上任意位置的文件 - -
-
- - - 终端命令 - 运行命令、安装软件和更改系统设置 - -
-
- - - 访问互联网 - 访问网站、发送数据并使用已启用的连接器 - -
-
- -
-
- - -
-
-
- ) -} - export default function AutomationEditor({ api, initialForm, @@ -262,8 +106,6 @@ export default function AutomationEditor({ const [advancedOpen, setAdvancedOpen] = useState(false) const [saving, setSaving] = useState(false) const [saveError, setSaveError] = useState('') - const [confirmOpen, setConfirmOpen] = useState(false) - const [confirmChecked, setConfirmChecked] = useState(false) const errors = useMemo(() => automationFormErrors(form), [form]) const visibleErrors = showErrors ? errors : {} const hasErrors = Object.keys(errors).length > 0 @@ -310,20 +152,6 @@ export default function AutomationEditor({ const submit = (): void => { setShowErrors(true) if (saving || hasErrors) return - if (shouldConfirmAutomationFullAccess( - editing ? 'edit' : 'create', - initialForm.permissionMode, - form.permissionMode - )) { - setConfirmChecked(false) - setConfirmOpen(true) - return - } - void persist() - } - - const confirmFullAccess = (): void => { - if (!confirmChecked || saving) return void persist() } @@ -427,22 +255,6 @@ export default function AutomationEditor({ disabled={saving || Boolean(form.projectId)} onChange={workspaceId => update({ workspaceId })} /> - } - value={form.permissionMode} - options={[ - { value: 'default-sandbox', label: '默认权限' }, - { value: 'full-access', label: '允许完全访问' } - ]} - disabled={saving} - onChange={permissionMode => update({ - permissionMode: permissionMode as AutomationFormState['permissionMode'] - })} - /> @@ -512,23 +324,12 @@ export default function AutomationEditor({ disabled={saving || requiredIncomplete} onClick={submit} > - {saving && !confirmOpen ? '保存中…' : '确定'} + {saving ? '保存中…' : '确定'} - {confirmOpen ? ( - { - if (!saving) setConfirmOpen(false) - }} - onConfirm={confirmFullAccess} - /> - ) : null} ) } diff --git a/src/renderer/src/components/ChatPane.tsx b/src/renderer/src/components/ChatPane.tsx index 340d8a3..54d4e47 100644 --- a/src/renderer/src/components/ChatPane.tsx +++ b/src/renderer/src/components/ChatPane.tsx @@ -20,7 +20,7 @@ import SourceSkillPicker from '../capabilities/SourceSkillPicker' import { ComposerControls, type ComposerSendPayload } from '../workbuddy/composer/ComposerControls' import { ComposerBranchLabel, ContextUsageRing } from '../workbuddy/composer/context-info/ComposerContextInfo' import { QuestionFloating } from '../workbuddy/conversation' -import type { AgentMode, ChatAttachment, MessageQuote, PermissionMode, UserContentBlock } from '@shared/conversation' +import type { AgentMode, ChatAttachment, MessageQuote, UserContentBlock } from '@shared/conversation' import type { UserMessageEditContent } from '../workbuddy/conversation/UserMessageEditor' import type { ChatMessage } from '@shared/types' import { getRevertChanges, type RevertChange } from '@shared/revert' @@ -113,8 +113,6 @@ export default function ChatPane({ rightVisible, onToggleRight, onOpenSettings, return () => { window.removeEventListener('ycode:select-skill', selectSkill); window.removeEventListener('ycode:composer-intent', intent); window.removeEventListener('ycode:composer-owner-migrated', migrate); window.removeEventListener('ycode:expert-input', expertInput) } }, []) useEffect(() => { setSkillPicker(null); setExpertPicker(null) }, [draftKey, hidden]) - const [permissionMode, setPermissionMode] = useState('default-sandbox') - const [confirmFullAccess, setConfirmFullAccess] = useState(true) const [editingMessageId, setEditingMessageId] = useState(null) const [rewindTarget, setRewindTarget] = useState(null) const [rewindError, setRewindError] = useState(null) @@ -238,7 +236,7 @@ export default function ChatPane({ rightVisible, onToggleRight, onOpenSettings, if (el) el.scrollTop = el.scrollHeight }, [currentId]) - const submit = async ({ text, attachments, resources, quotes, userContent, mode }: ComposerSendPayload): Promise => { + const submit = async ({ text, attachments, resources, quotes, userContent }: ComposerSendPayload): Promise => { if ((!text && !attachments.length && !quotes.length && !resources.length && !entry.skills?.length) || running || sending) return followBottom.current = true // 原版发送即清空输入框并立即切到对话页:先清空草稿,失败时再恢复(TODO 08)。 @@ -246,7 +244,7 @@ export default function ChatPane({ rightVisible, onToggleRight, onOpenSettings, const restore = { ...entry, text, attachments, resources, quotes, userContent } setDrafts((all) => ({ ...all, [submitKey]: { text: '', attachments: [] } })) try { - await send(text, { attachments, resources, quotes, userContent, skillIds: entry.skills?.map(item => item.id), permissionMode: mode }) + await send(text, { attachments, resources, quotes, userContent, skillIds: entry.skills?.map(item => item.id) }) // 会话创建后草稿所有权已迁移到会话 id;两处都清空,避免留下旧文本。 const destination = draftMoves.current.get(submitKey) ?? submitKey draftMoves.current.delete(submitKey) @@ -293,12 +291,12 @@ export default function ChatPane({ rightVisible, onToggleRight, onOpenSettings, } if (!owner || useChatStore.getState().currentSessionId !== owner) return await send(text, { attachments: edited?.attachments ?? message.attachments, resources: edited?.resources ?? message.resources, quotes: edited?.quotes ?? message.quotes, - userContent: edited?.userContent ?? message.userContent, skillIds: edited?.skillIds ?? message.skills?.map(item => item.id), permissionMode, fromMessageId: message.id, action: 'edit', rollbackArtifacts }) + userContent: edited?.userContent ?? message.userContent, skillIds: edited?.skillIds ?? message.skills?.map(item => item.id), fromMessageId: message.id, action: 'edit', rollbackArtifacts }) setEditingMessageId(null) } const regenerate = (_requestId: string, message: ChatMessage): Promise => { const preceding = messages.slice(0, messages.findIndex((item) => item.id === message.id)).findLast((item) => item.role === 'user') - return preceding ? send(preceding.content, { attachments: preceding.attachments, resources: preceding.resources, quotes: preceding.quotes, userContent: preceding.userContent, skillIds: preceding.skills?.map(item => item.id), permissionMode, fromMessageId: message.id, action: 'regenerate' }) : Promise.resolve() + return preceding ? send(preceding.content, { attachments: preceding.attachments, resources: preceding.resources, quotes: preceding.quotes, userContent: preceding.userContent, skillIds: preceding.skills?.map(item => item.id), fromMessageId: message.id, action: 'regenerate' }) : Promise.resolve() } const lastAssistantId = messages.findLast((message) => message.role === 'assistant')?.id const question = messages.flatMap((message) => message.interactions ?? []).find((item) => item.kind === 'question' && ['pending', 'responding', 'error'].includes(item.status)) @@ -402,8 +400,7 @@ export default function ChatPane({ rightVisible, onToggleRight, onOpenSettings, }} speech={{ transcribe: ({ wavBase64, mimeType }) => window.ycode.speech.transcribe({ audioBase64: wavBase64, mimeType }) }} onTranscript={(text) => setDraft((current) => `${current}${current ? ' ' : ''}${text}`)} - permissionMode={permissionMode} onModeChange={setPermissionMode} permissionPlacement={isHome ? 'footer' : 'toolbar'} confirmFullAccessOnChange={confirmFullAccess} - onSkipFutureFullAccessConfirmationChange={(skip) => setConfirmFullAccess(!skip)} + permissionPlacement={isHome ? 'footer' : 'toolbar'} onSend={submit} sending={sending || !!running} onStop={running ? cancel : undefined} modelSlot={ window.dispatchEvent(new CustomEvent('ycode:open-artifacts', { detail: { sessionId: currentId } }))} buildingPlanId={running ? messages.find((item) => item.role === 'user' && item.requestId === running.runId)?.planId : undefined} - onBuildPlan={async (plan) => { if (running || sending) throw new Error('请等待当前运行结束'); await send(`请执行以下计划:\n\n${planMarkdown(plan)}`, { permissionMode, planId: plan.id }) }} + onBuildPlan={async (plan) => { if (running || sending) throw new Error('请等待当前运行结束'); await send(`请执行以下计划:\n\n${planMarkdown(plan)}`, { planId: plan.id }) }} actions={{ busyState: running || sending ? 'talking' : 'idle', canRevert: rewindEntryVisible(message) && !rewinding, rollbackPending: rewinding, canRegenerate: !running && !sending, isSessionActive: true, isLastAssistant: message.id === lastAssistantId, diff --git a/src/renderer/src/components/GeneralSettingsPanel.tsx b/src/renderer/src/components/GeneralSettingsPanel.tsx index 8b15ce9..a6fcd71 100644 --- a/src/renderer/src/components/GeneralSettingsPanel.tsx +++ b/src/renderer/src/components/GeneralSettingsPanel.tsx @@ -211,7 +211,6 @@ export default function GeneralSettingsPanel(): React.JSX.Element { const openFolder = (kind: 'data' | 'workspace'): void => { void window.ycode.general.openFolder(kind).catch((reason) => setError(String((reason as Error)?.message ?? reason))) } const changeWorkspaceRoot = (): void => { void apply(window.ycode.general.changeWorkspaceRoot()) } const openNotificationSettings = (): void => { void window.ycode.general.openSystemNotificationSettings().catch((reason) => setError(String((reason as Error)?.message ?? reason))) } - const storage = storageUsagePresentation({ dataBytes: view?.dataBytes ?? 0, dataUsagePartial: view?.dataUsagePartial === true, diff --git a/src/renderer/src/projects/ProjectComposer.tsx b/src/renderer/src/projects/ProjectComposer.tsx index 6b45b90..055ab7c 100644 --- a/src/renderer/src/projects/ProjectComposer.tsx +++ b/src/renderer/src/projects/ProjectComposer.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react' -import type { PermissionMode } from '@shared/conversation' import type { ThinkingLevel } from '@shared/workspaces' import { ComposerControls, type ComposerSendPayload } from '../workbuddy/composer/ComposerControls' import SourceModelSelector from '../workbuddy/composer/model-selector/SourceModelSelector' @@ -9,7 +8,6 @@ import { useSettingsStore } from '../stores/settings' export interface ProjectTaskStartOptions { thinkingLevel?: ThinkingLevel - permissionMode?: PermissionMode } interface Props { @@ -39,7 +37,6 @@ export default function ProjectComposer({ workspaceId, value, sending, onChange, const workspaces = useSettingsStore(state => state.workspaces) const loaded = useSettingsStore(state => state.loaded) const load = useSettingsStore(state => state.load) - const [permissionMode, setPermissionMode] = useState('default-sandbox') const [thinkingLevel, setThinkingLevel] = useState('high') useEffect(() => { @@ -52,7 +49,6 @@ export default function ProjectComposer({ workspaceId, value, sending, onChange, } await onSend(payload.text, { thinkingLevel, - permissionMode: payload.mode, }) } @@ -94,9 +90,6 @@ export default function ProjectComposer({ workspaceId, value, sending, onChange, value={value} onChange={onChange} onSend={submit} - permissionMode={permissionMode} - onModeChange={setPermissionMode} - permissionLabels={{ 'default-sandbox': '默认权限', 'full-access': '允许完全访问' }} permissionPlacement="footer" modelSlot={modelSlot} workspaceFooter={workspaceFooter} diff --git a/src/renderer/src/workbuddy/composer/ComposerControls.tsx b/src/renderer/src/workbuddy/composer/ComposerControls.tsx index 80d75da..0962474 100644 --- a/src/renderer/src/workbuddy/composer/ComposerControls.tsx +++ b/src/renderer/src/workbuddy/composer/ComposerControls.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState, type ReactNode } from 'react' -import type { ChatAttachment, PermissionMode, UserContentBlock } from '@shared/conversation' +import type { ChatAttachment, UserContentBlock } from '@shared/conversation' import { trimUserContent } from '@shared/user-content' import { IconStop } from '../../components/icons' import { RuntimeSendIcon } from '../message-actions/RuntimeIcons' @@ -11,7 +11,6 @@ import { SourceFilesSubmenu } from './add-menu/SourceSubmenus' import type { MessageQuote } from '@shared/conversation' import { ReferenceContentEditor, type ReferenceContentEditorHandle } from '../references/ReferenceContentEditor' import { useReferenceViews, type DraftAttachment } from '../references/reference-views' -import SourcePermissionControl from './permission/SourcePermissionControl' export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024 @@ -28,7 +27,6 @@ export interface ComposerSendPayload { /** 引用进对话的内容块(WorkBuddy message.quote)。 */ quotes: MessageQuote[] userContent?: UserContentBlock[] - mode: PermissionMode } export interface ComposerControlsProps { @@ -54,11 +52,6 @@ export interface ComposerControlsProps { onReferencePath?(path: string): void | Promise speech?: SpeechTranscriber onTranscript?(text: string): void - permissionMode?: PermissionMode - onModeChange?(mode: PermissionMode): void | Promise - permissionLabels?: Partial> - confirmFullAccessOnChange?: boolean - onSkipFutureFullAccessConfirmationChange?(skip: boolean): void | Promise sessionId?: string | null modelSlot?: ReactNode /** 左组紧邻「+」之后:Git 分支标签(TODO 21)。 */ @@ -96,11 +89,10 @@ export function buildComposerSendPayload( text: string, attachments: ChatAttachment[], resources: string[], - mode: PermissionMode, quotes: MessageQuote[] = [], userContent?: UserContentBlock[] ): ComposerSendPayload { - return { text: text.trim(), attachments, resources: [...resources], quotes: [...quotes], mode, ...(userContent ? { userContent: trimUserContent(userContent) } : {}) } + return { text: text.trim(), attachments, resources: [...resources], quotes: [...quotes], ...(userContent ? { userContent: trimUserContent(userContent) } : {}) } } async function encodeFile(file: File): Promise { @@ -158,10 +150,6 @@ export function ComposerControls({ onReferencePath, speech, onTranscript, - permissionMode = 'default-sandbox', - onModeChange, - permissionLabels, - confirmFullAccessOnChange = true, sessionId, modelSlot, branchLabelSlot, @@ -375,7 +363,7 @@ export function ComposerControls({ sendInFlightRef.current = true beginBusy() try { - await onSend(buildComposerSendPayload(value, attachments, resourcesRef.current, permissionMode, quotesRef.current, richEditorRef.current?.content() ?? userContent)) + await onSend(buildComposerSendPayload(value, attachments, resourcesRef.current, quotesRef.current, richEditorRef.current?.content() ?? userContent)) } catch (reason) { setError(errorText(reason)) } finally { @@ -409,22 +397,9 @@ export function ComposerControls({ previouslyRich.current = referenceViews.length > 0 }, [referenceViews.length]) - const permissionControl = onModeChange ? { - beginBusy() - try { await onModeChange(mode) } finally { endBusy() } - }} - /> : null - - const homeFooter = (workspaceFooter || (permissionPlacement === 'footer' && permissionControl)) && ( + const homeFooter = workspaceFooter && (
{workspaceFooter}
- {permissionPlacement === 'footer' && permissionControl}
) @@ -507,7 +482,6 @@ export function ComposerControls({ }} /> {branchLabelSlot} {expertSlot} - {permissionPlacement === 'toolbar' && permissionControl}
diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index 0837a69..ec89b4c 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -25,7 +25,6 @@ import type { import type { ContextUsageInfo } from './types' import type { DownloadPayload, DownloadResult, ResourceEntry, ResourceRange, WorkspaceResource } from './resources' import type { ChatAttachment, InteractionResponse, MessageQuote, PermissionMode } from './conversation' -import type { SandboxStatus, SandboxPolicyDocument, SandboxPolicyUpdate, SandboxSelfTestSummary, SandboxOperationView, SandboxBrokerOperationView, SandboxWorkspaceView, SandboxGitHostView, SandboxGitRemoteResult } from './sandbox' import type { CustomModelInput, ModelRegistryView, ModelTestResult, ModelDiscoveryInput, ModelDiscoveryResult, ModelMetadata, ModelMetadataStatus, ModelProtocol } from './models' import type { SessionContext, WorkspaceRegistryView } from './workspaces' import type { BrowserHostApi } from './browser-host' @@ -127,16 +126,6 @@ export interface YCodeApi { } app: { info(): Ipc - sandboxStatus(): Ipc - sandboxPolicy(): Ipc - setSandboxPolicy(update: Pick): Ipc - sandboxSelfTest(): Ipc - sandboxOperation(id: string): Ipc - sandboxBrokerOperation(runId: string, requestId: string): Ipc - sandboxWorkspace(sessionId: string): Ipc - sandboxGit(sessionId: string): Ipc - sandboxGitRemoteOperation(sessionId: string, operationId: string): Ipc - onSandboxChanged(listener: () => void): () => void openConfigDir(): Ipc openSettings(section?: string, action?: 'new'): Ipc onSettingsChanged(listener: () => void): () => void