fix: remove redundant confirmation for configuration changes

Configuration apply previously required a separate native confirmation even in Agent Runtime Execute mode, and failed in clients without an approval callback. Use existing work-mode and tool authorization instead, preserving Ask read-only access and request-scoped plan validation. Update regression tests, tool descriptions and documentation.

Validation: 267 focused tests passed; typecheck and changed-file lint passed. Full tests timed out with failures in other modules; repository lint reported errors in unchanged files.

Release note: Execute 模式下通过 Agent Runtime 修改 GoodBuddy 配置不再额外弹出确认框,Ask 模式仍保持只读。
This commit is contained in:
mesalogo
2026-10-02 19:39:44 +08:00
parent dc635d185e
commit 02a6f5c4e7
14 changed files with 152 additions and 247 deletions
+56 -5
View File
@@ -26,6 +26,9 @@ import { browserTabIdSchema } from '../../shared/contracts'
import { BrowserService } from '../browser/browser-service'
import { createHash } from 'node:crypto'
import { DesktopDiagnostics, type DesktopDiagnosticFailureObserver } from '../desktop-diagnostics'
import { ApplicationSettingsStore } from '../application-settings-store'
import { CapabilityService } from '../capabilities/capability-service'
import { GoodBuddyConfigService } from '../goodbuddy-config-service'
const firstLibraryId = '11111111-1111-4111-8111-111111111111'
const secondLibraryId = '22222222-2222-4222-8222-222222222222'
@@ -640,7 +643,6 @@ describe('KnowledgeMcpGateway', () => {
'none',
{ access: 'read', workspacePath: process.cwd() }
)!
const authorizeApply = vi.fn(async () => true)
const writeToken = gateway.grant(
'config-write',
[],
@@ -648,8 +650,7 @@ describe('KnowledgeMcpGateway', () => {
'none',
{
access: 'write',
workspacePath: process.cwd(),
authorizeApply
workspacePath: process.cwd()
}
)!
@@ -685,13 +686,63 @@ describe('KnowledgeMcpGateway', () => {
expect(configService.apply).toHaveBeenCalledWith(
'config-write',
expect.any(Object),
expect.any(AbortSignal),
authorizeApply
expect.any(AbortSignal)
)
gateway.revoke(writeToken)
expect(configService.revokeRequest).toHaveBeenCalledWith('config-write')
})
it('applies a real configuration plan over MCP without an authorizer', async () => {
const directory = await mkdtemp(join(tmpdir(), 'config-gateway-'))
temporaryDirectories.push(directory)
const application = new ApplicationSettingsStore(join(directory, 'application.json'))
const capabilities = new CapabilityService(
join(directory, 'capabilities.json'),
join(directory, 'builtin'),
join(directory, 'imported'),
{
isAvailable: () => true,
encrypt: value => Buffer.from(value),
decrypt: value => value.toString()
}
)
const configService = new GoodBuddyConfigService(application, capabilities)
const gateway = new KnowledgeMcpGateway(createService().service, { configService })
gateways.push(gateway)
const token = gateway.grant('config-mcp', [], new AbortController().signal, 'none', {
access: 'write', workspacePath: directory
})!
await gateway.start()
const client = new Client({ name: 'config-apply-test', version: '1.0.0' })
try {
await client.connect(new StreamableHTTPClientTransport(new URL(gateway.getEndpoint()!), {
requestInit: { headers: { Authorization: `Bearer ${token}` } }
}))
const planned = await client.callTool({
name: 'goodbuddy_config_plan',
arguments: { operations: [{
operation: 'application.update', updates: { checkUpdatesOnStartup: false }
}] }
})
expect(planned.isError).not.toBe(true)
const content = planned.content as Array<{ type: string; text: string }>
const { plan } = JSON.parse(content[0]!.text)
expect(plan).not.toHaveProperty('requiresApproval')
expect((await application.get()).checkUpdatesOnStartup).toBe(true)
const applied = await client.callTool({
name: 'goodbuddy_config_apply', arguments: { planId: plan.planId }
})
expect(applied.isError).not.toBe(true)
expect((await application.get()).checkUpdatesOnStartup).toBe(false)
expect(configService.takePendingReload('config-mcp')).toBe('after-current-request')
await expect(client.callTool({
name: 'goodbuddy_config_apply', arguments: { planId: plan.planId }
})).rejects.toThrow('不存在')
} finally {
await client.close()
}
})
it('keeps scope server-side, strips markup, bounds model arguments, and drains references', async () => {
const { service, retrieveMany } = createService()
const gateway = new KnowledgeMcpGateway(service)
+3 -10
View File
@@ -68,10 +68,7 @@ import {
magicNotePlainText,
validateMagicNoteRichContent
} from '../magic-notes/rich-content'
import type {
GoodBuddyConfigApplyAuthorizer,
GoodBuddyConfigService
} from '../goodbuddy-config-service'
import type { GoodBuddyConfigService } from '../goodbuddy-config-service'
import {
createMcpTransport
} from '../capabilities/mcp-client-transport'
@@ -214,7 +211,6 @@ type Capability = {
magicNotesAccess: MagicNotesCapabilityAccess
configAccess: MagicNotesCapabilityAccess
configWorkspacePath?: string
authorizeConfigApply?: GoodBuddyConfigApplyAuthorizer
browserConversationId?: string
browserTabId?: BrowserTabId
browserUsageLease?: BrowserTabUsageLease
@@ -497,7 +493,6 @@ export class KnowledgeMcpGateway {
config?: {
access: MagicNotesCapabilityAccess
workspacePath: string
authorizeApply?: GoodBuddyConfigApplyAuthorizer
},
browserConversationId?: string,
browserTabId?: BrowserTabId,
@@ -573,8 +568,7 @@ export class KnowledgeMcpGateway {
browserUsageLease: effectiveBrowserUsageLease,
...(effectiveConfigAccess !== 'none'
? {
configWorkspacePath: config?.workspacePath,
authorizeConfigApply: config?.authorizeApply
configWorkspacePath: config?.workspacePath
}
: {}),
signal,
@@ -1403,8 +1397,7 @@ export class KnowledgeMcpGateway {
result: await service.apply(
capability.requestId,
input,
effectiveSignal,
capability.authorizeConfigApply
effectiveSignal
)
}
}
+2 -1
View File
@@ -1005,7 +1005,8 @@ describe('ModelToolProvider', () => {
)
).toMatchObject({
scopeKey: 'model:goodbuddy-config:apply',
allowPermanent: false
allowPermanent: false,
description: '该操作会按计划修改 GoodBuddy 应用偏好或扩展能力。'
})
})
+1 -1
View File
@@ -1331,7 +1331,7 @@ export class ModelToolProvider implements ModelToolProviderLike {
scopeKey: 'model:goodbuddy-config:apply',
title: '允许应用 GoodBuddy 配置计划?',
description:
'该操作会修改 GoodBuddy 应用偏好或扩展能力。主进程还会显示计划中的具体变更并再次要求确认。',
'该操作会按计划修改 GoodBuddy 应用偏好或扩展能力。',
toolName: tool.displayName,
argumentSummary,
allowPermanent: false
@@ -34,7 +34,7 @@ function fixture() {
settingsStore: { getResolvedSettings: async () => settings },
applicationSettingsStore: { get: async () => application },
capabilities: { getRuntimeSkillContext: async () => ({ packages: [{ directory: '/skills/test', id: 'test', digest: 'one' }] }),
getResolvedMcpServers: async () => [{ id: 'custom' }], getEnabledBuiltinMcpServerIds: async () => ['knowledge-base', 'obsidian'],
getResolvedMcpServers: async () => [{ id: 'custom' }], getEnabledBuiltinMcpServerIds: async () => ['knowledge-base', 'obsidian', 'goodbuddy-config'],
getObsidianSettings: async () => ({}) },
executionSpaceResolver: { resolveProject: () => ({ kind: 'local', rootPath: '/workspace', cacheIdentity: '/workspace' }) },
terminalManager: { closeOwner: vi.fn() }, localEnvironment: { launchEnvironmentProvider: () => process.env },
@@ -83,12 +83,22 @@ describe('native client coordinator', () => {
const { coordinator, gateway, start, conversation } = fixture()
await coordinator.open(1, 'conversation')
expect(gateway.grantCustomMcp).not.toHaveBeenCalled()
expect(gateway.grant).toHaveBeenLastCalledWith(
expect.any(String), ['library'], expect.any(AbortSignal), 'none',
{ access: 'read', workspacePath: '/workspace' },
undefined, undefined, undefined, { settings: {}, access: 'read' }, undefined
)
expect(start.mock.calls[0]![0]).toMatchObject({ skillDirectories: ['/skills/test'], mcpServers: [
{ serverName: 'goodbuddy-0', readOnlyTools: ['knowledge_search', 'obsidian_read_note'] }
] })
conversation.workMode = 'execute'
await coordinator.open(1, 'conversation')
expect(gateway.grantCustomMcp).toHaveBeenCalledOnce()
expect(gateway.grant).toHaveBeenLastCalledWith(
expect.any(String), ['library'], expect.any(AbortSignal), 'none',
{ access: 'write', workspacePath: '/workspace' },
undefined, undefined, undefined, { settings: {}, access: 'write' }, undefined
)
expect(start.mock.calls[1]![0].mcpServers).toHaveLength(2)
await coordinator.closeOwner(1)
await expect(coordinator.open(1, 'conversation')).rejects.toThrow('closed')