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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1005,7 +1005,8 @@ describe('ModelToolProvider', () => {
|
||||
)
|
||||
).toMatchObject({
|
||||
scopeKey: 'model:goodbuddy-config:apply',
|
||||
allowPermanent: false
|
||||
allowPermanent: false,
|
||||
description: '该操作会按计划修改 GoodBuddy 应用偏好或扩展能力。'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user