feat: add DSH plugin marketplace and shared MCP

GoodBuddy could share Skills across runtimes, but custom MCP remained limited and DeepSeek Harness could not manage third-party extensions. The app now provides a default-off DSH npm marketplace with managed installation, configuration, failure isolation, and packaged npm support, while assigned custom MCP is available to managed OpenCode, Continue Agent, and DeepSeek Harness in Execute.

Third-party DSH install scripts, initialization, and tools run with the current user's permissions. Ask remains read-only at dispatch, and turning off the marketplace hides management without disabling installed plugins.

Release note: 新增默认关闭的 DSH 插件市场,并让自定义 MCP 可分配给 OpenCode、Continue 和 DeepSeek Harness;安装第三方插件前会明确提示当前用户权限边界。
This commit is contained in:
mesalogo
2026-08-16 11:47:11 +08:00
parent 9e6f664e06
commit ff61b5f81d
67 changed files with 9337 additions and 443 deletions
+10
View File
@@ -94,6 +94,10 @@ import type {
DocumentParsingTestPurpose
} from './document-parsing-contracts'
import type { SettingsWarning } from './settings-warning-contracts'
import type {
RuntimeExtensionAction,
RuntimeExtensionMarketplaceSnapshot
} from './runtime-extension-contracts'
import type {
KnowledgeChunkDeleteInput,
KnowledgeChunkPage,
@@ -1518,6 +1522,12 @@ export type DesktopApi = {
profileId: string
) => Promise<CapabilitySnapshot>
}
runtimeExtensions: {
getSnapshot: () => Promise<RuntimeExtensionMarketplaceSnapshot>
apply: (
action: RuntimeExtensionAction
) => Promise<RuntimeExtensionMarketplaceSnapshot>
}
context: {
selectFiles: () => Promise<ContextAttachment[]>
onFileSelectionProgress: (
+2
View File
@@ -136,6 +136,8 @@ export const ipcChannels = {
capabilitiesRenameBrowserProfile: 'capabilities:browser-profile:rename',
capabilitiesDefaultBrowserProfile: 'capabilities:browser-profile:default',
capabilitiesRemoveBrowserProfile: 'capabilities:browser-profile:remove',
runtimeExtensionsSnapshot: 'runtime-extensions:snapshot',
runtimeExtensionsApply: 'runtime-extensions:apply',
contextSelectFiles: 'context:select-files',
contextFileSelectionProgress: 'context:file-selection-progress',
contextAddPastedImage: 'context:add-pasted-image',
@@ -0,0 +1,147 @@
import { describe, expect, it } from 'vitest'
import {
runtimeExtensionActionSchema,
runtimeExtensionCatalogEntrySchema,
runtimeExtensionMarketplaceSnapshotSchema
} from './runtime-extension-contracts'
const catalogEntry = {
id: 'web-research',
package: {
name: '@goodbuddy/dsh-web-research',
version: '1.2.3'
},
displayName: 'Web research',
description: 'Researches public web pages.',
repository: 'https://example.com/goodbuddy/web-research',
license: 'MIT'
}
describe('runtime extension contracts', () => {
it('accepts the minimal catalog metadata', () => {
expect(runtimeExtensionCatalogEntrySchema.parse(catalogEntry)).toEqual(
catalogEntry
)
})
it('requires exact semantic versions', () => {
expect(
runtimeExtensionActionSchema.safeParse({
type: 'install',
extensionId: catalogEntry.id,
package: { ...catalogEntry.package, version: '^1.2.3' }
}).success
).toBe(false)
})
it('rejects removed policy fields and rollback actions', () => {
expect(
runtimeExtensionCatalogEntrySchema.safeParse({
...catalogEntry,
permissions: []
}).success
).toBe(false)
expect(
runtimeExtensionActionSchema.safeParse({
type: 'rollback',
extensionId: catalogEntry.id
}).success
).toBe(false)
})
it('models snapshots with JSON-like configuration', () => {
const snapshot = {
marketplaceEnabled: true,
catalog: [catalogEntry],
installed: [
{
id: catalogEntry.id,
package: catalogEntry.package,
installedAt: '2026-08-16T00:00:00.000Z',
enabled: true,
integrity: `sha512-${Buffer.from('digest').toString(
'base64'
)}`,
configuration: {
resultLimit: 10,
filters: { domains: ['example.com'], exact: true },
optional: null
}
}
]
}
expect(
runtimeExtensionMarketplaceSnapshotSchema.parse(snapshot)
).toEqual(snapshot)
expect(
runtimeExtensionMarketplaceSnapshotSchema.safeParse({
...snapshot,
installed: [
{
...snapshot.installed[0],
entrypoint:
'C:\\Users\\tester\\runtime-extensions\\extensions\\web-research\\dist\\index.js'
}
]
}).success
).toBe(false)
expect(
runtimeExtensionMarketplaceSnapshotSchema.safeParse({
...snapshot,
warnings: []
}).success
).toBe(false)
})
it('supports only the explicit marketplace switch action', () => {
expect(
runtimeExtensionActionSchema.parse({
type: 'set-marketplace-enabled',
enabled: true
})
).toEqual({
type: 'set-marketplace-enabled',
enabled: true
})
expect(
runtimeExtensionActionSchema.safeParse({
type: 'set-marketplace-enabled',
enabled: true,
extensionId: 'unexpected'
}).success
).toBe(false)
})
it('bounds configuration size, depth, and collection width', () => {
let deeplyNested: Record<string, unknown> = {}
for (let depth = 0; depth < 18; depth += 1) {
deeplyNested = { nested: deeplyNested }
}
const action = {
type: 'configure',
extensionId: catalogEntry.id
}
expect(
runtimeExtensionActionSchema.safeParse({
...action,
configuration: { value: 'x'.repeat(65 * 1_024) }
}).success
).toBe(false)
expect(
runtimeExtensionActionSchema.safeParse({
...action,
configuration: deeplyNested
}).success
).toBe(false)
expect(
runtimeExtensionActionSchema.safeParse({
...action,
configuration: {
values: Array.from({ length: 257 }, () => true)
}
}).success
).toBe(false)
})
})
+260
View File
@@ -0,0 +1,260 @@
import { z } from 'zod'
export const runtimeExtensionStartupFailureCode = 'startup-failed'
export const legacyRuntimeExtensionStartupFailure =
'Extension failed to start.'
export const runtimeExtensionIdSchema = z
.string()
.min(1)
.max(128)
.regex(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/u)
export const runtimeExtensionPackageNameSchema = z
.string()
.min(1)
.max(214)
.regex(/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u)
export const runtimeExtensionVersionSchema = z
.string()
.min(1)
.max(64)
.regex(
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u
)
export const runtimeExtensionExactPackageSchema = z
.object({
name: runtimeExtensionPackageNameSchema,
version: runtimeExtensionVersionSchema
})
.strict()
export const runtimeExtensionIntegritySchema = z
.string()
.min(1)
.max(1_024)
.regex(
/^(?:sha(?:256|384|512)-[A-Za-z0-9+/]+={0,2})(?:\s+sha(?:256|384|512)-[A-Za-z0-9+/]+={0,2})*$/u
)
type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue }
type JsonObject = { [key: string]: JsonValue }
const MAXIMUM_CONFIGURATION_BYTES = 64 * 1024
const MAXIMUM_CONFIGURATION_DEPTH = 16
const MAXIMUM_CONFIGURATION_NODES = 4_096
const MAXIMUM_CONFIGURATION_ENTRIES = 256
const MAXIMUM_CONFIGURATION_KEY_LENGTH = 256
const MAXIMUM_CONFIGURATION_STRING_LENGTH = 32_768
function isBoundedJsonConfiguration(value: unknown): value is JsonObject {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value)
) {
return false
}
const pending: Array<{ value: unknown; depth: number }> = [
{ value, depth: 0 }
]
const seen = new Set<object>()
let nodes = 0
while (pending.length > 0) {
const current = pending.pop()!
nodes += 1
if (
nodes > MAXIMUM_CONFIGURATION_NODES ||
current.depth > MAXIMUM_CONFIGURATION_DEPTH
) {
return false
}
if (
current.value === null ||
typeof current.value === 'boolean'
) {
continue
}
if (typeof current.value === 'number') {
if (!Number.isFinite(current.value)) {
return false
}
continue
}
if (typeof current.value === 'string') {
if (
current.value.length >
MAXIMUM_CONFIGURATION_STRING_LENGTH
) {
return false
}
continue
}
if (
!current.value ||
typeof current.value !== 'object' ||
seen.has(current.value)
) {
return false
}
seen.add(current.value)
if (Array.isArray(current.value)) {
if (
current.value.length > MAXIMUM_CONFIGURATION_ENTRIES
) {
return false
}
for (const item of current.value) {
pending.push({
value: item,
depth: current.depth + 1
})
}
continue
}
const entries = Object.entries(current.value)
if (entries.length > MAXIMUM_CONFIGURATION_ENTRIES) {
return false
}
for (const [key, item] of entries) {
if (key.length > MAXIMUM_CONFIGURATION_KEY_LENGTH) {
return false
}
pending.push({
value: item,
depth: current.depth + 1
})
}
}
try {
return (
new TextEncoder().encode(JSON.stringify(value)).byteLength <=
MAXIMUM_CONFIGURATION_BYTES
)
} catch {
return false
}
}
export const runtimeExtensionConfigurationSchema =
z.custom<JsonObject>(
isBoundedJsonConfiguration,
'Extension configuration must be a bounded JSON object'
)
export const runtimeExtensionCatalogEntrySchema = z
.object({
id: runtimeExtensionIdSchema,
package: runtimeExtensionExactPackageSchema,
displayName: z.string().trim().min(1).max(128),
description: z.string().trim().min(1).max(2_000),
repository: z.string().url().max(2_048).optional(),
license: z.string().trim().min(1).max(128).optional()
})
.strict()
export const runtimeExtensionInstalledStateSchema = z
.object({
id: runtimeExtensionIdSchema,
package: runtimeExtensionExactPackageSchema,
entrypoint: z.string().min(1).max(32_768),
installedAt: z.string().datetime({ offset: true }),
enabled: z.boolean(),
configuration: runtimeExtensionConfigurationSchema,
integrity: runtimeExtensionIntegritySchema.optional(),
lastError: z.string().trim().min(1).max(1_000).optional()
})
.strict()
export const runtimeExtensionMarketplaceInstalledStateSchema =
runtimeExtensionInstalledStateSchema.omit({
entrypoint: true
})
export const runtimeExtensionMarketplaceSnapshotSchema = z
.object({
marketplaceEnabled: z.boolean(),
catalog: z.array(runtimeExtensionCatalogEntrySchema),
installed: z.array(
runtimeExtensionMarketplaceInstalledStateSchema
),
catalogError: z.string().trim().min(1).max(1_000).optional()
})
.strict()
export const runtimeExtensionInstallActionSchema = z
.object({
type: z.literal('install'),
extensionId: runtimeExtensionIdSchema,
package: runtimeExtensionExactPackageSchema
})
.strict()
export const runtimeExtensionEnableActionSchema = z
.object({
type: z.literal('set-enabled'),
extensionId: runtimeExtensionIdSchema,
enabled: z.boolean()
})
.strict()
export const runtimeExtensionRemoveActionSchema = z
.object({
type: z.literal('remove'),
extensionId: runtimeExtensionIdSchema
})
.strict()
export const runtimeExtensionConfigureActionSchema = z
.object({
type: z.literal('configure'),
extensionId: runtimeExtensionIdSchema,
configuration: runtimeExtensionConfigurationSchema
})
.strict()
export const runtimeExtensionMarketplaceEnableActionSchema = z
.object({
type: z.literal('set-marketplace-enabled'),
enabled: z.boolean()
})
.strict()
export const runtimeExtensionActionSchema = z.discriminatedUnion('type', [
runtimeExtensionMarketplaceEnableActionSchema,
runtimeExtensionInstallActionSchema,
runtimeExtensionEnableActionSchema,
runtimeExtensionRemoveActionSchema,
runtimeExtensionConfigureActionSchema
])
export type RuntimeExtensionExactPackage = z.infer<
typeof runtimeExtensionExactPackageSchema
>
export type RuntimeExtensionCatalogEntry = z.infer<
typeof runtimeExtensionCatalogEntrySchema
>
export type RuntimeExtensionInstalledState = z.infer<
typeof runtimeExtensionInstalledStateSchema
>
export type RuntimeExtensionMarketplaceInstalledState = z.infer<
typeof runtimeExtensionMarketplaceInstalledStateSchema
>
export type RuntimeExtensionMarketplaceSnapshot = z.infer<
typeof runtimeExtensionMarketplaceSnapshotSchema
>
export type RuntimeExtensionAction = z.infer<
typeof runtimeExtensionActionSchema
>
export type RuntimeExtensionConfiguration = z.infer<
typeof runtimeExtensionConfigurationSchema
>