feat: add DeepSeek Harness runtime

This commit is contained in:
lofyer
2026-08-14 10:04:34 +08:00
parent fca9888f83
commit 8286e120a1
69 changed files with 13660 additions and 351 deletions
+15
View File
@@ -28,6 +28,13 @@ describe('bundled runtime paths', () => {
'cli',
'dist',
'cn.js'
),
deepseekHarness: join(
'workspace',
'app',
'out',
'main',
'deepseek-harness-host-bootstrap.js'
)
})
})
@@ -55,6 +62,14 @@ describe('bundled runtime paths', () => {
'continue',
'dist',
'cn.js'
),
deepseekHarness: join(
'installed',
'resources',
'app.asar.unpacked',
'out',
'main',
'deepseek-harness-host-bootstrap.js'
)
})
})
+17
View File
@@ -3,8 +3,12 @@ import { join } from 'node:path'
export type BundledRuntimePaths = {
opencode: string
continue: string
deepseekHarness: string
}
export const bundledContinueVersion = '1.5.47'
export const bundledDeepSeekHarnessVersion = '0.1.0-rc.6'
export function resolveBundledRuntimePaths(input: {
appPath: string
resourcesPath: string
@@ -29,6 +33,13 @@ export function resolveBundledRuntimePaths(input: {
'continue',
'dist',
'cn.js'
),
deepseekHarness: join(
input.resourcesPath,
'app.asar.unpacked',
'out',
'main',
'deepseek-harness-host-bootstrap.js'
)
}
}
@@ -48,6 +59,12 @@ export function resolveBundledRuntimePaths(input: {
'cli',
'dist',
'cn.js'
),
deepseekHarness: join(
input.appPath,
'out',
'main',
'deepseek-harness-host-bootstrap.js'
)
}
}
+32
View File
@@ -71,6 +71,38 @@ function settings(
}
describe('createAgentRuntime model compatibility', () => {
it('does not treat the default model profile as the platform DeepSeek source', () => {
const defaultProfile = {
id: '00000000-0000-4000-8000-000000000001',
name: 'Default DeepSeek',
baseUrl: 'https://api.deepseek.com',
modelName: 'deepseek-chat',
protocol: 'openai-chat-completions' as const,
authentication: 'api-key' as const,
imageGenerationQuality: 'auto' as const,
apiKey: 'default-deepseek-key'
}
expect(() =>
createAgentRuntime(
process.cwd(),
settings({
provider: 'deepseek-harness',
modelBaseUrl: defaultProfile.baseUrl,
modelName: defaultProfile.modelName,
modelProtocol: defaultProfile.protocol,
modelAuthentication: defaultProfile.authentication,
apiKey: defaultProfile.apiKey,
modelProfiles: [defaultProfile],
runtimeSandboxMode: 'auto'
}),
{ deepseekHarnessLauncher: vi.fn() }
)
).toThrow(
'DeepSeek Harness 需要 api.deepseek.com 的 OpenAI Chat Completions 模型连接'
)
})
it('creates an available direct runtime for a no-auth model', async () => {
const runtime = createAgentRuntime(process.cwd(), settings())
+44
View File
@@ -1,6 +1,10 @@
import { ModelAgentRuntime } from './model-runtime'
import { ContinueAgentRuntime } from './continue-runtime'
import { OpenCodeRuntime } from './opencode-runtime'
import {
DeepSeekHarnessRuntime,
type DeepSeekHarnessRuntimeOptions
} from './deepseek-harness-runtime'
import type { AgentRuntime } from './runtime'
import { UnconfiguredAgentRuntime } from './unconfigured-runtime'
import type {
@@ -9,6 +13,7 @@ import type {
} from '../runtime-settings-store'
import {
defaultRuntimeSettings,
isDeepSeekHarnessModelProfile,
isAgentRuntimeModelProtocol
} from '../../shared/contracts'
import type {
@@ -21,6 +26,7 @@ import { resolveRuntimeSandbox } from './runtime-sandbox'
import type { BrowserToolService } from '../browser/browser-model-tools'
import type { ModelToolProviderLike } from './model-tool-provider'
import type { KnowledgeMcpGateway } from './knowledge-mcp-gateway'
import { ModelToolProvider } from './model-tool-provider'
const noSubagentTools: ModelToolProviderLike = {
listTools: async () => [],
@@ -41,6 +47,7 @@ export type AgentCapabilityContext = {
continueHostCacheRoot?: string
bundledRuntimePaths?: BundledRuntimePaths
continueHostLauncher?: ContinueHostLauncher
deepseekHarnessLauncher?: DeepSeekHarnessRuntimeOptions['launch']
browserService?: BrowserToolService
knowledgeGateway?: KnowledgeMcpGateway
webSearchEnabled?: boolean
@@ -102,6 +109,43 @@ export function createAgentRuntime(
settings?.runtimeSandboxMode ??
defaultRuntimeSettings.runtimeSandboxMode
if (provider === 'deepseek-harness') {
const profile = settings?.deepseekHarnessModelProfile
if (!profile || !isDeepSeekHarnessModelProfile(profile)) {
throw new Error(
'DeepSeek Harness 需要 api.deepseek.com 的 OpenAI Chat Completions 模型连接'
)
}
if (!profile.apiKey) {
throw new Error('DeepSeek Harness 模型连接未配置 API Key')
}
if (!capabilities.deepseekHarnessLauncher) {
throw new Error('DeepSeek Harness 受控 Host 启动器不可用')
}
if (sandboxMode === 'off') {
throw new Error('DeepSeek Harness Execute 需要启用 Runtime 沙箱')
}
return new DeepSeekHarnessRuntime({
defaultWorkspace: workspace,
baseUrl: profile.baseUrl,
model: profile.modelName,
launch: capabilities.deepseekHarnessLauncher,
credentialRefs: {
GOODBUDDY_DEEPSEEK_API_KEY: profile.apiKey
},
requiredSandboxEnforcement:
sandboxMode === 'strict' ? 'full' : 'partial',
skillPackages: capabilities.skillPackages,
toolProvider: new ModelToolProvider(
workspace,
capabilities.mcpServers,
undefined,
capabilities.knowledgeGateway,
false
)
})
}
if (provider === 'continue') {
if (
settings?.continueModelProfile &&
@@ -0,0 +1,709 @@
import { mkdir, mkdtemp, realpath, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import {
CallId,
type GenerateOptions,
type StreamChunk
} from '@deepseek-ai/dsh-llm'
import type { RuntimeEvent } from './runtime'
import {
ModelToolProvider,
type ModelToolCallContext
} from './model-tool-provider'
import type { ResolvedMcpServer } from '../capabilities/capability-service'
import {
createBoundedNdJsonStream,
startControlledDeepSeekHarnessHost,
type ControlledHarnessHost
} from '../deepseek-harness-host'
import {
DeepSeekHarnessRuntime,
type DeepSeekHarnessChild,
type DeepSeekHarnessLaunchOptions
} from './deepseek-harness-runtime'
import { GOODBUDDY_HARNESS_MAX_STEP_TOKENS } from './goodbuddy-harness-control-plane'
const MAX_FRAME_BYTES = 1024 * 1024
const CREDENTIAL_REF = 'GOODBUDDY_DEEPSEEK_API_KEY'
const SKILL_CALL_ID = 'e2e-skill-call'
const MCP_CALL_ID = 'e2e-mcp-call'
const ASK_MCP_CALL_ID = 'e2e-ask-mcp-call'
const MICRO_DELTA_COUNT = 30_000
function expectedSandbox() {
return process.platform === 'win32'
? { provider: 'windows-acl', enforcement: 'partial' as const }
: process.platform === 'darwin'
? { provider: 'seatbelt', enforcement: 'full' as const }
: { provider: 'local-linux', enforcement: 'full' as const }
}
function deferred<T>() {
let resolvePromise!: (value: T) => void
const promise = new Promise<T>((resolve) => {
resolvePromise = resolve
})
return { promise, resolve: resolvePromise }
}
function toolResultText(
options: GenerateOptions,
callId: string
): string | undefined {
for (const message of options.messages) {
for (const block of message.content) {
if (
block.type !== 'tool-result' ||
block.toolCallId !== callId
) {
continue
}
return block.content
.filter(
(
content
): content is Extract<
(typeof block.content)[number],
{ type: 'text' }
> => content.type === 'text'
)
.map((content) => content.text)
.join('\n')
}
}
return undefined
}
function latestUserText(options: GenerateOptions): string {
return options.messages
.filter(
(message) =>
message.role === 'user' &&
message.source.kind === 'user'
)
.flatMap((message) =>
message.content
.filter(
(
content
): content is Extract<
(typeof message.content)[number],
{ type: 'text' }
> => content.type === 'text'
)
.map((content) => content.text)
)
.at(-1) ?? ''
}
async function* toolCall(
callId: string,
name: string,
argumentsValue: Record<string, unknown>
): AsyncGenerator<StreamChunk> {
const id = CallId(callId)
const argumentsText = JSON.stringify(argumentsValue)
yield {
type: 'block-start',
index: 0,
blockType: 'tool-call'
}
yield {
type: 'tool-call-delta',
index: 0,
id,
name,
argumentsDelta: argumentsText
}
yield {
type: 'block-end',
index: 0,
block: {
type: 'tool-call',
id,
name,
arguments: argumentsText
}
}
yield {
type: 'usage',
usage: {
inputTokens: 10,
outputTokens: 5,
cacheReadTokens: 0,
cacheWriteTokens: 0
}
}
yield {
type: 'finish',
reason: { kind: 'tool-calls' }
}
}
async function* textResponse(
text: string
): AsyncGenerator<StreamChunk> {
yield {
type: 'block-start',
index: 0,
blockType: 'text'
}
yield {
type: 'text-delta',
index: 0,
text
}
yield {
type: 'block-end',
index: 0,
block: { type: 'text', text }
}
yield {
type: 'usage',
usage: {
inputTokens: 20,
outputTokens: 8,
cacheReadTokens: 0,
cacheWriteTokens: 0
}
}
yield {
type: 'finish',
reason: { kind: 'stop' }
}
}
async function* microDeltaResponse(): AsyncGenerator<StreamChunk> {
yield {
type: 'block-start',
index: 0,
blockType: 'reasoning'
}
for (let index = 0; index < MICRO_DELTA_COUNT; index += 1) {
yield {
type: 'reasoning-delta',
index: 0,
text: String(index % 10)
}
}
yield {
type: 'block-end',
index: 0,
block: {
type: 'reasoning',
text: Array.from(
{ length: MICRO_DELTA_COUNT },
(_value, index) => String(index % 10)
).join('')
}
}
yield {
type: 'usage',
usage: {
inputTokens: 20,
outputTokens: 8_000,
cacheReadTokens: 0,
cacheWriteTokens: 0
}
}
yield {
type: 'finish',
reason: { kind: 'stop' }
}
}
class FakeGameModel {
mcpToolName?: string
skillResult?: string
blueprint?: Record<string, unknown>
askToolResult?: string
executeToolNames: string[] = []
askToolNames: string[] = []
stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const prompt = latestUserText(options)
const toolNames = options.tools?.map((tool) => tool.name) ?? []
if (prompt.includes('ASK_BOUNDARY_PROBE')) {
this.askToolNames = toolNames
const result = toolResultText(options, ASK_MCP_CALL_ID)
if (!result) {
if (!this.mcpToolName) {
throw new Error('Fake model has no prior MCP tool identity')
}
return toolCall(ASK_MCP_CALL_ID, this.mcpToolName, {
theme: 'neon-ruins',
seed: 'ask-must-not-execute',
targetCount: 5
})
}
this.askToolResult = result
return textResponse('Ask mode MCP proxy unavailable as required.')
}
this.executeToolNames = toolNames
const skillResult = toolResultText(options, SKILL_CALL_ID)
if (!skillResult) {
return toolCall(SKILL_CALL_ID, 'skill', {
name: 'web-3d-game'
})
}
this.skillResult = skillResult
const blueprintResult = toolResultText(options, MCP_CALL_ID)
if (!blueprintResult) {
const mcpTool = options.tools?.find((tool) =>
tool.name.endsWith('_create_game_blueprint')
)
if (!mcpTool) {
throw new Error(
'Main-mediated 3D blueprint MCP tool was not exposed'
)
}
this.mcpToolName = mcpTool.name
return toolCall(MCP_CALL_ID, mcpTool.name, {
theme: 'neon-ruins',
seed: 'goodbuddy-0.9.0',
targetCount: 5
})
}
this.blueprint = JSON.parse(
blueprintResult
) as Record<string, unknown>
return textResponse(
'Loaded the Web 3D Game Skill and the approved Prism Relay blueprint.'
)
}
}
type HarnessModel = {
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
}
async function collect(
stream: AsyncGenerator<RuntimeEvent, void, void>
): Promise<RuntimeEvent[]> {
const events: RuntimeEvent[] = []
for await (const event of stream) {
events.push(event)
}
return events
}
function createInProcessLaunch(
dshHome: string,
model: HarnessModel
): {
launch(
options: DeepSeekHarnessLaunchOptions
): Promise<DeepSeekHarnessChild>
hosts: ControlledHarnessHost[]
} {
const hosts: ControlledHarnessHost[] = []
return {
hosts,
async launch(options) {
const clientToHost =
new TransformStream<Uint8Array, Uint8Array>()
const hostToClient =
new TransformStream<Uint8Array, Uint8Array>()
const exited = deferred<{
exitCode: number | null
signal?: string | null
}>()
const host = await startControlledDeepSeekHarnessHost({
workspace: options.cwd,
dshHome,
baseUrl: options.baseUrl,
api: 'openai-completions',
provider: 'goodbuddy',
model: options.model,
harnessVersion: '0.1.0-rc.6',
sandbox: expectedSandbox(),
credentialRefs: options.credentialRefs,
skillPackages: options.skillPackages,
stream: createBoundedNdJsonStream(
hostToClient.writable,
clientToHost.readable,
MAX_FRAME_BYTES
)
})
hosts.push(host)
host.context.on(
'llm/stream',
(request) => model.stream(request),
{ global: true, prepend: true }
)
let terminated = false
return {
stdin: clientToHost.writable,
stdout: hostToClient.readable,
exited: exited.promise,
async terminate() {
if (terminated) {
return
}
terminated = true
await host.dispose().catch(() => undefined)
await Promise.allSettled([
clientToHost.writable.close(),
hostToClient.writable.close()
])
exited.resolve({ exitCode: 0 })
}
}
}
}
}
describe('DeepSeek Harness real ACP control-plane E2E', () => {
it(
'coalesces micro reasoning deltas without losing content and caps each model step',
async () => {
const root = await realpath(
await mkdtemp(join(tmpdir(), 'goodbuddy-harness-acp-deltas-'))
)
const workspace = join(root, 'workspace')
const dshHome = join(root, 'dsh-home')
await Promise.all([mkdir(workspace), mkdir(dshHome)])
let observedRequest: GenerateOptions | undefined
const inProcess = createInProcessLaunch(dshHome, {
stream(options) {
observedRequest = options
return microDeltaResponse()
}
})
const runtime = new DeepSeekHarnessRuntime({
defaultWorkspace: workspace,
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-test',
launch: (options) => inProcess.launch(options),
credentialRefs: {
[CREDENTIAL_REF]: 'unused-in-memory-model-credential'
},
initializationTimeoutMs: 20_000,
promptTimeoutMs: 20_000,
shutdownTimeoutMs: 5_000
})
try {
const events = await collect(
runtime.run(
{
requestId: 'request-acp-deltas',
conversationId: 'acp-deltas',
prompt: 'Return the deterministic reasoning stream.',
workMode: 'execute'
},
new AbortController().signal
)
)
const reasoning = events.filter(
(
event
): event is Extract<
RuntimeEvent,
{ type: 'reasoning' }
> => event.type === 'reasoning'
)
expect(observedRequest?.maxTokens).toBe(
GOODBUDDY_HARNESS_MAX_STEP_TOKENS
)
expect(observedRequest?.system).toContain(
'act through the available tools'
)
expect(reasoning).toHaveLength(8)
expect(
reasoning.map((event) => event.delta).join('')
).toBe(
Array.from(
{ length: MICRO_DELTA_COUNT },
(_value, index) => String(index % 10)
).join('')
)
expect(events.at(-1)).toMatchObject({ type: 'done' })
} finally {
await runtime.dispose()
await Promise.allSettled(
inProcess.hosts.map((host) => host.dispose())
)
await rm(root, { recursive: true, force: true })
}
},
30_000
)
it(
'rejects the ACP prompt with a bounded model turn error',
async () => {
const root = await realpath(
await mkdtemp(join(tmpdir(), 'goodbuddy-harness-acp-error-'))
)
const workspace = join(root, 'workspace')
const dshHome = join(root, 'dsh-home')
await Promise.all([mkdir(workspace), mkdir(dshHome)])
const inProcess = createInProcessLaunch(dshHome, {
stream() {
throw new Error('synthetic model turn failed')
}
})
const runtime = new DeepSeekHarnessRuntime({
defaultWorkspace: workspace,
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-test',
launch: (options) => inProcess.launch(options),
credentialRefs: {
[CREDENTIAL_REF]: 'unused-in-memory-model-credential'
},
initializationTimeoutMs: 20_000,
promptTimeoutMs: 2_000,
shutdownTimeoutMs: 5_000
})
try {
await expect(
collect(
runtime.run(
{
requestId: 'request-acp-error',
conversationId: 'acp-error',
prompt: 'Trigger the synthetic model failure.',
workMode: 'ask'
},
new AbortController().signal
)
)
).rejects.toThrow('synthetic model turn failed')
} finally {
await runtime.dispose()
await Promise.allSettled(
inProcess.hosts.map((host) => host.dispose())
)
await rm(root, { recursive: true, force: true })
}
},
30_000
)
it(
'loads a native Skill, calls an approved real MCP, forwards events, and removes MCP in Ask',
async () => {
const root = await realpath(
await mkdtemp(join(tmpdir(), 'goodbuddy-harness-acp-e2e-'))
)
const workspace = join(root, 'workspace')
const dshHome = join(root, 'dsh-home')
await Promise.all([
mkdir(workspace),
mkdir(dshHome)
])
const provider = new ModelToolProvider(workspace, [
{
id: 'fbf42200-4e60-48d0-b5f2-e816db38ac54',
name: 'Local 3D Game Blueprint',
description: 'Deterministic integration fixture',
enabled: true,
allowDynamicTools: false,
assignments: ['deepseek-harness'],
secretConfigured: false,
transport: 'stdio',
command: process.execPath,
args: [
resolve(
'tests',
'fixtures',
'web-3d-game-mcp.mjs'
)
]
} satisfies ResolvedMcpServer
])
const callTool = vi.spyOn(provider, 'callTool')
const listTools = vi.spyOn(provider, 'listTools')
const fakeModel = new FakeGameModel()
const inProcess = createInProcessLaunch(dshHome, fakeModel)
const runtime = new DeepSeekHarnessRuntime({
defaultWorkspace: workspace,
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-test',
launch: (options) => inProcess.launch(options),
credentialRefs: {
[CREDENTIAL_REF]: 'unused-in-memory-model-credential'
},
skillPackages: [
{
id: 'web-3d-game',
directory: resolve(
'resources',
'skills',
'web-3d-game'
)
}
],
toolProvider: provider,
initializationTimeoutMs: 20_000,
promptTimeoutMs: 20_000,
shutdownTimeoutMs: 5_000
})
const authorize = vi.fn(
async (
request: Parameters<
NonNullable<
Parameters<DeepSeekHarnessRuntime['run']>[2]
>
>[0]
) =>
request.scopeKey.startsWith('model:mcp:')
? ('once' as const)
: ('deny' as const)
)
try {
const executeEvents = await collect(
runtime.run(
{
requestId: 'request-acp-execute',
conversationId: 'acp-e2e',
prompt:
'Use the Web 3D Game Skill and assigned blueprint MCP.',
workMode: 'execute'
},
new AbortController().signal,
authorize
)
)
expect(fakeModel.executeToolNames).toContain('skill')
expect(fakeModel.mcpToolName).toMatch(
/_create_game_blueprint$/u
)
expect(fakeModel.skillResult).toContain(
'window.__GOODBUDDY_GAME__'
)
expect(fakeModel.blueprint).toMatchObject({
title: 'Prism Relay',
objective: { targetCount: 5 },
acceptance: {
testSurface: 'window.__GOODBUDDY_GAME__'
}
})
expect(authorize).toHaveBeenCalledOnce()
expect(callTool).toHaveBeenCalledWith(
fakeModel.mcpToolName,
{
theme: 'neon-ruins',
seed: 'goodbuddy-0.9.0',
targetCount: 5
},
expect.any(AbortSignal),
{
conversationId: 'acp-e2e',
workMode: 'execute',
knowledgeCapabilityToken: undefined
} satisfies ModelToolCallContext
)
expect(executeEvents).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'tool',
callId: SKILL_CALL_ID,
name: 'skill',
state: 'pending'
}),
expect.objectContaining({
type: 'tool',
callId: SKILL_CALL_ID,
state: 'completed'
}),
expect.objectContaining({
type: 'tool',
callId: MCP_CALL_ID,
name: fakeModel.mcpToolName,
state: 'pending'
}),
expect.objectContaining({
type: 'tool',
callId: MCP_CALL_ID,
state: 'completed'
}),
expect.objectContaining({
type: 'text',
delta: expect.stringContaining('Prism Relay')
}),
expect.objectContaining({
type: 'model-usage',
runtime: 'deepseek-harness'
}),
expect.objectContaining({
type: 'done',
sessionId: expect.any(String)
})
])
)
expect(
executeEvents.filter(
(event) =>
event.type === 'tool' &&
event.state === 'running'
)
).toHaveLength(0)
const callsBeforeAsk = callTool.mock.calls.length
const listsBeforeAsk = listTools.mock.calls.length
const approvalsBeforeAsk = authorize.mock.calls.length
const askEvents = await collect(
runtime.run(
{
requestId: 'request-acp-ask',
conversationId: 'acp-e2e',
prompt:
'ASK_BOUNDARY_PROBE: attempt the previous MCP tool.',
workMode: 'ask'
},
new AbortController().signal,
authorize
)
)
expect(fakeModel.askToolNames).not.toContain(
fakeModel.mcpToolName
)
expect(fakeModel.askToolResult).toContain('unknown tool')
expect(callTool).toHaveBeenCalledTimes(callsBeforeAsk)
expect(listTools).toHaveBeenCalledTimes(listsBeforeAsk)
expect(authorize).toHaveBeenCalledTimes(approvalsBeforeAsk)
expect(askEvents).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'tool',
callId: ASK_MCP_CALL_ID,
name: fakeModel.mcpToolName,
state: 'pending'
}),
expect.objectContaining({
type: 'tool',
callId: ASK_MCP_CALL_ID,
state: 'failed'
}),
expect.objectContaining({
type: 'text',
delta: expect.stringContaining(
'MCP proxy unavailable'
)
}),
expect.objectContaining({ type: 'done' })
])
)
} finally {
await runtime.dispose()
await Promise.allSettled(
inProcess.hosts.map((host) => host.dispose())
)
await rm(root, { recursive: true, force: true })
}
},
60_000
)
})
@@ -0,0 +1,920 @@
import { describe, expect, it, vi } from 'vitest'
import { resolve } from 'node:path'
import type { RuntimeEvent } from './runtime'
import {
ModelToolProvider,
type ModelToolDefinition,
type ModelToolProviderLike
} from './model-tool-provider'
import type {
ResolvedMcpServer
} from '../capabilities/capability-service'
import {
DeepSeekHarnessRuntime,
harnessPromptError,
type DeepSeekHarnessAcpSdk,
type DeepSeekHarnessChild
} from './deepseek-harness-runtime'
import { RequestError } from '@agentclientprotocol/sdk'
type Permission = Parameters<
ReturnType<
ConstructorParameters<
DeepSeekHarnessAcpSdk['ClientSideConnection']
>[0]
>['requestPermission']
>[0]
function deferred<T>() {
let resolve!: (value: T) => void
let reject!: (error: unknown) => void
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
resolve = resolvePromise
reject = rejectPromise
})
return { promise, resolve, reject }
}
function setup(
options: {
toolProvider?: ModelToolProviderLike
promptTimeoutMs?: number
maxEventCharacters?: number
maxRequestOutputCharacters?: number
} = {}
) {
const exit = deferred<{
exitCode: number | null
signal?: string | null
}>()
const stderr = new TransformStream<Uint8Array, Uint8Array>()
const child: DeepSeekHarnessChild = {
stdin: new WritableStream<Uint8Array>(),
stdout: new ReadableStream<Uint8Array>(),
stderr: stderr.readable,
exited: exit.promise,
terminate: vi.fn()
}
let permissionHandler:
| ((params: Permission) => Promise<unknown>)
| undefined
let updateHandler:
| ((context: {
sessionId: string
update: Record<string, unknown>
}) => Promise<void>)
| undefined
let extensionHandler:
| ((
method: string,
params: Record<string, unknown>
) => Promise<Record<string, unknown>>)
| undefined
const requests: Array<{
method: string
params: Record<string, unknown>
}> = []
const notifications: Array<{
method: string
params: Record<string, unknown>
}> = []
const promptGates: Array<ReturnType<typeof deferred<{ stopReason: string }>>> =
[]
let sessionIndex = 0
const connectionClosed = deferred<void>()
const connectionController = new AbortController()
const requestAgent = async (
method: string,
params: Record<string, unknown>
) => {
requests.push({ method, params })
if (method === 'initialize') {
return {
protocolVersion: 1,
agentCapabilities: {}
}
}
if (method === 'session/new') {
sessionIndex += 1
return { sessionId: `session-${sessionIndex}` }
}
if (method === 'session/prompt') {
const gate = deferred<{ stopReason: string }>()
promptGates.push(gate)
return gate.promise
}
throw new Error(`unexpected request: ${method}`)
}
const notifyAgent = async (
method: string,
params: Record<string, unknown>
) => {
notifications.push({ method, params })
}
const agent = {
initialize: vi.fn((params: Record<string, unknown>) =>
requestAgent('initialize', params)
),
newSession: vi.fn((params: Record<string, unknown>) =>
requestAgent('session/new', params)
),
prompt: vi.fn((params: Record<string, unknown>) =>
requestAgent('session/prompt', params)
),
cancel: vi.fn((params: Record<string, unknown>) =>
notifyAgent('session/cancel', params)
),
extMethod: vi.fn(
async (method: string, params: Record<string, unknown>) => {
requests.push({ method, params })
if (method === 'goodbuddy/handshake') {
return {
controlProtocolVersion: 1,
harnessVersion: '0.1.0-rc.6',
acpProtocolVersion: 1,
supports: {
cancellation: true,
sessionRelease: true,
oneShotApproval: true,
reasoningEvents: true,
toolEvents: true,
usageEvents: true,
credentialResolution: true
},
sandbox: {
provider: 'test',
enforcement: 'full'
}
}
}
if (method === 'goodbuddy/session/prepare') {
return { prepared: true }
}
if (method === 'goodbuddy/session/release') {
return { released: true }
}
if (method === 'goodbuddy/shutdown') {
return { shutdown: true }
}
throw new Error(`unexpected extension: ${method}`)
}
),
extNotification: vi.fn()
}
const connection = {
...agent,
signal: connectionController.signal,
closed: connectionClosed.promise
}
const ClientSideConnection = vi.fn(function (
this: unknown,
toClient: (
connectedAgent: typeof agent
) => {
requestPermission: typeof permissionHandler
sessionUpdate: typeof updateHandler
extMethod: (
method: string,
params: Record<string, unknown>
) => Promise<Record<string, unknown>>
extNotification: (
method: string,
params: Record<string, unknown>
) => Promise<void>
}
) {
const client = toClient(agent)
permissionHandler = client.requestPermission
updateHandler = client.sessionUpdate
extensionHandler = client.extMethod
agent.extNotification.mockImplementation(
async (
method: string,
params: Record<string, unknown>
) => client.extNotification(method, params)
)
return connection
})
const sdk = {
PROTOCOL_VERSION: 1,
ClientSideConnection,
ndJsonStream: vi.fn(() => ({ stream: true }))
} as unknown as DeepSeekHarnessAcpSdk
const launch = vi.fn(async () => child)
const runtime = new DeepSeekHarnessRuntime({
defaultWorkspace: 'C:\\workspace',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-test',
launch,
loadAcpSdk: async () => sdk,
initializationTimeoutMs: 100,
promptTimeoutMs: options.promptTimeoutMs ?? 100,
shutdownTimeoutMs: 10,
maxStderrBytes: 16,
maxEventCharacters: options.maxEventCharacters,
maxRequestOutputCharacters:
options.maxRequestOutputCharacters,
toolProvider: options.toolProvider
})
const emit = async (
sessionId: string,
update: Record<string, unknown>
): Promise<void> => {
await updateHandler?.({ sessionId, update })
}
return {
runtime,
child,
stderr,
exit,
sdk,
launch,
requests,
notifications,
promptGates,
agent,
permission: async (request: Permission) =>
permissionHandler?.(request),
extension: (
method: string,
params: Record<string, unknown>
) => extensionHandler?.(method, params),
notify: (
method: string,
params: Record<string, unknown>
) => agent.extNotification(method, params),
emit
}
}
async function collect(
stream: AsyncGenerator<RuntimeEvent, void, void>
): Promise<RuntimeEvent[]> {
const events: RuntimeEvent[] = []
for await (const event of stream) {
events.push(event)
}
return events
}
function request(
conversationId: string,
workMode: 'ask' | 'execute' = 'execute'
) {
return {
requestId: `request-${conversationId}`,
conversationId,
prompt: 'hello',
workMode
} as const
}
function permission(sessionId: string): Permission {
return {
sessionId,
toolCall: {
toolCallId: 'call-1',
title: 'Run tests',
name: 'shell',
kind: 'execute',
rawInput: { command: 'npm test' }
},
options: [
{
optionId: 'allow-once',
name: 'Allow once',
kind: 'allow_once'
},
{
optionId: 'allow-always',
name: 'Always allow',
kind: 'allow_always'
},
{
optionId: 'reject',
name: 'Reject',
kind: 'reject_once'
}
]
}
}
function mcpTool(
name = 'mcp_deadbeef_cafebabe_game_asset'
): ModelToolDefinition {
return {
name,
displayName: 'Local Game Assets / game_asset',
description: 'Returns a deterministic local game asset manifest.',
inputSchema: {
type: 'object',
properties: {
kind: { type: 'string' }
},
required: ['kind'],
additionalProperties: false
},
source: 'mcp',
serverName: 'Local Game Assets'
}
}
function toolProvider(
tools: ModelToolDefinition[] = [mcpTool()]
): ModelToolProviderLike {
return {
listTools: vi.fn(async () => tools),
getApproval: vi.fn((tool, _arguments, summary) => ({
scopeKey: `model:mcp:${tool.name}`,
title: `允许调用 MCP 工具「${tool.displayName}」?`,
description: '调用本地测试 MCP。',
toolName: tool.displayName,
argumentSummary: summary,
allowPermanent: false
})),
callTool: vi.fn(async () => ({
parts: [
{
type: 'text' as const,
text: '{"asset":"cube"}'
}
],
contextBytes: 16
})),
releaseConversation: vi.fn(async () => undefined),
dispose: vi.fn(async () => undefined)
}
}
describe('DeepSeekHarnessRuntime', () => {
it('surfaces bounded internal Harness details from ACP errors', () => {
expect(
harnessPromptError(
RequestError.internalError({
details: 'DeepSeek provider rejected the request'
})
)
).toEqual(
new Error('DeepSeek provider rejected the request')
)
expect(
harnessPromptError(
RequestError.internalError({ unrelated: 'hidden' })
)
).toBeInstanceOf(RequestError)
})
it('uses ACP stdio, maps conversations to sessions, and streams text', async () => {
const harness = setup()
const first = collect(
harness.runtime.run(
request('one'),
new AbortController().signal
)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(1)
)
await harness.emit('session-1', {
sessionUpdate: 'agent_message_chunk',
content: { type: 'text', text: 'hello ' }
})
await harness.emit('session-1', {
sessionUpdate: 'agent_message_chunk',
content: { type: 'text', text: 'world' }
})
harness.promptGates[0]!.resolve({ stopReason: 'end_turn' })
expect(await first).toEqual([
expect.objectContaining({ type: 'status' }),
expect.objectContaining({ type: 'text', delta: 'hello ' }),
expect.objectContaining({ type: 'text', delta: 'world' }),
expect.objectContaining({
type: 'done',
sessionId: 'session-1'
})
])
expect(harness.sdk.ndJsonStream).toHaveBeenCalledWith(
harness.child.stdin,
harness.child.stdout
)
expect(harness.launch).toHaveBeenCalledWith({
cwd: 'C:\\workspace',
signal: expect.any(AbortSignal),
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-test',
credentialRefs: [],
requiredSandboxEnforcement: undefined,
skillPackages: []
})
expect(harness.requests).toContainEqual({
method: 'goodbuddy/session/prepare',
params: {
sessionId: 'session-1',
requestId: 'request-one',
mode: 'execute'
}
})
const second = collect(
harness.runtime.run(
request('one'),
new AbortController().signal
)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(2)
)
harness.promptGates[1]!.resolve({ stopReason: 'end_turn' })
await second
expect(
harness.requests.filter(({ method }) => method === 'session/new')
).toHaveLength(1)
await harness.runtime.dispose()
})
it('enforces the cumulative bridge limit against complete wire events', async () => {
const harness = setup({
maxEventCharacters: 1_000,
maxRequestOutputCharacters: 180
})
const running = collect(
harness.runtime.run(
request('output-limit'),
new AbortController().signal
)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(1)
)
await harness.notify('goodbuddy/session/event', {
sessionId: 'session-1',
requestId: 'request-output-limit',
type: 'reasoning',
delta: 'x'.repeat(40)
})
await harness.notify('goodbuddy/session/event', {
sessionId: 'session-1',
requestId: 'request-output-limit',
type: 'reasoning',
delta: 'y'.repeat(40)
})
harness.promptGates[0]!.resolve({ stopReason: 'end_turn' })
await expect(running).rejects.toThrow(
'请求累计输出超过安全限制'
)
await harness.runtime.dispose()
})
it('keeps independent conversation sessions distinct', async () => {
const harness = setup()
const first = collect(
harness.runtime.run(
request('one'),
new AbortController().signal
)
)
const second = collect(
harness.runtime.run(
request('two'),
new AbortController().signal
)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(2)
)
harness.promptGates[0]!.resolve({ stopReason: 'end_turn' })
harness.promptGates[1]!.resolve({ stopReason: 'end_turn' })
await Promise.all([first, second])
const prompts = harness.requests.filter(
({ method }) => method === 'session/prompt'
)
expect(prompts.map(({ params }) => params.sessionId).sort()).toEqual([
'session-1',
'session-2'
])
await harness.runtime.dispose()
})
it('fails Ask closed and never calls the authorizer', async () => {
const harness = setup()
const authorize = vi.fn().mockResolvedValue('once')
const running = collect(
harness.runtime.run(
request('ask', 'ask'),
new AbortController().signal,
authorize
)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(1)
)
await expect(
harness.permission(permission('session-1'))
).resolves.toEqual({
outcome: { outcome: 'selected', optionId: 'reject' }
})
expect(authorize).not.toHaveBeenCalled()
expect(harness.requests).toContainEqual({
method: 'goodbuddy/session/prepare',
params: {
sessionId: 'session-1',
requestId: 'request-ask',
mode: 'ask'
}
})
harness.promptGates[0]!.resolve({ stopReason: 'end_turn' })
await running
await harness.runtime.dispose()
})
it('authorizes Execute but can select only allow-once', async () => {
const harness = setup()
const authorize = vi.fn().mockResolvedValue('always')
const running = collect(
harness.runtime.run(
request('execute'),
new AbortController().signal,
authorize
)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(1)
)
await expect(
harness.permission(permission('session-1'))
).resolves.toEqual({
outcome: {
outcome: 'selected',
optionId: 'allow-once'
}
})
expect(authorize).toHaveBeenCalledWith({
scopeKey: 'deepseek-harness:shell',
title: 'Run tests',
description: 'DeepSeek Harness 请求一次性执行此工具',
toolName: 'shell',
argumentSummary: '{\n "command": "npm test"\n}',
allowPermanent: false
})
harness.promptGates[0]!.resolve({ stopReason: 'end_turn' })
await running
await harness.runtime.dispose()
})
it('lists only bounded MCP schemas without exposing server secrets', async () => {
const provider = toolProvider([
mcpTool(),
{
...mcpTool('workspace_read_text'),
source: 'builtin'
}
])
const harness = setup({ toolProvider: provider })
await harness.runtime.getStatus()
await expect(
harness.extension('goodbuddy/tools/list', {
sessionId: 'session-catalog'
})
).resolves.toEqual({
tools: [
{
name: mcpTool().name,
description: mcpTool().description,
inputSchema: mcpTool().inputSchema
}
]
})
expect(
JSON.stringify(
await harness.extension('goodbuddy/tools/list', {
sessionId: 'session-catalog'
})
)
).not.toContain('secret')
await harness.runtime.dispose()
})
it('rejects MCP calls in Ask mode without approval or execution', async () => {
const provider = toolProvider()
const harness = setup({ toolProvider: provider })
const authorize = vi.fn().mockResolvedValue('once')
const running = collect(
harness.runtime.run(
request('mcp-ask', 'ask'),
new AbortController().signal,
authorize
)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(1)
)
await expect(
harness.extension('goodbuddy/tools/call', {
sessionId: 'session-1',
name: mcpTool().name,
arguments: { kind: 'cube' }
})
).rejects.toThrow('需要 Execute 模式')
expect(authorize).not.toHaveBeenCalled()
expect(provider.callTool).not.toHaveBeenCalled()
harness.promptGates[0]!.resolve({ stopReason: 'end_turn' })
await running
await harness.runtime.dispose()
})
it('requires one-time approval before calling an assigned MCP tool', async () => {
const provider = toolProvider()
const harness = setup({ toolProvider: provider })
const authorize = vi.fn().mockResolvedValue('once')
const running = collect(
harness.runtime.run(
request('mcp-execute'),
new AbortController().signal,
authorize
)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(1)
)
await expect(
harness.extension('goodbuddy/tools/call', {
sessionId: 'session-1',
name: mcpTool().name,
arguments: { kind: 'cube' }
})
).resolves.toEqual({
content: [
{ type: 'text', text: '{"asset":"cube"}' }
]
})
expect(authorize).toHaveBeenCalledTimes(1)
expect(provider.callTool).toHaveBeenCalledWith(
mcpTool().name,
{ kind: 'cube' },
expect.any(AbortSignal),
expect.objectContaining({
conversationId: 'mcp-execute',
workMode: 'execute'
})
)
harness.promptGates[0]!.resolve({ stopReason: 'end_turn' })
await running
await harness.runtime.dispose()
})
it('lists and calls a real local stdio MCP through the Main proxy', async () => {
const provider = new ModelToolProvider(process.cwd(), [
{
id: 'fbf42200-4e60-48d0-b5f2-e816db38ac54',
name: 'Local 3D Game Blueprint',
description: 'Deterministic integration fixture',
enabled: true,
allowDynamicTools: false,
assignments: ['deepseek-harness'],
secretConfigured: false,
transport: 'stdio',
command: process.execPath,
args: [
resolve('tests', 'fixtures', 'web-3d-game-mcp.mjs')
]
} satisfies ResolvedMcpServer
])
const harness = setup({
toolProvider: provider,
promptTimeoutMs: 10_000
})
const authorize = vi.fn().mockResolvedValue('once')
const running = collect(
harness.runtime.run(
request('real-mcp'),
new AbortController().signal,
authorize
)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(1)
)
try {
const catalog = await harness.extension(
'goodbuddy/tools/list',
{ sessionId: 'session-1' }
)
const tool = (
catalog as {
tools: Array<{
name: string
description: string
inputSchema: Record<string, unknown>
}>
}
).tools.find((candidate) =>
candidate.name.endsWith('_create_game_blueprint')
)
expect(tool).toMatchObject({
description: expect.stringContaining(
'offline WebGL game design'
),
inputSchema: expect.objectContaining({ type: 'object' })
})
const result = await harness.extension(
'goodbuddy/tools/call',
{
sessionId: 'session-1',
name: tool!.name,
arguments: {
theme: 'neon-ruins',
seed: 'goodbuddy-0.9.0',
targetCount: 5
}
}
)
expect(result).toMatchObject({
content: [
{
type: 'text',
text: expect.stringContaining('"title":"Prism Relay"')
}
]
})
const blueprint = JSON.parse(
(
result as {
content: [{ type: 'text'; text: string }]
}
).content[0].text
) as Record<string, unknown>
expect(blueprint).toMatchObject({
acceptance: {
testSurface: 'window.__GOODBUDDY_GAME__'
}
})
expect(authorize).toHaveBeenCalledOnce()
} finally {
harness.promptGates[0]?.resolve({ stopReason: 'end_turn' })
await running.catch(() => undefined)
await harness.runtime.dispose()
}
})
it('does not execute an MCP tool when authorization is denied', async () => {
const provider = toolProvider()
const harness = setup({ toolProvider: provider })
const authorize = vi.fn().mockResolvedValue('deny')
const running = collect(
harness.runtime.run(
request('mcp-denied'),
new AbortController().signal,
authorize
)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(1)
)
await expect(
harness.extension('goodbuddy/tools/call', {
sessionId: 'session-1',
name: mcpTool().name,
arguments: { kind: 'cube' }
})
).rejects.toThrow('未获执行授权')
expect(provider.callTool).not.toHaveBeenCalled()
harness.promptGates[0]!.resolve({ stopReason: 'end_turn' })
await running
await harness.runtime.dispose()
})
it('validates MCP arguments before requesting authorization', async () => {
const provider = toolProvider()
const harness = setup({ toolProvider: provider })
const authorize = vi.fn().mockResolvedValue('once')
const running = collect(
harness.runtime.run(
request('mcp-invalid'),
new AbortController().signal,
authorize
)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(1)
)
await expect(
harness.extension('goodbuddy/tools/call', {
sessionId: 'session-1',
name: mcpTool().name,
arguments: {}
})
).rejects.toThrow('MCP 工具参数无效')
expect(authorize).not.toHaveBeenCalled()
expect(provider.callTool).not.toHaveBeenCalled()
harness.promptGates[0]!.resolve({ stopReason: 'end_turn' })
await running
await harness.runtime.dispose()
})
it('translates AbortSignal to session/cancel', async () => {
const harness = setup()
const controller = new AbortController()
const running = collect(
harness.runtime.run(request('abort'), controller.signal)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(1)
)
controller.abort(new Error('cancelled by user'))
harness.promptGates[0]!.resolve({ stopReason: 'cancelled' })
await expect(running).rejects.toThrow('cancelled by user')
expect(harness.notifications).toContainEqual({
method: 'session/cancel',
params: { sessionId: 'session-1' }
})
await harness.runtime.dispose()
})
it('fails on bounded stderr overflow without exposing stderr text', async () => {
const harness = setup()
const running = collect(
harness.runtime.run(
request('stderr'),
new AbortController().signal
)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(1)
)
const writer = harness.stderr.writable.getWriter()
await writer.write(
new TextEncoder().encode('private-secret-is-too-long')
)
await vi.waitFor(() =>
expect(harness.child.terminate).toHaveBeenCalled()
)
harness.promptGates[0]!.resolve({ stopReason: 'end_turn' })
await expect(running).rejects.toThrow('stderr 超过 16 字节')
await expect(running).rejects.not.toThrow('private-secret')
await harness.runtime.dispose()
})
it('reports process exit and fully disposes the connection and child', async () => {
const harness = setup()
await expect(harness.runtime.getStatus()).resolves.toMatchObject({
available: true
})
harness.exit.resolve({ exitCode: 9 })
await vi.waitFor(async () => {
const status = await harness.runtime.getStatus()
expect(status).toMatchObject({
available: false,
detail: 'DeepSeek Harness 进程意外退出(code 9'
})
})
await harness.runtime.dispose()
expect(harness.child.terminate).toHaveBeenCalled()
})
it('fails closed when the required bridge handshake is unavailable', async () => {
const harness = setup()
harness.agent.extMethod.mockRejectedValueOnce(
new Error('method not found')
)
await expect(harness.runtime.getStatus()).resolves.toMatchObject({
available: false,
detail: 'method not found'
})
expect(harness.child.terminate).toHaveBeenCalled()
})
it('times out a prompt, cancels it, and bounds disposal wait', async () => {
const harness = setup()
const running = collect(
harness.runtime.run(
request('timeout'),
new AbortController().signal
)
)
await expect(running).rejects.toThrow(
'DeepSeek Harness 请求超时'
)
expect(harness.notifications).toContainEqual({
method: 'session/cancel',
params: { sessionId: 'session-1' }
})
await expect(harness.runtime.dispose()).resolves.toBeUndefined()
})
})
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,147 @@
import { EventEmitter } from 'node:events'
import { mkdir, mkdtemp, realpath, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PassThrough } from 'node:stream'
import { describe, expect, it, vi } from 'vitest'
import {
DEEPSEEK_HARNESS_CONTROL_PROTOCOL,
DEEPSEEK_HARNESS_CONTROL_VERSION,
DEEPSEEK_HARNESS_CREDENTIAL_REF,
createDeepSeekHarnessUtilityLauncher,
parseHarnessControlMessage
} from './deepseek-harness-utility-launcher'
class FakeUtility extends EventEmitter {
readonly messages: unknown[] = []
readonly stderr = new PassThrough()
readonly pid = 123
killed = false
postMessage(message: unknown): void {
this.messages.push(message)
}
kill(): boolean {
this.killed = true
return true
}
}
async function fixture() {
const root = await realpath(
await mkdtemp(join(tmpdir(), 'goodbuddy-harness-launcher-'))
)
const workspace = join(root, 'workspace')
const dshHome = join(root, 'home')
const hostPath = join(
root,
'deepseek-harness-host-bootstrap.js'
)
await Promise.all([
mkdir(workspace),
mkdir(dshHome),
writeFile(hostPath, '', 'utf8')
])
return {
dshHome,
hostPath,
launchOptions: {
cwd: workspace,
signal: new AbortController().signal,
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat',
credentialRefs: [DEEPSEEK_HARNESS_CREDENTIAL_REF],
skillPackages: []
}
}
}
describe('DeepSeek Harness utility launcher', () => {
it('accepts only strict control messages and secret-free config', () => {
expect(
parseHarnessControlMessage({
protocol: DEEPSEEK_HARNESS_CONTROL_PROTOCOL,
version: DEEPSEEK_HARNESS_CONTROL_VERSION,
type: 'ready'
})
).toMatchObject({ type: 'ready' })
expect(
parseHarnessControlMessage({
protocol: DEEPSEEK_HARNESS_CONTROL_PROTOCOL,
version: DEEPSEEK_HARNESS_CONTROL_VERSION,
type: 'ready',
apiKey: 'must-not-pass'
})
).toBeUndefined()
})
it('waits for Host readiness and sends no credential value', async () => {
const { dshHome, hostPath, launchOptions } = await fixture()
const utility = new FakeUtility()
const fork = vi.fn(() => utility as never)
const launcher = createDeepSeekHarnessUtilityLauncher({
bundledHostPath: hostPath,
dshHome,
environment: { PATH: 'C:\\Tools' },
fork
})
const launching = launcher(launchOptions)
await vi.waitFor(() =>
expect(utility.messages).toHaveLength(1)
)
expect(JSON.stringify(utility.messages[0])).not.toContain(
'secret'
)
expect(utility.messages[0]).toMatchObject({
type: 'start',
config: {
baseUrl: 'https://api.deepseek.com',
credentialRefs: [DEEPSEEK_HARNESS_CREDENTIAL_REF]
}
})
utility.emit('message', {
protocol: DEEPSEEK_HARNESS_CONTROL_PROTOCOL,
version: DEEPSEEK_HARNESS_CONTROL_VERSION,
type: 'ready'
})
await expect(launching).resolves.toMatchObject({
stdin: expect.any(WritableStream),
stdout: expect.any(ReadableStream)
})
expect(fork).toHaveBeenCalledWith(
hostPath,
[],
expect.objectContaining({
cwd: launchOptions.cwd,
stdio: ['ignore', 'ignore', 'pipe']
})
)
})
it('fails closed on an invalid Host startup message', async () => {
const { dshHome, hostPath, launchOptions } = await fixture()
const utility = new FakeUtility()
const terminateProcess = vi.fn(() => {
utility.killed = true
})
const launcher = createDeepSeekHarnessUtilityLauncher({
bundledHostPath: hostPath,
dshHome,
environment: {},
fork: () => utility as never,
terminateProcess
})
const launching = launcher(launchOptions)
await vi.waitFor(() =>
expect(utility.messages).toHaveLength(1)
)
utility.emit('message', { type: 'ready' })
await expect(launching).rejects.toThrow('启动协议无效')
expect(terminateProcess).toHaveBeenCalledOnce()
})
})
@@ -0,0 +1,369 @@
import { Readable } from 'node:stream'
import { realpath, stat } from 'node:fs/promises'
import { isAbsolute } from 'node:path'
import type { UtilityProcess } from 'electron'
import { z } from 'zod'
import type {
DeepSeekHarnessChild,
DeepSeekHarnessLaunchOptions
} from './deepseek-harness-runtime'
import { createDeepSeekHarnessUtilityChild } from './deepseek-harness-utility-transport'
export const DEEPSEEK_HARNESS_CONTROL_PROTOCOL =
'goodbuddy.deepseek-harness.control'
export const DEEPSEEK_HARNESS_CONTROL_VERSION = 1
export const DEEPSEEK_HARNESS_HOST_VERSION = '0.1.0-rc.6'
export const DEEPSEEK_HARNESS_CREDENTIAL_REF =
'GOODBUDDY_DEEPSEEK_API_KEY'
const sandboxSchema = z
.object({
provider: z.string().min(1).max(64),
enforcement: z.enum(['full', 'partial'])
})
.strict()
const skillPackageSchema = z
.object({
id: z
.string()
.min(1)
.max(128)
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u),
directory: z.string().min(1).max(32_768).refine(isAbsolute)
})
.strict()
export const controlledHarnessHostConfigSchema = z
.object({
workspace: z.string().min(1).max(32_768).refine(isAbsolute),
dshHome: z.string().min(1).max(32_768).refine(isAbsolute),
baseUrl: z
.url()
.max(2_048)
.refine((value) => {
const url = new URL(value)
return (
url.protocol === 'https:' &&
url.hostname.toLowerCase() === 'api.deepseek.com' &&
!url.username &&
!url.password
)
}),
api: z.literal('openai-completions'),
provider: z.literal('goodbuddy'),
model: z.string().min(1).max(128),
harnessVersion: z.literal(DEEPSEEK_HARNESS_HOST_VERSION),
sandbox: sandboxSchema,
credentialRefs: z
.tuple([z.literal(DEEPSEEK_HARNESS_CREDENTIAL_REF)])
.readonly(),
skillPackages: z.array(skillPackageSchema).max(64),
maxFrameBytes: z.literal(1024 * 1024)
})
.strict()
export type ControlledHarnessBootstrapConfig = z.infer<
typeof controlledHarnessHostConfigSchema
>
export type DeepSeekHarnessControlMessage =
| {
protocol: typeof DEEPSEEK_HARNESS_CONTROL_PROTOCOL
version: typeof DEEPSEEK_HARNESS_CONTROL_VERSION
type: 'start'
config: ControlledHarnessBootstrapConfig
}
| {
protocol: typeof DEEPSEEK_HARNESS_CONTROL_PROTOCOL
version: typeof DEEPSEEK_HARNESS_CONTROL_VERSION
type: 'ready'
}
| {
protocol: typeof DEEPSEEK_HARNESS_CONTROL_PROTOCOL
version: typeof DEEPSEEK_HARNESS_CONTROL_VERSION
type: 'fatal'
code: string
}
export function parseHarnessControlMessage(
value: unknown
): DeepSeekHarnessControlMessage | undefined {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value)
) {
return undefined
}
const record = value as Record<string, unknown>
if (
record.protocol !== DEEPSEEK_HARNESS_CONTROL_PROTOCOL ||
record.version !== DEEPSEEK_HARNESS_CONTROL_VERSION
) {
return undefined
}
if (record.type === 'ready' && Object.keys(record).length === 3) {
return record as DeepSeekHarnessControlMessage
}
if (
record.type === 'fatal' &&
Object.keys(record).length === 4 &&
typeof record.code === 'string' &&
/^[A-Z][A-Z0-9_]{0,63}$/u.test(record.code)
) {
return record as DeepSeekHarnessControlMessage
}
if (
record.type === 'start' &&
Object.keys(record).length === 4
) {
const parsed = controlledHarnessHostConfigSchema.safeParse(
record.config
)
return parsed.success
? ({
protocol: DEEPSEEK_HARNESS_CONTROL_PROTOCOL,
version: DEEPSEEK_HARNESS_CONTROL_VERSION,
type: 'start',
config: parsed.data
} satisfies DeepSeekHarnessControlMessage)
: undefined
}
return undefined
}
export type DeepSeekHarnessFork = (
modulePath: string,
args: string[],
options: {
cwd: string
env: NodeJS.ProcessEnv
serviceName: string
stdio: ['ignore', 'ignore', 'pipe']
}
) => UtilityProcess
export type DeepSeekHarnessUtilityLauncherOptions = {
bundledHostPath: string
dshHome: string
environment: NodeJS.ProcessEnv
fork: DeepSeekHarnessFork
terminateProcess?: (utility: UtilityProcess) => void
startupTimeoutMs?: number
}
function expectedSandbox(): ControlledHarnessBootstrapConfig['sandbox'] {
return process.platform === 'win32'
? { provider: 'windows-acl', enforcement: 'partial' }
: process.platform === 'darwin'
? { provider: 'seatbelt', enforcement: 'full' }
: { provider: 'local-linux', enforcement: 'full' }
}
function hasControlCharacter(value: string): boolean {
for (const character of value) {
const codePoint = character.codePointAt(0)
if (
codePoint !== undefined &&
(codePoint <= 0x1f || codePoint === 0x7f)
) {
return true
}
}
return false
}
export function createDeepSeekHarnessUtilityLauncher(
launcherOptions: DeepSeekHarnessUtilityLauncherOptions
): (options: DeepSeekHarnessLaunchOptions) => Promise<DeepSeekHarnessChild> {
return async (options) => {
options.signal.throwIfAborted()
const hostPath = launcherOptions.bundledHostPath
if (!isAbsolute(hostPath)) {
throw new Error('DeepSeek Harness Host 路径必须为绝对路径')
}
if (!isAbsolute(options.cwd) || !isAbsolute(launcherOptions.dshHome)) {
throw new Error(
'DeepSeek Harness 工作区和隔离目录必须为绝对路径'
)
}
if (
options.model.length === 0 ||
options.model.length > 128 ||
hasControlCharacter(options.model)
) {
throw new Error('DeepSeek Harness 模型名称无效')
}
const canonicalSkillPackages = await Promise.all(
options.skillPackages.map(async (skill) => {
const directory = await realpath(skill.directory)
const metadata = await stat(directory)
if (!metadata.isDirectory()) {
throw new Error(
'DeepSeek Harness Skill 路径必须为目录'
)
}
return {
id: skill.id,
directory
}
})
)
const [canonicalHostPath, canonicalWorkspace, canonicalDshHome] =
await Promise.all([
realpath(hostPath),
realpath(options.cwd),
realpath(launcherOptions.dshHome)
])
const [hostMetadata, workspaceMetadata, homeMetadata] =
await Promise.all([
stat(canonicalHostPath),
stat(canonicalWorkspace),
stat(canonicalDshHome)
])
if (
!hostMetadata.isFile() ||
!workspaceMetadata.isDirectory() ||
!homeMetadata.isDirectory()
) {
throw new Error(
'DeepSeek Harness Host、工作区或隔离目录类型无效'
)
}
const sandbox = expectedSandbox()
if (
options.requiredSandboxEnforcement === 'full' &&
sandbox.enforcement !== 'full'
) {
throw new Error(
'DeepSeek Harness 当前平台只能提供部分沙箱强制'
)
}
if (
options.baseUrl !== 'https://api.deepseek.com' &&
options.baseUrl !== 'https://api.deepseek.com/'
) {
throw new Error(
'DeepSeek Harness 仅允许 api.deepseek.com'
)
}
if (
options.credentialRefs.length !== 1 ||
options.credentialRefs[0] !==
DEEPSEEK_HARNESS_CREDENTIAL_REF
) {
throw new Error('DeepSeek Harness 凭据引用不受信任')
}
options.signal.throwIfAborted()
const utility = launcherOptions.fork(canonicalHostPath, [], {
cwd: canonicalWorkspace,
env: launcherOptions.environment,
serviceName: 'GoodBuddy DeepSeek Harness Host',
stdio: ['ignore', 'ignore', 'pipe']
})
let terminated = false
const terminate = (): void => {
if (terminated) {
return
}
terminated = true
if (launcherOptions.terminateProcess) {
launcherOptions.terminateProcess(utility)
} else {
utility.kill()
}
}
const startupTimeoutMs =
launcherOptions.startupTimeoutMs ?? 10_000
let timer: ReturnType<typeof setTimeout> | undefined
let onAbort: (() => void) | undefined
try {
await new Promise<void>((resolve, reject) => {
const cleanup = (): void => {
if (timer) {
clearTimeout(timer)
}
if (onAbort) {
options.signal.removeEventListener('abort', onAbort)
}
utility.removeListener('message', onMessage)
utility.removeListener('exit', onExit)
}
const fail = (error: Error): void => {
cleanup()
terminate()
reject(error)
}
const onMessage = (message: unknown): void => {
const control = parseHarnessControlMessage(message)
if (!control) {
fail(new Error('DeepSeek Harness Host 启动协议无效'))
return
}
if (control.type === 'ready') {
cleanup()
resolve()
} else if (control.type === 'fatal') {
fail(
new Error(
`DeepSeek Harness Host 启动失败(${control.code}`
)
)
}
}
const onExit = (exitCode: number): void => {
fail(
new Error(
`DeepSeek Harness Host 启动前退出(code ${exitCode}`
)
)
}
onAbort = () => {
fail(
options.signal.reason instanceof Error
? options.signal.reason
: new Error('DeepSeek Harness Host 启动已取消')
)
}
utility.on('message', onMessage)
utility.on('exit', onExit)
options.signal.addEventListener('abort', onAbort, {
once: true
})
timer = setTimeout(
() =>
fail(new Error('DeepSeek Harness Host 启动握手超时')),
startupTimeoutMs
)
const config = controlledHarnessHostConfigSchema.parse({
workspace: canonicalWorkspace,
dshHome: canonicalDshHome,
baseUrl: options.baseUrl,
api: 'openai-completions',
provider: 'goodbuddy',
model: options.model,
harnessVersion: DEEPSEEK_HARNESS_HOST_VERSION,
sandbox,
credentialRefs: [DEEPSEEK_HARNESS_CREDENTIAL_REF],
skillPackages: canonicalSkillPackages,
maxFrameBytes: 1024 * 1024
})
utility.postMessage({
protocol: DEEPSEEK_HARNESS_CONTROL_PROTOCOL,
version: DEEPSEEK_HARNESS_CONTROL_VERSION,
type: 'start',
config
} satisfies DeepSeekHarnessControlMessage)
})
return createDeepSeekHarnessUtilityChild(utility, {
stderrToWeb: (stderr) =>
Readable.toWeb(stderr) as ReadableStream<Uint8Array>,
terminateProcess: terminate
})
} catch (error) {
terminate()
throw error
}
}
}
@@ -0,0 +1,365 @@
import { describe, expect, it, vi } from 'vitest'
import {
DEEPSEEK_HARNESS_BYTE_PROTOCOL,
DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION,
DEEPSEEK_HARNESS_MAX_CHUNK_BYTES,
createDeepSeekHarnessHostTransport,
createDeepSeekHarnessUtilityChild,
type DeepSeekHarnessParentPortLike
} from './deepseek-harness-utility-transport'
type Listener = (value: unknown) => void
class LinkedPort {
peer?: LinkedPort
readonly sent: unknown[] = []
private readonly listeners = new Set<Listener>()
postMessage(message: unknown): void {
this.sent.push(message)
queueMicrotask(() => {
for (const listener of this.peer?.listeners ?? []) {
listener(message)
}
})
}
subscribe(listener: Listener): () => void {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}
}
class FakeUtility {
readonly port = new LinkedPort()
readonly stderr = 'node-stderr'
readonly kill = vi.fn(() => true)
private readonly listeners = {
message: new Set<(message: unknown) => void>(),
exit: new Set<(exitCode: number) => void>()
}
constructor(hostPort: LinkedPort) {
this.port.peer = hostPort
hostPort.peer = this.port
this.port.subscribe((message) => {
for (const listener of this.listeners.message) {
listener(message)
}
})
}
postMessage(message: unknown): void {
this.port.postMessage(message)
}
on(event: 'message', listener: (message: unknown) => void): void
on(event: 'exit', listener: (exitCode: number) => void): void
on(
event: keyof typeof this.listeners,
listener: ((message: unknown) => void) | ((exitCode: number) => void)
): void {
if (event === 'message') {
this.listeners.message.add(listener as (message: unknown) => void)
} else {
this.listeners.exit.add(listener as (exitCode: number) => void)
}
}
removeListener(event: 'message', listener: (message: unknown) => void): void
removeListener(event: 'exit', listener: (exitCode: number) => void): void
removeListener(
event: keyof typeof this.listeners,
listener: ((message: unknown) => void) | ((exitCode: number) => void)
): void {
if (event === 'message') {
this.listeners.message.delete(listener as (message: unknown) => void)
} else {
this.listeners.exit.delete(listener as (exitCode: number) => void)
}
}
emitMessage(message: unknown): void {
for (const listener of this.listeners.message) {
listener(message)
}
}
emitExit(exitCode: number): void {
for (const listener of this.listeners.exit) {
listener(exitCode)
}
}
}
function asParentPort(port: LinkedPort): DeepSeekHarnessParentPortLike {
const wrapped = new Map<Listener, () => void>()
return {
postMessage: (message) => port.postMessage(message),
on: (_event, listener) => {
const adapter: Listener = (data) => listener({ data })
wrapped.set(listener as Listener, port.subscribe(adapter))
},
removeListener: (_event, listener) => {
wrapped.get(listener as Listener)?.()
wrapped.delete(listener as Listener)
}
}
}
function setup() {
const hostPort = new LinkedPort()
const utility = new FakeUtility(hostPort)
const stderr = new ReadableStream<Uint8Array>()
const stderrToWeb = vi.fn(() => stderr)
const child = createDeepSeekHarnessUtilityChild(utility, { stderrToWeb })
const host = createDeepSeekHarnessHostTransport(asParentPort(hostPort))
return { child, host, hostPort, utility, stderr, stderrToWeb }
}
const tick = () => new Promise<void>((resolve) => queueMicrotask(resolve))
describe('DeepSeek Harness utility byte transport', () => {
it('ignores trusted control-plane messages that share the UtilityProcess port', async () => {
const { child, hostPort, utility } = setup()
await tick()
utility.kill.mockClear()
utility.emitMessage({
protocol: 'goodbuddy.deepseek-harness.control',
version: 1,
type: 'ready'
})
const reader = child.stdout.getReader()
const reading = reader.read()
hostPort.postMessage({
protocol: DEEPSEEK_HARNESS_BYTE_PROTOCOL,
version: DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION,
type: 'data',
stream: 'stdout',
seq: 0,
bytes: Uint8Array.of(7)
})
await expect(reading).resolves.toEqual({
done: false,
value: Uint8Array.of(7)
})
expect(utility.kill).toHaveBeenCalledOnce()
})
it('fails closed for malformed control-plane lookalikes', async () => {
const { child, utility } = setup()
const reader = child.stdout.getReader()
utility.emitMessage({
protocol: 'goodbuddy.deepseek-harness.control',
version: 1,
type: 'ready',
unexpected: true
})
await expect(reader.read()).rejects.toThrow('PROTOCOL_VIOLATION')
expect(utility.kill).toHaveBeenCalledOnce()
})
it('transports bytes in both directions and adapts stderr and exit', async () => {
const { child, host, utility, stderr, stderrToWeb } = setup()
const childWriter = child.stdin.getWriter()
const hostInput = host.stdin.getReader()
const hostWriter = host.stdout.getWriter()
const childOutput = child.stdout.getReader()
await childWriter.write(Uint8Array.of(1, 2, 3))
await expect(hostInput.read()).resolves.toEqual({
done: false,
value: Uint8Array.of(1, 2, 3)
})
await hostWriter.write(Uint8Array.of(4, 5))
await expect(childOutput.read()).resolves.toEqual({
done: false,
value: Uint8Array.of(4, 5)
})
expect(stderrToWeb).toHaveBeenCalledWith('node-stderr')
expect(child.stderr).toBe(stderr)
utility.emitExit(7)
await expect(child.exited).resolves.toEqual({ exitCode: 7 })
})
it('splits chunks at 64 KiB and waits for ACK backpressure', async () => {
const { child, host, utility } = setup()
const writer = child.stdin.getWriter()
const bytes = new Uint8Array(DEEPSEEK_HARNESS_MAX_CHUNK_BYTES + 3)
bytes.fill(9)
let settled = false
const writing = writer.write(bytes).then(() => {
settled = true
})
await tick()
expect(settled).toBe(false)
expect(utility.port.sent).toHaveLength(1)
expect(utility.port.sent[0]).toMatchObject({
type: 'data',
seq: 0,
bytes: expect.objectContaining({
byteLength: DEEPSEEK_HARNESS_MAX_CHUNK_BYTES
})
})
const reader = host.stdin.getReader()
expect((await reader.read()).value).toHaveLength(
DEEPSEEK_HARNESS_MAX_CHUNK_BYTES
)
await tick()
expect(utility.port.sent).toHaveLength(2)
expect(utility.port.sent[1]).toMatchObject({
type: 'data',
seq: 1,
bytes: Uint8Array.of(9, 9, 9)
})
expect((await reader.read()).value).toEqual(Uint8Array.of(9, 9, 9))
await writing
expect(settled).toBe(true)
})
it('applies bounded receiver backpressure until the queued chunk is read', async () => {
const { child, host, utility } = setup()
const writer = child.stdin.getWriter()
await writer.write(Uint8Array.of(1))
let secondSettled = false
const second = writer.write(Uint8Array.of(2)).then(() => {
secondSettled = true
})
await tick()
expect(secondSettled).toBe(false)
expect(utility.port.sent).toHaveLength(2)
const reader = host.stdin.getReader()
await expect(reader.read()).resolves.toMatchObject({
value: Uint8Array.of(1)
})
await tick()
await second
expect(secondSettled).toBe(true)
})
it.each([
['unknown message', { surprise: true }],
[
'unknown type',
{
protocol: DEEPSEEK_HARNESS_BYTE_PROTOCOL,
version: DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION,
type: 'wat'
}
],
[
'extra field',
{
protocol: DEEPSEEK_HARNESS_BYTE_PROTOCOL,
version: DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION,
type: 'ack',
stream: 'stdin',
seq: 0,
extra: true
}
],
[
'oversized chunk',
{
protocol: DEEPSEEK_HARNESS_BYTE_PROTOCOL,
version: DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION,
type: 'data',
stream: 'stdout',
seq: 0,
bytes: new Uint8Array(DEEPSEEK_HARNESS_MAX_CHUNK_BYTES + 1)
}
]
])('fails closed for %s without including payloads in errors', async (_, message) => {
const { child, utility } = setup()
const reader = child.stdout.getReader()
utility.emitMessage(message)
await expect(reader.read()).rejects.toThrow(
'DeepSeek Harness byte transport failed (PROTOCOL_VIOLATION)'
)
expect(utility.kill).toHaveBeenCalledTimes(1)
expect(String(await reader.closed.catch((error) => error))).not.toContain(
'surprise'
)
})
it('fails closed for duplicate and out-of-order sequence numbers', async () => {
const first = setup()
first.utility.emitMessage({
protocol: DEEPSEEK_HARNESS_BYTE_PROTOCOL,
version: DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION,
type: 'data',
stream: 'stdout',
seq: 1,
bytes: Uint8Array.of(1)
})
await expect(first.child.stdout.getReader().read()).rejects.toThrow(
'PROTOCOL_VIOLATION'
)
expect(first.utility.kill).toHaveBeenCalledOnce()
const second = setup()
const reader = second.child.stdout.getReader()
second.utility.emitMessage({
protocol: DEEPSEEK_HARNESS_BYTE_PROTOCOL,
version: DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION,
type: 'data',
stream: 'stdout',
seq: 0,
bytes: Uint8Array.of(1)
})
await reader.read()
second.utility.emitMessage({
protocol: DEEPSEEK_HARNESS_BYTE_PROTOCOL,
version: DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION,
type: 'data',
stream: 'stdout',
seq: 0,
bytes: Uint8Array.of(1)
})
await expect(reader.read()).rejects.toThrow('PROTOCOL_VIOLATION')
expect(second.utility.kill).toHaveBeenCalledOnce()
})
it('propagates close and cancellation idempotently', async () => {
const { child, host, utility } = setup()
const writer = child.stdin.getWriter()
const reader = host.stdin.getReader()
const closing = writer.close()
await expect(reader.read()).resolves.toEqual({
done: true,
value: undefined
})
await closing
const childOutput = child.stdout.getReader()
await childOutput.cancel()
const hostWriter = host.stdout.getWriter()
await expect(hostWriter.write(Uint8Array.of(8))).rejects.toThrow(
'REMOTE_CANCELLED'
)
child.terminate()
child.terminate()
expect(utility.kill).toHaveBeenCalledTimes(1)
})
it('cancels a chunk waiting behind the bounded readable queue', async () => {
const { child, host } = setup()
const writer = child.stdin.getWriter()
await writer.write(Uint8Array.of(1))
const pendingWrite = writer.write(Uint8Array.of(2))
await tick()
await host.stdin.cancel()
await expect(pendingWrite).rejects.toThrow('REMOTE_CANCELLED')
})
})
@@ -0,0 +1,678 @@
import type { DeepSeekHarnessChild } from './deepseek-harness-runtime'
export const DEEPSEEK_HARNESS_BYTE_PROTOCOL =
'goodbuddy.deepseek-harness.byte-stream'
export const DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION = 1
export const DEEPSEEK_HARNESS_MAX_CHUNK_BYTES = 64 * 1024
type StreamName = 'stdin' | 'stdout'
type ForwardType = 'data' | 'close' | 'abort'
type MessageBase = {
protocol: typeof DEEPSEEK_HARNESS_BYTE_PROTOCOL
version: typeof DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION
stream: StreamName
seq: number
}
type ProtocolMessage =
| (MessageBase & {
type: 'data'
bytes: Uint8Array
})
| (MessageBase & { type: 'close' })
| (MessageBase & { type: 'abort' })
| (MessageBase & { type: 'ack' })
| (MessageBase & { type: 'cancel' })
| {
protocol: typeof DEEPSEEK_HARNESS_BYTE_PROTOCOL
version: typeof DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION
type: 'fail'
}
type Deferred = {
readonly promise: Promise<void>
resolve(): void
reject(error: Error): void
}
type PendingSend = {
readonly seq: number
readonly deferred: Deferred
}
type SenderState = {
readonly stream: StreamName
nextSeq: number
pending?: PendingSend
finished: boolean
cancelled: boolean
controller?: WritableStreamDefaultController
}
type ReceiverState = {
readonly stream: StreamName
nextSeq: number
pendingBytes?: Uint8Array
finished: boolean
cancelled: boolean
controller?: ReadableStreamDefaultController<Uint8Array>
}
type MessagePortAdapter = {
postMessage(message: ProtocolMessage): void
subscribe(listener: (message: unknown) => void): () => void
}
type EndpointOptions = {
readonly senderStream: StreamName
readonly receiverStream: StreamName
readonly onFailure?: () => void
}
const CONTROL_PROTOCOL = 'goodbuddy.deepseek-harness.control'
const PROTOCOL_KEYS = ['protocol', 'version', 'type'] as const
const STREAM_KEYS = [...PROTOCOL_KEYS, 'stream', 'seq'] as const
const DATA_KEYS = [...STREAM_KEYS, 'bytes'] as const
const MAX_SEQUENCE = Number.MAX_SAFE_INTEGER
class ByteTransportError extends Error {
constructor(code: string) {
super(`DeepSeek Harness byte transport failed (${code})`)
this.name = 'ByteTransportError'
}
}
function deferred(): Deferred {
let resolvePromise: (() => void) | undefined
let rejectPromise: ((error: Error) => void) | undefined
const promise = new Promise<void>((resolve, reject) => {
resolvePromise = resolve
rejectPromise = reject
})
return {
promise,
resolve: () => resolvePromise?.(),
reject: (error) => rejectPromise?.(error)
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return false
}
const prototype = Object.getPrototypeOf(value)
return prototype === Object.prototype || prototype === null
}
function hasExactKeys(
value: Record<string, unknown>,
expected: readonly string[]
): boolean {
const keys = Object.keys(value)
return (
keys.length === expected.length &&
expected.every((key) => Object.prototype.hasOwnProperty.call(value, key))
)
}
function isSequence(value: unknown): value is number {
return (
typeof value === 'number' &&
Number.isSafeInteger(value) &&
value >= 0 &&
value <= MAX_SEQUENCE
)
}
function parseMessage(value: unknown): ProtocolMessage | undefined {
if (
!isRecord(value) ||
value.protocol !== DEEPSEEK_HARNESS_BYTE_PROTOCOL ||
value.version !== DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION ||
typeof value.type !== 'string'
) {
return undefined
}
if (value.type === 'fail') {
return hasExactKeys(value, PROTOCOL_KEYS)
? (value as ProtocolMessage)
: undefined
}
if (
!['data', 'close', 'abort', 'ack', 'cancel'].includes(value.type) ||
(value.stream !== 'stdin' && value.stream !== 'stdout') ||
!isSequence(value.seq)
) {
return undefined
}
if (value.type === 'data') {
if (
!hasExactKeys(value, DATA_KEYS) ||
!(value.bytes instanceof Uint8Array) ||
value.bytes.byteLength === 0 ||
value.bytes.byteLength > DEEPSEEK_HARNESS_MAX_CHUNK_BYTES
) {
return undefined
}
return value as ProtocolMessage
}
return hasExactKeys(value, STREAM_KEYS)
? (value as ProtocolMessage)
: undefined
}
function isControlMessage(value: unknown): boolean {
if (
!isRecord(value) ||
value.protocol !== CONTROL_PROTOCOL ||
value.version !== 1 ||
typeof value.type !== 'string'
) {
return false
}
if (value.type === 'ready') {
return hasExactKeys(value, PROTOCOL_KEYS)
}
if (value.type === 'fatal') {
return (
hasExactKeys(value, [...PROTOCOL_KEYS, 'code']) &&
typeof value.code === 'string' &&
/^[A-Z][A-Z0-9_]{0,63}$/u.test(value.code)
)
}
return (
value.type === 'start' &&
hasExactKeys(value, [...PROTOCOL_KEYS, 'config']) &&
isRecord(value.config)
)
}
class ByteTransportEndpoint {
readonly writable: WritableStream<Uint8Array>
readonly readable: ReadableStream<Uint8Array>
private readonly sender: SenderState
private readonly receiver: ReceiverState
private readonly unsubscribe: () => void
private failed = false
private disposed = false
constructor(
private readonly port: MessagePortAdapter,
private readonly options: EndpointOptions
) {
this.sender = {
stream: options.senderStream,
nextSeq: 0,
finished: false,
cancelled: false
}
this.receiver = {
stream: options.receiverStream,
nextSeq: 0,
finished: false,
cancelled: false
}
this.writable = new WritableStream<Uint8Array>(
{
start: (controller) => {
this.sender.controller = controller
},
write: async (chunk) => {
if (!(chunk instanceof Uint8Array)) {
throw new ByteTransportError('INVALID_WRITE')
}
for (
let offset = 0;
offset < chunk.byteLength;
offset += DEEPSEEK_HARNESS_MAX_CHUNK_BYTES
) {
const bytes = chunk.slice(
offset,
offset + DEEPSEEK_HARNESS_MAX_CHUNK_BYTES
)
await this.sendForward('data', bytes)
}
},
close: () => this.sendForward('close'),
abort: () => this.sendForward('abort')
},
new CountQueuingStrategy({ highWaterMark: 1 })
)
this.readable = new ReadableStream<Uint8Array>(
{
start: (controller) => {
this.receiver.controller = controller
},
pull: () => {
this.flushReceiver()
},
cancel: () => {
this.cancelReceiver()
}
},
new CountQueuingStrategy({ highWaterMark: 1 })
)
this.unsubscribe = this.port.subscribe((message) => {
if (isControlMessage(message)) {
return
}
this.handleMessage(message)
})
}
dispose(code = 'CLOSED'): void {
if (this.disposed) {
return
}
this.disposed = true
this.unsubscribe()
const error = new ByteTransportError(code)
this.sender.pending?.deferred.reject(error)
this.sender.pending = undefined
try {
this.sender.controller?.error(error)
} catch {
// The stream may already be closed.
}
try {
this.receiver.controller?.error(error)
} catch {
// The stream may already be closed.
}
}
private fail(code: string, notifyPeer: boolean): void {
if (this.failed || this.disposed) {
return
}
this.failed = true
if (notifyPeer) {
try {
this.port.postMessage({
protocol: DEEPSEEK_HARNESS_BYTE_PROTOCOL,
version: DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION,
type: 'fail'
})
} catch {
// The local endpoint still closes if peer notification fails.
}
}
this.dispose(code)
this.options.onFailure?.()
}
private post(message: ProtocolMessage): boolean {
if (this.failed || this.disposed) {
return false
}
try {
this.port.postMessage(message)
return true
} catch {
this.fail('CHANNEL_FAILURE', false)
return false
}
}
private async sendForward(
type: ForwardType,
bytes?: Uint8Array
): Promise<void> {
if (
this.failed ||
this.disposed ||
this.sender.finished ||
this.sender.cancelled
) {
throw new ByteTransportError(
this.sender.cancelled ? 'REMOTE_CANCELLED' : 'CLOSED'
)
}
if (this.sender.pending || this.sender.nextSeq > MAX_SEQUENCE) {
this.fail('LOCAL_STATE', true)
throw new ByteTransportError('LOCAL_STATE')
}
const waiting = deferred()
const seq = this.sender.nextSeq
this.sender.pending = { seq, deferred: waiting }
const message: ProtocolMessage =
type === 'data'
? {
protocol: DEEPSEEK_HARNESS_BYTE_PROTOCOL,
version: DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION,
type,
stream: this.sender.stream,
seq,
bytes: bytes as Uint8Array
}
: {
protocol: DEEPSEEK_HARNESS_BYTE_PROTOCOL,
version: DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION,
type,
stream: this.sender.stream,
seq
}
if (!this.post(message)) {
await waiting.promise
return
}
await waiting.promise
if (type !== 'data') {
this.sender.finished = true
}
}
private handleMessage(rawMessage: unknown): void {
const message = parseMessage(rawMessage)
if (!message) {
this.fail('PROTOCOL_VIOLATION', true)
return
}
if (message.type === 'fail') {
this.fail('REMOTE_FAILURE', false)
return
}
if (message.type === 'ack') {
this.handleAck(message)
return
}
if (message.type === 'cancel') {
this.handleCancel(message)
return
}
this.handleForward(message)
}
private handleAck(
message: MessageBase & { type: 'ack' }
): void {
const pending = this.sender.pending
if (
message.stream !== this.sender.stream ||
!pending ||
message.seq !== pending.seq
) {
this.fail('PROTOCOL_VIOLATION', true)
return
}
this.sender.pending = undefined
this.sender.nextSeq += 1
pending.deferred.resolve()
}
private handleCancel(
message: MessageBase & { type: 'cancel' }
): void {
const pending = this.sender.pending
if (
message.stream !== this.sender.stream ||
this.sender.finished ||
this.sender.cancelled ||
message.seq !== (pending?.seq ?? this.sender.nextSeq)
) {
this.fail('PROTOCOL_VIOLATION', true)
return
}
this.sender.cancelled = true
this.sender.pending = undefined
const error = new ByteTransportError('REMOTE_CANCELLED')
pending?.deferred.reject(error)
try {
this.sender.controller?.error(error)
} catch {
// The stream may already be closed.
}
}
private handleForward(
message: Extract<ProtocolMessage, { type: ForwardType }>
): void {
if (
message.stream !== this.receiver.stream ||
this.receiver.finished ||
this.receiver.cancelled ||
message.seq !== this.receiver.nextSeq
) {
this.fail('PROTOCOL_VIOLATION', true)
return
}
this.receiver.nextSeq += 1
if (message.type === 'data') {
if (this.receiver.pendingBytes) {
this.fail('PROTOCOL_VIOLATION', true)
return
}
this.receiver.pendingBytes = message.bytes.slice()
this.flushReceiver()
return
}
this.receiver.finished = true
if (message.type === 'close') {
try {
this.receiver.controller?.close()
} catch {
this.fail('LOCAL_STATE', true)
return
}
} else {
try {
this.receiver.controller?.error(
new ByteTransportError('REMOTE_ABORTED')
)
} catch {
// The stream may already have been cancelled.
}
}
this.sendAck(message.seq)
}
private flushReceiver(): void {
const controller = this.receiver.controller
const bytes = this.receiver.pendingBytes
if (
!controller ||
!bytes ||
this.receiver.cancelled ||
this.receiver.finished ||
(controller.desiredSize ?? 0) <= 0
) {
return
}
this.receiver.pendingBytes = undefined
controller.enqueue(bytes)
this.sendAck(this.receiver.nextSeq - 1)
}
private sendAck(seq: number): void {
this.post({
protocol: DEEPSEEK_HARNESS_BYTE_PROTOCOL,
version: DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION,
type: 'ack',
stream: this.receiver.stream,
seq
})
}
private cancelReceiver(): void {
if (
this.receiver.cancelled ||
this.receiver.finished ||
this.failed ||
this.disposed
) {
return
}
this.receiver.cancelled = true
const cancelSeq = this.receiver.pendingBytes
? this.receiver.nextSeq - 1
: this.receiver.nextSeq
this.receiver.pendingBytes = undefined
this.post({
protocol: DEEPSEEK_HARNESS_BYTE_PROTOCOL,
version: DEEPSEEK_HARNESS_BYTE_PROTOCOL_VERSION,
type: 'cancel',
stream: this.receiver.stream,
seq: cancelSeq
})
}
}
export type DeepSeekHarnessUtilityProcessLike<Stderr = unknown> = {
postMessage(message: unknown): void
on(event: 'message', listener: (message: unknown) => void): unknown
on(event: 'exit', listener: (exitCode: number) => void): unknown
removeListener(
event: 'message',
listener: (message: unknown) => void
): unknown
removeListener(event: 'exit', listener: (exitCode: number) => void): unknown
kill(): boolean
readonly pid?: number
readonly stderr?: Stderr | null
}
export type DeepSeekHarnessUtilityChildOptions<Stderr> = {
stderrToWeb?: (stderr: Stderr) => ReadableStream<Uint8Array>
terminateProcess?: (
utilityProcess: DeepSeekHarnessUtilityProcessLike<Stderr>
) => void
}
/**
* Adapts an Electron UtilityProcess without importing Electron at runtime.
* Configure the utility process with piped stderr and inject Node's
* Readable.toWeb when stderr capture is required.
*/
export function createDeepSeekHarnessUtilityChild<Stderr = unknown>(
utilityProcess: DeepSeekHarnessUtilityProcessLike<Stderr>,
options: DeepSeekHarnessUtilityChildOptions<Stderr> = {}
): DeepSeekHarnessChild {
let killed = false
const killOnce = (): void => {
if (killed) {
return
}
killed = true
if (options.terminateProcess) {
options.terminateProcess(utilityProcess)
} else {
utilityProcess.kill()
}
}
const endpoint = new ByteTransportEndpoint(
{
postMessage: (message) => utilityProcess.postMessage(message),
subscribe: (listener) => {
const onMessage = (message: unknown): void => listener(message)
utilityProcess.on('message', onMessage)
return () => utilityProcess.removeListener('message', onMessage)
}
},
{
senderStream: 'stdin',
receiverStream: 'stdout',
onFailure: killOnce
}
)
let settleExit:
| ((result: { exitCode: number | null; signal?: string | null }) => void)
| undefined
const exited = new Promise<{
exitCode: number | null
signal?: string | null
}>((resolve) => {
settleExit = resolve
})
let exitedSettled = false
const onExit = (exitCode: number): void => {
if (exitedSettled) {
return
}
exitedSettled = true
killed = true
endpoint.dispose('PROCESS_EXITED')
settleExit?.({ exitCode })
}
utilityProcess.on('exit', onExit)
const stderr =
utilityProcess.stderr != null && options.stderrToWeb
? options.stderrToWeb(utilityProcess.stderr)
: undefined
return {
stdin: endpoint.writable,
stdout: endpoint.readable,
stderr,
exited,
terminate: () => {
endpoint.dispose('TERMINATED')
killOnce()
}
}
}
type ParentPortMessageEvent = {
readonly data: unknown
}
export type DeepSeekHarnessParentPortLike = {
postMessage(message: unknown): void
on(
event: 'message',
listener: (event: ParentPortMessageEvent) => void
): unknown
removeListener(
event: 'message',
listener: (event: ParentPortMessageEvent) => void
): unknown
}
export type DeepSeekHarnessHostTransport = {
readonly stdin: ReadableStream<Uint8Array>
readonly stdout: WritableStream<Uint8Array>
dispose(): void
}
/** Creates the host-side streams backed by process.parentPort-like messaging. */
export function createDeepSeekHarnessHostTransport(
parentPort: DeepSeekHarnessParentPortLike
): DeepSeekHarnessHostTransport {
const endpoint = new ByteTransportEndpoint(
{
postMessage: (message) => parentPort.postMessage(message),
subscribe: (listener) => {
const onMessage = (event: ParentPortMessageEvent): void =>
listener(event.data)
parentPort.on('message', onMessage)
return () => parentPort.removeListener('message', onMessage)
}
},
{
senderStream: 'stdout',
receiverStream: 'stdin'
}
)
return {
stdin: endpoint.readable,
stdout: endpoint.writable,
dispose: () => endpoint.dispose()
}
}
@@ -0,0 +1,333 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import type { Stream } from '@agentclientprotocol/sdk'
import { resolve } from 'node:path'
import {
GOODBUDDY_HANDSHAKE,
GOODBUDDY_PREPARE,
GoodBuddyCredentialProvider,
GoodBuddyHarnessControlPlane,
GoodBuddySandboxRetryLedger,
createBoundedAcpStream
} from './goodbuddy-harness-control-plane'
function execution(
callId: string,
name: string,
args: Record<string, unknown>
) {
return {
callId,
rootCallId: callId,
name,
arguments: args,
signal: new AbortController().signal,
token: Symbol('execution')
} as never
}
const sandboxDenied = {
isError: false,
value: {
sandbox: {
denied: true
}
},
content: []
} as const
function controlPlane() {
return new GoodBuddyHarnessControlPlane({} as Context, {
provider: 'goodbuddy',
model: 'deepseek-test',
workspace: resolve('workspace'),
harnessVersion: '0.1.0-rc.6',
sandbox: { provider: 'test', enforcement: 'full' },
credentialRefs: ['GOODBUDDY_API_KEY'],
skills: []
})
}
function stubAgentContext() {
const listeners = new Map<
string,
(...args: unknown[]) => unknown
>()
const extNotification = vi.fn(async () => undefined)
const handle = {
agent: {
session: {
id: 'session-output',
header: { id: 'session-output' },
events: []
},
cancel: vi.fn()
}
}
const ctx = {
on: vi.fn(
(
name: string,
listener: (...args: unknown[]) => unknown
) => {
listeners.set(name, listener)
return vi.fn()
}
)
} as unknown as Context
const subject = new GoodBuddyHarnessControlPlane(ctx, {
provider: 'goodbuddy',
model: 'deepseek-test',
workspace: resolve('workspace'),
harnessVersion: '0.1.0-rc.6',
sandbox: { provider: 'test', enforcement: 'full' },
credentialRefs: ['GOODBUDDY_API_KEY'],
skills: [],
maxEventCharacters: 10_000,
maxRequestCharacters: 180
})
const internals = subject as unknown as {
connection: {
extNotification: typeof extNotification
}
sessions: Map<
string,
{
handle: typeof handle
inflight: {
requestId: string
messageId: string
resolve: (reason: string) => void
reject: (error: unknown) => void
emittedCharacters: number
eventTail: Promise<void>
eventError?: unknown
}
}
>
observeSessions(): void
}
internals.connection = { extNotification }
internals.sessions.set('session-output', {
handle,
inflight: {
requestId: 'request-output',
messageId: 'message-output',
resolve: vi.fn(),
reject: vi.fn(),
emittedCharacters: 0,
eventTail: Promise.resolve()
}
})
internals.observeSessions()
return { listeners, extNotification, handle, internals }
}
describe('GoodBuddy Harness internal control plane', () => {
it('requires a versioned handshake before privileged extensions', async () => {
const subject = controlPlane()
await expect(
subject.extensionMethod(GOODBUDDY_PREPARE, {
sessionId: 'session',
requestId: 'request',
mode: 'execute'
})
).rejects.toThrow('GoodBuddy handshake is required')
await expect(
subject.extensionMethod(GOODBUDDY_HANDSHAKE, {
controlProtocolVersion: 9
})
).rejects.toThrow(
'incompatible GoodBuddy Harness control protocol'
)
await expect(
subject.extensionMethod(GOODBUDDY_HANDSHAKE, {
controlProtocolVersion: 1
})
).resolves.toMatchObject({
controlProtocolVersion: 1,
supports: {
cancellation: true,
sessionRelease: true,
oneShotApproval: true,
credentialResolution: true
},
sandbox: { enforcement: 'full' }
})
})
it('keeps credentials memory-only, allowlisted, and read-only', async () => {
const provider = new GoodBuddyCredentialProvider(
new Context(),
new Set(['GOODBUDDY_API_KEY'])
)
const resolver = vi
.fn()
.mockResolvedValue('secret-from-main')
provider.bind(resolver)
await expect(
provider.resolve('GOODBUDDY_API_KEY' as never)
).resolves.toEqual({
value: 'secret-from-main',
source: 'goodbuddy-main'
})
await expect(
provider.resolve('OTHER_KEY' as never)
).resolves.toBeUndefined()
expect(resolver).toHaveBeenCalledTimes(1)
await expect(
provider.set('GOODBUDDY_API_KEY' as never, 'x')
).rejects.toThrow('read-only')
})
it('fails closed on oversized inbound and outbound ACP frames', async () => {
const inbound = new TransformStream<
Record<string, unknown>,
Record<string, unknown>
>()
const outbound = new TransformStream<
Record<string, unknown>,
Record<string, unknown>
>()
const stream = createBoundedAcpStream(
({
readable: inbound.readable,
writable: outbound.writable
} as unknown as Stream),
16
)
const inputWriter = inbound.writable.getWriter()
const reader = stream.readable.getReader()
const read = reader.read()
await inputWriter.write({ value: 'too-long-for-frame' })
await expect(read).rejects.toThrow('input frame exceeds')
const writer = stream.writable.getWriter()
await expect(
writer.write({ value: 'too-long-for-frame' } as never)
).rejects.toThrow('output frame exceeds')
})
it('counts the complete emitted envelope against the request limit', async () => {
const { listeners, extNotification, handle, internals } =
stubAgentContext()
const sessionEvent = listeners.get('session/event')!
sessionEvent(
handle.agent.session,
{
type: 'assistant/chunk',
data: {
chunk: {
type: 'text-delta',
text: 'x'.repeat(80)
}
}
}
)
sessionEvent(
handle.agent.session,
{
type: 'assistant/chunk',
data: {
chunk: {
type: 'usage',
usage: {
inputTokens: 1,
outputTokens: 1,
cacheReadTokens: 0,
cacheWriteTokens: 0
}
}
}
}
)
await internals.sessions.get('session-output')!.inflight.eventTail
expect(extNotification).toHaveBeenCalledTimes(1)
expect(handle.agent.cancel).toHaveBeenCalledWith({
kind: 'user'
})
expect(
internals.sessions.get('session-output')!.inflight.eventError
).toEqual(
new Error(
'GoodBuddy Harness control request output exceeds safety limit'
)
)
expect(
internals.sessions.get('session-output')!.inflight.emittedCharacters
).toBeGreaterThan(180)
})
it('requires a matching real denial and consumes it once', () => {
const ledger = new GoodBuddySandboxRetryLedger()
const deniedArguments = {
command: 'type C:\\outside\\file.txt',
description: 'Read an outside file'
}
const retry = {
...deniedArguments,
sandbox_permissions: 'danger-full-access',
justification: 'The requested file is outside the workspace.'
}
expect(ledger.consumeRetry('pwsh', retry)).toBe(false)
ledger.record(
execution('denial-1', 'pwsh', deniedArguments),
sandboxDenied as never
)
expect(
ledger.consumeRetry('pwsh', {
...retry,
command: 'type C:\\different\\file.txt'
})
).toBe(false)
expect(ledger.consumeRetry('bash', retry)).toBe(false)
expect(ledger.consumeRetry('pwsh', retry)).toBe(true)
expect(ledger.consumeRetry('pwsh', retry)).toBe(false)
})
it('rejects non-denials, narrow escalation, and reordered ambiguity', () => {
const ledger = new GoodBuddySandboxRetryLedger()
const deniedArguments = {
description: 'Read an outside file',
command: 'cat /outside/file'
}
ledger.record(execution('success', 'bash', deniedArguments), {
isError: false,
value: {},
content: []
} as never)
expect(
ledger.consumeRetry('bash', {
command: 'cat /outside/file',
description: 'Read an outside file',
sandbox_permissions: 'danger-full-access',
justification: 'The requested file is outside the workspace.'
})
).toBe(false)
ledger.record(
execution('denial-2', 'bash', deniedArguments),
sandboxDenied as never
)
expect(
ledger.consumeRetry('bash', {
command: 'cat /outside/file',
description: 'Read an outside file',
sandbox_permissions: 'workspace-write',
justification: 'Retry in workspace-write.'
})
).toBe(false)
expect(
ledger.consumeRetry('bash', {
command: 'cat /outside/file',
description: 'Read an outside file',
sandbox_permissions: 'danger-full-access',
justification: 'The requested file is outside the workspace.'
})
).toBe(true)
})
})
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
buildExplicitProfileRuntimeEnvironment,
buildControlledHarnessEnvironment,
buildRuntimeEnvironment
} from './process-environment'
@@ -89,4 +90,30 @@ describe('buildRuntimeEnvironment', () => {
NODE_TLS_REJECT_UNAUTHORIZED: '0'
})
})
it('builds a credential-free, telemetry-disabled Harness environment', () => {
expect(
buildControlledHarnessEnvironment('C:\\isolated-dsh', {
PATH: 'C:\\Tools',
TEMP: 'C:\\Temp',
OPENAI_API_KEY: 'must-not-leak',
DEEPSEEK_API_KEY: 'must-not-leak',
DSH_HOME: 'C:\\user-dsh',
NODE_OPTIONS: '--require malicious.js'
})
).toMatchObject({
PATH: 'C:\\Tools',
TEMP: 'C:\\Temp',
DSH_HOME: 'C:\\isolated-dsh',
DSH_TELEMETRY_DISABLED: '1',
DO_NOT_TRACK: '1',
OTEL_SDK_DISABLED: 'true'
})
expect(
buildControlledHarnessEnvironment('C:\\isolated-dsh', {
OPENAI_API_KEY: 'must-not-leak',
DEEPSEEK_API_KEY: 'must-not-leak'
})
).not.toHaveProperty('OPENAI_API_KEY')
})
})
+17
View File
@@ -92,3 +92,20 @@ export function buildExplicitProfileRuntimeEnvironment(
}
return environment
}
export function buildControlledHarnessEnvironment(
dshHome: string,
source: NodeJS.ProcessEnv = process.env
): NodeJS.ProcessEnv {
const environment = buildExplicitProfileRuntimeEnvironment(
{
DSH_HOME: dshHome,
DSH_TELEMETRY_DISABLED: '1',
...runtimePrivacyEnvironment
},
undefined,
source
)
delete environment.NODE_TLS_REJECT_UNAUTHORIZED
return environment
}
+53 -6
View File
@@ -36,7 +36,8 @@ describe('runtime discovery', () => {
expect(detection).toMatchObject({
available: true,
path: await realpath(process.execPath)
path: await realpath(process.execPath),
source: 'configured'
})
expect(detection.version).toMatch(/^\d+\.\d+\.\d+/u)
})
@@ -69,7 +70,8 @@ describe('runtime discovery', () => {
expect(detection).toMatchObject({
available: true,
path: await realpath(process.execPath)
path: await realpath(process.execPath),
source: 'automatic'
})
})
@@ -83,7 +85,8 @@ describe('runtime discovery', () => {
expect(detection).toMatchObject({
available: true,
path: await realpath(process.execPath)
path: await realpath(process.execPath),
source: 'configured'
})
expect(detection.detail).not.toContain('内置')
})
@@ -101,7 +104,8 @@ describe('runtime discovery', () => {
expect(detection).toMatchObject({
available: true,
path: await realpath(process.execPath)
path: await realpath(process.execPath),
source: 'bundled'
})
expect(detection.detail).toContain('内置')
})
@@ -115,15 +119,57 @@ describe('runtime discovery', () => {
binaryPath: '',
bundledPath: bundledScript,
bundledValidation: 'canonical-file',
bundledVersion: '1.5.47',
binaryNames: ['goodbuddy-runtime-that-does-not-exist'],
label: 'Script Runtime'
})
expect(detection).toMatchObject({
available: true,
path: await realpath(bundledScript)
path: await realpath(bundledScript),
version: '1.5.47',
source: 'bundled'
})
expect(detection.detail).toBe(
'内置 Script Runtime 1.5.47 已就绪'
)
})
it('accepts a controlled bundled harness when no custom host is configured', async () => {
const bundledScript = fileURLToPath(import.meta.url)
const detection = await detectRuntimeBinary({
binaryPath: '',
bundledPath: bundledScript,
bundledValidation: 'canonical-file',
bundledVersion: '0.1.0-rc.6',
binaryNames: [],
label: 'GoodBuddy DeepSeek Harness Host'
})
expect(detection).toMatchObject({
available: true,
path: await realpath(bundledScript),
version: '0.1.0-rc.6',
source: 'bundled'
})
expect(detection.detail).toContain('内置')
})
it('does not discover arbitrary DeepSeek Harness hosts from PATH', async () => {
process.env.PATH = dirname(process.execPath)
process.env.Path = dirname(process.execPath)
await expect(
detectRuntimeBinary({
binaryPath: '',
allowAutomaticDiscovery: false,
binaryNames: [basename(process.execPath)],
label: 'GoodBuddy DeepSeek Harness Host'
})
).resolves.toEqual({
available: false,
detail: expect.stringContaining('未自动检测到')
})
expect(detection.detail).toBe('内置 Script Runtime 已就绪')
})
it('returns both runtime detections without exposing PATH contents', async () => {
@@ -144,6 +190,7 @@ describe('runtime discovery', () => {
available: true,
path: await realpath(process.execPath)
})
expect(result.deepseekHarness.available).toBe(false)
expect(JSON.stringify(result)).not.toContain(privatePathValue)
})
})
+68 -34
View File
@@ -21,6 +21,8 @@ export type RuntimeBinaryDiscoveryInput = {
binaryPath: string
bundledPath?: string
bundledValidation?: 'execute' | 'canonical-file'
bundledVersion?: string
allowAutomaticDiscovery?: boolean
binaryNames: readonly string[]
label: string
}
@@ -246,13 +248,14 @@ function availableDetection(
label: string,
path: string,
version?: string,
bundled = false
source: 'bundled' | 'configured' | 'automatic' = 'automatic'
): RuntimeBinaryDetection {
return {
available: true,
path,
version,
detail: `${bundled ? '内置 ' : ''}${label}${
source,
detail: `${source === 'bundled' ? '内置 ' : ''}${label}${
version ? ` ${version}` : ''
} 已就绪`
}
@@ -264,6 +267,36 @@ export async function detectRuntimeBinary(
const configuredPath = input.binaryPath.trim()
let configuredPathProblem: 'relative' | 'invalid' | 'validation' | undefined
const detectBundled = async (): Promise<
RuntimeBinaryDetection | undefined
> => {
const bundledPath = input.bundledPath?.trim()
if (!bundledPath) {
return undefined
}
const canonicalPath = await canonicalFile(bundledPath)
if (!canonicalPath) {
return undefined
}
if (input.bundledValidation === 'canonical-file') {
return availableDetection(
input.label,
canonicalPath,
input.bundledVersion,
'bundled'
)
}
const validation = await validateVersion(canonicalPath)
return validation.valid
? availableDetection(
input.label,
canonicalPath,
validation.version,
'bundled'
)
: undefined
}
if (configuredPath) {
if (!isAbsolute(configuredPath)) {
configuredPathProblem = 'relative'
@@ -277,7 +310,8 @@ export async function detectRuntimeBinary(
return availableDetection(
input.label,
canonicalPath,
validation.version
validation.version,
'configured'
)
}
configuredPathProblem = 'validation'
@@ -285,47 +319,31 @@ export async function detectRuntimeBinary(
}
}
const bundledPath = input.bundledPath?.trim()
if (bundledPath) {
const canonicalPath = await canonicalFile(bundledPath)
if (canonicalPath) {
if (input.bundledValidation === 'canonical-file') {
return availableDetection(
input.label,
canonicalPath,
undefined,
true
)
const bundled = await detectBundled()
if (bundled) {
return bundled
}
let foundAutomaticCandidate = false
if (input.allowAutomaticDiscovery !== false) {
for (const candidate of automaticCandidates(input.binaryNames)) {
const canonicalPath = await canonicalFile(candidate)
if (!canonicalPath) {
continue
}
foundAutomaticCandidate = true
const validation = await validateVersion(canonicalPath)
if (validation.valid) {
return availableDetection(
input.label,
canonicalPath,
validation.version,
true
'automatic'
)
}
}
}
let foundAutomaticCandidate = false
for (const candidate of automaticCandidates(input.binaryNames)) {
const canonicalPath = await canonicalFile(candidate)
if (!canonicalPath) {
continue
}
foundAutomaticCandidate = true
const validation = await validateVersion(canonicalPath)
if (validation.valid) {
return availableDetection(
input.label,
canonicalPath,
validation.version
)
}
}
let detail: string
if (foundAutomaticCandidate || configuredPathProblem === 'validation') {
detail = `${input.label} 候选未通过 --version 安全验证`
@@ -349,9 +367,14 @@ export async function detectAgentRuntimes(input: {
bundledPaths?: {
opencode: string
continue: string
deepseekHarness: string
}
bundledVersions?: {
continue: string
deepseekHarness: string
}
}): Promise<AgentRuntimeDetection> {
const [opencode, continueRuntime] = await Promise.all([
const [opencode, continueRuntime, deepseekHarness] = await Promise.all([
detectRuntimeBinary({
binaryPath: input.opencodeBinaryPath,
bundledPath: input.bundledPaths?.opencode,
@@ -362,13 +385,24 @@ export async function detectAgentRuntimes(input: {
binaryPath: input.continueBinaryPath,
bundledPath: input.bundledPaths?.continue,
bundledValidation: 'canonical-file',
bundledVersion: input.bundledVersions?.continue,
binaryNames: ['cn'],
label: 'Continue CLI'
}),
detectRuntimeBinary({
binaryPath: '',
bundledPath: input.bundledPaths?.deepseekHarness,
bundledValidation: 'canonical-file',
bundledVersion: input.bundledVersions?.deepseekHarness,
allowAutomaticDiscovery: false,
binaryNames: [],
label: 'GoodBuddy DeepSeek Harness Host'
})
])
return {
opencode,
continue: continueRuntime
continue: continueRuntime,
deepseekHarness
}
}
+78 -1
View File
@@ -10,6 +10,7 @@ const defaultProfileId = '00000000-0000-4000-8000-000000000001'
const secondProfileId = '00000000-0000-4000-8000-000000000002'
const responsesProfileId = '00000000-0000-4000-8000-000000000003'
const imageProfileId = '00000000-0000-4000-8000-000000000004'
const deepseekProfileId = '00000000-0000-4000-8000-000000000005'
function settings(
overrides: Partial<ResolvedRuntimeSettings> = {}
@@ -62,6 +63,16 @@ function settings(
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: 'image-key'
},
{
id: deepseekProfileId,
name: 'DeepSeek',
baseUrl: 'https://api.deepseek.com',
modelName: 'deepseek-chat',
protocol: 'openai-chat-completions',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: 'deepseek-key'
}
],
defaultModelProfileId: defaultProfileId,
@@ -154,13 +165,62 @@ describe('runtime selection', () => {
).toThrow('自动启动')
})
it('selects DeepSeek Harness only with an official compatible profile', () => {
const selected = applyRuntimeSelection(settings(), {
provider: 'deepseek-harness',
profileId: deepseekProfileId
})
expect(selected.target).toBe('deepseek-harness')
expect(selected.settings).toMatchObject({
provider: 'deepseek-harness',
deepseekHarnessModelProfile: { id: deepseekProfileId }
})
expect(() =>
applyRuntimeSelection(settings(), {
provider: 'deepseek-harness',
profileId: secondProfileId
})
).toThrow('api.deepseek.com')
})
it('keeps the controlled platform DeepSeek profile when selected without a profile ID', () => {
const base = settings()
const platformProfile = {
...base.modelProfiles[4]!,
id: 'goodbuddy-platform-deepseek',
name: '平台 DeepSeek',
modelName: 'deepseek-v4-flash'
}
const selected = applyRuntimeSelection(
settings({ deepseekHarnessModelProfile: platformProfile }),
{ provider: 'deepseek-harness' }
)
expect(selected.settings).toMatchObject({
provider: 'deepseek-harness',
deepseekHarnessModelProfile: {
id: 'goodbuddy-platform-deepseek',
modelName: 'deepseek-v4-flash'
}
})
})
it('resolves Agent Runtime backends from the global Runtime configuration', () => {
const base = settings()
const configured = settings({
opencodeModelProfile: base.modelProfiles[1],
continueModelProfile: base.modelProfiles[2]
continueModelProfile: base.modelProfiles[2],
deepseekHarnessModelProfile: base.modelProfiles[4]
})
expect(
resolveConfiguredAgentRuntimeSelection(configured, {
provider: 'deepseek-harness'
})
).toEqual({
provider: 'deepseek-harness',
profileId: deepseekProfileId
})
expect(
resolveConfiguredAgentRuntimeSelection(configured, {
provider: 'opencode',
@@ -189,6 +249,23 @@ describe('runtime selection', () => {
})
})
it('keeps the controlled platform DeepSeek source profile-free across configured selection repair', () => {
const base = settings()
const configured = settings({
deepseekHarnessModelProfile: {
...base.modelProfiles[4]!,
id: 'goodbuddy-platform-deepseek',
name: '平台 DeepSeek'
}
})
expect(
resolveConfiguredAgentRuntimeSelection(configured, {
provider: 'deepseek-harness'
})
).toEqual({ provider: 'deepseek-harness' })
})
it('routes legacy automatic settings through local OpenCode when the Server is blank', () => {
expect(getConfiguredRuntimeTarget(settings())).toBe('opencode')
expect(
+43 -5
View File
@@ -1,11 +1,18 @@
import { isAgentRuntimeModelProtocol } from '../../shared/contracts'
import {
isAgentRuntimeModelProtocol,
isDeepSeekHarnessModelProfile
} from '../../shared/contracts'
import type { AgentRuntimeSelection } from '../../shared/runtime-selection-contracts'
import type {
ResolvedModelProfile,
ResolvedRuntimeSettings
} from '../runtime-settings-store'
export type SelectedRuntimeTarget = 'model' | 'opencode' | 'continue'
export type SelectedRuntimeTarget =
| 'model'
| 'opencode'
| 'continue'
| 'deepseek-harness'
function requireProfile(
settings: ResolvedRuntimeSettings,
@@ -26,6 +33,9 @@ export function getConfiguredRuntimeTarget(
if (settings.provider === 'continue') {
return 'continue'
}
if (settings.provider === 'deepseek-harness') {
return 'deepseek-harness'
}
if (
settings.provider === 'opencode' ||
settings.provider === 'auto'
@@ -41,17 +51,24 @@ export function resolveConfiguredAgentRuntimeSelection(
): AgentRuntimeSelection {
if (
selection.provider !== 'opencode' &&
selection.provider !== 'continue'
selection.provider !== 'continue' &&
selection.provider !== 'deepseek-harness'
) {
return selection
}
const profile =
selection.provider === 'opencode'
? settings.opencodeModelProfile
: settings.continueModelProfile
: selection.provider === 'continue'
? settings.continueModelProfile
: settings.deepseekHarnessModelProfile
return {
provider: selection.provider,
...(profile ? { profileId: profile.id } : {})
...(profile && settings.modelProfiles.some(
(candidate) => candidate.id === profile.id
)
? { profileId: profile.id }
: {})
}
}
@@ -114,6 +131,27 @@ export function applyRuntimeSelection(
}
}
if (selection.provider === 'deepseek-harness') {
const selectedProfile =
profile ?? settings.deepseekHarnessModelProfile
if (
selectedProfile &&
!isDeepSeekHarnessModelProfile(selectedProfile)
) {
throw new Error(
'DeepSeek Harness 独立模型连接仅支持 api.deepseek.com 的 OpenAI Chat Completions 协议'
)
}
return {
target: 'deepseek-harness',
settings: {
...settings,
provider: 'deepseek-harness',
deepseekHarnessModelProfile: selectedProfile
}
}
}
if (
profile &&
!isAgentRuntimeModelProtocol(profile.protocol)
+1 -1
View File
@@ -32,7 +32,7 @@ export type RuntimeModelUsageEvent = {
requestId: string
type: 'model-usage'
callId: string
runtime: 'model' | 'continue' | 'opencode'
runtime: 'model' | 'continue' | 'opencode' | 'deepseek-harness'
provider: string
model: string
inputTokens: number
@@ -63,6 +63,13 @@ describe('bundled skills', () => {
expect(snapshot.skills.map((skill) => skill.id)).toContain(
'product-marketing'
)
expect(snapshot.skills).toContainEqual(
expect.objectContaining({
id: 'web-3d-game',
name: 'Web 3D Game',
assignments: expect.arrayContaining(['deepseek-harness'])
})
)
})
it('injects every enabled bundled skill with its resolved directory', async () => {
@@ -76,4 +83,19 @@ describe('bundled skills', () => {
expect(instructions).toContain(join(builtinSkillsRoot, skill.id))
}
})
it('exposes the 3D game Skill as a native Harness package', async () => {
const service = await createService()
await expect(
service.getRuntimeSkillContext('deepseek-harness')
).resolves.toMatchObject({
packages: expect.arrayContaining([
{
id: 'web-3d-game',
directory: join(builtinSkillsRoot, 'web-3d-game')
}
])
})
})
})
@@ -246,7 +246,12 @@ describe('CapabilityService', () => {
id: 'document-writing',
source: 'builtin',
enabled: true,
assignments: ['model', 'opencode', 'continue']
assignments: [
'model',
'opencode',
'continue',
'deepseek-harness'
]
}
]
})
@@ -625,9 +630,31 @@ describe('CapabilityService', () => {
})
})
it('rejects MCP assignments to Agent Runtimes', async () => {
it('allows Harness MCP assignment and rejects unsupported Agent Runtimes', async () => {
const { service } = await createService()
await expect(
service.saveMcpServer(undefined, {
name: 'Harness MCP',
description: '',
enabled: true,
allowDynamicTools: false,
assignments: ['deepseek-harness'],
secret: { action: 'keep' },
transport: 'stdio',
command: 'node',
args: ['server.js']
})
).resolves.toMatchObject({
mcpServers: [
expect.objectContaining({
assignments: ['deepseek-harness']
})
]
})
await expect(
service.getResolvedMcpServers('deepseek-harness')
).resolves.toHaveLength(1)
await expect(
service.saveMcpServer(undefined, {
name: 'Agent MCP',
@@ -640,7 +667,7 @@ describe('CapabilityService', () => {
command: 'node',
args: ['server.js']
})
).rejects.toThrow('只能分配给直连模型')
).rejects.toThrow('只能分配给直连模型或 DeepSeek Harness')
})
it('migrates legacy OpenCode MCP assignments to the direct model', async () => {
+13 -4
View File
@@ -269,7 +269,12 @@ function emptyStoredCapabilities(
function defaultSkillState(): z.infer<typeof skillStateSchema> {
return {
enabled: true,
assignments: ['model', 'opencode', 'continue']
assignments: [
'model',
'opencode',
'continue',
'deepseek-harness'
]
}
}
@@ -1606,10 +1611,14 @@ export class CapabilityService {
const value = mcpServerInputSchema.parse(input)
if (
value.assignments.some(
(assignment) => assignment !== 'model'
(assignment) =>
assignment !== 'model' &&
assignment !== 'deepseek-harness'
)
) {
throw new Error('当前版本的 MCP Server 只能分配给直连模型')
throw new Error(
'当前版本的 MCP Server 只能分配给直连模型或 DeepSeek Harness'
)
}
const state = await this.load()
const id = serverId ? mcpServerIdSchema.parse(serverId) : randomUUID()
@@ -1809,7 +1818,7 @@ export class CapabilityService {
async getResolvedMcpServers(
target: RuntimeTarget
): Promise<ResolvedMcpServer[]> {
if (target !== 'model') {
if (target !== 'model' && target !== 'deepseek-harness') {
return []
}
const state = await this.load()
+112
View File
@@ -0,0 +1,112 @@
import {
DEEPSEEK_HARNESS_CONTROL_PROTOCOL,
DEEPSEEK_HARNESS_CONTROL_VERSION,
parseHarnessControlMessage,
type DeepSeekHarnessControlMessage
} from './agent/deepseek-harness-utility-launcher'
import { createDeepSeekHarnessHostTransport } from './agent/deepseek-harness-utility-transport'
import {
createBoundedNdJsonStream,
ControlledHarnessHostStartupError,
installHarnessDiagnosticGuard,
startControlledDeepSeekHarnessHost,
type ControlledHarnessHost
} from './deepseek-harness-host'
const parentPort = process.parentPort
const restoreDiagnostics = installHarnessDiagnosticGuard()
// The Windows ACL sandbox launches its JavaScript runner through
// `process.execPath`. Inside an Electron UtilityProcess that path is Electron,
// so descendants must opt into Electron's supported Node execution mode.
if (process.platform === 'win32') {
process.env.ELECTRON_RUN_AS_NODE = '1'
}
let host: ControlledHarnessHost | undefined
let transport:
| ReturnType<typeof createDeepSeekHarnessHostTransport>
| undefined
let starting = false
let closed = false
function post(message: DeepSeekHarnessControlMessage): void {
if (!closed) {
parentPort.postMessage(message)
}
}
async function close(): Promise<void> {
if (closed) {
return
}
closed = true
await host?.dispose().catch(() => undefined)
transport?.dispose()
restoreDiagnostics()
}
function fatal(code: string): void {
post({
protocol: DEEPSEEK_HARNESS_CONTROL_PROTOCOL,
version: DEEPSEEK_HARNESS_CONTROL_VERSION,
type: 'fatal',
code
})
void close().finally(() => {
process.exitCode = 1
})
}
parentPort.on('message', (event) => {
const message = parseHarnessControlMessage(event.data)
if (!message) {
// Once the byte transport is installed, non-control messages belong to
// that transport's listener on the shared UtilityProcess port.
if (transport) {
return
}
fatal('INVALID_START')
return
}
if (message.type !== 'start') {
fatal('INVALID_START')
return
}
if (starting || host || closed) {
fatal('DUPLICATE_START')
return
}
starting = true
transport = createDeepSeekHarnessHostTransport(parentPort)
void startControlledDeepSeekHarnessHost({
...message.config,
stream: createBoundedNdJsonStream(
transport.stdout,
transport.stdin,
message.config.maxFrameBytes
)
})
.then((startedHost) => {
host = startedHost
starting = false
post({
protocol: DEEPSEEK_HARNESS_CONTROL_PROTOCOL,
version: DEEPSEEK_HARNESS_CONTROL_VERSION,
type: 'ready'
})
})
.catch((error: unknown) => {
transport?.dispose()
fatal(
error instanceof ControlledHarnessHostStartupError
? error.code
: 'HOST_START_FAILED'
)
})
})
process.once('disconnect', () => {
void close()
})
process.once('SIGTERM', () => {
void close()
})
+325
View File
@@ -0,0 +1,325 @@
import { describe, expect, it } from 'vitest'
import {
createBoundedNdJsonStream,
installHarnessDiagnosticGuard,
startControlledDeepSeekHarnessHost
} from './deepseek-harness-host'
import { vi } from 'vitest'
import { mkdir, mkdtemp, realpath, writeFile } from 'node:fs/promises'
import type {
Agent,
CreateAgentOptions
} from '@deepseek-ai/dsh-agent'
import { GOODBUDDY_HARNESS_MAX_STEP_TOKENS } from './agent/goodbuddy-harness-control-plane'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
const expectedSandbox =
process.platform === 'win32'
? { provider: 'windows-acl', enforcement: 'partial' as const }
: process.platform === 'darwin'
? { provider: 'seatbelt', enforcement: 'full' as const }
: { provider: 'local-linux', enforcement: 'full' as const }
async function readAllMessages(
readable: ReadableStream<unknown>
): Promise<unknown[]> {
const values: unknown[] = []
for await (const value of readable) {
values.push(value)
}
return values
}
describe('controlled DeepSeek Harness host', () => {
it('rejects unsupported endpoint protocols before Cordis starts', async () => {
await expect(
startControlledDeepSeekHarnessHost({
workspace: 'C:\\workspace',
baseUrl: 'file:///private/config',
api: 'openai-completions',
provider: 'goodbuddy',
model: 'deepseek-test',
harnessVersion: '0.1.0-rc.6',
sandbox: { provider: 'test', enforcement: 'full' },
credentialRefs: ['GOODBUDDY_API_KEY'],
dshHome: 'C:\\controlled-dsh-home',
skillPackages: []
})
).rejects.toThrow('trusted HTTPS DeepSeek endpoint')
})
it('suppresses console payloads instead of contaminating stdout', () => {
const restore = installHarnessDiagnosticGuard()
const originalWrite = process.stderr.write
const writes: string[] = []
process.stderr.write = ((value: string | Uint8Array) => {
writes.push(String(value))
return true
}) as typeof process.stderr.write
try {
console.log('prompt and secret must not reach protocol stdout')
expect(writes.join('')).toBe(
'DeepSeek Harness diagnostic suppressed\n'
)
expect(writes.join('')).not.toContain('secret')
} finally {
process.stderr.write = originalWrite
restore()
}
})
it('verifies the real local sandbox before advertising capabilities', async () => {
const root = await realpath(
await mkdtemp(join(tmpdir(), 'goodbuddy-harness-host-'))
)
const inbound = new TransformStream<
Record<string, unknown>,
Record<string, unknown>
>()
const outbound = new TransformStream<
Record<string, unknown>,
Record<string, unknown>
>()
const host = await startControlledDeepSeekHarnessHost({
workspace: root,
dshHome: root,
baseUrl: 'https://api.deepseek.com',
api: 'openai-completions',
provider: 'goodbuddy',
model: 'deepseek-test',
harnessVersion: '0.1.0-rc.6',
sandbox: expectedSandbox,
credentialRefs: ['GOODBUDDY_API_KEY'],
skillPackages: [],
stream: {
readable: inbound.readable,
writable: outbound.writable
} as never
})
await host.dispose()
})
it('canonicalizes workspace aliases before binding the host', async () => {
const root = await realpath(
await mkdtemp(join(tmpdir(), 'goodbuddy-harness-alias-'))
)
const alias = join(root, '..', basename(root))
const inbound = new TransformStream<
Record<string, unknown>,
Record<string, unknown>
>()
const outbound = new TransformStream<
Record<string, unknown>,
Record<string, unknown>
>()
const host = await startControlledDeepSeekHarnessHost({
workspace: alias,
dshHome: alias,
baseUrl: 'https://api.deepseek.com',
api: 'openai-completions',
provider: 'goodbuddy',
model: 'deepseek-test',
harnessVersion: '0.1.0-rc.6',
sandbox: expectedSandbox,
credentialRefs: ['GOODBUDDY_API_KEY'],
skillPackages: [],
stream: {
readable: inbound.readable,
writable: outbound.writable
} as never
})
await host.dispose()
})
it('loads only explicitly supplied Skill packages into a session scope', async () => {
const root = await realpath(
await mkdtemp(join(tmpdir(), 'goodbuddy-harness-skill-'))
)
const skillDirectory = join(root, 'web-3d-game')
await mkdir(skillDirectory)
await writeFile(
join(skillDirectory, 'SKILL.md'),
[
'---',
'name: web-3d-game',
'description: Build a playable browser 3D game.',
'---',
'',
'# Web 3D game',
'',
'Create and validate a playable project.'
].join('\n'),
'utf8'
)
const inbound = new TransformStream<
Record<string, unknown>,
Record<string, unknown>
>()
const outbound = new TransformStream<
Record<string, unknown>,
Record<string, unknown>
>()
const host = await startControlledDeepSeekHarnessHost({
workspace: root,
dshHome: root,
baseUrl: 'https://api.deepseek.com',
api: 'openai-completions',
provider: 'goodbuddy',
model: 'deepseek-test',
harnessVersion: '0.1.0-rc.6',
sandbox: expectedSandbox,
credentialRefs: ['GOODBUDDY_API_KEY'],
skillPackages: [
{ id: 'web-3d-game', directory: skillDirectory }
],
stream: {
readable: inbound.readable,
writable: outbound.writable
} as never
})
let createdContext: typeof host.context | undefined
let createdAgent: Agent | undefined
const create = vi
.spyOn(host.context.agents, 'create')
.mockImplementation(async (options: CreateAgentOptions) => {
const agentContext = host.context.extend({
isolate: ['skills', 'tools']
})
createdContext = agentContext
await options.setup?.(agentContext)
const agent = {
options: options.agentOptions ?? {},
session: {
id: options.sessionId,
header: { cwd: options.meta?.cwd ?? root },
events: [],
append: vi.fn()
},
ctx: agentContext,
cancel: vi.fn()
}
createdAgent = agent as never
return {
agent,
dispose: async () => {
await agentContext.fiber.dispose()
}
} as never
})
const api = (
host.controlPlane as unknown as {
createAgentApi(): {
newSession(params: {
cwd: string
mcpServers: never[]
}): Promise<{ sessionId: string }>
}
}
).createAgentApi()
const session = await api.newSession({
cwd: root,
mcpServers: []
})
expect(session.sessionId).toBeTruthy()
expect(
(
await createdContext!.skills.list({
cwd: root,
scope: createdAgent
})
).map((skill) => skill.name)
).toEqual(['web-3d-game'])
expect(
createdContext!.tools
.schemas(createdAgent)
.map((tool) => tool.name)
).toContain('skill')
const loadedSkill = await createdContext!.tools.execute({
callId: 'skill-call',
name: 'skill',
arguments: { name: 'web-3d-game' },
agent: createdAgent,
signal: new AbortController().signal
} as never)
expect(loadedSkill).toMatchObject({
isError: false,
value: {
name: 'web-3d-game',
content: expect.stringContaining(
'Create and validate a playable project.'
)
}
})
expect(create).toHaveBeenCalledTimes(1)
expect(create).toHaveBeenCalledWith(
expect.objectContaining({
agentOptions: {
provider: 'goodbuddy',
model: 'deepseek-test',
maxTokens: GOODBUDDY_HARNESS_MAX_STEP_TOKENS
}
})
)
const assembly = await createdContext!.systemPrompt.assemble({
agent: createdAgent,
scope: createdAgent
})
expect(
assembly.sections.find(
(section) =>
section.name === 'goodbuddy:controlled-execution'
)?.text
).toContain('create or update the requested workspace files promptly')
await host.dispose()
})
it('frames fragmented and coalesced ACP messages individually', async () => {
const inbound = new TransformStream<Uint8Array, Uint8Array>()
const outbound = new TransformStream<Uint8Array, Uint8Array>()
const stream = createBoundedNdJsonStream(
outbound.writable,
inbound.readable,
24
)
const reading = readAllMessages(stream.readable)
const writer = inbound.writable.getWriter()
const encoder = new TextEncoder()
await writer.write(encoder.encode('{"text":"你'))
await writer.write(
encoder.encode('好"}\n{"value":"1234567890"}\n')
)
await writer.close()
await expect(reading).resolves.toEqual([
{ text: '你好' },
{ value: '1234567890' }
])
})
it('rejects oversized ACP frames at EOF in both directions', async () => {
const inbound = new TransformStream<Uint8Array, Uint8Array>()
const outbound = new TransformStream<Uint8Array, Uint8Array>()
const stream = createBoundedNdJsonStream(
outbound.writable,
inbound.readable,
8
)
const reading = readAllMessages(stream.readable)
const inputWriter = inbound.writable.getWriter()
await inputWriter.write(
new TextEncoder().encode('{"value":"too large"}')
)
await inputWriter.close()
await expect(reading).rejects.toThrow('input frame exceeds')
const outputWriter = stream.writable.getWriter()
await expect(
outputWriter.write({ value: 'too large' } as never)
).rejects.toThrow('output frame exceeds')
})
})
+580
View File
@@ -0,0 +1,580 @@
import { Context, type Fiber } from '@deepseek-ai/cordis'
import { readFile, realpath, stat } from 'node:fs/promises'
import { isAbsolute, join } from 'node:path'
import { parse as parseYaml } from 'yaml'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import SandboxedBash from '@deepseek-ai/dsh-bash-sandbox'
import SandboxedPwsh from '@deepseek-ai/dsh-pwsh-sandbox'
import SandboxedFileSystem from '@deepseek-ai/dsh-fs-sandbox'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import * as PiAiLlm from '@deepseek-ai/dsh-llm-pi-ai'
import ApprovalService from '@deepseek-ai/dsh-user-approval'
import LocalSandbox from '@deepseek-ai/dsh-sandbox-local'
import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy'
import SessionStore from '@deepseek-ai/dsh-session'
import SkillRegistry from '@deepseek-ai/dsh-skill'
import LocalSubprocess from '@deepseek-ai/dsh-subprocess-local'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import * as ToolPwsh from '@deepseek-ai/dsh-tool-pwsh'
import * as ShellEnv from '@deepseek-ai/dsh-shell-env'
import {
GoodBuddyCredentialProvider,
GoodBuddyHarnessControlPlane,
createBoundedAcpStream,
type GoodBuddyHarnessControlConfig
} from './agent/goodbuddy-harness-control-plane'
import type { Stream } from '@agentclientprotocol/sdk'
import type { SandboxEnforcement } from '@deepseek-ai/dsh-sandbox'
const DEFAULT_MAX_FRAME_BYTES = 1024 * 1024
const MAX_DIAGNOSTIC_BYTES = 64 * 1024
export type ControlledHarnessHostConfig = Omit<
GoodBuddyHarnessControlConfig,
'stream' | 'skills'
> & {
workspace: string
baseUrl: string
api: 'openai-completions'
maxFrameBytes?: number
stream?: Stream
dshHome: string
skillPackages: readonly {
id: string
directory: string
}[]
}
export type ControlledHarnessHost = {
readonly context: Context
readonly controlPlane: GoodBuddyHarnessControlPlane
dispose(): Promise<void>
}
export type ControlledHarnessHostStartupCode =
| 'HOST_PLUGIN_GRAPH_FAILED'
| 'HOST_SANDBOX_CONFIGURATION_FAILED'
| 'HOST_SANDBOX_EXECUTION_FAILED'
| 'HOST_SANDBOX_PROBE_ABORTED'
| 'HOST_SANDBOX_PROBE_EXIT_FAILED'
| 'HOST_SANDBOX_PROBE_RUNNER_FAILED'
| 'HOST_SANDBOX_PROBE_TIMED_OUT'
| 'HOST_CONTROL_PLANE_FAILED'
export class ControlledHarnessHostStartupError extends Error {
constructor(
readonly code: ControlledHarnessHostStartupCode,
options?: ErrorOptions
) {
super(code, options)
this.name = 'ControlledHarnessHostStartupError'
}
}
async function verifySandboxExecution(
ctx: Context,
expected: GoodBuddyHarnessControlConfig['sandbox'],
workspace: string
): Promise<void> {
const result = await ctx.shell.run(
ctx.shell.resolve({
command:
process.platform === 'win32'
? 'Write-Output goodbuddy-sandbox-probe'
: 'printf goodbuddy-sandbox-probe',
workdir: workspace,
timeoutMs: 10_000,
stdoutMaxBytes: 1_024,
sandboxPolicy: {
mode: 'read-only',
workspaceRoot: workspace
}
})
)
if (
result.sandbox?.enforcement !== expected.enforcement
) {
throw new Error(
'Controlled Harness sandbox execution probe failed'
)
}
if (result.timedOut) {
throw new ControlledHarnessHostStartupError(
'HOST_SANDBOX_PROBE_TIMED_OUT'
)
}
if (result.aborted) {
throw new ControlledHarnessHostStartupError(
'HOST_SANDBOX_PROBE_ABORTED'
)
}
if (result.sandbox?.runnerFailed) {
throw new ControlledHarnessHostStartupError(
'HOST_SANDBOX_PROBE_RUNNER_FAILED'
)
}
if (result.exitCode !== 0) {
throw new ControlledHarnessHostStartupError(
'HOST_SANDBOX_PROBE_EXIT_FAILED'
)
}
}
type PluginSpec = {
plugin: Parameters<Context['plugin']>[0]
config?: unknown
}
function validateHostConfig(
config: ControlledHarnessHostConfig
): void {
const endpoint = URL.canParse(config.baseUrl)
? new URL(config.baseUrl)
: undefined
if (
config.api !== 'openai-completions' ||
!endpoint ||
endpoint.protocol !== 'https:' ||
endpoint.hostname.toLowerCase() !== 'api.deepseek.com' ||
endpoint.username ||
endpoint.password
) {
throw new Error(
'Controlled Harness requires the trusted HTTPS DeepSeek endpoint'
)
}
if (!config.credentialRefs.length) {
throw new Error(
'Controlled Harness requires a Main-side credential reference'
)
}
if (!isAbsolute(config.workspace) || !isAbsolute(config.dshHome)) {
throw new Error(
'Controlled Harness requires absolute workspace and home paths'
)
}
}
async function canonicalizeHostConfig(
config: ControlledHarnessHostConfig
): Promise<ControlledHarnessHostConfig> {
const [workspace, dshHome] = await Promise.all([
realpath(config.workspace),
realpath(config.dshHome)
])
const [workspaceMetadata, homeMetadata] = await Promise.all([
stat(workspace),
stat(dshHome)
])
if (!workspaceMetadata.isDirectory() || !homeMetadata.isDirectory()) {
throw new Error(
'Controlled Harness workspace and home must be directories'
)
}
const skillPackages = await Promise.all(
config.skillPackages.map(async (skill) => {
const directory = await realpath(skill.directory)
const metadata = await stat(directory)
if (!metadata.isDirectory()) {
throw new Error(
'Controlled Harness Skill path must be a directory'
)
}
return { ...skill, directory }
})
)
return { ...config, workspace, dshHome, skillPackages }
}
async function loadControlledSkills(
skillPackages: ControlledHarnessHostConfig['skillPackages']
): Promise<GoodBuddyHarnessControlConfig['skills']> {
return Promise.all(
skillPackages.map(async (skill) => {
const manifest = await readFile(
join(skill.directory, 'SKILL.md'),
'utf8'
)
if (Buffer.byteLength(manifest, 'utf8') > 2 * 1024 * 1024) {
throw new Error('Controlled Harness Skill is too large')
}
const match =
/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]+)$/u.exec(
manifest
)
if (!match?.[1] || !match[2]?.trim()) {
throw new Error('Controlled Harness Skill manifest is invalid')
}
const metadata = parseYaml(match[1]) as Record<string, unknown>
const name =
typeof metadata.id === 'string'
? metadata.id
: metadata.name
const description = metadata.description
if (
name !== skill.id ||
typeof name !== 'string' ||
!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(name) ||
typeof description !== 'string'
) {
throw new Error('Controlled Harness Skill metadata is invalid')
}
return {
name,
description: description
.replace(/\s+/gu, ' ')
.trim()
.slice(0, 500),
content: match[2].trim(),
directory: skill.directory
}
})
)
}
function sandboxProviderName(): string {
return process.platform === 'win32'
? 'windows-acl'
: process.platform === 'darwin'
? 'seatbelt'
: 'local-linux'
}
function verifySandbox(
sandbox: {
confine(
argv: readonly string[],
policy: {
mode: 'read-only'
workspaceRoot: string
}
): {
enforcement: SandboxEnforcement
}
},
config: ControlledHarnessHostConfig
): GoodBuddyHarnessControlConfig['sandbox'] {
const expectedEnforcement: SandboxEnforcement =
process.platform === 'win32' ? 'partial' : 'full'
const probe = sandbox.confine(
process.platform === 'win32'
? ['cmd.exe', '/d', '/s', '/c', 'exit 0']
: ['/usr/bin/env', 'true'],
{
mode: 'read-only',
workspaceRoot: config.workspace
}
)
if (probe.enforcement !== expectedEnforcement) {
throw new Error(
'Controlled Harness sandbox enforcement probe returned an unexpected result'
)
}
if (config.sandbox.enforcement !== probe.enforcement) {
throw new Error(
'Controlled Harness sandbox capability does not match the verified provider'
)
}
return {
provider: sandboxProviderName(),
enforcement: probe.enforcement
}
}
/**
* Boots a fixed, programmatic Cordis graph. It never imports app-boot, a
* profile loader, settings-file, local credentials, persistence, telemetry,
* web, HMR, marketplace/plugin discovery, direct MCP clients, jobs,
* subagents, hooks, or workflow packages. The control plane registers only
* Main-selected Skill snapshots and Main-mediated MCP tool proxies.
*/
export async function startControlledDeepSeekHarnessHost(
input: ControlledHarnessHostConfig
): Promise<ControlledHarnessHost> {
validateHostConfig(input)
const config = await canonicalizeHostConfig(input)
const skills = await loadControlledSkills(config.skillPackages)
process.env.DSH_TELEMETRY_DISABLED = '1'
const ctx = new Context()
const specs: PluginSpec[] = [
{ plugin: LlmRuntime },
{ plugin: SessionStore },
{ plugin: SkillRegistry },
{
plugin: SystemPrompt,
config: {
persona: '',
includeHarnessIdentity: false,
includeRuntimeContext: true
}
},
{ plugin: ToolRuntime, config: { mode: 'native' } },
{ plugin: AgentRegistry },
{
plugin: GoodBuddyCredentialProvider,
config: new Set(config.credentialRefs)
},
{
plugin: PiAiLlm,
config: {
providers: {
[config.provider]: {
apiKeyEnv: config.credentialRefs[0],
api: config.api,
baseURL: config.baseUrl,
models: [{ id: config.model, input: ['text'] }]
}
}
}
},
{
plugin: SandboxPolicy,
config: {
mode: 'read-only',
workspaceRoot: config.workspace
}
},
{ plugin: ApprovalService, config: { policy: 'never' } },
{ plugin: LocalSubprocess },
{ plugin: LocalSandbox },
{ plugin: SandboxedFileSystem, config: { cwd: config.workspace } },
{ plugin: ShellEnv, config: { dshHome: config.dshHome } },
{
plugin:
process.platform === 'win32'
? SandboxedPwsh
: SandboxedBash,
config: { timeoutMs: 60_000 }
},
{ plugin: ToolFs },
{
plugin:
process.platform === 'win32' ? ToolPwsh : ToolBash,
config: { enableRunInBackground: false }
},
{ plugin: TokenMeter, config: {} },
{
plugin: AgentLoop,
config: { agents: [], maxParallelToolCalls: 10 }
}
]
const fibers: Fiber[] = []
let startupCode: ControlledHarnessHostStartupCode =
'HOST_PLUGIN_GRAPH_FAILED'
try {
for (const spec of specs) {
fibers.push(
ctx.plugin(
spec.plugin,
...(spec.config === undefined ? [] : [spec.config])
)
)
}
await Promise.all(fibers)
const credentialProvider = ctx.credentials
if (!(credentialProvider instanceof GoodBuddyCredentialProvider)) {
throw new Error(
'Controlled Harness credential provider failed to start'
)
}
startupCode = 'HOST_SANDBOX_CONFIGURATION_FAILED'
const verifiedSandbox = verifySandbox(ctx.sandbox, config)
startupCode = 'HOST_SANDBOX_EXECUTION_FAILED'
await verifySandboxExecution(
ctx,
verifiedSandbox,
config.workspace
)
startupCode = 'HOST_CONTROL_PLANE_FAILED'
const rawStream =
config.stream ??
createBoundedNdJsonStream(
stdoutStream(),
stdinStream(),
config.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES
)
const controlPlane = new GoodBuddyHarnessControlPlane(ctx, {
...config,
skills,
sandbox: verifiedSandbox,
stream: createBoundedAcpStream(
rawStream,
config.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES
)
})
controlPlane.bindCredentialProvider(credentialProvider)
controlPlane.start()
return {
context: ctx,
controlPlane,
async dispose() {
await controlPlane.dispose()
await ctx.fiber.dispose()
}
}
} catch (error) {
await ctx.fiber.dispose().catch(() => undefined)
if (error instanceof ControlledHarnessHostStartupError) {
throw error
}
throw new ControlledHarnessHostStartupError(startupCode, {
cause: error
})
}
}
export function createBoundedNdJsonStream(
output: WritableStream<Uint8Array>,
input: ReadableStream<Uint8Array>,
maxFrameBytes: number
): Stream {
const decoder = new TextDecoder('utf-8', { fatal: true })
const encoder = new TextEncoder()
return {
readable: new ReadableStream({
async start(controller) {
const reader = input.getReader()
let pending = ''
const emitCompleteFrames = (): void => {
let newline = pending.indexOf('\n')
while (newline >= 0) {
const line = pending.slice(0, newline).trim()
pending = pending.slice(newline + 1)
if (
line &&
Buffer.byteLength(line, 'utf8') > maxFrameBytes
) {
throw new Error('ACP input frame exceeds safety limit')
}
if (line) {
controller.enqueue(JSON.parse(line))
}
newline = pending.indexOf('\n')
}
if (Buffer.byteLength(pending, 'utf8') > maxFrameBytes) {
throw new Error('ACP input frame exceeds safety limit')
}
}
try {
while (true) {
const { value, done } = await reader.read()
if (done) {
pending += decoder.decode()
emitCompleteFrames()
break
}
pending += decoder.decode(value, { stream: true })
emitCompleteFrames()
}
const line = pending.trim()
if (line) {
if (Buffer.byteLength(line, 'utf8') > maxFrameBytes) {
throw new Error('ACP input frame exceeds safety limit')
}
controller.enqueue(JSON.parse(line))
}
controller.close()
} catch (error) {
controller.error(error)
} finally {
reader.releaseLock()
}
}
}),
writable: new WritableStream({
async write(message) {
const serialized = JSON.stringify(message)
if (
Buffer.byteLength(serialized, 'utf8') >
maxFrameBytes
) {
throw new Error('ACP output frame exceeds safety limit')
}
const bytes = encoder.encode(`${serialized}\n`)
const writer = output.getWriter()
try {
await writer.write(bytes)
} finally {
writer.releaseLock()
}
},
async close() {
const writer = output.getWriter()
try {
await writer.close()
} finally {
writer.releaseLock()
}
},
async abort(reason) {
await output.abort(reason)
}
})
}
}
function stdoutStream(): WritableStream<Uint8Array> {
return new WritableStream({
write(chunk) {
return new Promise<void>((resolve, reject) => {
process.stdout.write(chunk, (error) =>
error ? reject(error) : resolve()
)
})
}
})
}
function stdinStream(): ReadableStream<Uint8Array> {
return new ReadableStream({
start(controller) {
process.stdin.on('data', (chunk: Buffer) =>
controller.enqueue(new Uint8Array(chunk))
)
process.stdin.once('end', () => controller.close())
process.stdin.once('error', (error) =>
controller.error(error)
)
}
})
}
/**
* Keep diagnostics bounded and protocol-free. Call this in the utility entry
* before Cordis plugins start; no user content or secret is forwarded.
*/
export function installHarnessDiagnosticGuard(): () => void {
let bytes = 0
const original = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
debug: console.debug
}
const diagnostic = (): void => {
const line = 'DeepSeek Harness diagnostic suppressed\n'
const size = Buffer.byteLength(line)
if (bytes + size <= MAX_DIAGNOSTIC_BYTES) {
bytes += size
process.stderr.write(line)
}
}
console.log = diagnostic
console.info = diagnostic
console.warn = diagnostic
console.error = diagnostic
console.debug = diagnostic
return () => {
console.log = original.log
console.info = original.info
console.warn = original.warn
console.error = original.error
console.debug = original.debug
}
}
+58 -2
View File
@@ -10,8 +10,10 @@ import {
utilityProcess
} from 'electron'
import { homedir } from 'node:os'
import { mkdir } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import spawn from 'cross-spawn'
import { ipcChannels } from '../shared/ipc-channels'
import {
createAgentRuntime,
@@ -75,6 +77,11 @@ import { DocumentOcrBroker } from './document-ocr-broker'
import { DocumentParsingService } from './document-parsing-service'
import { ReleaseNotesService } from './release-notes-service'
import { GoodBuddyConfigService } from './goodbuddy-config-service'
import {
createDeepSeekHarnessUtilityLauncher,
type DeepSeekHarnessFork
} from './agent/deepseek-harness-utility-launcher'
import { buildControlledHarnessEnvironment } from './agent/process-environment'
const shortcut = 'CommandOrControl+Shift+Space'
const mainModuleDirectory = dirname(fileURLToPath(import.meta.url))
@@ -209,6 +216,36 @@ const launchContinueHost: ContinueHostLauncher = (
return child
}
const forkDeepSeekHarness: DeepSeekHarnessFork = (
modulePath,
args,
options
) =>
utilityProcess.fork(modulePath, args, {
...options,
allowLoadingUnsignedLibraries: false,
disclaim: false
})
function terminateHarnessUtilityProcess(
child: ReturnType<DeepSeekHarnessFork>
): void {
if (process.platform === 'win32' && child.pid) {
const killer = spawn(
'taskkill.exe',
['/PID', String(child.pid), '/T', '/F'],
{
shell: false,
stdio: 'ignore',
windowsHide: true
}
)
killer.unref()
return
}
child.kill()
}
const launchWechatSidecar: WechatSidecarLauncher = () => {
const utilityChild = utilityProcess.fork(
join(mainModuleDirectory, 'wechat-sidecar.js'),
@@ -403,6 +440,24 @@ if (hasSingleInstanceLock) {
resourcesPath: process.resourcesPath,
packaged: app.isPackaged
})
const deepSeekHarnessHome = join(
app.getPath('userData'),
'deepseek-harness'
)
await mkdir(deepSeekHarnessHome, {
recursive: true,
mode: 0o700
})
const launchDeepSeekHarness =
createDeepSeekHarnessUtilityLauncher({
bundledHostPath: bundledRuntimePaths.deepseekHarness,
dshHome: deepSeekHarnessHome,
environment: buildControlledHarnessEnvironment(
deepSeekHarnessHome
),
fork: forkDeepSeekHarness,
terminateProcess: terminateHarnessUtilityProcess
})
knowledgeService = new KnowledgeService({
databasePath: join(app.getPath('userData'), 'knowledge.sqlite'),
managedRoot: join(app.getPath('userData'), 'knowledge'),
@@ -465,8 +520,8 @@ if (hasSingleInstanceLock) {
] =
await Promise.all([
capabilityService.getRuntimeSkillContext(target),
target === 'model'
? capabilityService.getResolvedMcpServers('model')
target === 'model' || target === 'deepseek-harness'
? capabilityService.getResolvedMcpServers(target)
: Promise.resolve([]),
target === 'model'
? capabilityService.getComputerCapabilityStatus(
@@ -487,6 +542,7 @@ if (hasSingleInstanceLock) {
),
bundledRuntimePaths,
continueHostLauncher: launchContinueHost,
deepseekHarnessLauncher: launchDeepSeekHarness,
browserService:
browserCapability?.enabled && browserCapability.supported
? browserService
+74 -1
View File
@@ -2935,6 +2935,78 @@ describe('registerIpcHandlers agent terminal state', () => {
}
)
it('keeps Ask fail-closed and auto-allows DeepSeek Harness Execute tools', async () => {
const receivedAuthorizers: unknown[] = []
const executeDecisions: string[] = []
const runtime = {
runtimeId: 'deepseek-harness',
capability: 'chat',
requiresToolApproval: false,
supportsToolExecution: true,
getStatus: vi.fn(),
dispose: vi.fn(),
async *run(
request: { requestId: string; workMode?: string },
_signal: AbortSignal,
authorize?: (request: {
scopeKey: string
title: string
description: string
}) => Promise<string>
) {
receivedAuthorizers.push(authorize)
if (request.workMode === 'execute') {
executeDecisions.push(
(await authorize?.({
scopeKey: 'deepseek-harness:write_file',
title: '写入文件',
description: '一次性沙箱升级'
})) ?? 'missing'
)
}
yield { requestId: request.requestId, type: 'done' }
}
}
const harness = createHarness(runtime)
harness.approvalBroker.request.mockResolvedValue('once')
for (const [index, workMode] of (
['ask', 'execute'] as const
).entries()) {
const requestId = `3f496642-f47d-4e0a-8944-a32c77b0d6e${index}`
harness.handler?.(trustedEvent(harness.webContents), {
requestId,
conversationId: `conversation-${index}`,
prompt: 'run the task',
workMode
})
await vi.waitFor(() =>
expect(
harness.assistantDatabase.updateTaskStatus
).toHaveBeenCalledWith(requestId, 'completed')
)
}
expect(receivedAuthorizers).toEqual([
expect.any(Function),
expect.any(Function)
])
expect(executeDecisions).toEqual(['once'])
await expect(
(
receivedAuthorizers[0] as (
request: Record<string, string>
) => Promise<string>
)({
scopeKey: 'deepseek-harness:write_file',
title: '写入文件',
description: 'must be denied'
})
).resolves.toBe('deny')
expect(harness.approvalBroker.request).not.toHaveBeenCalled()
await harness.dispose()
})
it.each(['model', 'opencode'] as const)(
'normalizes legacy interactive Plan requests to Ask for %s',
async (runtimeId) => {
@@ -3936,7 +4008,8 @@ describe('registerIpcHandlers agent terminal state', () => {
harness.getResolvedSettings.mockResolvedValue({
toolApproval: 'always',
subagentSmartRoutingEnabled: false,
continueModelProfile: { id: configuredProfileId }
continueModelProfile: { id: configuredProfileId },
modelProfiles: [{ id: configuredProfileId }]
})
vi.mocked(
harness.assistantDatabase.listProjects
+31 -6
View File
@@ -163,7 +163,11 @@ import {
import { resolveConfiguredAgentRuntimeSelection } from './agent/runtime-selection'
import { safeToolErrorDetail } from './agent/approval-summary'
import { ReasoningTagStreamParser } from './agent/reasoning-stream'
import type { BundledRuntimePaths } from './agent/bundled-runtimes'
import {
bundledContinueVersion,
bundledDeepSeekHarnessVersion,
type BundledRuntimePaths
} from './agent/bundled-runtimes'
import type { SelectedRuntimeResolver } from './agent/selected-runtime-manager'
import {
type MagicNotesCapabilityAccess,
@@ -1258,6 +1262,8 @@ export function registerIpcHandlers(
!agentRuntimeSelected
? (await settingsStore.getPolicySettings()).toolApproval
: undefined
const automaticHarnessRuntime =
requestRuntime.runtimeId === 'deepseek-harness'
const authorize: RuntimeAuthorizer = async (approvalRequest) => {
controller.signal.throwIfAborted()
if (schedule.workMode !== 'execute') {
@@ -1266,6 +1272,9 @@ export function registerIpcHandlers(
if (origin === 'delegation') {
return 'deny'
}
if (automaticHarnessRuntime) {
return 'once'
}
if (origin === 'channel') {
return channelToolPolicy === 'policy' ? 'deny' : 'once'
}
@@ -2456,16 +2465,26 @@ export function registerIpcHandlers(
throw error
}
}
const automaticHarnessRuntime =
selectedRuntime.runtimeId === 'deepseek-harness'
const executeToolPolicy =
request.workMode === 'execute' && !agentRuntimeSelected
? (await settingsStore.getPolicySettings()).toolApproval
: 'policy'
const authorize: RuntimeAuthorizer = async () => {
controller.signal.throwIfAborted()
return request.workMode === 'execute' &&
if (
request.workMode !== 'execute'
) {
return 'deny'
}
if (
automaticHarnessRuntime ||
executeToolPolicy !== 'policy'
? 'once'
: 'deny'
) {
return 'once'
}
return 'deny'
}
let smartRoute:
| ReturnType<typeof routeSubagent>
@@ -2771,7 +2790,11 @@ export function registerIpcHandlers(
return detectAgentRuntimes({
opencodeBinaryPath: settings.opencodeBinaryPath,
continueBinaryPath: settings.continueBinaryPath,
bundledPaths: bundledRuntimePaths
bundledPaths: bundledRuntimePaths,
bundledVersions: {
continue: bundledContinueVersion,
deepseekHarness: bundledDeepSeekHarnessVersion
}
})
}
)
@@ -2781,7 +2804,9 @@ export function registerIpcHandlers(
async (event, input: unknown): Promise<string | undefined> => {
assertTrustedSender(event, window)
const kind = runtimeFileSelectionKindSchema.parse(input)
const binary = kind.endsWith('Binary')
const binary =
kind === 'opencodeBinary' ||
kind === 'continueBinary'
const configRuntime =
kind === 'opencodeConfig'
? 'opencode'
+199 -5
View File
@@ -42,6 +42,7 @@ function settings(
continueBinaryPath: '',
continueConfigPath: '',
continueMode: 'chat',
deepseekHarnessModelSource: { kind: 'platform' },
runtimeSandboxMode: 'auto',
knowledgeEmbeddingEnabled: false,
knowledgeEmbeddingBaseUrl:
@@ -103,6 +104,199 @@ describe('RuntimeSettingsStore', () => {
})
})
it('migrates DeepSeek Harness to controlled platform mode and stores an official profile', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const versionFourteen = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
versionFourteen.version = 14
delete versionFourteen.deepseekHarnessModelSource
delete versionFourteen.deepseekHarnessBinaryPath
await writeFile(filePath, JSON.stringify(versionFourteen), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getResolvedSettings()).resolves.toMatchObject({
deepseekHarnessModelProfile: undefined
})
const profileId = '00000000-0000-4000-8000-000000000044'
await migrated.update(
settings({
provider: 'deepseek-harness',
modelProfiles: [
{
id: profileId,
name: 'DeepSeek',
baseUrl: 'https://api.deepseek.com',
modelName: 'deepseek-chat',
protocol: 'openai-chat-completions',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: { action: 'replace', value: 'deepseek-secret' }
}
],
defaultModelProfileId: profileId,
deepseekHarnessModelSource: { kind: 'profile', profileId }
})
)
await expect(migrated.getResolvedSettings()).resolves.toMatchObject({
provider: 'deepseek-harness',
deepseekHarnessModelProfile: {
id: profileId,
apiKey: 'deepseek-secret'
}
})
})
it('resolves a controlled platform DeepSeek profile without exposing its credential', async () => {
const apiKey = 'platform-deepseek-secret'
const { store } = await createStore({
GOODBUDDY_MODEL_API_KEY: apiKey,
GOODBUDDY_MODEL_BASE_URL: 'https://api.deepseek.com/',
GOODBUDDY_MODEL_NAME: 'deepseek-v4-flash'
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelProtocol: 'anthropic-messages',
deepseekHarnessModelProfile: {
id: 'goodbuddy-platform-deepseek',
name: '平台 DeepSeek',
baseUrl: 'https://api.deepseek.com/',
modelName: 'deepseek-v4-flash',
protocol: 'openai-chat-completions',
authentication: 'api-key',
supportsImageInput: false,
imageGenerationQuality: 'auto',
apiKey
}
})
const publicSettings = await store.getPublicSettings()
expect(JSON.stringify(publicSettings)).not.toContain(apiKey)
expect(publicSettings.modelProtocol).toBe('anthropic-messages')
})
it.each([
[
'a non-DeepSeek endpoint',
{
GOODBUDDY_MODEL_API_KEY: 'platform-key',
GOODBUDDY_MODEL_BASE_URL: 'https://deepseek.example',
GOODBUDDY_MODEL_NAME: 'deepseek-chat'
}
],
[
'an insecure DeepSeek endpoint',
{
GOODBUDDY_MODEL_API_KEY: 'platform-key',
GOODBUDDY_MODEL_BASE_URL: 'http://api.deepseek.com',
GOODBUDDY_MODEL_NAME: 'deepseek-chat'
}
],
[
'a DeepSeek endpoint path',
{
GOODBUDDY_MODEL_API_KEY: 'platform-key',
GOODBUDDY_MODEL_BASE_URL: 'https://api.deepseek.com/v1',
GOODBUDDY_MODEL_NAME: 'deepseek-chat'
}
],
[
'a missing API key',
{
GOODBUDDY_MODEL_BASE_URL: 'https://api.deepseek.com',
GOODBUDDY_MODEL_NAME: 'deepseek-chat'
}
]
])('does not resolve platform DeepSeek from %s', async (_, environment) => {
const { store } = await createStore(environment)
await expect(store.getResolvedSettings()).resolves.toMatchObject({
deepseekHarnessModelProfile: undefined
})
})
it('drops the legacy custom Harness Host path and ignores its environment override', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const versionFifteen = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
versionFifteen.version = 15
versionFifteen.deepseekHarnessBinaryPath =
'C:\\untrusted\\custom-harness.js'
await writeFile(filePath, JSON.stringify(versionFifteen), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {
GOODBUDDY_DEEPSEEK_HARNESS_BINARY:
'C:\\environment\\custom-harness.js'
})
const publicSettings = await migrated.getPublicSettings()
const resolvedSettings = await migrated.getResolvedSettings()
expect(publicSettings).not.toHaveProperty(
'deepseekHarnessBinaryPath'
)
expect(publicSettings.configured).not.toHaveProperty(
'deepseekHarnessBinaryPath'
)
expect(resolvedSettings).not.toHaveProperty(
'deepseekHarnessBinaryPath'
)
await migrated.update(settings())
const persisted = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
expect(persisted.version).toBe(16)
expect(persisted).not.toHaveProperty(
'deepseekHarnessBinaryPath'
)
})
it('rejects incompatible DeepSeek Harness model profiles', () => {
const profileId = '00000000-0000-4000-8000-000000000045'
expect(
runtimeSettingsInputSchema.safeParse(
settings({
modelProfiles: [
{
id: profileId,
name: 'Other compatible API',
baseUrl: 'https://other.example/v1',
modelName: 'deepseek-chat',
protocol: 'openai-chat-completions',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: { action: 'keep' }
}
],
defaultModelProfileId: profileId,
deepseekHarnessModelSource: { kind: 'profile', profileId }
})
).success
).toBe(false)
expect(
runtimeSettingsInputSchema.safeParse(
settings({
modelProfiles: [
{
id: profileId,
name: 'DeepSeek without API key',
baseUrl: 'https://api.deepseek.com',
modelName: 'deepseek-chat',
protocol: 'openai-chat-completions',
authentication: 'none',
imageGenerationQuality: 'auto',
apiKey: { action: 'clear' }
}
],
defaultModelProfileId: profileId,
deepseekHarnessModelSource: { kind: 'profile', profileId }
})
).success
).toBe(false)
})
it('always enables bundled OpenCode when the Server address is blank', async () => {
const { filePath, store } = await createStore({
GOODBUDDY_OPENCODE_EMBEDDED: 'false'
@@ -331,7 +525,7 @@ describe('RuntimeSettingsStore', () => {
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
}
expect(persisted.version).toBe(14)
expect(persisted.version).toBe(16)
})
it('migrates version 11 and removes the obsolete intranet toggle', async () => {
@@ -351,7 +545,7 @@ describe('RuntimeSettingsStore', () => {
version: number
intranetCompatibilityEnabled?: boolean
}
expect(persisted.version).toBe(14)
expect(persisted.version).toBe(16)
expect(persisted).not.toHaveProperty('intranetCompatibilityEnabled')
})
@@ -940,7 +1134,7 @@ describe('RuntimeSettingsStore', () => {
version: number
modelProfiles: Array<Record<string, unknown>>
}
expect(persisted.version).toBe(14)
expect(persisted.version).toBe(16)
expect(persisted.modelProfiles).toContainEqual(
expect.objectContaining({
id: imageId,
@@ -1186,7 +1380,7 @@ describe('RuntimeSettingsStore', () => {
unknown
>
expect(saved).toMatchObject({
version: 14,
version: 16,
provider: 'model',
continueBinaryPath: '',
continueMode: 'chat',
@@ -1465,7 +1659,7 @@ describe('RuntimeSettingsStore', () => {
version: number
modelProfiles: Array<Record<string, unknown>>
}
expect(persisted.version).toBe(14)
expect(persisted.version).toBe(16)
expect(persisted.modelProfiles[0]).not.toHaveProperty('credential')
})
+241 -98
View File
@@ -11,6 +11,7 @@ import {
defaultRuntimeSettings,
imageGenerationQualitySchema,
isAgentRuntimeModelProtocol,
isDeepSeekHarnessModelProfile,
modelAuthenticationSchema,
modelProtocolSchema,
runtimeModelSourceSchema,
@@ -164,7 +165,7 @@ const version13StoredSettingsSchema = version12StoredSettingsSchema
.max(20)
})
const storedSettingsSchema = version13StoredSettingsSchema
const version14StoredSettingsSchema = version13StoredSettingsSchema
.omit({ version: true })
.extend({
version: z.literal(14),
@@ -185,7 +186,24 @@ const storedSettingsSchema = version13StoredSettingsSchema
knowledgeRerankCredential: credentialSchema
})
const version15StoredSettingsSchema = version14StoredSettingsSchema
.omit({ version: true })
.extend({
version: z.literal(15),
deepseekHarnessModelSource: runtimeModelSourceSchema,
deepseekHarnessBinaryPath: runtimePathSchema.default('')
})
const storedSettingsSchema = version15StoredSettingsSchema
.omit({ version: true, deepseekHarnessBinaryPath: true })
.extend({
version: z.literal(16)
})
type StoredSettings = z.infer<typeof storedSettingsSchema>
type Version15StoredSettings = z.infer<
typeof version15StoredSettingsSchema
>
type Version10StoredSettings = z.infer<
typeof version10StoredSettingsSchema
>
@@ -198,6 +216,9 @@ type Version12StoredSettings = z.infer<
type Version13StoredSettings = z.infer<
typeof version13StoredSettingsSchema
>
type Version14StoredSettings = z.infer<
typeof version14StoredSettingsSchema
>
const version3StoredSettingsSchema = version4StoredSettingsSchema
.omit({ version: true, continueMode: true })
@@ -242,6 +263,7 @@ const embeddingCredentialPayloadSchema = z.object({
})
const rerankCredentialPayloadSchema = embeddingCredentialPayloadSchema
const platformDeepSeekProfileId = 'goodbuddy-platform-deepseek'
export type CredentialCipher = SettingsCredentialCipher
@@ -258,6 +280,7 @@ export type ResolvedRuntimeSettings = {
defaultModelProfileId: string
opencodeModelProfile?: ResolvedModelProfile
continueModelProfile?: ResolvedModelProfile
deepseekHarnessModelProfile?: ResolvedModelProfile
opencodeBaseUrl: string
opencodeEmbedded: boolean
opencodeBinaryPath: string
@@ -297,7 +320,7 @@ export type ResolvedModelProfile = {
}
const defaultSettings: StoredSettings = {
version: 14,
version: 16,
provider: defaultRuntimeSettings.provider,
modelProfiles: [
{
@@ -328,6 +351,7 @@ const defaultSettings: StoredSettings = {
continueBinaryPath: defaultRuntimeSettings.continueBinaryPath,
continueConfigPath: defaultRuntimeSettings.continueConfigPath,
continueMode: defaultRuntimeSettings.continueMode,
deepseekHarnessModelSource: { kind: 'platform' },
runtimeSandboxMode: defaultRuntimeSettings.runtimeSandboxMode,
subagentSmartRoutingEnabled:
defaultRuntimeSettings.subagentSmartRoutingEnabled,
@@ -411,7 +435,7 @@ function migrateVersion12(
function migrateVersion13(
settings: Version13StoredSettings
): StoredSettings {
return {
return migrateVersion14({
...settings,
version: 14,
knowledgeRerankEnabled:
@@ -421,6 +445,30 @@ function migrateVersion13(
knowledgeRerankModel:
defaultRuntimeSettings.knowledgeRerankModel,
knowledgeRerankCredential: undefined
})
}
function migrateVersion14(
settings: Version14StoredSettings
): StoredSettings {
return {
...settings,
version: 16,
deepseekHarnessModelSource: { kind: 'platform' }
}
}
function migrateVersion15(
settings: Version15StoredSettings
): StoredSettings {
const {
deepseekHarnessBinaryPath: _obsolete,
...current
} = settings
void _obsolete
return {
...current,
version: 16
}
}
@@ -483,6 +531,19 @@ function normalizeStoredSettings(settings: StoredSettings): StoredSettings {
? { kind: 'profile', profileId: fallbackProfileId }
: { kind: 'platform' }
}
const normalizeDeepSeekHarnessSource = (
source: RuntimeSettings['deepseekHarnessModelSource']
): NonNullable<RuntimeSettings['deepseekHarnessModelSource']> => {
if (!source || source.kind === 'platform') {
return { kind: 'platform' }
}
const profile = modelProfiles.find(
(candidate) => candidate.id === source.profileId
)
return profile && isDeepSeekHarnessModelProfile(profile)
? source
: { kind: 'platform' }
}
const opencodeBaseUrl = settings.opencodeBaseUrl.trim()
if (opencodeBaseUrl) {
const url = new URL(opencodeBaseUrl)
@@ -508,6 +569,9 @@ function normalizeStoredSettings(settings: StoredSettings): StoredSettings {
continueModelSource: normalizeSource(
settings.continueModelSource
),
deepseekHarnessModelSource: normalizeDeepSeekHarnessSource(
settings.deepseekHarnessModelSource
),
opencodeBaseUrl,
opencodeEmbedded: !opencodeBaseUrl
}
@@ -683,7 +747,7 @@ export class RuntimeSettingsStore {
const parsed: unknown = JSON.parse(contents)
assertSupportedSettingsVersion(
parsed,
14,
16,
(version) =>
`当前 GoodBuddy 不支持 Runtime 设置版本 ${version},请升级应用后重试`
)
@@ -691,110 +755,122 @@ export class RuntimeSettingsStore {
if (current.success) {
this.settings = current.data
} else {
const version13 =
version13StoredSettingsSchema.safeParse(parsed)
if (version13.success) {
this.settings = migrateVersion13(version13.data)
const version15 =
version15StoredSettingsSchema.safeParse(parsed)
if (version15.success) {
this.settings = migrateVersion15(version15.data)
} else {
const version12 =
version12StoredSettingsSchema.safeParse(parsed)
if (version12.success) {
this.settings = migrateVersion12(version12.data)
const version14 =
version14StoredSettingsSchema.safeParse(parsed)
if (version14.success) {
this.settings = migrateVersion14(version14.data)
} else {
const version11 =
version11StoredSettingsSchema.safeParse(parsed)
if (version11.success) {
this.settings = migrateVersion11(version11.data)
const version13 =
version13StoredSettingsSchema.safeParse(parsed)
if (version13.success) {
this.settings = migrateVersion13(version13.data)
} else {
const version10 =
version10StoredSettingsSchema.safeParse(parsed)
if (version10.success) {
this.settings = migrateVersion10(version10.data)
const version12 =
version12StoredSettingsSchema.safeParse(parsed)
if (version12.success) {
this.settings = migrateVersion12(version12.data)
} else {
const version9 =
version9StoredSettingsSchema.safeParse(parsed)
if (version9.success) {
this.settings = migrateVersion9(version9.data)
const version11 =
version11StoredSettingsSchema.safeParse(parsed)
if (version11.success) {
this.settings = migrateVersion11(version11.data)
} else {
const version8 =
version8StoredSettingsSchema.safeParse(parsed)
if (version8.success) {
this.settings = migrateVersion8(version8.data)
const version10 =
version10StoredSettingsSchema.safeParse(parsed)
if (version10.success) {
this.settings = migrateVersion10(version10.data)
} else {
const version7 =
version7StoredSettingsSchema.safeParse(parsed)
if (version7.success) {
this.settings = migrateVersion7(version7.data)
const version9 =
version9StoredSettingsSchema.safeParse(parsed)
if (version9.success) {
this.settings = migrateVersion9(version9.data)
} else {
const version6 =
version6StoredSettingsSchema.safeParse(parsed)
if (version6.success) {
this.settings = migrateVersion6(version6.data)
const version8 =
version8StoredSettingsSchema.safeParse(parsed)
if (version8.success) {
this.settings = migrateVersion8(version8.data)
} else {
const version5 =
version5StoredSettingsSchema.safeParse(parsed)
if (version5.success) {
this.settings = migrateVersion5(version5.data)
const version7 =
version7StoredSettingsSchema.safeParse(parsed)
if (version7.success) {
this.settings = migrateVersion7(version7.data)
} else {
const version4 =
version4StoredSettingsSchema.safeParse(parsed)
if (version4.success) {
this.settings = migrateVersion4(version4.data)
const version6 =
version6StoredSettingsSchema.safeParse(parsed)
if (version6.success) {
this.settings = migrateVersion6(version6.data)
} else {
const version3 =
version3StoredSettingsSchema.safeParse(parsed)
if (version3.success) {
this.settings = migrateVersion4({
...version3.data,
version: 4,
continueMode: 'chat'
})
const version5 =
version5StoredSettingsSchema.safeParse(parsed)
if (version5.success) {
this.settings = migrateVersion5(version5.data)
} else {
const version2 =
version2StoredSettingsSchema.safeParse(parsed)
if (version2.success) {
this.settings = migrateVersion4({
version: 4,
provider: version2.data.provider,
modelBaseUrl: version2.data.modelBaseUrl,
modelName: version2.data.modelName,
opencodeBaseUrl: version2.data.opencodeBaseUrl,
opencodeEmbedded: version2.data.opencodeEmbedded,
opencodeBinaryPath: '',
opencodeConfigPath: '',
continueBinaryPath: migrateContinueCommand(
version2.data.continueCommand
),
continueConfigPath: '',
continueMode: 'chat',
workspacePath: version2.data.workspacePath,
credential: version2.data.credential,
toolApproval: version2.data.toolApproval
})
const version4 =
version4StoredSettingsSchema.safeParse(parsed)
if (version4.success) {
this.settings = migrateVersion4(version4.data)
} else {
const legacy =
legacyStoredSettingsSchema.parse(parsed)
this.settings = migrateVersion4({
version: 4,
provider:
legacy.provider === 'bigtoken'
? 'model'
: legacy.provider,
modelBaseUrl: legacy.bigtokenBaseUrl,
modelName: legacy.bigtokenModel,
opencodeBaseUrl: legacy.opencodeBaseUrl,
opencodeEmbedded: legacy.opencodeEmbedded,
opencodeBinaryPath: '',
opencodeConfigPath: '',
continueBinaryPath: migrateContinueCommand(
legacy.continueCommand
),
continueConfigPath: '',
continueMode: 'chat',
workspacePath: legacy.workspacePath,
credential: legacy.credential,
toolApproval: legacy.toolApproval
})
const version3 =
version3StoredSettingsSchema.safeParse(parsed)
if (version3.success) {
this.settings = migrateVersion4({
...version3.data,
version: 4,
continueMode: 'chat'
})
} else {
const version2 =
version2StoredSettingsSchema.safeParse(parsed)
if (version2.success) {
this.settings = migrateVersion4({
version: 4,
provider: version2.data.provider,
modelBaseUrl: version2.data.modelBaseUrl,
modelName: version2.data.modelName,
opencodeBaseUrl: version2.data.opencodeBaseUrl,
opencodeEmbedded: version2.data.opencodeEmbedded,
opencodeBinaryPath: '',
opencodeConfigPath: '',
continueBinaryPath: migrateContinueCommand(
version2.data.continueCommand
),
continueConfigPath: '',
continueMode: 'chat',
workspacePath: version2.data.workspacePath,
credential: version2.data.credential,
toolApproval: version2.data.toolApproval
})
} else {
const legacy =
legacyStoredSettingsSchema.parse(parsed)
this.settings = migrateVersion4({
version: 4,
provider:
legacy.provider === 'bigtoken'
? 'model'
: legacy.provider,
modelBaseUrl: legacy.bigtokenBaseUrl,
modelName: legacy.bigtokenModel,
opencodeBaseUrl: legacy.opencodeBaseUrl,
opencodeEmbedded: legacy.opencodeEmbedded,
opencodeBinaryPath: '',
opencodeConfigPath: '',
continueBinaryPath: migrateContinueCommand(
legacy.continueCommand
),
continueConfigPath: '',
continueMode: 'chat',
workspacePath: legacy.workspacePath,
credential: legacy.credential,
toolApproval: legacy.toolApproval
})
}
}
}
}
}
@@ -965,6 +1041,43 @@ export class RuntimeSettingsStore {
)
}
private resolvePlatformDeepSeekProfile(): ResolvedModelProfile | undefined {
const apiKey = this.environment.GOODBUDDY_MODEL_API_KEY?.trim()
const baseUrl = this.environment.GOODBUDDY_MODEL_BASE_URL?.trim()
const modelName = this.environment.GOODBUDDY_MODEL_NAME?.trim()
if (!apiKey || !baseUrl || !modelName) {
return undefined
}
try {
const endpoint = new URL(baseUrl)
if (
endpoint.protocol !== 'https:' ||
endpoint.hostname.toLowerCase() !== 'api.deepseek.com' ||
endpoint.port ||
endpoint.pathname !== '/' ||
endpoint.search ||
endpoint.hash ||
endpoint.username ||
endpoint.password
) {
return undefined
}
} catch {
return undefined
}
return {
id: platformDeepSeekProfileId,
name: '平台 DeepSeek',
baseUrl,
modelName,
protocol: 'openai-chat-completions',
authentication: 'api-key',
supportsImageInput: false,
imageGenerationQuality: 'auto',
apiKey
}
}
private resolveEffectiveModelSettings(settings: StoredSettings): {
apiKey?: string
baseUrl: string
@@ -1235,6 +1348,8 @@ export class RuntimeSettingsStore {
? { kind: 'platform' }
: settings.opencodeModelSource,
continueModelSource: settings.continueModelSource,
deepseekHarnessModelSource:
settings.deepseekHarnessModelSource,
secureStorageAvailable: this.cipher.isAvailable(),
toolApproval: settings.toolApproval,
configured: {
@@ -1246,7 +1361,9 @@ export class RuntimeSettingsStore {
continueConfigPath: settings.continueConfigPath,
workspacePath: settings.workspacePath || homedir(),
opencodeModelSource: settings.opencodeModelSource,
continueModelSource: settings.continueModelSource
continueModelSource: settings.continueModelSource,
deepseekHarnessModelSource:
settings.deepseekHarnessModelSource
},
...(this.loadWarnings.length > 0
? { warnings: [...this.loadWarnings] }
@@ -1284,6 +1401,12 @@ export class RuntimeSettingsStore {
settings.continueModelSource.kind === 'profile'
? profilesById.get(settings.continueModelSource.profileId)
: undefined
const deepseekHarnessModelProfile =
settings.deepseekHarnessModelSource.kind === 'profile'
? profilesById.get(
settings.deepseekHarnessModelSource.profileId
)
: this.resolvePlatformDeepSeekProfile()
return {
provider: settings.provider,
modelBaseUrl: effective.baseUrl,
@@ -1297,6 +1420,7 @@ export class RuntimeSettingsStore {
defaultModelProfileId: settings.defaultModelProfileId,
opencodeModelProfile,
continueModelProfile,
deepseekHarnessModelProfile,
...agent,
subagentSmartRoutingEnabled:
settings.subagentSmartRoutingEnabled,
@@ -1589,15 +1713,34 @@ export class RuntimeSettingsStore {
: repairRuntimeSource(current.continueModelSource)
validateRuntimeSource(opencodeModelSource, 'OpenCode')
validateRuntimeSource(continueModelSource, 'Continue')
const requestedDeepSeekHarnessSource =
input.deepseekHarnessModelSource ??
current.deepseekHarnessModelSource
if (requestedDeepSeekHarnessSource.kind === 'profile') {
const profile = modelProfiles.find(
(candidate) =>
candidate.id === requestedDeepSeekHarnessSource.profileId
)
if (!profile) {
throw new Error('DeepSeek Harness 引用的模型连接不存在')
}
if (!isDeepSeekHarnessModelProfile(profile)) {
throw new Error(
'DeepSeek Harness 模型连接仅支持 api.deepseek.com 的 OpenAI Chat Completions 协议'
)
}
}
const next: StoredSettings = {
...current,
version: 14,
version: 16,
provider: input.provider,
modelProfiles,
defaultModelProfileId,
opencodeModelSource,
continueModelSource,
deepseekHarnessModelSource:
requestedDeepSeekHarnessSource,
opencodeBaseUrl,
opencodeEmbedded: !opencodeBaseUrl,
opencodeBinaryPath,
+16 -2
View File
@@ -243,6 +243,11 @@ const api: DesktopApi = {
continue: {
available: false,
detail: '未检测到 Continue'
},
deepseekHarness: {
available: true,
path: 'bundled://deepseek-harness',
detail: 'Bundled Harness Adapter ready'
}
})),
selectRuntimeFile: vi.fn(async () => undefined),
@@ -3076,6 +3081,11 @@ describe('App', () => {
name: /^Continue · .*sonnet-5$/u
})
).toBeInTheDocument()
expect(
screen.getByRole('menuitemradio', {
name: /^DeepSeek Harness · /u
})
).toBeInTheDocument()
fireEvent.click(
screen.getByRole('menuitemradio', {
name: /^.*sonnet-5$/u
@@ -3190,12 +3200,16 @@ describe('App', () => {
const continueModel = screen.getByRole('menuitemradio', {
name: /^Continue · .*sonnet-5$/u
})
const deepseekHarness = screen.getByRole('menuitemradio', {
name: /^DeepSeek Harness · /u
})
expect(directModel).toBeEnabled()
expect(secondDirectModel).toBeEnabled()
expect(openCodeModel).toBeEnabled()
expect(continueModel).toBeEnabled()
expect(screen.getAllByRole('menuitemradio')).toHaveLength(4)
expect(within(runtimeMenu).getAllByRole('separator')).toHaveLength(3)
expect(deepseekHarness).toBeEnabled()
expect(screen.getAllByRole('menuitemradio')).toHaveLength(5)
expect(within(runtimeMenu).getAllByRole('separator')).toHaveLength(4)
expect(within(runtimeMenu).queryByRole('menu')).not.toBeInTheDocument()
expect(
screen.queryByRole('menuitemradio', {
+68 -2
View File
@@ -295,6 +295,7 @@ function isAgentRuntime(
runtime: AgentRuntimeStatus | undefined
): boolean {
return runtime?.id === 'opencode' || runtime?.id === 'continue'
|| runtime?.id === 'deepseek-harness'
}
function supportsSubagentSmartRouting(
@@ -899,6 +900,13 @@ function getRuntimeSelectionLabel(
? `Continue · ${labels.modelUnavailable}`
: 'Continue'
}
if (selection.provider === 'deepseek-harness') {
return profile
? `DeepSeek Harness · ${profile.name}`
: requestedProfileMissing
? `DeepSeek Harness · ${labels.modelUnavailable}`
: 'DeepSeek Harness'
}
return status
? `${labels.automatic} · ${status.label}`
: labels.automaticSelection
@@ -906,7 +914,7 @@ function getRuntimeSelectionLabel(
function getConfiguredAgentRuntimeSource(
settings: RuntimeSettings,
provider: 'opencode' | 'continue',
provider: 'opencode' | 'continue' | 'deepseek-harness',
labels: {
modelUnavailable: string
selectModel: string
@@ -921,7 +929,12 @@ function getConfiguredAgentRuntimeSource(
(candidate) => candidate.id === selection.profileId
)
: undefined
const runtimeLabel = provider === 'opencode' ? 'OpenCode' : 'Continue'
const runtimeLabel =
provider === 'opencode'
? 'OpenCode'
: provider === 'continue'
? 'Continue'
: 'DeepSeek Harness'
if ('profileId' in selection) {
return {
label: `${runtimeLabel} · ${profile?.name ?? labels.modelUnavailable}`,
@@ -1801,6 +1814,12 @@ function App(): React.JSX.Element {
const continueMenuSelection = runtimeSettings
? getRuntimeSelectionForProvider('continue', runtimeSettings)
: undefined
const deepseekHarnessMenuSelection = runtimeSettings
? getRuntimeSelectionForProvider(
'deepseek-harness',
runtimeSettings
)
: undefined
const openCodeMenuSource = runtimeSettings
? getConfiguredAgentRuntimeSource(
runtimeSettings,
@@ -1815,6 +1834,13 @@ function App(): React.JSX.Element {
configuredRuntimeLabels
)
: undefined
const deepseekHarnessMenuSource = runtimeSettings
? getConfiguredAgentRuntimeSource(
runtimeSettings,
'deepseek-harness',
configuredRuntimeLabels
)
: undefined
useEffect(() => {
if (!runtimeMenuOpen) {
return
@@ -6397,6 +6423,46 @@ function App(): React.JSX.Element {
className="runtime-picker__divider"
role="separator"
/>
<strong role="presentation">
{t('runtime.deepseekHarnessGroup')}
</strong>
{deepseekHarnessMenuSelection &&
deepseekHarnessMenuSource && (
<button
aria-checked={
activeRuntimeSelectionKey ===
agentRuntimeSelectionKey(
deepseekHarnessMenuSelection
)
}
onClick={() =>
void switchRuntime(
deepseekHarnessMenuSelection
)
}
role="menuitemradio"
tabIndex={
activeRuntimeSelectionKey ===
agentRuntimeSelectionKey(
deepseekHarnessMenuSelection
)
? 0
: -1
}
type="button"
>
<span>
{deepseekHarnessMenuSource.label}
</span>
<small>
{deepseekHarnessMenuSource.detail}
</small>
</button>
)}
<div
className="runtime-picker__divider"
role="separator"
/>
<button
onClick={() => {
setRuntimeMenuOpen(false)
@@ -501,6 +501,11 @@ describe('ChannelSettingsSection', () => {
expect(
within(backend).getByRole('option', { name: 'Continue' })
).toBeInTheDocument()
expect(
within(backend).getByRole('option', {
name: 'DeepSeek Harness(预览 · 仅 DeepSeek'
})
).toBeInTheDocument()
fireEvent.change(backend, {
target: {
+16 -3
View File
@@ -196,7 +196,7 @@ function usableChannelModelProfiles(
}
function configuredRuntimeSelection(
provider: 'opencode' | 'continue'
provider: 'opencode' | 'continue' | 'deepseek-harness'
): AgentRuntimeSelection {
return { provider }
}
@@ -231,7 +231,11 @@ function runtimeSelectionDescription(
return t('channels.project.automaticDescription')
}
const runtimeLabel =
selection.provider === 'opencode' ? 'OpenCode' : 'Continue'
selection.provider === 'opencode'
? 'OpenCode'
: selection.provider === 'continue'
? 'Continue'
: 'DeepSeek Harness'
return t('channels.project.runtimeDescription', {
runtime: runtimeLabel
})
@@ -255,6 +259,9 @@ function ChannelProjectControls({
const continueSelection = configuredRuntimeSelection(
'continue'
)
const deepseekHarnessSelection = configuredRuntimeSelection(
'deepseek-harness'
)
const directProfiles = usableChannelModelProfiles(runtimeSettings)
const selectedDirectProfileId =
draft.runtimeSelection.provider === 'model'
@@ -274,7 +281,8 @@ function ChannelProjectControls({
profileId: profile.id
})),
openCodeSelection,
continueSelection
continueSelection,
deepseekHarnessSelection
]
const selectionByKey = new Map(
selections.map((selection) => [
@@ -377,6 +385,11 @@ function ChannelProjectControls({
<option value={agentRuntimeSelectionKey(continueSelection)}>
Continue
</option>
<option
value={agentRuntimeSelectionKey(deepseekHarnessSelection)}
>
{t('channels.project.deepseekHarnessOption')}
</option>
</optgroup>
</select>
<small>
@@ -213,6 +213,21 @@ describe('DocumentParsingSettingsSection', () => {
expect(screen.getByText(label)).toBeInTheDocument()
})
it('localizes built-in OCR model metadata in English', async () => {
await changeUiLocale('en-US')
render(<DocumentParsingSettingsSection />)
expect(await screen.findByText('PP-OCRv6 Tiny')).toBeInTheDocument()
expect(
screen.getByText(
'The official lightweight PaddleOCR Chinese model for local CPU recognition of scanned PDFs and images.'
)
).toBeInTheDocument()
expect(screen.getByText('Chinese / English')).toBeInTheDocument()
expect(screen.queryByText('轻量中文 OCR 模型')).not.toBeInTheDocument()
})
it('localizes recovered document parsing settings warnings', async () => {
await changeUiLocale('en-US')
getSnapshot.mockResolvedValueOnce({
@@ -423,6 +423,16 @@ export function DocumentParsingSettingsSection({
const installedModel = snapshot.ocrModels.installed.find(
(entry) => entry.id === draft.localOcrModelId
)
const modelDisplayName = model
? t(`documentParsing.ocr.catalog.${model.id}.displayName`, {
defaultValue: model.displayName
})
: ''
const modelDescription = model
? t(`documentParsing.ocr.catalog.${model.id}.description`, {
defaultValue: model.description
})
: ''
const modelOperation = snapshot.ocrModels.operations.find(
(operation) => operation.modelId === draft.localOcrModelId
)
@@ -682,9 +692,13 @@ export function DocumentParsingSettingsSection({
const installed = snapshot.ocrModels.installed.some(
(candidate) => candidate.id === entry.id
)
const entryDisplayName = t(
`documentParsing.ocr.catalog.${entry.id}.displayName`,
{ defaultValue: entry.displayName }
)
return (
<option key={entry.id} value={entry.id}>
{entry.displayName} ·{' '}
{entryDisplayName} ·{' '}
{installed
? t('documentParsing.ocr.installedOption')
: t('documentParsing.ocr.downloadableOption')}
@@ -712,18 +726,25 @@ export function DocumentParsingSettingsSection({
<div className="document-ocr-model__header">
<div className="document-ocr-model__summary">
<div className="document-ocr-model__name">
<strong>{model.displayName}</strong>
<strong>{modelDisplayName}</strong>
{model.recommended && (
<span className="speech-model-tag speech-model-tag--recommended">
{t('documentParsing.ocr.recommended')}
</span>
)}
</div>
<p>{model.description}</p>
<p>{modelDescription}</p>
<div className="document-ocr-model__tags">
<span className="speech-model-tag">ModelScope</span>
<span className="speech-model-tag">
{model.languages.join(' / ')}
{model.languages
.map((language) =>
t(
`documentParsing.ocr.languages.${language}`,
{ defaultValue: language }
)
)
.join(' / ')}
</span>
<span className="speech-model-tag">
{model.runtime}
@@ -753,7 +774,7 @@ export function DocumentParsingSettingsSection({
<button
aria-label={t(
'documentParsing.ocr.accessibility.openRepository',
{ name: model.displayName }
{ name: modelDisplayName }
)}
className="secondary-button document-ocr-model__repository"
onClick={() =>
@@ -797,7 +818,7 @@ export function DocumentParsingSettingsSection({
<button
aria-label={t(
'documentParsing.ocr.accessibility.cancelOperation',
{ name: model.displayName }
{ name: modelDisplayName }
)}
className="secondary-button"
onClick={() =>
@@ -815,7 +836,7 @@ export function DocumentParsingSettingsSection({
<button
aria-label={t(
'documentParsing.ocr.accessibility.exportModelZip',
{ name: model.displayName }
{ name: modelDisplayName }
)}
className="secondary-button"
disabled={busyModelId === model.id}
@@ -827,7 +848,7 @@ export function DocumentParsingSettingsSection({
.exportOcrModelArchive(model.id),
t(
'documentParsing.ocr.notifications.exportedZip',
{ name: model.displayName }
{ name: modelDisplayName }
)
)
}
@@ -839,7 +860,7 @@ export function DocumentParsingSettingsSection({
<button
aria-label={t(
'documentParsing.ocr.accessibility.deleteModel',
{ name: model.displayName }
{ name: modelDisplayName }
)}
className={
confirmingRemove === model.id
@@ -861,7 +882,7 @@ export function DocumentParsingSettingsSection({
<button
aria-label={t(
'documentParsing.ocr.accessibility.downloadModel',
{ name: model.displayName }
{ name: modelDisplayName }
)}
className="primary-button"
disabled={busyModelId === model.id}
@@ -882,7 +903,7 @@ export function DocumentParsingSettingsSection({
pendingModelSelection
? 'documentParsing.ocr.notifications.installedAndSelected'
: 'documentParsing.ocr.notifications.installed',
{ name: model.displayName }
{ name: modelDisplayName }
)
)
}
@@ -896,7 +917,7 @@ export function DocumentParsingSettingsSection({
<button
aria-label={t(
'documentParsing.ocr.accessibility.importModelZip',
{ name: model.displayName }
{ name: modelDisplayName }
)}
className="secondary-button"
disabled={busyModelId === model.id}
@@ -917,7 +938,7 @@ export function DocumentParsingSettingsSection({
pendingModelSelection
? 'documentParsing.ocr.notifications.importedAndSelected'
: 'documentParsing.ocr.notifications.importedZip',
{ name: model.displayName }
{ name: modelDisplayName }
)
)
}
@@ -938,7 +959,7 @@ export function DocumentParsingSettingsSection({
<progress
aria-label={t(
'documentParsing.ocr.accessibility.downloadProgress',
{ name: model.displayName }
{ name: modelDisplayName }
)}
max={100}
{...(modelProgress === undefined
+9 -5
View File
@@ -37,7 +37,10 @@ import {
} from './SettingsPrimitives'
import { PageTabs } from './WorkspacePrimitives'
const configurableMcpTargets: RuntimeTarget[] = ['model']
const configurableMcpTargets: RuntimeTarget[] = [
'model',
'deepseek-harness'
]
type McpSettingsTab = 'builtin' | 'computer' | 'custom'
type McpEditor = {
@@ -76,9 +79,9 @@ function editorFromServer(server: McpServerSummary): McpEditor {
description: server.description,
enabled: server.enabled,
allowDynamicTools: server.allowDynamicTools,
assignments: server.assignments.includes('model')
? ['model']
: [],
assignments: server.assignments.filter((target) =>
configurableMcpTargets.includes(target)
),
transport: server.transport,
command: server.transport === 'stdio' ? server.command : '',
args: server.transport === 'stdio' ? server.args.join('\n') : '',
@@ -101,7 +104,8 @@ export function McpSettingsSection({
const runtimeLabels: Record<RuntimeTarget, string> = {
model: t('mcp.runtimeLabels.model'),
opencode: t('mcp.runtimeLabels.opencode'),
continue: t('mcp.runtimeLabels.continue')
continue: t('mcp.runtimeLabels.continue'),
'deepseek-harness': 'DeepSeek Harness'
}
const diagnosticStatusLabels: Record<
CapabilityDiagnosticReport['status'],
+273 -32
View File
@@ -81,6 +81,7 @@ const runtimeSettings: RuntimeSettings = {
kind: 'profile',
profileId: modelProfileId
},
deepseekHarnessModelSource: { kind: 'platform' },
secureStorageAvailable: true,
toolApproval: 'always'
}
@@ -118,11 +119,22 @@ const detectAgentRuntimes = vi.fn<
available: true,
path: 'C:\\Tools\\opencode.exe',
version: '1.2.3',
source: 'automatic',
detail: '通过 PATH 检测'
},
continue: {
available: false,
detail: '未检测到 Continue'
available: true,
path: 'bundled://continue',
version: '1.5.47',
source: 'bundled',
detail: '内置 Continue CLI 1.5.47 已就绪'
},
deepseekHarness: {
available: true,
path: 'bundled://deepseek-harness',
version: '0.1.0-rc.6',
source: 'bundled',
detail: '内置 Harness Adapter 已就绪'
}
}))
const selectRuntimeFile = vi.fn<
@@ -162,10 +174,16 @@ const capabilitySnapshot = {
source: 'builtin' as const,
digest: 'a'.repeat(64),
enabled: true,
assignments: ['model', 'opencode', 'continue'] as (
assignments: [
'model',
'opencode',
'continue',
'deepseek-harness'
] as (
| 'model'
| 'opencode'
| 'continue'
| 'deepseek-harness'
)[]
}
],
@@ -612,8 +630,21 @@ describe('SettingsPanel runtime files', () => {
screen.getByRole('button', { name: 'Save settings' })
).toBeInTheDocument()
expect(
screen.getByText(/OpenCode and Continue are bundled with GoodBuddy/u)
screen.queryByText(/OpenCode and Continue are bundled with GoodBuddy/u)
).not.toBeInTheDocument()
expect(
screen.getByText(
'Automatically detected OpenCode 1.2.3'
)
).toBeInTheDocument()
expect(screen.queryByText('通过 PATH 检测')).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Continue' }))
expect(
screen.getByText('Bundled Continue 1.5.47 is ready')
).toBeInTheDocument()
expect(
screen.queryByText('内置 Continue CLI 1.5.47 已就绪')
).not.toBeInTheDocument()
})
it('does not translate user-defined model connection names', async () => {
@@ -933,7 +964,9 @@ describe('SettingsPanel runtime files', () => {
continueConfigPath: '',
workspacePath: 'C:\\Workspace',
opencodeModelSource: runtimeSettings.opencodeModelSource,
continueModelSource: runtimeSettings.continueModelSource
continueModelSource: runtimeSettings.continueModelSource,
deepseekHarnessModelSource:
runtimeSettings.deepseekHarnessModelSource
}
})
render(
@@ -1267,7 +1300,7 @@ describe('SettingsPanel runtime files', () => {
})
it('automatically detects runtimes and displays path, version, and detail', async () => {
it('places Runtime detection details in the semantic overview card', async () => {
render(
<SettingsPanel
{...heartbeatSettingsProps}
@@ -1279,16 +1312,53 @@ describe('SettingsPanel runtime files', () => {
)
expect(detectAgentRuntimes).toHaveBeenCalledOnce()
const runtimeLabel = await screen.findByText('Runtime', {
selector: 'dt'
})
const overview = runtimeLabel.closest<HTMLElement>(
'.runtime-overview'
)
if (!overview) {
throw new Error('Missing OpenCode Runtime overview')
}
expect(
await screen.findByText(
within(overview).getByText('GoodBuddy 内置 OpenCode')
).toBeInTheDocument()
expect(
within(overview).getByText('模型配置:', { selector: 'dt' })
).toBeInTheDocument()
const status = within(overview).getByText('已就绪')
expect(status.tagName).toBe('DD')
expect(status).toHaveAttribute('aria-live', 'polite')
expect(
within(overview).getByText('C:\\Tools\\opencode.exe')
).toHaveClass('runtime-overview__path')
expect(within(overview).getByText('1.2.3')).toBeInTheDocument()
expect(
within(overview).getByText('已自动检测到 OpenCode 1.2.3')
).toBeInTheDocument()
expect(
screen.queryByText(
'已就绪 · C:\\Tools\\opencode.exe · 1.2.3 · 通过 PATH 检测'
)
).toBeInTheDocument()
await screen.findByText(/OpenCode 和 Continue 已随 GoodBuddy 内置/)
).not.toBeInTheDocument()
await screen.findByText('GoodBuddy 内置 OpenCode')
fireEvent.click(screen.getByRole('button', { name: 'Continue' }))
const continueOverview = screen
.getByText('GoodBuddy 内置 Continue')
.closest<HTMLElement>('.runtime-overview')
if (!continueOverview) {
throw new Error('Missing Continue Runtime overview')
}
expect(
screen.getByText('尚未就绪 · 未检测到 Continue')
within(continueOverview).getByText('已就绪')
).toBeInTheDocument()
expect(
within(continueOverview).getByText('1.5.47')
).toBeInTheDocument()
expect(
within(continueOverview).getByText('bundled://continue')
).toHaveClass('runtime-overview__path')
expect(
screen.queryByRole('button', { name: '重新检测 Continue' })
@@ -1302,6 +1372,177 @@ describe('SettingsPanel runtime files', () => {
)
})
it('configures DeepSeek Harness only with Chat Completions or platform settings', async () => {
const harnessProfileId =
'00000000-0000-4000-8000-000000000051'
getRuntime.mockResolvedValueOnce({
...runtimeSettings,
modelProfiles: [
runtimeSettings.modelProfiles[0]!,
{
...runtimeSettings.modelProfiles[0]!,
id: harnessProfileId,
name: 'DeepSeek Chat',
baseUrl: 'https://api.deepseek.com/v1',
modelName: 'deepseek-chat',
protocol: 'openai-chat-completions'
}
],
deepseekHarnessModelSource: {
kind: 'profile',
profileId: harnessProfileId
}
} as unknown as RuntimeSettings)
render(
<SettingsPanel
{...heartbeatSettingsProps}
open
onClearLocalData={vi.fn(async () => {})}
onClose={vi.fn()}
onSaved={vi.fn()}
/>
)
fireEvent.click(
await screen.findByRole('button', {
name: 'DeepSeek Harness(预览)'
})
)
expect(
screen.getByText('开发者预览 · 仅支持 DeepSeek')
).toBeInTheDocument()
expect(
screen.getByText(/当前仅支持 DeepSeek 模型/)
).toBeInTheDocument()
const harnessOverview = screen
.getByText('GoodBuddy 内置 DeepSeek Harness')
.closest<HTMLElement>('.runtime-overview')
if (!harnessOverview) {
throw new Error('Missing DeepSeek Harness overview')
}
expect(
within(harnessOverview).getByText('已就绪')
).toHaveAttribute('aria-live', 'polite')
expect(
within(harnessOverview).getByText(
'bundled://deepseek-harness'
)
).toHaveClass('runtime-overview__path')
expect(
within(harnessOverview).getByText('0.1.0-rc.6')
).toBeInTheDocument()
expect(
within(harnessOverview).getByText(
'内置 DeepSeek Harness 0.1.0-rc.6 已就绪'
)
).toBeInTheDocument()
expect(
screen.queryByText(/自定义 Harness Host/)
).not.toBeInTheDocument()
fireEvent.click(screen.getByText('高级设置'))
expect(
screen.getByText(
/始终使用 GoodBuddy 内置并固定版本的 Host/
)
).toBeInTheDocument()
expect(
screen.queryByRole('button', {
name: /选择.*Harness/
})
).not.toBeInTheDocument()
const source = screen.getByLabelText(
'DeepSeek Harness GoodBuddy 模型连接'
)
expect(
within(source).getByRole('option', { name: '默认模型(不兼容)' })
).toBeDisabled()
expect(
within(source).getByRole('option', { name: 'DeepSeek Chat' })
).not.toBeDisabled()
fireEvent.click(
screen.getByRole('radio', {
name: /使用平台 DeepSeek 环境配置/
})
)
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
await waitFor(() =>
expect(updateRuntime).toHaveBeenCalledWith(
expect.objectContaining({
deepseekHarnessModelSource: { kind: 'platform' }
})
)
)
})
it('normalizes an environment-managed DeepSeek profile to the platform source when saving', async () => {
getRuntime.mockResolvedValueOnce({
...runtimeSettings,
modelBaseUrl: 'https://api.deepseek.com',
modelName: 'deepseek-chat',
modelProtocol: 'openai-chat-completions',
modelProfiles: [
{
...runtimeSettings.modelProfiles[0]!,
baseUrl: 'https://api.deepseek.com',
modelName: 'deepseek-chat',
protocol: 'openai-chat-completions',
credentialSource: 'environment'
}
],
deepseekHarnessModelSource: { kind: 'platform' },
configured: {
modelProfiles: [
{
...runtimeSettings.modelProfiles[0]!,
baseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
protocol: 'openai-chat-completions',
credentialSource: 'environment'
}
],
opencodeBaseUrl: '',
opencodeBinaryPath: '',
opencodeConfigPath: '',
continueBinaryPath: '',
continueConfigPath: '',
workspacePath: 'C:\\Workspace',
opencodeModelSource: runtimeSettings.opencodeModelSource,
continueModelSource: runtimeSettings.continueModelSource,
deepseekHarnessModelSource: { kind: 'platform' }
}
} as unknown as RuntimeSettings)
render(
<SettingsPanel
{...heartbeatSettingsProps}
open
onClearLocalData={vi.fn(async () => {})}
onClose={vi.fn()}
onSaved={vi.fn()}
/>
)
fireEvent.click(
await screen.findByRole('button', {
name: 'DeepSeek Harness(预览)'
})
)
fireEvent.click(
screen.getByRole('radio', {
name: /使用 GoodBuddy 模型连接/
})
)
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
await waitFor(() =>
expect(updateRuntime).toHaveBeenCalledWith(
expect.objectContaining({
deepseekHarnessModelSource: { kind: 'platform' }
})
)
)
})
it('selects, warns about, clears, and saves a custom binary', async () => {
render(
<SettingsPanel
@@ -1313,7 +1554,7 @@ describe('SettingsPanel runtime files', () => {
/>
)
await screen.findByText(/OpenCode 和 Continue 已随 GoodBuddy 内置/)
await screen.findByText('GoodBuddy 内置 OpenCode')
fireEvent.click(screen.getByRole('button', { name: 'Continue' }))
fireEvent.click(screen.getByText('高级设置'))
const input = await screen.findByLabelText('Continue 可执行文件路径')
@@ -1362,28 +1603,22 @@ describe('SettingsPanel runtime files', () => {
/>
)
await screen.findByText('GoodBuddy 内置 OpenCode')
expect(
await screen.findByText(
/OpenCode 和 Continue 已随 GoodBuddy 内置/
)
).toBeInTheDocument()
expect(
screen.getByText(/配置可兼容的直连文本模型后即可使用/)
).toBeInTheDocument()
screen.queryByText(/配置可兼容的直连文本模型后即可使用/)
).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'OpenCode' }))
.toHaveAttribute('aria-pressed', 'true')
expect(screen.queryByText('默认 Runtime')).not.toBeInTheDocument()
expect(
screen.getByText(/GoodBuddy 内置 OpenCode/)
).toBeInTheDocument()
screen.getAllByText(/GoodBuddy 内置 OpenCode/).length
).toBeGreaterThan(0)
expect(
screen.getByText(/模型配置:/).closest('.runtime-note')
).toHaveTextContent(
'跟随 GoodBuddy · 默认模型(sonnet-5'
)
expect(
screen.getByText(/^ ·/u)
).toBeInTheDocument()
expect(screen.getByText('已就绪')).toBeInTheDocument()
expect(screen.getByText('高级设置').closest('details'))
.not.toHaveAttribute('open')
expect(
@@ -1408,15 +1643,15 @@ describe('SettingsPanel runtime files', () => {
fireEvent.click(screen.getByRole('button', { name: 'Continue' }))
expect(
screen.getByText(/GoodBuddy 内置 Continue/)
).toBeInTheDocument()
screen.getAllByText(/GoodBuddy 内置 Continue/).length
).toBeGreaterThan(0)
expect(
screen.getByText(/模型配置:/).closest('.runtime-note')
).toHaveTextContent(
'跟随 GoodBuddy · 默认模型(sonnet-5'
)
expect(screen.getByText('尚未就绪 · 未检测到 Continue'))
.toBeInTheDocument()
expect(screen.getByText('已就绪')).toBeInTheDocument()
expect(screen.getByText('1.5.47')).toBeInTheDocument()
expect(screen.getByText('高级设置').closest('details'))
.not.toHaveAttribute('open')
})
@@ -1437,7 +1672,7 @@ describe('SettingsPanel runtime files', () => {
/>
)
await screen.findByText(/OpenCode 和 Continue 已随 GoodBuddy 内置/)
await screen.findByText('GoodBuddy 内置 OpenCode')
fireEvent.click(screen.getByRole('button', { name: 'Continue' }))
fireEvent.click(screen.getByText('高级设置'))
expect(
@@ -1497,7 +1732,7 @@ describe('SettingsPanel runtime files', () => {
/>
)
await screen.findByText(/OpenCode 和 Continue 已随 GoodBuddy 内置/)
await screen.findByText('GoodBuddy 内置 OpenCode')
fireEvent.click(screen.getByText('高级设置'))
expect(screen.getByLabelText('OpenCode Server 地址')).toHaveValue('')
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
@@ -2433,8 +2668,11 @@ describe('SettingsPanel runtime files', () => {
expect(
screen.getByText(/新导入的 Skill 默认启用/)
).toHaveTextContent(
'分配给直连模型、OpenCodeContinue'
'分配给直连模型、OpenCodeContinue 和 DeepSeek Harness'
)
expect(
screen.getByLabelText('DeepSeek Harness')
).toBeChecked()
fireEvent.click(
screen.getByRole('button', { name: '导入 Skill 目录' })
)
@@ -2633,10 +2871,10 @@ describe('SettingsPanel runtime files', () => {
within(mcpTabs).getByRole('tab', { name: '自定义 MCP' })
)
expect(
screen.getByText(/自定义 MCP 当前仅用于直连模型/)
screen.getByText(/自定义 MCP 可分配给直连模型或 DeepSeek Harness/)
).toHaveTextContent('新建时默认分配给直连模型')
expect(
screen.getByText(/Runtime 自有 MCP 配置不在此处管理/)
screen.getByText(/服务凭据不会进入 Harness Utility/)
).toBeInTheDocument()
expect(
await screen.findByText('尚未配置 MCP Server')
@@ -2663,6 +2901,9 @@ describe('SettingsPanel runtime files', () => {
})
).not.toBeChecked()
expect(within(dialog).getByLabelText('模型')).toBeChecked()
expect(
within(dialog).getByLabelText('DeepSeek Harness')
).not.toBeChecked()
expect(
within(dialog).queryByLabelText('OpenCode')
).not.toBeInTheDocument()
+384 -87
View File
@@ -25,11 +25,11 @@ import type {
RuntimeSettingsInput,
RuntimeModelSource
} from '../../shared/contracts'
import type { AgentRuntimeSelection } from '../../shared/runtime-selection-contracts'
import {
defaultModelProfileId as builtInDefaultModelProfileId,
defaultRuntimeSettings,
isAgentRuntimeModelProtocol
isAgentRuntimeModelProtocol,
isDeepSeekHarnessModelProfile
} from '../../shared/contracts'
import { McpSettingsSection } from './McpSettingsSection'
import { RolePromptSettingsSection } from './RolePromptSettingsSection'
@@ -59,7 +59,9 @@ import type {
import { useUiLocale } from './i18n/UiLocaleProvider'
type ModelType = 'llm' | 'embedding' | 'rerank' | 'speech'
type AgentRuntimeType = RuntimeConfigActionInput['runtime']
type AgentRuntimeType =
| RuntimeConfigActionInput['runtime']
| 'deepseek-harness'
type ModelProfileDraft = RuntimeSettings['modelProfiles'][number] & {
supportsImageInput: boolean
apiKey: string
@@ -140,7 +142,9 @@ function configuredRuntimeSettings(
continueConfigPath: settings.continueConfigPath,
workspacePath: settings.workspacePath,
opencodeModelSource: settings.opencodeModelSource,
continueModelSource: settings.continueModelSource
continueModelSource: settings.continueModelSource,
deepseekHarnessModelSource:
settings.deepseekHarnessModelSource
}
}
@@ -158,6 +162,7 @@ function hydrateRuntimeSettings(
defaultModelProfileId: (value: string) => void
opencodeModelSource: (value: RuntimeModelSource) => void
continueModelSource: (value: RuntimeModelSource) => void
deepseekHarnessModelSource: (value: RuntimeModelSource) => void
opencodeBaseUrl: (value: string) => void
opencodeBinaryPath: (value: string) => void
opencodeConfigPath: (value: string) => void
@@ -205,6 +210,9 @@ function hydrateRuntimeSettings(
setters.defaultModelProfileId(value.defaultModelProfileId)
setters.opencodeModelSource(configured.opencodeModelSource)
setters.continueModelSource(configured.continueModelSource)
setters.deepseekHarnessModelSource(
configured.deepseekHarnessModelSource ?? { kind: 'platform' }
)
setters.opencodeBaseUrl(configured.opencodeBaseUrl)
setters.opencodeBinaryPath(configured.opencodeBinaryPath)
setters.opencodeConfigPath(configured.opencodeConfigPath)
@@ -241,7 +249,7 @@ function hydrateRuntimeSettings(
}
type RuntimeConfigCardProps = {
runtime: AgentRuntimeType
runtime: RuntimeConfigActionInput['runtime']
runtimeLabel: string
description: string
fileKind: Extract<
@@ -351,6 +359,74 @@ function RuntimeConfigCard({
)
}
function RuntimeOverviewCard({
detection,
detectionLabel,
detecting,
modelConfiguration,
recommendation,
runtime
}: {
detection: AgentRuntimeDetection['opencode'] | undefined
detectionLabel: string
detecting: boolean
modelConfiguration: string
recommendation: string
runtime: string
}): React.JSX.Element {
const { t } = useTranslation('settings')
const localizedDetail =
detection?.available && detection.source
? t(`runtime.detection.details.${detection.source}`, {
runtime: detectionLabel,
versionSuffix: detection.version ? ` ${detection.version}` : ''
})
: detection?.detail
const status = detecting
? t('runtime.detection.detecting')
: detection?.available
? t('runtime.detection.ready')
: detection
? t('runtime.detection.unavailable')
: t('runtime.detection.notDetected')
return (
<div className="runtime-note runtime-overview">
<dl className="runtime-overview__details">
<dt>{t('runtime.runtimeLabel')}</dt>
<dd>{runtime}</dd>
<dt>{t('runtime.modelConfigurationLabel')}</dt>
<dd>{modelConfiguration}</dd>
<dt>{t('runtime.detection.statusLabel')}</dt>
<dd aria-atomic="true" aria-live="polite">
{status}
</dd>
{!detecting && detection?.available && (
<>
<dt>{t('runtime.detection.pathLabel')}</dt>
<dd className="runtime-overview__path">
{detection.path}
</dd>
{detection.version && (
<>
<dt>{t('runtime.detection.versionLabel')}</dt>
<dd>{detection.version}</dd>
</>
)}
</>
)}
{!detecting && localizedDetail && (
<>
<dt>{t('runtime.detection.detailLabel')}</dt>
<dd>{localizedDetail}</dd>
</>
)}
</dl>
<p>{recommendation}</p>
</div>
)
}
export function SettingsPanel({
open,
presentation = 'modal',
@@ -389,6 +465,8 @@ export function SettingsPanel({
useState<RuntimeModelSource>({ kind: 'platform' })
const [continueModelSource, setContinueModelSource] =
useState<RuntimeModelSource>({ kind: 'platform' })
const [deepseekHarnessModelSource, setDeepseekHarnessModelSource] =
useState<RuntimeModelSource>({ kind: 'platform' })
const [opencodeBaseUrl, setOpencodeBaseUrl] = useState<string>(
defaultRuntimeSettings.opencodeBaseUrl
)
@@ -495,6 +573,7 @@ export function SettingsPanel({
defaultModelProfileId: setDefaultModelProfileId,
opencodeModelSource: setOpencodeModelSource,
continueModelSource: setContinueModelSource,
deepseekHarnessModelSource: setDeepseekHarnessModelSource,
opencodeBaseUrl: setOpencodeBaseUrl,
opencodeBinaryPath: setOpencodeBinaryPath,
opencodeConfigPath: setOpencodeConfigPath,
@@ -714,6 +793,12 @@ export function SettingsPanel({
profileInputs.find(
(profile) => profile.id === defaultProfile.id
) ?? profileInputs[0]!
const normalizedDeepseekHarnessModelSource =
deepseekHarnessModelSource.kind === 'profile' &&
deepseekHarnessModelSource.profileId === defaultProfile.id &&
defaultProfile.credentialSource === 'environment'
? { kind: 'platform' as const }
: deepseekHarnessModelSource
const value = await window.goodbuddy.settings.updateRuntime({
provider,
modelBaseUrl: defaultProfileInput.baseUrl,
@@ -758,6 +843,8 @@ export function SettingsPanel({
defaultModelProfileId: defaultProfile.id,
opencodeModelSource,
continueModelSource,
deepseekHarnessModelSource:
normalizedDeepseekHarnessModelSource,
toolApproval,
subagentSmartRoutingEnabled
})
@@ -823,8 +910,12 @@ export function SettingsPanel({
const runtimeSource =
agentRuntimeType === 'opencode'
? savedSettings.opencodeModelSource
: savedSettings.continueModelSource
const runtimeSelection: AgentRuntimeSelection =
: agentRuntimeType === 'continue'
? savedSettings.continueModelSource
: savedSettings.deepseekHarnessModelSource ?? {
kind: 'platform'
}
const runtimeSelection =
runtimeSource.kind === 'profile'
? {
provider: agentRuntimeType,
@@ -1017,6 +1108,24 @@ export function SettingsPanel({
) {
setContinueModelSource(runtimeFallback)
}
if (
deepseekHarnessModelSource.kind === 'profile' &&
deepseekHarnessModelSource.profileId === id
) {
const harnessFallback = remaining.find(
(profile) =>
profile.id === defaultModelProfileId &&
profile.protocol === 'openai-chat-completions'
) ?? remaining.find(
(profile) =>
profile.protocol === 'openai-chat-completions'
)
setDeepseekHarnessModelSource(
harnessFallback
? { kind: 'profile', profileId: harnessFallback.id }
: { kind: 'platform' }
)
}
}
const selectDefaultModelProfile = (
@@ -1046,6 +1155,16 @@ export function SettingsPanel({
) {
setContinueModelSource(nextRuntimeSource)
}
if (
deepseekHarnessModelSource.kind === 'profile' &&
deepseekHarnessModelSource.profileId === previousDefaultProfileId &&
profile.protocol === 'openai-chat-completions'
) {
setDeepseekHarnessModelSource({
kind: 'profile',
profileId: profile.id
})
}
}
const parseModelSource = (value: string): RuntimeModelSource =>
@@ -1061,6 +1180,10 @@ export function SettingsPanel({
profile: ModelProfileDraft
): boolean => isAgentRuntimeModelProtocol(profile.protocol)
const isDeepseekHarnessCompatible = (
profile: ModelProfileDraft
): boolean => isDeepSeekHarnessModelProfile(profile)
const selectedModelProfile =
modelProfiles.find(
(profile) => profile.id === selectedModelProfileId
@@ -1074,16 +1197,27 @@ export function SettingsPanel({
modelProfiles.find((profile) =>
isAgentRuntimeModelProtocol(profile.protocol)
)
const defaultDeepseekHarnessModelProfile =
modelProfiles.find(
(profile) =>
profile.id === defaultModelProfileId &&
isDeepseekHarnessCompatible(profile)
) ??
modelProfiles.find(isDeepseekHarnessCompatible)
const activeRuntimeModelSource =
agentRuntimeType === 'opencode'
? opencodeModelSource
: continueModelSource
: agentRuntimeType === 'continue'
? continueModelSource
: deepseekHarnessModelSource
const activeRuntimeModelProfile =
activeRuntimeModelSource.kind === 'profile'
? modelProfiles.find(
(profile) =>
profile.id === activeRuntimeModelSource.profileId &&
isAgentRuntimeModelProtocol(profile.protocol)
(agentRuntimeType === 'deepseek-harness'
? isDeepseekHarnessCompatible(profile)
: isAgentRuntimeModelProtocol(profile.protocol))
)
: undefined
const savedRoleModelProfiles = (settings?.modelProfiles ?? [])
@@ -1101,32 +1235,6 @@ export function SettingsPanel({
? settings?.defaultModelProfileId
: undefined
const detectionSummary = (
value: AgentRuntimeDetection['opencode'] | undefined
): React.JSX.Element => (
<div className="credential-state" aria-live="polite">
<TerminalSquare size={15} />
<span>
{value
? value.available
? [
t('runtime.detection.ready'),
value.path,
value.version,
value.detail
]
.filter(Boolean)
.join(' · ')
: t('runtime.detection.notReady', {
detail: value.detail
})
: detecting
? t('runtime.detection.detecting')
: t('runtime.detection.notDetected')}
</span>
</div>
)
return (
<div
className={
@@ -1230,7 +1338,9 @@ export function SettingsPanel({
runtime:
agentRuntimeType === 'opencode'
? 'OpenCode'
: 'Continue'
: agentRuntimeType === 'continue'
? 'Continue'
: 'DeepSeek Harness'
})}
</button>
)}
@@ -1402,13 +1512,14 @@ export function SettingsPanel({
onChange={setAgentRuntimeType}
options={[
{ label: 'OpenCode', value: 'opencode' },
{ label: 'Continue', value: 'continue' }
{ label: 'Continue', value: 'continue' },
{
label: t('runtime.deepseekHarness.selectorLabel'),
value: 'deepseek-harness'
}
]}
value={agentRuntimeType}
/>
<small>
{t('runtime.selectorDescription')}
</small>
</div>
{agentRuntimeType === 'opencode' && (
@@ -1420,37 +1531,39 @@ export function SettingsPanel({
<small>{t('runtime.bundledDescription')}</small>
</div>
</div>
<div className="runtime-note">
<strong>{t('runtime.runtimeLabel')}</strong>
{t('runtime.bundledRuntime', { runtime: 'OpenCode' })}
<br />
<strong>{t('runtime.modelConfigurationLabel')}</strong>
{activeRuntimeModelSource.kind === 'platform'
? t('runtime.ownConfiguration', {
runtime: 'OpenCode'
})
: activeRuntimeModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
activeRuntimeModelProfile
),
model: activeRuntimeModelProfile.modelName
<RuntimeOverviewCard
detection={detection?.opencode}
detectionLabel="OpenCode"
detecting={detecting}
modelConfiguration={
activeRuntimeModelSource.kind === 'platform'
? t('runtime.ownConfiguration', {
runtime: 'OpenCode'
})
: defaultTextModelProfile
: activeRuntimeModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
defaultTextModelProfile
activeRuntimeModelProfile
),
model: defaultTextModelProfile.modelName
model: activeRuntimeModelProfile.modelName
})
: t('runtime.noCompatibleModel')}
<br />
{t('runtime.opencode.recommendation')}
</div>
: defaultTextModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
defaultTextModelProfile
),
model: defaultTextModelProfile.modelName
})
: t('runtime.noCompatibleModel')
}
recommendation={t('runtime.opencode.recommendation')}
runtime={t('runtime.bundledRuntime', {
runtime: 'OpenCode'
})}
/>
<div className="runtime-note">
{t('runtime.permissions')}
</div>
{detectionSummary(detection?.opencode)}
<details className="settings-section">
<summary>{t('runtime.advanced')}</summary>
<p className="settings-panel__description">
@@ -1650,37 +1763,39 @@ export function SettingsPanel({
<small>{t('runtime.bundledDescription')}</small>
</div>
</div>
<div className="runtime-note">
<strong>{t('runtime.runtimeLabel')}</strong>
{t('runtime.bundledRuntime', { runtime: 'Continue' })}
<br />
<strong>{t('runtime.modelConfigurationLabel')}</strong>
{activeRuntimeModelSource.kind === 'platform'
? t('runtime.ownConfiguration', {
runtime: 'Continue'
})
: activeRuntimeModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
activeRuntimeModelProfile
),
model: activeRuntimeModelProfile.modelName
<RuntimeOverviewCard
detection={detection?.continue}
detectionLabel="Continue"
detecting={detecting}
modelConfiguration={
activeRuntimeModelSource.kind === 'platform'
? t('runtime.ownConfiguration', {
runtime: 'Continue'
})
: defaultTextModelProfile
: activeRuntimeModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
defaultTextModelProfile
activeRuntimeModelProfile
),
model: defaultTextModelProfile.modelName
model: activeRuntimeModelProfile.modelName
})
: t('runtime.noCompatibleModel')}
<br />
{t('runtime.continue.recommendation')}
</div>
: defaultTextModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
defaultTextModelProfile
),
model: defaultTextModelProfile.modelName
})
: t('runtime.noCompatibleModel')
}
recommendation={t('runtime.continue.recommendation')}
runtime={t('runtime.bundledRuntime', {
runtime: 'Continue'
})}
/>
<div className="runtime-note">
{t('runtime.permissions')}
</div>
{detectionSummary(detection?.continue)}
<details className="settings-section">
<summary>{t('runtime.advanced')}</summary>
<p className="settings-panel__description">
@@ -1845,6 +1960,159 @@ export function SettingsPanel({
</button>
</details>
</div>
)}
{agentRuntimeType === 'deepseek-harness' && (
<div className="settings-section">
<div className="settings-section__title">
<TerminalSquare size={17} />
<div>
<strong>{t('runtime.deepseekHarness.title')}</strong>
<small>
{t('runtime.deepseekHarness.previewDescription')}
</small>
</div>
</div>
<p className="settings-notice">
{t('runtime.deepseekHarness.deepseekOnlyNotice')}
</p>
<RuntimeOverviewCard
detection={detection?.deepseekHarness}
detectionLabel="DeepSeek Harness"
detecting={detecting}
modelConfiguration={
activeRuntimeModelSource.kind === 'platform'
? t('runtime.deepseekHarness.platformSource')
: activeRuntimeModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
activeRuntimeModelProfile
),
model: activeRuntimeModelProfile.modelName
})
: t('runtime.noCompatibleModel')
}
recommendation={t(
'runtime.deepseekHarness.description'
)}
runtime={t('runtime.bundledRuntime', {
runtime: 'DeepSeek Harness'
})}
/>
<fieldset className="runtime-source-options">
<legend>{t('runtime.sourceLegend')}</legend>
<label>
<input
checked={
deepseekHarnessModelSource.kind === 'profile'
}
disabled={!defaultDeepseekHarnessModelProfile}
name="deepseek-harness-model-source"
onChange={() => {
if (defaultDeepseekHarnessModelProfile) {
setDeepseekHarnessModelSource({
kind: 'profile',
profileId:
defaultDeepseekHarnessModelProfile.id
})
}
}}
type="radio"
/>
<span>
<strong>
{t(
'runtime.deepseekHarness.goodBuddySource'
)}
</strong>
<small>
{t(
'runtime.deepseekHarness.goodBuddySourceDescription'
)}
</small>
</span>
</label>
<label>
<input
checked={
deepseekHarnessModelSource.kind === 'platform'
}
name="deepseek-harness-model-source"
onChange={() =>
setDeepseekHarnessModelSource({
kind: 'platform'
})
}
type="radio"
/>
<span>
<strong>
{t('runtime.deepseekHarness.platformSource')}
</strong>
<small>
{t(
'runtime.deepseekHarness.platformSourceDescription'
)}
</small>
</span>
</label>
</fieldset>
{deepseekHarnessModelSource.kind === 'profile' && (
<label className="field">
<span>{t('runtime.goodBuddyConnection')}</span>
<select
aria-label={`DeepSeek Harness ${t(
'runtime.goodBuddyConnection'
)}`}
onChange={(event) =>
setDeepseekHarnessModelSource(
parseModelSource(event.target.value)
)
}
value={deepseekHarnessModelSource.profileId}
>
{modelProfiles.map((profile) => (
<option
disabled={
!isDeepseekHarnessCompatible(profile)
}
key={profile.id}
value={profile.id}
>
{modelProfileDisplayName(profile)}
{isDeepseekHarnessCompatible(profile)
? ''
: t('runtime.incompatibleSuffix')}
</option>
))}
</select>
<small>
{t(
'runtime.deepseekHarness.connectionDescription'
)}
</small>
</label>
)}
<details className="settings-section">
<summary>{t('runtime.advanced')}</summary>
<p className="settings-panel__description">
{t(
'runtime.deepseekHarness.advancedDescription'
)}
</p>
<button
className="secondary-button"
disabled={detecting}
onClick={() => void detectRuntimes()}
type="button"
>
{detecting
? t('actions.detecting')
: t('actions.redetectRuntime', {
runtime: 'DeepSeek Harness'
})}
</button>
</details>
</div>
)}
</>
)}
@@ -2078,6 +2346,29 @@ export function SettingsPanel({
) {
setContinueModelSource(runtimeFallback)
}
if (
protocol !== 'openai-chat-completions' &&
deepseekHarnessModelSource.kind ===
'profile' &&
deepseekHarnessModelSource.profileId ===
profile.id
) {
const harnessFallback =
modelProfiles.find(
(candidate) =>
candidate.id !== profile.id &&
candidate.protocol ===
'openai-chat-completions'
)
setDeepseekHarnessModelSource(
harnessFallback
? {
kind: 'profile',
profileId: harnessFallback.id
}
: { kind: 'platform' }
)
}
}
}
value={profile.protocol}
@@ -2246,7 +2537,13 @@ export function SettingsPanel({
: t('model.profile.incompatible'),
openCodeCompatibility: isOpenCodeCompatible(profile)
? t('model.profile.compatible')
: t('model.profile.incompatibleImageProtocol')
: t('model.profile.incompatibleImageProtocol'),
deepseekHarnessCompatibility:
isDeepseekHarnessCompatible(profile)
? t('model.profile.compatible')
: t(
'model.profile.incompatibleHarnessProtocol'
)
})}
</small>
</div>
+8 -1
View File
@@ -154,7 +154,14 @@ export function SkillsSettingsSection(): React.JSX.Element {
</div>
<div className="runtime-assignments">
<small>{t('skills.assignedTo')}</small>
{(['model', 'opencode', 'continue'] as RuntimeTarget[]).map(
{(
[
'model',
'opencode',
'continue',
'deepseek-harness'
] as RuntimeTarget[]
).map(
(target) => (
<label key={target}>
<input
@@ -117,6 +117,8 @@ export const app = {
switching: 'Switching…',
picker: 'Runtime and model',
directModels: 'Direct models',
deepseekHarnessGroup:
'DeepSeek Harness (Developer preview · DeepSeek only)',
manage: 'Manage Runtime and model connections',
errors: {
readStatus: 'Failed to read Agent Runtime status',
@@ -31,6 +31,8 @@ export const integrations = {
unavailableProfile: '{{name}} · {{modelName}} (unavailable)',
missingProfile: 'The previous direct model no longer exists',
noTextModels: 'No text models are available',
deepseekHarnessOption:
'DeepSeek Harness (Preview · DeepSeek only)',
missingSelection:
'The selected direct model no longer exists. Choose another model.',
imageOnlySelection:
@@ -142,7 +144,8 @@ export const integrations = {
runtimeLabels: {
model: 'Model',
opencode: 'OpenCode',
continue: 'Continue'
continue: 'Continue',
'deepseek-harness': 'DeepSeek Harness'
},
diagnosticStatuses: {
available: 'Available',
@@ -171,7 +174,7 @@ export const integrations = {
custom: 'Custom MCP'
},
customNotice:
'Custom MCP currently works only with direct models. New servers are assigned to direct models by default and loaded only in Execute mode. Runtime-owned MCP configuration is not managed here.',
'Custom MCP can be assigned to direct models or DeepSeek Harness. New servers target direct models by default and load only in Execute mode. GoodBuddy proxies Harness tools in the main process, so server credentials never enter the Harness Utility.',
securityNotice:
'Built-in tools are provided by GoodBuddy and are not MCP servers. Custom MCP servers and tools run with the current users permissions, so add only trusted services. Remote access tokens are encrypted in secure system storage, and tool calls still require GoodBuddy approval.',
computer: {
@@ -35,8 +35,10 @@ export const settings = {
},
runtime: {
label: 'Agent Runtime',
navigationDescription: 'OpenCode, Continue, and workspace settings',
description: 'OpenCode, Continue, and workspace settings'
navigationDescription:
'OpenCode, Continue, DeepSeek Harness, and workspace settings',
description:
'OpenCode, Continue, DeepSeek Harness, and workspace settings'
},
security: {
label: 'Security and data',
@@ -155,8 +157,19 @@ export const settings = {
detection: {
ready: 'Ready',
notReady: 'Not ready · {{detail}}',
unavailable: 'Not ready',
detecting: 'Detecting…',
notDetected: 'Not detected'
notDetected: 'Not detected',
statusLabel: 'Status:',
pathLabel: 'Path:',
versionLabel: 'Version:',
detailLabel: 'Detection details:',
details: {
bundled: 'Bundled {{runtime}}{{versionSuffix}} is ready',
configured: 'Custom {{runtime}}{{versionSuffix}} is ready',
automatic:
'Automatically detected {{runtime}}{{versionSuffix}}'
}
},
workspace: {
title: 'Default workspace',
@@ -164,8 +177,6 @@ export const settings = {
'Agents use this location only when the current project has no root folder',
directoryLabel: 'Default workspace folder'
},
selectorDescription:
'OpenCode and Continue are bundled with GoodBuddy. Configure a compatible direct text model to use them.',
bundledDescription:
'Bundled GoodBuddy Runtime that follows the text model connection by default',
runtimeLabel: 'Runtime:',
@@ -222,6 +233,25 @@ export const settings = {
binaryPath: 'Continue executable path',
missingConfigWarning:
'Continue remains unavailable without a configuration file and will not load a remote default model anonymously.'
},
deepseekHarness: {
selectorLabel: 'DeepSeek Harness (Preview)',
title: 'DeepSeek Harness',
previewDescription: 'Developer preview · DeepSeek only',
deepseekOnlyNotice:
'DeepSeek Harness currently supports DeepSeek models only and is not intended for other model providers.',
description:
'GoodBuddy maintains the fixed Host and control protocol internally and uses pinned Harness libraries underneath. Execute tool calls receive automatic one-time authorization, Ask remains read-only, and cancellation and workspace safety boundaries remain in place. It does not integrate with the DSH plugin or marketplace mechanisms.',
goodBuddySource: 'Use a GoodBuddy model connection',
goodBuddySourceDescription:
'Only OpenAI-compatible Chat Completions connections are available. The connection must point to a DeepSeek model.',
platformSource: 'Use platform DeepSeek environment settings',
platformSourceDescription:
'Reads platform-managed DeepSeek settings from the launch environment without exposing credentials to the renderer.',
connectionDescription:
'The internal GoodBuddy Harness Runtime currently accepts only the OpenAI-compatible Chat Completions protocol.',
advancedDescription:
'This Runtime always uses GoodBuddys bundled, version-pinned Host. It does not load external DSH plugins, marketplace packages, user profiles, or custom Hosts.'
}
},
documentParsing: {
@@ -317,6 +347,28 @@ export const settings = {
slow: 'Slow'
}
},
languages: {
: 'Chinese',
: 'English',
'50 种语言': '50 languages'
},
catalog: {
'pp-ocrv6-tiny': {
displayName: 'PP-OCRv6 Tiny',
description:
'The official lightweight PaddleOCR Chinese model for local CPU recognition of scanned PDFs and images.'
},
'pp-ocrv6-small': {
displayName: 'PP-OCRv6 Small',
description:
'The official PaddleOCR 50-language model balancing recognition quality, speed, and local resource use.'
},
'pp-ocrv6-medium': {
displayName: 'PP-OCRv6 Medium',
description:
'The official high-quality PaddleOCR 50-language model with slower recognition, higher memory use, and greater latency.'
}
},
installed: 'Installed and verified',
download: 'Download',
downloadAndSelect: 'Download and enable',
@@ -448,12 +500,14 @@ export const settings = {
imageQualityDescription:
'Used only for OpenAI-compatible image generation requests.',
compatibilitySummary:
'Direct model: {{directCapability}} · Continue: {{continueCompatibility}} · OpenCode: {{openCodeCompatibility}}',
'Direct model: {{directCapability}} · Continue: {{continueCompatibility}} · OpenCode: {{openCodeCompatibility}} · DeepSeek Harness: {{deepseekHarnessCompatibility}}',
textChat: 'Text chat',
compatible: 'Compatible',
incompatible: 'Incompatible',
incompatibleImageProtocol:
'Incompatible (image generation protocol is unsupported)',
incompatibleHarnessProtocol:
'Incompatible (Chat Completions only)',
secureStorageWarning:
'Secure system key storage is unavailable. Use an environment variable to avoid storing an API Key in plaintext.'
},
@@ -251,7 +251,8 @@ export const settingsSections = {
runtimeLabels: {
model: 'Model',
opencode: 'OpenCode',
continue: 'Continue'
continue: 'Continue',
'deepseek-harness': 'DeepSeek Harness'
},
errors: {
readFailed: 'Could not load Skills',
@@ -264,7 +265,7 @@ export const settingsSections = {
},
listLabel: 'Skills list',
notice:
'Skills inject local capability instructions into selected targets without changing the Runtimes own configuration. Newly imported Skills are enabled by default and assigned to the direct model, OpenCode, and Continue.',
'Skills inject local capability instructions into selected targets without changing the Runtimes own configuration. Newly imported Skills are enabled by default and assigned to the direct model, OpenCode, Continue, and DeepSeek Harness.',
loading: 'Loading Skills…',
source: {
builtin: 'Built in',
@@ -113,6 +113,7 @@ export const app = {
switching: '切换中…',
picker: 'Runtime 和模型',
directModels: '直连模型',
deepseekHarnessGroup: 'DeepSeek Harness(开发者预览 · 仅 DeepSeek',
manage: '管理 Runtime 和模型连接',
errors: {
readStatus: 'Agent Runtime 状态读取失败',
@@ -26,6 +26,7 @@ export const integrations = {
unavailableProfile: '{{name}} · {{modelName}}(不可用)',
missingProfile: '原直连模型已不存在',
noTextModels: '暂无可用文本模型',
deepseekHarnessOption: 'DeepSeek Harness(预览 · 仅 DeepSeek',
missingSelection: '所选直连模型已不存在,请重新选择。',
imageOnlySelection:
'所选连接仅支持图片生成,请选择文本模型或 Agent Runtime。',
@@ -132,7 +133,8 @@ export const integrations = {
runtimeLabels: {
model: '模型',
opencode: 'OpenCode',
continue: 'Continue'
continue: 'Continue',
'deepseek-harness': 'DeepSeek Harness'
},
diagnosticStatuses: {
available: '可用',
@@ -158,7 +160,7 @@ export const integrations = {
custom: '自定义 MCP'
},
customNotice:
'自定义 MCP 当前仅用于直连模型,新建时默认分配给直连模型,并仅在 Execute 模式加载。Runtime 自有 MCP 配置不在此处管理。',
'自定义 MCP 可分配给直连模型或 DeepSeek Harness,新建时默认分配给直连模型,并仅在 Execute 模式加载。Harness 工具由 GoodBuddy 主进程代理,服务凭据不会进入 Harness Utility。',
securityNotice:
'内置工具由 GoodBuddy 提供,不属于 MCP Server。自定义 MCP Server 及其工具具有当前用户权限,请仅添加可信服务;远程访问令牌将由系统安全存储加密,工具调用前仍需 GoodBuddy 审批。',
computer: {
@@ -29,8 +29,8 @@ export const settings = {
},
runtime: {
label: 'Agent Runtime',
navigationDescription: 'OpenCode、Continue 与工作区',
description: 'OpenCode、Continue 与工作区'
navigationDescription: 'OpenCode、Continue、DeepSeek Harness 与工作区',
description: 'OpenCode、Continue、DeepSeek Harness 与工作区'
},
security: {
label: '安全与数据',
@@ -141,16 +141,24 @@ export const settings = {
detection: {
ready: '已就绪',
notReady: '尚未就绪 · {{detail}}',
unavailable: '尚未就绪',
detecting: '正在检测…',
notDetected: '尚未检测'
notDetected: '尚未检测',
statusLabel: '状态:',
pathLabel: '路径:',
versionLabel: '版本:',
detailLabel: '检测详情:',
details: {
bundled: '内置 {{runtime}}{{versionSuffix}} 已就绪',
configured: '自定义 {{runtime}}{{versionSuffix}} 已就绪',
automatic: '已自动检测到 {{runtime}}{{versionSuffix}}'
}
},
workspace: {
title: '默认工作区',
description: '当前项目未设置根目录时,Agent 才使用此默认位置',
directoryLabel: '默认工作区目录'
},
selectorDescription:
'OpenCode 和 Continue 已随 GoodBuddy 内置;配置可兼容的直连文本模型后即可使用。',
bundledDescription: 'GoodBuddy 内置 Runtime,默认跟随文本模型连接',
runtimeLabel: 'Runtime',
modelConfigurationLabel: '模型配置:',
@@ -203,6 +211,25 @@ export const settings = {
binaryPath: 'Continue 可执行文件路径',
missingConfigWarning:
'未指定配置文件时 Continue 将保持不可用,不会匿名加载远程默认模型。'
},
deepseekHarness: {
selectorLabel: 'DeepSeek Harness(预览)',
title: 'DeepSeek Harness',
previewDescription: '开发者预览 · 仅支持 DeepSeek',
deepseekOnlyNotice:
'DeepSeek Harness 当前仅支持 DeepSeek 模型,不适用于其他模型提供商。',
description:
'由 GoodBuddy 内部维护固定 Host 与控制协议,复用锁定的 Harness 底层库;Execute 工具调用自动单次授权,Ask 保持只读,并保留取消和工作区安全边界;不接入 DSH 插件或市场机制。',
goodBuddySource: '使用 GoodBuddy 模型连接',
goodBuddySourceDescription:
'只能选择 OpenAI 兼容 Chat Completions 连接;该连接必须指向 DeepSeek 模型。',
platformSource: '使用平台 DeepSeek 环境配置',
platformSourceDescription:
'从启动环境读取平台管理的 DeepSeek 配置,不会在渲染进程中显示凭据。',
connectionDescription:
'GoodBuddy 内部 Harness Runtime 目前仅接受 OpenAI 兼容 Chat Completions 协议。',
advancedDescription:
'该 Runtime 始终使用 GoodBuddy 内置并固定版本的 Host,不加载外部 DSH 插件、市场包、用户 profile 或自定义 Host。'
}
},
documentParsing: {
@@ -287,6 +314,28 @@ export const settings = {
slow: '慢'
}
},
languages: {
: '中文',
: '英语',
'50 种语言': '50 种语言'
},
catalog: {
'pp-ocrv6-tiny': {
displayName: 'PP-OCRv6 Tiny',
description:
'PaddleOCR 官方轻量中文 OCR 模型,适合扫描 PDF 和图片的本地 CPU 识别。'
},
'pp-ocrv6-small': {
displayName: 'PP-OCRv6 Small',
description:
'PaddleOCR 官方 50 语言 OCR 模型,在识别质量、速度和本地资源占用之间取得平衡。'
},
'pp-ocrv6-medium': {
displayName: 'PP-OCRv6 Medium',
description:
'PaddleOCR 官方 50 语言高质量 OCR 模型,识别较慢,并需要更多内存且具有更高延迟。'
}
},
installed: '已安装并校验',
download: '下载',
downloadAndSelect: '下载并启用',
@@ -410,11 +459,13 @@ export const settings = {
},
imageQualityDescription: '仅用于 OpenAI 兼容图像生成请求。',
compatibilitySummary:
'直连模型:{{directCapability}} · Continue{{continueCompatibility}} · OpenCode{{openCodeCompatibility}}',
'直连模型:{{directCapability}} · Continue{{continueCompatibility}} · OpenCode{{openCodeCompatibility}} · DeepSeek Harness{{deepseekHarnessCompatibility}}',
textChat: '文本对话',
compatible: '兼容',
incompatible: '不兼容',
incompatibleImageProtocol: '不兼容(不支持图像生成协议)',
incompatibleHarnessProtocol:
'不兼容(仅支持 Chat Completions',
secureStorageWarning:
'当前系统密钥服务不可用。为了避免明文落盘,请使用环境变量提供 API Key。'
},
@@ -233,7 +233,8 @@ export const settingsSections = {
runtimeLabels: {
model: '模型',
opencode: 'OpenCode',
continue: 'Continue'
continue: 'Continue',
'deepseek-harness': 'DeepSeek Harness'
},
errors: {
readFailed: '读取 Skills 失败',
@@ -246,7 +247,7 @@ export const settingsSections = {
},
listLabel: 'Skills 列表',
notice:
'Skill 以本地能力说明注入所选目标,不会写入 Runtime 自有配置。新导入的 Skill 默认启用,并分配给直连模型、OpenCodeContinue。',
'Skill 以本地能力说明注入所选目标,不会写入 Runtime 自有配置。新导入的 Skill 默认启用,并分配给直连模型、OpenCodeContinue 和 DeepSeek Harness。',
loading: '正在读取 Skills…',
source: {
builtin: '内置',
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest'
import type { RuntimeSettings } from '../../shared/contracts'
import {
getDefaultRuntimeSelection,
getRuntimeSelectionForProvider
} from './runtime-selection'
const harnessProfileId = '00000000-0000-4000-8000-000000000071'
function harnessSettings(
source: { kind: 'platform' } | { kind: 'profile'; profileId: string }
): RuntimeSettings {
return {
provider: 'deepseek-harness',
deepseekHarnessModelSource: source
} as RuntimeSettings
}
describe('DeepSeek Harness runtime selection', () => {
it('uses the configured Chat Completions profile', () => {
const selection = getRuntimeSelectionForProvider(
'deepseek-harness',
harnessSettings({
kind: 'profile',
profileId: harnessProfileId
})
)
expect(selection).toEqual({
provider: 'deepseek-harness',
profileId: harnessProfileId
} satisfies Record<string, string>)
})
it('uses platform settings without a profile id', () => {
const settings = harnessSettings({ kind: 'platform' })
expect(getDefaultRuntimeSelection(settings)).toEqual({
provider: 'deepseek-harness'
})
})
})
+10 -6
View File
@@ -2,7 +2,7 @@ import type { RuntimeSettings } from '../../shared/contracts'
import type { AgentRuntimeSelection } from '../../shared/runtime-selection-contracts'
export function getRuntimeSelectionForProvider(
provider: 'model' | 'opencode' | 'continue',
provider: 'model' | 'opencode' | 'continue' | 'deepseek-harness',
settings: RuntimeSettings
): AgentRuntimeSelection {
if (provider === 'model') {
@@ -14,7 +14,9 @@ export function getRuntimeSelectionForProvider(
const source =
provider === 'opencode'
? settings.opencodeModelSource
: settings.continueModelSource
: provider === 'continue'
? settings.continueModelSource
: settings.deepseekHarnessModelSource ?? { kind: 'platform' }
return {
provider,
...(source.kind === 'profile' ? { profileId: source.profileId } : {})
@@ -24,12 +26,14 @@ export function getRuntimeSelectionForProvider(
export function getDefaultRuntimeSelection(
settings: RuntimeSettings
): AgentRuntimeSelection {
const provider = settings.provider
if (
settings.provider === 'model' ||
settings.provider === 'opencode' ||
settings.provider === 'continue'
provider === 'model' ||
provider === 'opencode' ||
provider === 'continue' ||
provider === 'deepseek-harness'
) {
return getRuntimeSelectionForProvider(settings.provider, settings)
return getRuntimeSelectionForProvider(provider, settings)
}
return settings.opencodeBaseUrl || settings.opencodeEmbedded
? getRuntimeSelectionForProvider('opencode', settings)
+31
View File
@@ -4903,6 +4903,37 @@ button > svg {
font-weight: 650;
}
.runtime-overview {
display: grid;
gap: var(--space-3);
}
.runtime-overview__details {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
gap: var(--space-1) var(--space-3);
margin: 0;
}
.runtime-overview__details dt {
color: var(--text-primary);
font-weight: 650;
}
.runtime-overview__details dd {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
}
.runtime-overview__path {
word-break: break-word;
}
.runtime-overview > p {
margin: 0;
}
.model-service-form {
display: grid;
gap: var(--space-3);
+3 -2
View File
@@ -19,13 +19,14 @@ const controlCharacterFreeString = (maximumLength: number) =>
export const runtimeTargetSchema = z.enum([
'model',
'opencode',
'continue'
'continue',
'deepseek-harness'
])
export type RuntimeTarget = z.infer<typeof runtimeTargetSchema>
export const capabilityAssignmentsSchema = z
.array(runtimeTargetSchema)
.max(3)
.max(4)
.refine(
(assignments) => new Set(assignments).size === assignments.length,
'Runtime 分配不能重复'
+54 -3
View File
@@ -234,7 +234,8 @@ export const runtimeProviderSchema = z.enum([
'auto',
'model',
'opencode',
'continue'
'continue',
'deepseek-harness'
])
export const toolApprovalPolicySchema = z.enum([
@@ -391,6 +392,26 @@ export const runtimeModelSourceSchema = z.discriminatedUnion('kind', [
.strict()
])
export function isDeepSeekHarnessModelProfile(
profile: Pick<
ModelConnectionSettings,
'baseUrl' | 'protocol' | 'authentication'
>
): boolean {
if (
profile.protocol !== 'openai-chat-completions' ||
profile.authentication !== 'api-key'
) {
return false
}
try {
return new URL(profile.baseUrl).hostname.toLowerCase() ===
'api.deepseek.com'
} catch {
return false
}
}
export const runtimeSettingsInputSchema = z
.object({
provider: runtimeProviderSchema,
@@ -440,6 +461,8 @@ export const runtimeSettingsInputSchema = z
defaultModelProfileId: modelProfileIdSchema.optional(),
opencodeModelSource: runtimeModelSourceSchema.optional(),
continueModelSource: runtimeModelSourceSchema.optional(),
deepseekHarnessModelSource: runtimeModelSourceSchema
.default({ kind: 'platform' }),
toolApproval: toolApprovalPolicySchema
}).strict()
.superRefine((settings, context) => {
@@ -505,7 +528,8 @@ export const runtimeSettingsInputSchema = z
}
for (const [key, source] of [
['opencodeModelSource', settings.opencodeModelSource],
['continueModelSource', settings.continueModelSource]
['continueModelSource', settings.continueModelSource],
['deepseekHarnessModelSource', settings.deepseekHarnessModelSource]
] as const) {
if (source?.kind === 'profile' && !ids.has(source.profileId)) {
context.addIssue({
@@ -551,6 +575,24 @@ export const runtimeSettingsInputSchema = z
'Continue 独立模型连接仅支持文本对话协议,不支持图像生成协议'
})
}
const deepseekHarnessSource = settings.deepseekHarnessModelSource
const deepseekHarnessProfile =
deepseekHarnessSource?.kind === 'profile'
? settings.modelProfiles.find(
(profile) => profile.id === deepseekHarnessSource.profileId
)
: undefined
if (
deepseekHarnessProfile &&
!isDeepSeekHarnessModelProfile(deepseekHarnessProfile)
) {
context.addIssue({
code: 'custom',
path: ['deepseekHarnessModelSource'],
message:
'DeepSeek Harness 独立模型连接仅支持 api.deepseek.com 的 OpenAI Chat Completions 协议'
})
}
}
if (
settings.opencodeBaseUrl &&
@@ -615,6 +657,7 @@ export type ConfiguredRuntimeSettings = {
workspacePath: string
opencodeModelSource: RuntimeModelSource
continueModelSource: RuntimeModelSource
deepseekHarnessModelSource?: RuntimeModelSource
}
export type RuntimeSettings = {
@@ -659,6 +702,7 @@ export type RuntimeSettings = {
defaultModelProfileId: string
opencodeModelSource: RuntimeModelSource
continueModelSource: RuntimeModelSource
deepseekHarnessModelSource?: RuntimeModelSource
secureStorageAvailable: boolean
toolApproval: RuntimeSettingsInput['toolApproval']
configured?: ConfiguredRuntimeSettings
@@ -716,7 +760,12 @@ export type WindowCaptureOption = {
}
export type AgentRuntimeStatus = {
id: 'setup' | 'model' | 'opencode' | 'continue'
id:
| 'setup'
| 'model'
| 'opencode'
| 'continue'
| 'deepseek-harness'
label: string
available: boolean
detail: string
@@ -729,6 +778,7 @@ export type RuntimeBinaryDetection =
available: true
path: string
version?: string
source?: 'bundled' | 'configured' | 'automatic'
detail: string
}
| {
@@ -741,6 +791,7 @@ export type RuntimeBinaryDetection =
export type AgentRuntimeDetection = {
opencode: RuntimeBinaryDetection
continue: RuntimeBinaryDetection
deepseekHarness: RuntimeBinaryDetection
}
export const approvalDecisionSchema = z.enum([
+47 -1
View File
@@ -23,6 +23,12 @@ export const agentRuntimeSelectionSchema = z.discriminatedUnion(
provider: z.literal('continue'),
profileId: runtimeSelectionProfileIdSchema.optional()
})
.strict(),
z
.object({
provider: z.literal('deepseek-harness'),
profileId: runtimeSelectionProfileIdSchema.optional()
})
.strict()
]
)
@@ -34,6 +40,7 @@ export type AgentRuntimeSelection = z.infer<
export type RuntimeSelectionRepairSettings = {
modelProfiles: ReadonlyArray<{
id: string
baseUrl?: string
protocol?: string
authentication?: 'api-key' | 'none'
apiKeyConfigured?: boolean
@@ -45,6 +52,9 @@ export type RuntimeSelectionRepairSettings = {
continueModelSource:
| { kind: 'platform' }
| { kind: 'profile'; profileId: string }
deepseekHarnessModelSource?:
| { kind: 'platform' }
| { kind: 'profile'; profileId: string }
}
type ChannelModelProfile = RuntimeSelectionRepairSettings['modelProfiles'][number]
@@ -61,6 +71,25 @@ export function isChannelModelProfileUsable(
)
}
function isDeepSeekHarnessRepairProfileUsable(
profile: ChannelModelProfile
): boolean {
if (
profile.protocol !== 'openai-chat-completions' ||
profile.authentication !== 'api-key' ||
profile.apiKeyConfigured === false ||
!profile.baseUrl
) {
return false
}
try {
return new URL(profile.baseUrl).hostname.toLowerCase() ===
'api.deepseek.com'
} catch {
return false
}
}
export function repairChannelRuntimeSelection(
selection: AgentRuntimeSelection,
settings: RuntimeSelectionRepairSettings
@@ -86,6 +115,21 @@ export function repairChannelRuntimeSelection(
) {
return { provider: selection.provider }
}
if (selection.provider === 'deepseek-harness') {
const repaired = repairAgentRuntimeSelection(selection, settings)
if (
repaired.provider === 'deepseek-harness' &&
repaired.profileId
) {
const profile = settings.modelProfiles.find(
(candidate) => candidate.id === repaired.profileId
)
if (profile && isDeepSeekHarnessRepairProfileUsable(profile)) {
return repaired
}
}
return { provider: 'deepseek-harness' }
}
const repaired = repairAgentRuntimeSelection(selection, settings)
if (repaired.provider !== 'model') {
return repaired
@@ -120,7 +164,9 @@ export function repairAgentRuntimeSelection(
const source =
selection.provider === 'opencode'
? settings.opencodeModelSource
: settings.continueModelSource
: selection.provider === 'continue'
? settings.continueModelSource
: settings.deepseekHarnessModelSource ?? { kind: 'platform' }
return {
provider: selection.provider,
...(source.kind === 'profile'