feat: add natural language configuration tools
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
knowledgeScopedDataTools,
|
||||
magicNoteScopedDataTools
|
||||
} from './scoped-data-tools'
|
||||
import { goodbuddyConfigTools } from './goodbuddy-config-tools'
|
||||
|
||||
export type BuiltinMcpServerSummary = {
|
||||
id: string
|
||||
@@ -49,5 +50,19 @@ export const builtinMcpServers = [
|
||||
access: 'mixed',
|
||||
authorization: 'conversation-scoped',
|
||||
requiresFeature: 'magic-notes'
|
||||
},
|
||||
{
|
||||
id: 'goodbuddy-config',
|
||||
name: 'GoodBuddy 配置',
|
||||
description:
|
||||
'发现常见配置示例,读取脱敏配置,并通过计划和原生确认管理应用偏好、Skills 与 MCP。',
|
||||
tools: goodbuddyConfigTools.map(({ name, summary, access }) => ({
|
||||
name,
|
||||
description: summary,
|
||||
access
|
||||
})),
|
||||
assignments: ['model', 'opencode', 'continue'],
|
||||
access: 'mixed',
|
||||
authorization: 'conversation-scoped'
|
||||
}
|
||||
] as const satisfies readonly BuiltinMcpServerSummary[]
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
goodbuddyConfigApplyInputSchema,
|
||||
goodbuddyConfigCapabilities,
|
||||
goodbuddyConfigCapabilitiesOutputSchema,
|
||||
goodbuddyConfigCommonExamples,
|
||||
goodbuddyConfigGetOutputSchema,
|
||||
goodbuddyConfigOperationNameSchema,
|
||||
goodbuddyConfigOperationSchema,
|
||||
goodbuddyConfigPlanInputSchema,
|
||||
goodbuddyConfigPlanOutputSchema
|
||||
} from './goodbuddy-config-contracts'
|
||||
import {
|
||||
goodbuddyConfigToolByName,
|
||||
goodbuddyConfigTools
|
||||
} from './goodbuddy-config-tools'
|
||||
|
||||
describe('GoodBuddy configuration contracts', () => {
|
||||
it('publishes one valid generated example for every operation', () => {
|
||||
const operationNames = goodbuddyConfigOperationNameSchema.options
|
||||
|
||||
expect(goodbuddyConfigCommonExamples).toHaveLength(
|
||||
operationNames.length
|
||||
)
|
||||
expect(
|
||||
goodbuddyConfigCommonExamples.map(({ operation }) => operation)
|
||||
).toEqual(operationNames)
|
||||
for (const example of goodbuddyConfigCommonExamples) {
|
||||
expect(goodbuddyConfigOperationSchema.parse(example)).toEqual(
|
||||
example
|
||||
)
|
||||
}
|
||||
expect(
|
||||
goodbuddyConfigCapabilitiesOutputSchema.parse(
|
||||
goodbuddyConfigCapabilities
|
||||
)
|
||||
).toEqual(goodbuddyConfigCapabilities)
|
||||
})
|
||||
|
||||
it('accepts a bounded sequence of strongly typed operations', () => {
|
||||
const input = {
|
||||
operations: [
|
||||
{
|
||||
operation: 'application.update',
|
||||
updates: {
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'after-save-manual'
|
||||
}
|
||||
},
|
||||
{
|
||||
operation: 'skill.setAssignments',
|
||||
skillId: 'document-writing',
|
||||
assignments: ['model', 'continue']
|
||||
},
|
||||
{
|
||||
operation: 'mcp.add',
|
||||
connection: {
|
||||
name: 'Project MCP',
|
||||
description: '',
|
||||
allowDynamicTools: false,
|
||||
transport: 'http',
|
||||
url: 'https://mcp.example.com/tools'
|
||||
},
|
||||
enabled: false,
|
||||
assignments: ['model']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
expect(goodbuddyConfigPlanInputSchema.parse(input)).toEqual(input)
|
||||
})
|
||||
|
||||
it('keeps custom MCP assignments limited to the direct model', () => {
|
||||
expect(() =>
|
||||
goodbuddyConfigOperationSchema.parse({
|
||||
operation: 'mcp.add',
|
||||
connection: {
|
||||
name: 'Project MCP',
|
||||
description: '',
|
||||
allowDynamicTools: false,
|
||||
transport: 'http',
|
||||
url: 'https://mcp.example.com/tools'
|
||||
},
|
||||
enabled: true,
|
||||
assignments: ['continue']
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('never accepts MCP secrets or credential-bearing remote URLs', () => {
|
||||
const baseConnection = {
|
||||
name: 'Project MCP',
|
||||
description: '',
|
||||
allowDynamicTools: false,
|
||||
transport: 'http',
|
||||
url: 'https://mcp.example.com/tools'
|
||||
}
|
||||
|
||||
expect(() =>
|
||||
goodbuddyConfigOperationSchema.parse({
|
||||
operation: 'mcp.add',
|
||||
connection: {
|
||||
...baseConnection,
|
||||
secret: { action: 'replace', value: 'do-not-accept' }
|
||||
},
|
||||
enabled: true,
|
||||
assignments: ['model']
|
||||
})
|
||||
).toThrow()
|
||||
expect(() =>
|
||||
goodbuddyConfigOperationSchema.parse({
|
||||
operation: 'mcp.update',
|
||||
serverId: '00000000-0000-4000-8000-000000000001',
|
||||
connection: {
|
||||
...baseConnection,
|
||||
url: 'https://user:password@mcp.example.com/tools'
|
||||
}
|
||||
})
|
||||
).toThrow()
|
||||
expect(() =>
|
||||
goodbuddyConfigOperationSchema.parse({
|
||||
operation: 'mcp.update',
|
||||
serverId: '00000000-0000-4000-8000-000000000001',
|
||||
connection: {
|
||||
...baseConnection,
|
||||
url: 'https://mcp.example.com/tools?token=secret'
|
||||
}
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('only exposes redacted MCP summaries from get', () => {
|
||||
const snapshot = {
|
||||
application: {
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
},
|
||||
skills: [],
|
||||
mcpServers: [
|
||||
{
|
||||
id: '00000000-0000-4000-8000-000000000001',
|
||||
name: 'Project MCP',
|
||||
description: '',
|
||||
enabled: true,
|
||||
allowDynamicTools: false,
|
||||
assignments: ['model'],
|
||||
secretConfigured: true,
|
||||
transport: 'http'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
expect(goodbuddyConfigGetOutputSchema.parse(snapshot)).toEqual(
|
||||
snapshot
|
||||
)
|
||||
expect(() =>
|
||||
goodbuddyConfigGetOutputSchema.parse({
|
||||
...snapshot,
|
||||
mcpServers: [
|
||||
{
|
||||
...snapshot.mcpServers[0],
|
||||
url: 'https://mcp.example.com?token=secret',
|
||||
secret: 'secret'
|
||||
}
|
||||
]
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('requires an expiring, approved plan and applies only its ID', () => {
|
||||
const operation = goodbuddyConfigCommonExamples[0]!
|
||||
expect(
|
||||
goodbuddyConfigPlanOutputSchema.parse({
|
||||
planId: '00000000-0000-4000-8000-000000000002',
|
||||
expiresAt: '2030-01-01T00:00:00.000Z',
|
||||
operations: [operation],
|
||||
steps: [
|
||||
{
|
||||
index: 0,
|
||||
operation: operation.operation,
|
||||
summary: 'Update startup checks.',
|
||||
risk: 'low',
|
||||
reload: 'none',
|
||||
destructive: false
|
||||
}
|
||||
],
|
||||
overallRisk: 'low',
|
||||
reload: 'none',
|
||||
requiresApproval: true
|
||||
}).requiresApproval
|
||||
).toBe(true)
|
||||
expect(
|
||||
goodbuddyConfigApplyInputSchema.parse({
|
||||
planId: '00000000-0000-4000-8000-000000000002'
|
||||
})
|
||||
).toEqual({
|
||||
planId: '00000000-0000-4000-8000-000000000002'
|
||||
})
|
||||
expect(() =>
|
||||
goodbuddyConfigApplyInputSchema.parse({
|
||||
planId: '00000000-0000-4000-8000-000000000002',
|
||||
operations: [operation]
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('catalogs the four request-scoped MCP tools', () => {
|
||||
expect(goodbuddyConfigTools.map(({ name }) => name)).toEqual([
|
||||
'goodbuddy_config_capabilities',
|
||||
'goodbuddy_config_get',
|
||||
'goodbuddy_config_plan',
|
||||
'goodbuddy_config_apply'
|
||||
])
|
||||
expect(
|
||||
goodbuddyConfigToolByName.get('goodbuddy_config_apply')?.access
|
||||
).toBe(
|
||||
'write'
|
||||
)
|
||||
expect(
|
||||
goodbuddyConfigTools
|
||||
.filter(({ name }) => name !== 'goodbuddy_config_apply')
|
||||
.every(({ access }) => access === 'read')
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,554 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
applicationSettingsSchema,
|
||||
applicationSettingsUpdateSchema
|
||||
} from './application-settings-contracts'
|
||||
import {
|
||||
capabilityAssignmentsSchema,
|
||||
mcpServerIdSchema,
|
||||
mcpTransportSchema,
|
||||
skillIdSchema,
|
||||
skillSummarySchema
|
||||
} from './capability-contracts'
|
||||
|
||||
export const GOODBUDDY_CONFIG_MAX_OPERATIONS = 32
|
||||
|
||||
const boundedPathSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(4_096)
|
||||
.refine(
|
||||
(value) =>
|
||||
[...value].every((character) => {
|
||||
const code = character.charCodeAt(0)
|
||||
return code > 31 && code !== 127
|
||||
}),
|
||||
'Path contains control characters'
|
||||
)
|
||||
|
||||
const mcpNameSchema = z.string().trim().min(1).max(80)
|
||||
const mcpDescriptionSchema = z.string().trim().max(500)
|
||||
const mcpCommandSchema = z.string().trim().min(1).max(4_096)
|
||||
const mcpArgumentSchema = z.string().trim().min(1).max(4_096)
|
||||
|
||||
const publicMcpUrlSchema = z
|
||||
.string()
|
||||
.url()
|
||||
.max(2_048)
|
||||
.superRefine((value, context) => {
|
||||
const url = new URL(value)
|
||||
if (!['http:', 'https:'].includes(url.protocol)) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
message: 'MCP URL must use HTTP or HTTPS'
|
||||
})
|
||||
}
|
||||
if (url.username || url.password || url.search || url.hash) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
message:
|
||||
'MCP URL must not contain credentials, query parameters, or fragments'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const mcpConnectionShape = {
|
||||
name: mcpNameSchema,
|
||||
description: mcpDescriptionSchema,
|
||||
allowDynamicTools: z.boolean()
|
||||
}
|
||||
|
||||
const directModelAssignmentsSchema = z
|
||||
.array(z.literal('model'))
|
||||
.max(1)
|
||||
|
||||
/**
|
||||
* Secret-free MCP connection settings accepted from a model. Authentication
|
||||
* material is intentionally absent and must be managed through the trusted UI.
|
||||
*/
|
||||
export const goodbuddyConfigMcpConnectionSchema = z.discriminatedUnion(
|
||||
'transport',
|
||||
[
|
||||
z
|
||||
.object({
|
||||
...mcpConnectionShape,
|
||||
transport: z.literal('stdio'),
|
||||
command: mcpCommandSchema,
|
||||
args: z.array(mcpArgumentSchema).max(64)
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
...mcpConnectionShape,
|
||||
transport: z.literal('http'),
|
||||
url: publicMcpUrlSchema
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
...mcpConnectionShape,
|
||||
transport: z.literal('sse'),
|
||||
url: publicMcpUrlSchema
|
||||
})
|
||||
.strict()
|
||||
]
|
||||
)
|
||||
export type GoodBuddyConfigMcpConnection = z.infer<
|
||||
typeof goodbuddyConfigMcpConnectionSchema
|
||||
>
|
||||
|
||||
const applicationUpdateOperationSchema = z
|
||||
.object({
|
||||
operation: z.literal('application.update'),
|
||||
updates: applicationSettingsUpdateSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
const skillImportOperationSchema = z
|
||||
.object({
|
||||
operation: z.literal('skill.import'),
|
||||
sourcePath: boundedPathSchema,
|
||||
enabled: z.boolean(),
|
||||
assignments: capabilityAssignmentsSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
const skillSetEnabledOperationSchema = z
|
||||
.object({
|
||||
operation: z.literal('skill.setEnabled'),
|
||||
skillId: skillIdSchema,
|
||||
enabled: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
|
||||
const skillSetAssignmentsOperationSchema = z
|
||||
.object({
|
||||
operation: z.literal('skill.setAssignments'),
|
||||
skillId: skillIdSchema,
|
||||
assignments: capabilityAssignmentsSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
const skillRemoveOperationSchema = z
|
||||
.object({
|
||||
operation: z.literal('skill.remove'),
|
||||
skillId: skillIdSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
const mcpAddOperationSchema = z
|
||||
.object({
|
||||
operation: z.literal('mcp.add'),
|
||||
connection: goodbuddyConfigMcpConnectionSchema,
|
||||
enabled: z.boolean(),
|
||||
assignments: directModelAssignmentsSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
const mcpUpdateOperationSchema = z
|
||||
.object({
|
||||
operation: z.literal('mcp.update'),
|
||||
serverId: mcpServerIdSchema,
|
||||
connection: goodbuddyConfigMcpConnectionSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
const mcpSetEnabledOperationSchema = z
|
||||
.object({
|
||||
operation: z.literal('mcp.setEnabled'),
|
||||
serverId: mcpServerIdSchema,
|
||||
enabled: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
|
||||
const mcpSetAssignmentsOperationSchema = z
|
||||
.object({
|
||||
operation: z.literal('mcp.setAssignments'),
|
||||
serverId: mcpServerIdSchema,
|
||||
assignments: directModelAssignmentsSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
const mcpRemoveOperationSchema = z
|
||||
.object({
|
||||
operation: z.literal('mcp.remove'),
|
||||
serverId: mcpServerIdSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const goodbuddyConfigOperationSchema = z.discriminatedUnion(
|
||||
'operation',
|
||||
[
|
||||
applicationUpdateOperationSchema,
|
||||
skillImportOperationSchema,
|
||||
skillSetEnabledOperationSchema,
|
||||
skillSetAssignmentsOperationSchema,
|
||||
skillRemoveOperationSchema,
|
||||
mcpAddOperationSchema,
|
||||
mcpUpdateOperationSchema,
|
||||
mcpSetEnabledOperationSchema,
|
||||
mcpSetAssignmentsOperationSchema,
|
||||
mcpRemoveOperationSchema
|
||||
]
|
||||
)
|
||||
export type GoodBuddyConfigOperation = z.infer<
|
||||
typeof goodbuddyConfigOperationSchema
|
||||
>
|
||||
|
||||
export const goodbuddyConfigOperationNameSchema = z.enum([
|
||||
'application.update',
|
||||
'skill.import',
|
||||
'skill.setEnabled',
|
||||
'skill.setAssignments',
|
||||
'skill.remove',
|
||||
'mcp.add',
|
||||
'mcp.update',
|
||||
'mcp.setEnabled',
|
||||
'mcp.setAssignments',
|
||||
'mcp.remove'
|
||||
])
|
||||
export type GoodBuddyConfigOperationName = z.infer<
|
||||
typeof goodbuddyConfigOperationNameSchema
|
||||
>
|
||||
|
||||
export const goodbuddyConfigRiskSchema = z.enum([
|
||||
'low',
|
||||
'medium',
|
||||
'high'
|
||||
])
|
||||
export type GoodBuddyConfigRisk = z.infer<
|
||||
typeof goodbuddyConfigRiskSchema
|
||||
>
|
||||
|
||||
export const goodbuddyConfigReloadSchema = z.enum([
|
||||
'none',
|
||||
'after-current-request'
|
||||
])
|
||||
export type GoodBuddyConfigReload = z.infer<
|
||||
typeof goodbuddyConfigReloadSchema
|
||||
>
|
||||
|
||||
type OperationRegistry = {
|
||||
[Name in GoodBuddyConfigOperationName]: {
|
||||
summary: string
|
||||
risk: GoodBuddyConfigRisk
|
||||
reload: GoodBuddyConfigReload
|
||||
destructive: boolean
|
||||
exampleRequest: string
|
||||
example: Extract<GoodBuddyConfigOperation, { operation: Name }>
|
||||
}
|
||||
}
|
||||
|
||||
export const goodbuddyConfigOperationRegistry = {
|
||||
'application.update': {
|
||||
summary: 'Update one or more public application preferences.',
|
||||
risk: 'low',
|
||||
reload: 'none',
|
||||
destructive: false,
|
||||
exampleRequest: '关闭启动时自动检查更新。',
|
||||
example: {
|
||||
operation: 'application.update',
|
||||
updates: { checkUpdatesOnStartup: false }
|
||||
}
|
||||
},
|
||||
'skill.import': {
|
||||
summary: 'Import one Skill directory or ZIP from a local path.',
|
||||
risk: 'high',
|
||||
reload: 'after-current-request',
|
||||
destructive: false,
|
||||
exampleRequest: '导入当前工作区的 meeting-helper Skill。',
|
||||
example: {
|
||||
operation: 'skill.import',
|
||||
sourcePath: './meeting-helper',
|
||||
enabled: true,
|
||||
assignments: ['model']
|
||||
}
|
||||
},
|
||||
'skill.setEnabled': {
|
||||
summary: 'Enable or disable an installed Skill.',
|
||||
risk: 'medium',
|
||||
reload: 'after-current-request',
|
||||
destructive: false,
|
||||
exampleRequest: '启用 meeting-helper Skill。',
|
||||
example: {
|
||||
operation: 'skill.setEnabled',
|
||||
skillId: 'meeting-helper',
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
'skill.setAssignments': {
|
||||
summary: 'Choose the runtimes that can use an installed Skill.',
|
||||
risk: 'medium',
|
||||
reload: 'after-current-request',
|
||||
destructive: false,
|
||||
exampleRequest: '让 meeting-helper 可用于直连模型和 OpenCode。',
|
||||
example: {
|
||||
operation: 'skill.setAssignments',
|
||||
skillId: 'meeting-helper',
|
||||
assignments: ['model', 'opencode']
|
||||
}
|
||||
},
|
||||
'skill.remove': {
|
||||
summary: 'Permanently remove an imported Skill.',
|
||||
risk: 'high',
|
||||
reload: 'after-current-request',
|
||||
destructive: true,
|
||||
exampleRequest: '删除已导入的 meeting-helper Skill。',
|
||||
example: {
|
||||
operation: 'skill.remove',
|
||||
skillId: 'meeting-helper'
|
||||
}
|
||||
},
|
||||
'mcp.add': {
|
||||
summary: 'Add a secret-free MCP server configuration.',
|
||||
risk: 'high',
|
||||
reload: 'after-current-request',
|
||||
destructive: false,
|
||||
exampleRequest:
|
||||
'添加一个使用 npx 启动的本地 MCP,先保持禁用,只分配给直连模型。',
|
||||
example: {
|
||||
operation: 'mcp.add',
|
||||
connection: {
|
||||
name: 'Local tools',
|
||||
description: 'Local project tools',
|
||||
allowDynamicTools: false,
|
||||
transport: 'stdio',
|
||||
command: 'npx',
|
||||
args: ['-y', '@example/mcp-server']
|
||||
},
|
||||
enabled: false,
|
||||
assignments: ['model']
|
||||
}
|
||||
},
|
||||
'mcp.update': {
|
||||
summary: 'Replace the public connection settings for an MCP server.',
|
||||
risk: 'high',
|
||||
reload: 'after-current-request',
|
||||
destructive: false,
|
||||
exampleRequest: '把 Project tools MCP 的地址改为新的 HTTPS 地址。',
|
||||
example: {
|
||||
operation: 'mcp.update',
|
||||
serverId: '00000000-0000-4000-8000-000000000001',
|
||||
connection: {
|
||||
name: 'Project tools',
|
||||
description: 'Tools served on the local network',
|
||||
allowDynamicTools: true,
|
||||
transport: 'http',
|
||||
url: 'https://mcp.example.com/tools'
|
||||
}
|
||||
}
|
||||
},
|
||||
'mcp.setEnabled': {
|
||||
summary: 'Enable or disable a configured MCP server.',
|
||||
risk: 'high',
|
||||
reload: 'after-current-request',
|
||||
destructive: false,
|
||||
exampleRequest: '启用指定的 MCP Server。',
|
||||
example: {
|
||||
operation: 'mcp.setEnabled',
|
||||
serverId: '00000000-0000-4000-8000-000000000001',
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
'mcp.setAssignments': {
|
||||
summary: 'Choose the runtimes that can use an MCP server.',
|
||||
risk: 'high',
|
||||
reload: 'after-current-request',
|
||||
destructive: false,
|
||||
exampleRequest: '只把指定 MCP Server 分配给直连模型。',
|
||||
example: {
|
||||
operation: 'mcp.setAssignments',
|
||||
serverId: '00000000-0000-4000-8000-000000000001',
|
||||
assignments: ['model']
|
||||
}
|
||||
},
|
||||
'mcp.remove': {
|
||||
summary: 'Permanently remove an MCP server configuration.',
|
||||
risk: 'high',
|
||||
reload: 'after-current-request',
|
||||
destructive: true,
|
||||
exampleRequest: '删除指定的 MCP Server 配置。',
|
||||
example: {
|
||||
operation: 'mcp.remove',
|
||||
serverId: '00000000-0000-4000-8000-000000000001'
|
||||
}
|
||||
}
|
||||
} as const satisfies OperationRegistry
|
||||
|
||||
export const goodbuddyConfigCommonExamples =
|
||||
goodbuddyConfigOperationNameSchema.options.map(
|
||||
(operation) => goodbuddyConfigOperationRegistry[operation].example
|
||||
)
|
||||
|
||||
export const goodbuddyConfigOperationDescriptorSchema = z
|
||||
.object({
|
||||
operation: goodbuddyConfigOperationNameSchema,
|
||||
summary: z.string().min(1).max(240),
|
||||
risk: goodbuddyConfigRiskSchema,
|
||||
reload: goodbuddyConfigReloadSchema,
|
||||
destructive: z.boolean(),
|
||||
exampleRequest: z.string().min(1).max(240),
|
||||
example: goodbuddyConfigOperationSchema
|
||||
})
|
||||
.strict()
|
||||
export type GoodBuddyConfigOperationDescriptor = z.infer<
|
||||
typeof goodbuddyConfigOperationDescriptorSchema
|
||||
>
|
||||
|
||||
export const goodbuddyConfigOperationDescriptors =
|
||||
goodbuddyConfigOperationNameSchema.options.map((operation) => ({
|
||||
operation,
|
||||
...goodbuddyConfigOperationRegistry[operation]
|
||||
}))
|
||||
|
||||
export const goodbuddyConfigCapabilitiesInputSchema = z
|
||||
.object({})
|
||||
.strict()
|
||||
export type GoodBuddyConfigCapabilitiesInput = z.infer<
|
||||
typeof goodbuddyConfigCapabilitiesInputSchema
|
||||
>
|
||||
|
||||
export const goodbuddyConfigCapabilitiesOutputSchema = z
|
||||
.object({
|
||||
server: z.literal('goodbuddy_config'),
|
||||
version: z.literal(1),
|
||||
authorization: z.literal('request-scoped'),
|
||||
secretPolicy: z.literal('never-exposed-or-accepted'),
|
||||
applyRequiresApproval: z.literal(true),
|
||||
operations: z.array(goodbuddyConfigOperationDescriptorSchema).length(10)
|
||||
})
|
||||
.strict()
|
||||
export type GoodBuddyConfigCapabilitiesOutput = z.infer<
|
||||
typeof goodbuddyConfigCapabilitiesOutputSchema
|
||||
>
|
||||
|
||||
export const goodbuddyConfigCapabilities = {
|
||||
server: 'goodbuddy_config',
|
||||
version: 1,
|
||||
authorization: 'request-scoped',
|
||||
secretPolicy: 'never-exposed-or-accepted',
|
||||
applyRequiresApproval: true,
|
||||
operations: goodbuddyConfigOperationDescriptors
|
||||
} as const satisfies GoodBuddyConfigCapabilitiesOutput
|
||||
|
||||
/**
|
||||
* Deliberately omits commands, arguments, URLs, and credential values.
|
||||
*/
|
||||
export const goodbuddyConfigMcpSummarySchema = z
|
||||
.object({
|
||||
id: mcpServerIdSchema,
|
||||
name: mcpNameSchema,
|
||||
description: mcpDescriptionSchema,
|
||||
enabled: z.boolean(),
|
||||
allowDynamicTools: z.boolean(),
|
||||
assignments: capabilityAssignmentsSchema,
|
||||
secretConfigured: z.boolean(),
|
||||
transport: mcpTransportSchema
|
||||
})
|
||||
.strict()
|
||||
export type GoodBuddyConfigMcpSummary = z.infer<
|
||||
typeof goodbuddyConfigMcpSummarySchema
|
||||
>
|
||||
|
||||
export const goodbuddyConfigSnapshotSchema = z
|
||||
.object({
|
||||
application: applicationSettingsSchema,
|
||||
skills: z.array(skillSummarySchema).max(256),
|
||||
mcpServers: z.array(goodbuddyConfigMcpSummarySchema).max(64)
|
||||
})
|
||||
.strict()
|
||||
export type GoodBuddyConfigSnapshot = z.infer<
|
||||
typeof goodbuddyConfigSnapshotSchema
|
||||
>
|
||||
|
||||
export const goodbuddyConfigGetInputSchema = z.object({}).strict()
|
||||
export type GoodBuddyConfigGetInput = z.infer<
|
||||
typeof goodbuddyConfigGetInputSchema
|
||||
>
|
||||
|
||||
export const goodbuddyConfigGetOutputSchema =
|
||||
goodbuddyConfigSnapshotSchema
|
||||
export type GoodBuddyConfigGetOutput = GoodBuddyConfigSnapshot
|
||||
|
||||
export const goodbuddyConfigPlanInputSchema = z
|
||||
.object({
|
||||
operations: z
|
||||
.array(goodbuddyConfigOperationSchema)
|
||||
.min(1)
|
||||
.max(GOODBUDDY_CONFIG_MAX_OPERATIONS)
|
||||
})
|
||||
.strict()
|
||||
export type GoodBuddyConfigPlanInput = z.infer<
|
||||
typeof goodbuddyConfigPlanInputSchema
|
||||
>
|
||||
|
||||
export const goodbuddyConfigPlanStepSchema = z
|
||||
.object({
|
||||
index: z.number().int().nonnegative(),
|
||||
operation: goodbuddyConfigOperationNameSchema,
|
||||
summary: z.string().min(1).max(12_000),
|
||||
risk: goodbuddyConfigRiskSchema,
|
||||
reload: goodbuddyConfigReloadSchema,
|
||||
destructive: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
export type GoodBuddyConfigPlanStep = z.infer<
|
||||
typeof goodbuddyConfigPlanStepSchema
|
||||
>
|
||||
|
||||
export const goodbuddyConfigPlanOutputSchema = z
|
||||
.object({
|
||||
planId: z.string().uuid(),
|
||||
expiresAt: z.string().datetime(),
|
||||
operations: z
|
||||
.array(goodbuddyConfigOperationSchema)
|
||||
.min(1)
|
||||
.max(GOODBUDDY_CONFIG_MAX_OPERATIONS),
|
||||
steps: z
|
||||
.array(goodbuddyConfigPlanStepSchema)
|
||||
.min(1)
|
||||
.max(GOODBUDDY_CONFIG_MAX_OPERATIONS),
|
||||
overallRisk: goodbuddyConfigRiskSchema,
|
||||
reload: goodbuddyConfigReloadSchema,
|
||||
requiresApproval: z.literal(true)
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, context) => {
|
||||
if (value.operations.length !== value.steps.length) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Plan operations and steps must have the same length'
|
||||
})
|
||||
}
|
||||
})
|
||||
export type GoodBuddyConfigPlanOutput = z.infer<
|
||||
typeof goodbuddyConfigPlanOutputSchema
|
||||
>
|
||||
|
||||
export const goodbuddyConfigApplyInputSchema = z
|
||||
.object({
|
||||
planId: z.string().uuid()
|
||||
})
|
||||
.strict()
|
||||
export type GoodBuddyConfigApplyInput = z.infer<
|
||||
typeof goodbuddyConfigApplyInputSchema
|
||||
>
|
||||
|
||||
export const goodbuddyConfigApplyOutputSchema = z
|
||||
.object({
|
||||
planId: z.string().uuid(),
|
||||
status: z.enum(['applied', 'partially-applied']),
|
||||
appliedOperations: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(GOODBUDDY_CONFIG_MAX_OPERATIONS),
|
||||
reload: goodbuddyConfigReloadSchema,
|
||||
snapshot: goodbuddyConfigSnapshotSchema,
|
||||
error: z.string().min(1).max(2_000).optional()
|
||||
})
|
||||
.strict()
|
||||
export type GoodBuddyConfigApplyOutput = z.infer<
|
||||
typeof goodbuddyConfigApplyOutputSchema
|
||||
>
|
||||
@@ -0,0 +1,91 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
goodbuddyConfigApplyInputSchema,
|
||||
goodbuddyConfigApplyOutputSchema,
|
||||
goodbuddyConfigCapabilitiesInputSchema,
|
||||
goodbuddyConfigCapabilitiesOutputSchema,
|
||||
goodbuddyConfigGetInputSchema,
|
||||
goodbuddyConfigGetOutputSchema,
|
||||
goodbuddyConfigPlanInputSchema,
|
||||
goodbuddyConfigPlanOutputSchema
|
||||
} from './goodbuddy-config-contracts'
|
||||
|
||||
export type GoodBuddyConfigToolDefinition = {
|
||||
name: string
|
||||
title: string
|
||||
description: string
|
||||
summary: string
|
||||
access: 'read' | 'write'
|
||||
inputSchema: z.ZodType
|
||||
outputSchema: z.ZodType
|
||||
}
|
||||
|
||||
export const goodbuddyConfigToolCatalog = {
|
||||
capabilities: {
|
||||
name: 'goodbuddy_config_capabilities',
|
||||
title: 'Discover GoodBuddy configuration capabilities',
|
||||
description:
|
||||
'List supported secret-free configuration operations, examples, risk levels, and reload effects. This tool does not read or change settings.',
|
||||
summary: 'Discover supported configuration operations and examples.',
|
||||
access: 'read',
|
||||
inputSchema: goodbuddyConfigCapabilitiesInputSchema,
|
||||
outputSchema: goodbuddyConfigCapabilitiesOutputSchema
|
||||
},
|
||||
get: {
|
||||
name: 'goodbuddy_config_get',
|
||||
title: 'Read sanitized GoodBuddy settings',
|
||||
description:
|
||||
'Read public application preferences, Skill summaries, and redacted MCP summaries for this request. Credentials and connection details are never returned.',
|
||||
summary: 'Read sanitized application, Skill, and MCP settings.',
|
||||
access: 'read',
|
||||
inputSchema: goodbuddyConfigGetInputSchema,
|
||||
outputSchema: goodbuddyConfigGetOutputSchema
|
||||
},
|
||||
plan: {
|
||||
name: 'goodbuddy_config_plan',
|
||||
title: 'Plan GoodBuddy configuration changes',
|
||||
description:
|
||||
'Validate and normalize a bounded sequence of strongly typed changes without applying it. The returned plan is scoped to this request and expires.',
|
||||
summary: 'Validate configuration changes and inspect their effects.',
|
||||
access: 'read',
|
||||
inputSchema: goodbuddyConfigPlanInputSchema,
|
||||
outputSchema: goodbuddyConfigPlanOutputSchema
|
||||
},
|
||||
apply: {
|
||||
name: 'goodbuddy_config_apply',
|
||||
title: 'Apply an approved GoodBuddy configuration plan',
|
||||
description:
|
||||
'Apply a previously planned request-scoped change after GoodBuddy approval controls authorize it. Raw operations and secrets are not accepted. If a later operation fails, the result reports partial application and the remaining operations are not attempted.',
|
||||
summary: 'Apply one approved request-scoped configuration plan.',
|
||||
access: 'write',
|
||||
inputSchema: goodbuddyConfigApplyInputSchema,
|
||||
outputSchema: goodbuddyConfigApplyOutputSchema
|
||||
}
|
||||
} as const satisfies Record<
|
||||
'capabilities' | 'get' | 'plan' | 'apply',
|
||||
GoodBuddyConfigToolDefinition
|
||||
>
|
||||
|
||||
export const goodbuddyConfigToolKeys = [
|
||||
'capabilities',
|
||||
'get',
|
||||
'plan',
|
||||
'apply'
|
||||
] as const
|
||||
|
||||
export const goodbuddyConfigTools = [
|
||||
goodbuddyConfigToolCatalog.capabilities,
|
||||
goodbuddyConfigToolCatalog.get,
|
||||
goodbuddyConfigToolCatalog.plan,
|
||||
goodbuddyConfigToolCatalog.apply
|
||||
] as const satisfies readonly GoodBuddyConfigToolDefinition[]
|
||||
|
||||
export type GoodBuddyConfigToolName =
|
||||
(typeof goodbuddyConfigTools)[number]['name']
|
||||
|
||||
export const goodbuddyConfigToolByName = new Map<
|
||||
GoodBuddyConfigToolName,
|
||||
(typeof goodbuddyConfigTools)[number]
|
||||
>(
|
||||
goodbuddyConfigTools.map((tool) => [tool.name, tool])
|
||||
)
|
||||
@@ -1,4 +1,8 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
goodbuddyConfigTools,
|
||||
type GoodBuddyConfigToolName
|
||||
} from './goodbuddy-config-tools'
|
||||
|
||||
export type ScopedDataToolAccess = 'read' | 'write'
|
||||
|
||||
@@ -230,10 +234,12 @@ export const magicNoteScopedDataTools = [
|
||||
|
||||
export const scopedDataTools = [
|
||||
...knowledgeScopedDataTools,
|
||||
...magicNoteScopedDataTools
|
||||
...magicNoteScopedDataTools,
|
||||
...goodbuddyConfigTools
|
||||
] as const
|
||||
|
||||
export type ScopedDataToolName = (typeof scopedDataTools)[number]['name']
|
||||
export type { GoodBuddyConfigToolName }
|
||||
|
||||
export const scopedDataToolByName = new Map<
|
||||
ScopedDataToolName,
|
||||
@@ -254,6 +260,14 @@ export const magicNoteWriteToolNames = magicNoteScopedDataTools
|
||||
.filter((tool) => tool.access === 'write')
|
||||
.map((tool) => tool.name)
|
||||
|
||||
export const goodbuddyConfigReadToolNames = goodbuddyConfigTools
|
||||
.filter((tool) => tool.access === 'read')
|
||||
.map((tool) => tool.name)
|
||||
|
||||
export const goodbuddyConfigWriteToolNames = goodbuddyConfigTools
|
||||
.filter((tool) => tool.access === 'write')
|
||||
.map((tool) => tool.name)
|
||||
|
||||
export const scopedReadToolNames = scopedDataTools
|
||||
.filter((tool) => tool.access === 'read')
|
||||
.map((tool) => tool.name)
|
||||
|
||||
Reference in New Issue
Block a user