feat: add native runtime customization
OpenCode and Continue customization was previously planned but unavailable, and Runtime-native capabilities were not represented consistently across providers. GoodBuddy now provides secure Main-owned customization settings, truthful native inventories, OpenCode Agents and Commands, Continue Rules and Prompts, DSH Web Search/Fetch, MCP Prompt and Resource metadata, and manual compaction where supported. Native capabilities are presented in eleven accessible tabs with Tools separated from Commands, LSP, and Formatters. Tool source and Ask/Execute availability are explicit, external OpenCode remains connection-only, Continue reports unsupported static tool discovery instead of advertising unreachable Skills, and disposable inventory probes avoid retaining background runtimes. Ask remains read-only at the Runtime boundary, Execute keeps the existing authorization controls, and credentials remain confined to Main. Release note: 新增 OpenCode、Continue 与 DeepSeek Harness 的 Runtime 原生定制与真实能力清单;工具来源、Ask/Execute 可用性、上下文压缩和 MCP 元数据现在可清晰查看,同时继续保持 Main 进程凭据保护与现有权限边界。
This commit is contained in:
@@ -0,0 +1,530 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
assistantIdSchema,
|
||||
conversationContextCompressionStateSchema,
|
||||
conversationHistoryMessageSchema,
|
||||
maximumConversationHistoryCharacters,
|
||||
maximumConversationHistoryMessages
|
||||
} from './assistant-contracts'
|
||||
import { agentRuntimeSelectionSchema } from './runtime-selection-contracts'
|
||||
|
||||
export const customizableRuntimeProviderSchema = z.enum([
|
||||
'opencode',
|
||||
'continue',
|
||||
'deepseek-harness'
|
||||
])
|
||||
|
||||
export type CustomizableRuntimeProvider = z.infer<
|
||||
typeof customizableRuntimeProviderSchema
|
||||
>
|
||||
|
||||
export const runtimeCustomizationLimits = {
|
||||
presets: 12,
|
||||
rulesPerPreset: 32,
|
||||
promptsPerPreset: 32,
|
||||
nameCharacters: 120,
|
||||
descriptionCharacters: 500,
|
||||
contentCharacters: 20_000
|
||||
} as const
|
||||
|
||||
export const runtimeNativeInventoryLimits = {
|
||||
agents: 100,
|
||||
tools: 200,
|
||||
commands: 200,
|
||||
lsp: 100,
|
||||
formatters: 100,
|
||||
mcpServers: 100,
|
||||
skills: 200,
|
||||
rules: 200,
|
||||
prompts: 200,
|
||||
resources: 200
|
||||
} as const
|
||||
|
||||
export const boundedRuntimeIdentifierSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(128)
|
||||
.refine(
|
||||
(value) =>
|
||||
[...value].every((character) => {
|
||||
const code = character.charCodeAt(0)
|
||||
return code > 31 && code !== 127
|
||||
}),
|
||||
'Runtime 标识包含控制字符'
|
||||
)
|
||||
|
||||
const boundedRuntimeLabelSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(200)
|
||||
|
||||
const boundedRuntimeDescriptionSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.max(2_000)
|
||||
.optional()
|
||||
|
||||
export const continueRuleSchema = z
|
||||
.object({
|
||||
id: assistantIdSchema,
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(runtimeCustomizationLimits.nameCharacters),
|
||||
content: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(runtimeCustomizationLimits.contentCharacters),
|
||||
enabled: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type ContinueRule = z.infer<typeof continueRuleSchema>
|
||||
|
||||
export const runtimePromptTemplateSchema = z
|
||||
.object({
|
||||
id: assistantIdSchema,
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(runtimeCustomizationLimits.nameCharacters),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(runtimeCustomizationLimits.descriptionCharacters)
|
||||
.optional(),
|
||||
prompt: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(runtimeCustomizationLimits.contentCharacters)
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type RuntimePromptTemplate = z.infer<
|
||||
typeof runtimePromptTemplateSchema
|
||||
>
|
||||
|
||||
export const continueConfigurationPresetSchema = z
|
||||
.object({
|
||||
id: assistantIdSchema,
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(runtimeCustomizationLimits.nameCharacters),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(runtimeCustomizationLimits.descriptionCharacters)
|
||||
.optional(),
|
||||
rules: z
|
||||
.array(continueRuleSchema)
|
||||
.max(runtimeCustomizationLimits.rulesPerPreset),
|
||||
prompts: z
|
||||
.array(runtimePromptTemplateSchema)
|
||||
.max(runtimeCustomizationLimits.promptsPerPreset)
|
||||
})
|
||||
.strict()
|
||||
.superRefine((preset, context) => {
|
||||
const ids = [
|
||||
...preset.rules.map((rule) => rule.id),
|
||||
...preset.prompts.map((prompt) => prompt.id)
|
||||
]
|
||||
if (new Set(ids).size !== ids.length) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Continue 预设中的规则与 Prompt ID 不得重复'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export type ContinueConfigurationPreset = z.infer<
|
||||
typeof continueConfigurationPresetSchema
|
||||
>
|
||||
|
||||
export const runtimeCustomizationSettingsSchema = z
|
||||
.object({
|
||||
opencode: z
|
||||
.object({
|
||||
defaultAgent: boundedRuntimeIdentifierSchema.optional()
|
||||
})
|
||||
.strict(),
|
||||
continue: z
|
||||
.object({
|
||||
defaultPresetId: assistantIdSchema.optional(),
|
||||
presets: z
|
||||
.array(continueConfigurationPresetSchema)
|
||||
.max(runtimeCustomizationLimits.presets)
|
||||
})
|
||||
.strict()
|
||||
})
|
||||
.strict()
|
||||
.superRefine((settings, context) => {
|
||||
const presetIds = settings.continue.presets.map(
|
||||
(preset) => preset.id
|
||||
)
|
||||
if (new Set(presetIds).size !== presetIds.length) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
path: ['continue', 'presets'],
|
||||
message: 'Continue 预设 ID 不得重复'
|
||||
})
|
||||
}
|
||||
if (
|
||||
settings.continue.defaultPresetId &&
|
||||
!presetIds.includes(settings.continue.defaultPresetId)
|
||||
) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
path: ['continue', 'defaultPresetId'],
|
||||
message: '默认 Continue 预设不存在'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export type RuntimeCustomizationSettings = z.infer<
|
||||
typeof runtimeCustomizationSettingsSchema
|
||||
>
|
||||
|
||||
export const defaultRuntimeCustomizationSettings: RuntimeCustomizationSettings =
|
||||
{
|
||||
opencode: {},
|
||||
continue: {
|
||||
presets: []
|
||||
}
|
||||
}
|
||||
|
||||
export const runtimeControlSchema = z.discriminatedUnion(
|
||||
'provider',
|
||||
[
|
||||
z
|
||||
.object({
|
||||
provider: z.literal('opencode'),
|
||||
agent: boundedRuntimeIdentifierSchema.optional(),
|
||||
command: z
|
||||
.object({
|
||||
name: boundedRuntimeIdentifierSchema,
|
||||
arguments: z.string().max(100_000)
|
||||
})
|
||||
.strict()
|
||||
.optional()
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
provider: z.literal('continue'),
|
||||
presetId: assistantIdSchema.optional()
|
||||
})
|
||||
.strict()
|
||||
]
|
||||
)
|
||||
|
||||
export type RuntimeControl = z.infer<typeof runtimeControlSchema>
|
||||
|
||||
export const runtimeNativeSnapshotInputSchema = z
|
||||
.object({
|
||||
provider: customizableRuntimeProviderSchema,
|
||||
profileId: assistantIdSchema.optional(),
|
||||
projectId: assistantIdSchema.optional()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type RuntimeNativeSnapshotInput = z.infer<
|
||||
typeof runtimeNativeSnapshotInputSchema
|
||||
>
|
||||
|
||||
export const runtimeNativeSkillSchema = z
|
||||
.object({
|
||||
id: boundedRuntimeIdentifierSchema,
|
||||
name: boundedRuntimeLabelSchema,
|
||||
description: boundedRuntimeDescriptionSchema,
|
||||
source: z
|
||||
.enum(['global', 'workspace', 'plugin', 'runtime', 'unknown'])
|
||||
.default('unknown')
|
||||
})
|
||||
.strict()
|
||||
|
||||
const runtimeNativeMcpServerSchema = z
|
||||
.object({
|
||||
id: boundedRuntimeIdentifierSchema,
|
||||
name: boundedRuntimeLabelSchema,
|
||||
status: z.enum([
|
||||
'connected',
|
||||
'disabled',
|
||||
'failed',
|
||||
'needs-auth',
|
||||
'unsupported',
|
||||
'unknown'
|
||||
]),
|
||||
detail: z.string().trim().max(500).optional()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const runtimeNativePromptSchema = z
|
||||
.object({
|
||||
id: boundedRuntimeIdentifierSchema,
|
||||
name: boundedRuntimeLabelSchema,
|
||||
description: boundedRuntimeDescriptionSchema,
|
||||
prompt: z.string().trim().min(1).max(20_000),
|
||||
source: z.enum([
|
||||
'runtime',
|
||||
'mcp',
|
||||
'configuration',
|
||||
'preset'
|
||||
])
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type RuntimeNativePrompt = z.infer<
|
||||
typeof runtimeNativePromptSchema
|
||||
>
|
||||
|
||||
export const runtimeNativeRuleSchema = z
|
||||
.object({
|
||||
id: boundedRuntimeIdentifierSchema,
|
||||
name: boundedRuntimeLabelSchema,
|
||||
description: boundedRuntimeDescriptionSchema,
|
||||
content: z.string().trim().min(1).max(20_000),
|
||||
source: z.enum([
|
||||
'global',
|
||||
'workspace',
|
||||
'configuration',
|
||||
'runtime'
|
||||
])
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type RuntimeNativeRule = z.infer<
|
||||
typeof runtimeNativeRuleSchema
|
||||
>
|
||||
|
||||
const runtimeNativeResourceSchema = z
|
||||
.object({
|
||||
id: boundedRuntimeIdentifierSchema,
|
||||
name: boundedRuntimeLabelSchema,
|
||||
uri: z.string().trim().min(1).max(2_048),
|
||||
description: boundedRuntimeDescriptionSchema,
|
||||
mimeType: z.string().trim().min(1).max(200).optional(),
|
||||
server: boundedRuntimeLabelSchema.optional()
|
||||
})
|
||||
.strict()
|
||||
|
||||
const runtimeNativeAgentSchema = z
|
||||
.object({
|
||||
id: boundedRuntimeIdentifierSchema,
|
||||
name: boundedRuntimeLabelSchema,
|
||||
description: boundedRuntimeDescriptionSchema,
|
||||
mode: z.enum(['primary', 'subagent', 'all']),
|
||||
native: z.boolean(),
|
||||
hidden: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
|
||||
const runtimeNativeCommandSchema = z
|
||||
.object({
|
||||
id: boundedRuntimeIdentifierSchema,
|
||||
name: boundedRuntimeLabelSchema,
|
||||
description: boundedRuntimeDescriptionSchema,
|
||||
source: z.enum(['command', 'mcp', 'skill', 'runtime']),
|
||||
agent: boundedRuntimeIdentifierSchema.optional()
|
||||
})
|
||||
.strict()
|
||||
|
||||
const runtimeNativeLspSchema = z
|
||||
.object({
|
||||
id: boundedRuntimeIdentifierSchema,
|
||||
name: boundedRuntimeLabelSchema,
|
||||
status: z.enum(['connected', 'error', 'not-loaded']),
|
||||
detail: z.string().trim().max(500).optional()
|
||||
})
|
||||
.strict()
|
||||
|
||||
const runtimeNativeFormatterSchema = z
|
||||
.object({
|
||||
id: boundedRuntimeIdentifierSchema,
|
||||
name: boundedRuntimeLabelSchema,
|
||||
enabled: z.boolean(),
|
||||
extensions: z
|
||||
.array(z.string().trim().min(1).max(32))
|
||||
.max(100)
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const runtimeNativeToolSchema = z
|
||||
.object({
|
||||
id: boundedRuntimeIdentifierSchema,
|
||||
name: boundedRuntimeLabelSchema,
|
||||
description: boundedRuntimeDescriptionSchema,
|
||||
kind: z.enum([
|
||||
'read',
|
||||
'write',
|
||||
'shell',
|
||||
'network',
|
||||
'agent',
|
||||
'interaction',
|
||||
'other'
|
||||
]),
|
||||
source: z.enum([
|
||||
'runtime',
|
||||
'plugin',
|
||||
'mcp',
|
||||
'skill',
|
||||
'unknown'
|
||||
]),
|
||||
ask: z.enum(['allowed', 'blocked', 'conditional']),
|
||||
execute: z.enum(['allowed', 'blocked', 'conditional'])
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type RuntimeNativeTool = z.infer<
|
||||
typeof runtimeNativeToolSchema
|
||||
>
|
||||
|
||||
export const runtimeNativeInventoryStatusSchema = z.enum([
|
||||
'available',
|
||||
'partial',
|
||||
'unavailable',
|
||||
'connection-only',
|
||||
'unsupported'
|
||||
])
|
||||
|
||||
export type RuntimeNativeInventoryStatus = z.infer<
|
||||
typeof runtimeNativeInventoryStatusSchema
|
||||
>
|
||||
|
||||
export const runtimeContextCapabilitySchema = z
|
||||
.object({
|
||||
strategy: z.enum([
|
||||
'native',
|
||||
'goodbuddy-summary',
|
||||
'unsupported'
|
||||
]),
|
||||
manualCompact: z.boolean(),
|
||||
detail: z.string().trim().min(1).max(500)
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type RuntimeContextCapability = z.infer<
|
||||
typeof runtimeContextCapabilitySchema
|
||||
>
|
||||
|
||||
export const runtimeNativeSnapshotSchema = z
|
||||
.object({
|
||||
provider: customizableRuntimeProviderSchema,
|
||||
available: z.boolean(),
|
||||
inventoryStatus: runtimeNativeInventoryStatusSchema,
|
||||
detail: z.string().trim().min(1).max(1_000),
|
||||
agents: z
|
||||
.array(runtimeNativeAgentSchema)
|
||||
.max(runtimeNativeInventoryLimits.agents),
|
||||
tools: z
|
||||
.array(runtimeNativeToolSchema)
|
||||
.max(runtimeNativeInventoryLimits.tools),
|
||||
toolsSupported: z.boolean(),
|
||||
commands: z
|
||||
.array(runtimeNativeCommandSchema)
|
||||
.max(runtimeNativeInventoryLimits.commands),
|
||||
lsp: z
|
||||
.array(runtimeNativeLspSchema)
|
||||
.max(runtimeNativeInventoryLimits.lsp),
|
||||
formatters: z
|
||||
.array(runtimeNativeFormatterSchema)
|
||||
.max(runtimeNativeInventoryLimits.formatters),
|
||||
mcpServers: z
|
||||
.array(runtimeNativeMcpServerSchema)
|
||||
.max(runtimeNativeInventoryLimits.mcpServers),
|
||||
skills: z
|
||||
.array(runtimeNativeSkillSchema)
|
||||
.max(runtimeNativeInventoryLimits.skills),
|
||||
rules: z
|
||||
.array(runtimeNativeRuleSchema)
|
||||
.max(runtimeNativeInventoryLimits.rules),
|
||||
prompts: z
|
||||
.array(runtimeNativePromptSchema)
|
||||
.max(runtimeNativeInventoryLimits.prompts),
|
||||
resources: z
|
||||
.array(runtimeNativeResourceSchema)
|
||||
.max(runtimeNativeInventoryLimits.resources),
|
||||
resourcesSupported: z.boolean(),
|
||||
context: runtimeContextCapabilitySchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type RuntimeNativeSnapshot = z.infer<
|
||||
typeof runtimeNativeSnapshotSchema
|
||||
>
|
||||
|
||||
export const runtimeConversationCompactInputSchema = z
|
||||
.object({
|
||||
requestId: assistantIdSchema,
|
||||
conversationId: assistantIdSchema,
|
||||
projectId: assistantIdSchema.optional(),
|
||||
runtimeSelection: agentRuntimeSelectionSchema,
|
||||
history: z
|
||||
.array(conversationHistoryMessageSchema)
|
||||
.max(maximumConversationHistoryMessages),
|
||||
historyMessageIds: z
|
||||
.array(assistantIdSchema)
|
||||
.max(maximumConversationHistoryMessages),
|
||||
contextCompressionState:
|
||||
conversationContextCompressionStateSchema.optional()
|
||||
})
|
||||
.strict()
|
||||
.superRefine((request, context) => {
|
||||
if (
|
||||
request.history.length !== request.historyMessageIds.length
|
||||
) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
path: ['historyMessageIds'],
|
||||
message: '会话历史消息 ID 必须与历史消息一一对应'
|
||||
})
|
||||
}
|
||||
if (
|
||||
new Set(request.historyMessageIds).size !==
|
||||
request.historyMessageIds.length
|
||||
) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
path: ['historyMessageIds'],
|
||||
message: '会话历史消息 ID 不得重复'
|
||||
})
|
||||
}
|
||||
if (
|
||||
request.history.reduce(
|
||||
(total, message) => total + message.content.length,
|
||||
0
|
||||
) > maximumConversationHistoryCharacters
|
||||
) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
path: ['history'],
|
||||
message: `会话历史总长度不能超过 ${maximumConversationHistoryCharacters.toLocaleString()} 个字符`
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export type RuntimeConversationCompactInput = z.infer<
|
||||
typeof runtimeConversationCompactInputSchema
|
||||
>
|
||||
|
||||
export const runtimeConversationCompactResultSchema = z
|
||||
.object({
|
||||
provider: z.enum(['opencode', 'continue']),
|
||||
strategy: z.enum(['native', 'goodbuddy-summary']),
|
||||
compacted: z.boolean(),
|
||||
detail: z.string().trim().min(1).max(500),
|
||||
contextCompressionState:
|
||||
conversationContextCompressionStateSchema.optional()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type RuntimeConversationCompactResult = z.infer<
|
||||
typeof runtimeConversationCompactResultSchema
|
||||
>
|
||||
Reference in New Issue
Block a user