chore: prepare GoodBuddy 0.8.5

This commit is contained in:
lofyer
2026-08-07 10:45:01 +08:00
parent e20cb447af
commit 17e66a3369
54 changed files with 1306 additions and 1640 deletions
@@ -3,13 +3,11 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi
} from 'vitest'
import { setIntranetCompatibilityReader } from '../intranet-compatibility-policy'
import {
CapabilityService,
type CapabilityCipher,
@@ -23,10 +21,6 @@ import { CapabilityDiagnostics } from './capability-diagnostics'
const temporaryDirectories: string[] = []
beforeEach(() => {
setIntranetCompatibilityReader(() => false)
})
const cipher: CapabilityCipher = {
isAvailable: () => true,
encrypt: (value) => Buffer.from(`encrypted:${value}`),
@@ -125,7 +119,6 @@ async function createService(
}
afterEach(async () => {
setIntranetCompatibilityReader(() => true)
delete process.env.GOODBUDDY_CAPABILITY_SERVICE_SECRET
await Promise.all(
temporaryDirectories.splice(0).map((directory) =>
@@ -323,21 +316,6 @@ describe('CapabilityService', () => {
})
})
it('never sends a bearer token over non-loopback HTTP', async () => {
const { service } = await createService()
await expect(
service.saveMcpServer(undefined, {
name: 'Unsafe remote',
description: '',
enabled: true,
assignments: ['model'],
secret: { action: 'replace', value: 'secret-token-value' },
transport: 'http',
url: 'http://mcp.example.com/mcp'
})
).rejects.toThrow('只能通过 HTTPS')
})
it('allows bearer tokens over the full IPv4 loopback range', async () => {
const { service } = await createService()
@@ -361,8 +339,7 @@ describe('CapabilityService', () => {
})
})
it('allows bearer tokens over HTTP in intranet compatibility mode', async () => {
setIntranetCompatibilityReader(() => true)
it('allows bearer tokens over HTTP on any configured host', async () => {
const { service } = await createService()
const snapshot = await service.saveMcpServer(undefined, {
@@ -390,27 +367,9 @@ describe('CapabilityService', () => {
await expect(
service.getResolvedMcpServer(server.id)
).resolves.toMatchObject({ secret: 'secret-token-value' })
setIntranetCompatibilityReader(() => false)
await expect(
service.getResolvedMcpServer(server.id)
).rejects.toThrow('只能通过 HTTPS')
await expect(
service.getResolvedMcpServers('model')
).resolves.toEqual([])
await expect(service.getSnapshot()).resolves.toMatchObject({
mcpServers: [
expect.objectContaining({
id: server.id,
enabled: false,
secretConfigured: true
})
]
})
})
it('rejects bearer tokens over public HTTP in intranet compatibility mode', async () => {
setIntranetCompatibilityReader(() => true)
it('allows public HTTP MCP servers with or without bearer tokens', async () => {
const { service } = await createService()
await expect(
@@ -423,24 +382,33 @@ describe('CapabilityService', () => {
transport: 'http',
url: 'http://mcp.example.com/mcp'
})
).rejects.toThrow('只能通过 HTTPS')
})
it('rejects public HTTP MCP servers without bearer tokens', async () => {
setIntranetCompatibilityReader(() => true)
const { service } = await createService()
).resolves.toMatchObject({
mcpServers: [
expect.objectContaining({
url: 'http://mcp.example.com/mcp',
secretConfigured: true
})
]
})
await expect(
service.saveMcpServer(undefined, {
name: 'Public plaintext MCP',
name: 'Public MCP without token',
description: '',
enabled: true,
assignments: ['model'],
secret: { action: 'clear' },
transport: 'http',
url: 'http://mcp.example.com/mcp'
url: 'http://mcp.example.com/no-token'
})
).rejects.toThrow('只能通过 HTTPS')
).resolves.toMatchObject({
mcpServers: expect.arrayContaining([
expect.objectContaining({
url: 'http://mcp.example.com/no-token',
secretConfigured: false
})
])
})
})
it('rejects MCP assignments to Agent Runtimes', async () => {
@@ -51,33 +51,11 @@ import {
isComputerCapabilitySupported,
type ComputerCapabilityImplementationKind
} from './computer-capability-catalog'
import { isIntranetCompatibilityEnabled } from '../intranet-compatibility-policy'
import {
isIntranetHostname,
isLoopbackHostname
} from '../../shared/intranet-hostname'
const MAX_SKILL_FILE_BYTES = 2 * 1024 * 1024
const MAX_SKILL_PACKAGE_BYTES = 10 * 1024 * 1024
const MAX_SKILL_PACKAGE_FILES = 128
const MAX_SKILL_DEPTH = 6
function canUseRemoteMcpUrl(url: string): boolean {
const parsed = new URL(url)
const hostname = parsed.hostname.toLowerCase()
return (
parsed.protocol === 'https:' ||
(
parsed.protocol === 'http:' &&
(
isLoopbackHostname(hostname) ||
(isIntranetCompatibilityEnabled() &&
isIntranetHostname(hostname))
)
)
)
}
const skillMetadataSchema = z
.object({
id: skillIdSchema,
@@ -998,15 +976,6 @@ export class CapabilityService {
.toString('base64')
}
}
if (
value.transport !== 'stdio' &&
!canUseRemoteMcpUrl(value.url)
) {
throw new Error(
'远程 MCP 只能通过 HTTPS、本机回环或已启用兼容模式的内网 HTTP 地址连接'
)
}
const stored: StoredMcpServer =
value.transport === 'stdio'
? {
@@ -1081,14 +1050,6 @@ export class CapabilityService {
throw new Error('MCP 访问令牌无法解密,请重新配置')
}
}
if (
server.transport !== 'stdio' &&
!canUseRemoteMcpUrl(server.url)
) {
throw new Error(
'远程 MCP 只能通过 HTTPS、本机回环或已启用兼容模式的内网 HTTP 地址连接'
)
}
return {
...this.toMcpSummary(server),
secret
@@ -1130,40 +1091,12 @@ export class CapabilityService {
: ''
}
quarantineIncompatibleMcpServers(): Promise<string[]> {
return this.queue(async () => {
const state = await this.load()
const incompatibleIds = state.mcpServers
.filter(
(server) =>
server.enabled &&
server.transport !== 'stdio' &&
!canUseRemoteMcpUrl(server.url)
)
.map((server) => server.id)
if (incompatibleIds.length === 0) {
return []
}
const incompatible = new Set(incompatibleIds)
await this.persist({
...state,
mcpServers: state.mcpServers.map((server) =>
incompatible.has(server.id)
? { ...server, enabled: false }
: server
)
})
return incompatibleIds
})
}
async getResolvedMcpServers(
target: RuntimeTarget
): Promise<ResolvedMcpServer[]> {
if (target !== 'model') {
return []
}
await this.quarantineIncompatibleMcpServers()
const state = await this.load()
const assigned = state.mcpServers.filter(
(server) => server.enabled && server.assignments.includes(target)
+2 -35
View File
@@ -1,42 +1,13 @@
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import type {
FetchLike,
Transport
} from '@modelcontextprotocol/sdk/shared/transport.js'
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
import type { ResolvedMcpServer } from './capability-service'
import {
isCuratedMcpLaunchDescriptor,
type CuratedMcpLaunchDescriptor
} from './curated-mcp-launch'
function validateRemoteUrl(value: string): URL {
const url = new URL(value)
const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/gu, '')
if (
hostname === '169.254.169.254' ||
hostname === 'metadata.google.internal' ||
hostname.endsWith('.internal.metadata')
) {
throw new Error('MCP 地址不能指向云平台元数据服务')
}
return url
}
function createRestrictedFetch(origin: string): FetchLike {
return async (input, init) => {
const url = new URL(String(input))
if (url.origin !== origin) {
throw new Error('MCP Server 尝试访问未授权的跨域地址')
}
return fetch(url, {
...init,
redirect: 'error'
})
}
}
export function createMcpTransport(
server: ResolvedMcpServer | CuratedMcpLaunchDescriptor
): Transport {
@@ -64,7 +35,7 @@ export function createMcpTransport(
})
}
const url = validateRemoteUrl(server.url)
const url = new URL(server.url)
const requestInit: RequestInit | undefined = server.secret
? {
headers: {
@@ -72,11 +43,8 @@ export function createMcpTransport(
}
}
: undefined
const safeFetch = createRestrictedFetch(url.origin)
return server.transport === 'http'
? new StreamableHTTPClientTransport(url, {
fetch: safeFetch,
requestInit,
reconnectionOptions: {
initialReconnectionDelay: 500,
@@ -86,7 +54,6 @@ export function createMcpTransport(
}
})
: new SSEClientTransport(url, {
fetch: safeFetch,
requestInit
})
}
+1 -1
View File
@@ -122,7 +122,7 @@ describe('testMcpServer', () => {
},
reconnectionOptions: { maxRetries: 0 }
})
expect(options).toHaveProperty('fetch')
expect(options).not.toHaveProperty('fetch')
})
it('closes the client and returns a controlled error on failure', async () => {