refactor(sdk): remove unused root tool filter

This commit is contained in:
Tianyi Cui
2026-08-25 12:21:50 +08:00
parent 09eda93884
commit d35459e3c1
13 changed files with 12 additions and 135 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md
2026-08-23-python-sdk-dsh-profile-runtime.md: dcb4f77048d516f0e187b611dc09c224fba47b58
2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 505b49506ae4c4be4809d588448b2c593d3ebaf5
2026-08-23-python-sdk-dsh-profile-runtime.md: 4af7812db6818b65c754a43ec1a7f973d1cbcbf9
2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 155e7d2ae0b0ba3e4163dd85a31de90bef9d588a
@@ -24,7 +24,7 @@ Every Python launch requires either explicit `dsh_home` or a non-empty `DSH_HOME
Persistent SDK customization uses the same profile interfaces as direct CLI use. `dsh plugin --profile <name> ...` manages external dependencies and bundle order, `$DSH_HOME/profiles/<name>/cordis.patch.yml` owns persistent row changes, the home patch applies machine-local changes across profiles, and Python `patches` supplies invocation-specific overlays. A selected profile is valid only when it retains an SDK server row. Missing profiles, bundles, server rows, and invalid patches fail without a complete-config fallback; a profile that remains alive without serving JSON-RPC fails the independently bounded initialization handshake with a diagnostic naming that profile.
The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) lists one repository-owned bundle that inserts its complete explicit tree without `dsh-base`. Its persistent Bash and string-replace editor are present by composition rather than a server filter; dynamic runtime context, workspace instructions, settings, managed credentials, telemetry, compaction, and every other base row are absent. The same runtime still packages the full `sdk` and `web` profiles as separate choices.
The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) lists one repository-owned bundle that inserts its complete explicit tree without `dsh-base`. Its persistent Bash and string-replace editor are present by composition; the shared JSON-RPC server exposes no root-agent tool filter. Dynamic runtime context, workspace instructions, settings, managed credentials, telemetry, compaction, and every other base row are absent. The same runtime still packages the full `sdk` and `web` profiles as separate choices.
The runtime wheel installs a `dsh` console command. Ordinary profile and SDK execution remains Node-free; external package management requires a caller-installed `pnpm`.
@@ -24,7 +24,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml
持久 SDK 自定义使用与直接 CLI 相同的 profile 接口。`dsh plugin --profile <name> ...` 管理外部依赖与 bundle 顺序,`$DSH_HOME/profiles/<name>/cordis.patch.yml` 负责持久配置项变更,home patch 对所有 profile 应用机器本地变更,Python `patches` 则提供单次启动 overlay。所选 profile 只有保留 SDK server 配置项时才有效。缺失 profile、bundle、server 配置项或非法 patch 都会直接失败,不存在完整配置回退;保持运行却不提供 JSON-RPC 服务的 profile 会在独立有界的初始化握手中失败,诊断会指明该 profile。
[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)只列出一个仓库自有组合包,该组合包会插入不含 `dsh-base` 的完整显式配置树。持久 Bash 与字符串替换 editor 通过组合存在,而不是通过 server 筛选;动态运行时上下文、workspace 指令、settings、托管凭据、遥测、compaction 与其他所有 base 配置项均不存在。同一运行时仍会把完整 `sdk``web` profile 作为独立选择打包。
[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)只列出一个仓库自有组合包,该组合包会插入不含 `dsh-base` 的完整显式配置树。持久 Bash 与字符串替换 editor 通过组合存在;共享 JSON-RPC server 不暴露根 agent 工具筛选器。动态运行时上下文、workspace 指令、settings、托管凭据、遥测、compaction 与其他所有 base 配置项均不存在。同一运行时仍会把完整 `sdk``web` profile 作为独立选择打包。
运行时 wheel 安装 `dsh` 控制台命令。普通 profile 与 SDK 运行仍不需要 Node;外部包管理要求调用方自行安装 `pnpm`
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: 19f292ee40861ffa6f8e7fdef4d8717189a2149c
config-catalog.zh.md: 96f9721cae20a1a0ed172349c0189bc47576c75d
config-catalog.md: 54cf64bdf6ff5c8ee51b6ecedaf218267778d19f
config-catalog.zh.md: e887edf989d279435ae04beb40172341f5125b1e
-7
View File
@@ -1714,13 +1714,6 @@ Requires: `agents`
export interface JsonRpcConfig {
/** Report max-token turn/subagent termination as a successful SDK result. */
maxTokensAsSuccess?: boolean
/** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */
toolFilter?: {
/** Global tool names that remain visible. */
allow?: string[]
/** Global tool names removed from visibility. */
deny?: string[]
}
/** Transport input override; production uses `process.stdin`. */
input?: Readable
/** Transport output override; production uses `process.stdout`. */
-7
View File
@@ -1716,13 +1716,6 @@ export interface Config {
export interface JsonRpcConfig {
/** Report max-token turn/subagent termination as a successful SDK result. */
maxTokensAsSuccess?: boolean
/** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */
toolFilter?: {
/** Global tool names that remain visible. */
allow?: string[]
/** Global tool names removed from visibility. */
deny?: string[]
}
/** Transport input override; production uses `process.stdin`. */
input?: Readable
/** Transport output override; production uses `process.stdout`. */
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/sdk/server/README.md
README.md: e6d36df7e387e895941c082226ac955c69f16ca4
README.zh.md: 13c026edc491ebdbec298b8c9cd330b03d675db7
README.md: f10c45e2df06383d6726f249bb6db2015a7fb418
README.zh.md: 7bf3bcdab2a318fbfcba7b5a6c3a60ff883f47b6
+2 -2
View File
@@ -10,7 +10,7 @@ The `jsonrpc` plugin serves newline-delimited JSON-RPC over stdio so out-of-proc
## Config
`maxTokensAsSuccess` defaults to `false` and affects only the deployment-mapped status on `subagent.finished`; root-session prompts have no prompt-level status. Optional `toolFilter.allow` and `toolFilter.deny` restrict each SDK-created root agent through `ctx.tools.restrict()`. An allow list excludes later global tool registrations that it does not name, so a fixed SDK deployment cannot silently gain model-facing tools when its base bundle expands. Unknown names and an empty filter fail when the first session is created. `JsonRpcConfig.input`, `output`, and `exit` are runtime-only transport hooks; production uses process stdio and `process.exit`.
`maxTokensAsSuccess` defaults to `false` and affects only the deployment-mapped status on `subagent.finished`; root-session prompts have no prompt-level status. The profile composition owns each root agent's tools. `JsonRpcConfig.input`, `output`, and `exit` are runtime-only transport hooks; production uses process stdio and `process.exit`.
## stdout is the protocol
@@ -30,7 +30,7 @@ The plugin answers `shutdown`, flushes the response, disposes the root context s
#### What the model sees
For each accepted `session/prompt`, text and durable content references enter one user message verbatim. Inline `SdkEncodedImageBlock` values are validated and committed through the composition's attachment store first, so the session log retains content-addressed image references rather than base64 bytes. This package adds no system-prompt prose or tool schema; those come from the other plugins in the composition. A configured `toolFilter` projects that composition's global tool registry before the request is assembled and executed.
For each accepted `session/prompt`, text and durable content references enter one user message verbatim. Inline `SdkEncodedImageBlock` values are validated and committed through the composition's attachment store first, so the session log retains content-addressed image references rather than base64 bytes. This package adds no system-prompt prose or tool schema; those come from the other plugins in the composition.
#### Token effect
+2 -2
View File
@@ -10,7 +10,7 @@
## 配置
`maxTokensAsSuccess` 默认为 `false`,且只影响 `subagent.finished` 上由部署映射的状态;根会话提示词没有提示词级状态。可选的 `toolFilter.allow``toolFilter.deny` 通过 `ctx.tools.restrict()` 限制每个由 SDK 创建的根 agent。Allow 列表会排除之后出现但未指名的全局工具,因此固定的 SDK 部署不会在基础 bundle 扩展时静默获得面向模型的新工具。未知名称与空筛选器会在创建首个会话时明确失败`JsonRpcConfig.input``output``exit` 是仅供运行时使用的传输钩子;生产环境使用进程 stdio 和 `process.exit`
`maxTokensAsSuccess` 默认为 `false`,且只影响 `subagent.finished` 上由部署映射的状态;根会话提示词没有提示词级状态。Profile 组合负责每个根 agent 的工具`JsonRpcConfig.input``output``exit` 是仅供运行时使用的传输钩子;生产环境使用进程 stdio 和 `process.exit`
## stdout 即协议
@@ -30,7 +30,7 @@ Stdout 只承载 JSON-RPC 帧。部署不得组合 stdout logger;诊断应写
#### 模型看到的内容
对于每个已接受的 `session/prompt`,文本和持久内容引用会原样进入一条用户消息。内联 `SdkEncodedImageBlock` 会先通过组合中的附件存储完成校验与提交,因此会话日志保留内容寻址的图片引用而不是 base64 字节。此包不会添加系统提示词文本或工具 schema;这些内容来自组合中的其他插件。配置的 `toolFilter` 会在请求组装与执行前投影该组合的全局工具注册表。
对于每个已接受的 `session/prompt`,文本和持久内容引用会原样进入一条用户消息。内联 `SdkEncodedImageBlock` 会先通过组合中的附件存储完成校验与提交,因此会话日志保留内容寻址的图片引用而不是 base64 字节。此包不会添加系统提示词文本或工具 schema;这些内容来自组合中的其他插件。
#### Token 影响
-13
View File
@@ -25,13 +25,6 @@ export const inject = ['agents']
export interface JsonRpcConfig {
/** Report max-token turn/subagent termination as a successful SDK result. */
maxTokensAsSuccess?: boolean
/** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */
toolFilter?: {
/** Global tool names that remain visible. */
allow?: string[]
/** Global tool names removed from visibility. */
deny?: string[]
}
/** Transport input override; production uses `process.stdin`. */
input?: Readable
/** Transport output override; production uses `process.stdout`. */
@@ -42,11 +35,6 @@ export interface JsonRpcConfig {
export const Config: Schema<JsonRpcConfig> = Schema.object({
maxTokensAsSuccess: Schema.boolean().default(false),
// Preserve omission; Schemastery's materialized empty object is not a valid restriction.
toolFilter: Schema.object({
allow: Schema.array(Schema.string()).default(undefined as unknown as string[]),
deny: Schema.array(Schema.string()).default(undefined as unknown as string[]),
}).default(undefined as unknown as { allow: string[]; deny: string[] }),
})
/**
@@ -71,7 +59,6 @@ export function apply(ctx: Context, config: JsonRpcConfig): void {
const transport = new JsonRpcLineTransport(input, output)
const server = new HarnessSdkJsonRpcServer(ctx, transport, {
maxTokensAsSuccess: resolvedConfig.maxTokensAsSuccess,
...resolvedConfig.toolFilter === undefined ? {} : { toolFilter: resolvedConfig.toolFilter },
})
// Share one exit task so racing shutdown requests cannot dispose the root or
-7
View File
@@ -15,7 +15,6 @@ import { carrierKeyOf, type Scoped } from '@deepseek-ai/dsh-scope'
import { SessionId } from '@deepseek-ai/dsh-session'
import type SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent'
import type { ToolRestriction } from '@deepseek-ai/dsh-tools'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import type {
InitializeParams,
@@ -61,8 +60,6 @@ function subagentParentOf(carrier: Scoped<SubagentRuntime>): Agent {
export interface HarnessSdkJsonRpcServerOptions {
/** Report max-token termination as an accepted result instead of an infrastructure error. */
maxTokensAsSuccess?: boolean
/** Restrict each SDK-created root agent to an explicit subset of global tools. */
toolFilter?: ToolRestriction
}
function successStatus(reason: string, options: HarnessSdkJsonRpcServerOptions): 'ok' | 'error' {
@@ -256,7 +253,6 @@ export class HarnessSdkJsonRpcServer {
// rows in the host plane, so this agent reads them from the global layer. A
// deployment that configures a roster has to join one here first
// (@deepseek-ai/dsh-agent-presets README, "Composing a child agent").
const toolFilter = this.options.toolFilter
const handle = await this.ctx.agents.create({
sessionId: SessionId(sessionId),
meta: { cwd: this.cwd },
@@ -265,9 +261,6 @@ export class HarnessSdkJsonRpcServer {
model: this.model,
...this.maxTokens === undefined ? {} : { maxTokens: this.maxTokens },
},
...toolFilter === undefined
? {}
: { setup: (agentCtx: Context) => { agentCtx.tools.restrict(toolFilter) } },
})
const rec: SessionRecord = { handle }
this.sessions.set(sessionId, rec)
@@ -11,7 +11,6 @@ import * as agentCore from '@deepseek-ai/dsh-agent-spine-demo'
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { defineTool } from '@deepseek-ai/dsh-tools'
import * as jsonrpc from '../src/index.ts'
/**
@@ -72,7 +71,6 @@ async function mountPlugin(
writeDelayMs?: number
failFlush?: boolean
beforeServer?: (ctx: Context) => Promise<void> | void
toolFilter?: jsonrpc.JsonRpcConfig['toolFilter']
} = {},
): Promise<ApplyHarness> {
const ctx = new Context()
@@ -122,7 +120,6 @@ async function mountPlugin(
input,
output,
exit,
...options.toolFilter === undefined ? {} : { toolFilter: options.toolFilter },
})
const frames = (): Record<string, unknown>[] =>
@@ -289,51 +286,6 @@ describe('dsh-sdk-jsonrpc-server plugin apply', () => {
}
})
it('applies the configured root-agent tool filter through the Loader plugin', async () => {
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-apply-tool-filter-'))
const llmServer = await mockCompletionServer()
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
vi.stubEnv('DEEPSEEK_BASE_URL', llmServer.url)
const harness = await mountPlugin(storageDir, {
toolFilter: { allow: ['kept'] },
beforeServer: (ctx) => {
for (const name of ['kept', 'excluded']) {
ctx.tools.register(defineTool({
name,
description: name,
parameters: {},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
execute: async () => name,
}))
}
},
})
try {
harness.send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { cwd: storageDir, provider: 'deepseek-official', model: 'filtered-model' } })
await harness.waitForFrame(frame => frame.id === 1, 'initialize response')
harness.send({
jsonrpc: '2.0',
id: 2,
method: 'session/prompt',
params: { sessionId: 'filtered', contentBlocks: [{ type: 'text', text: 'inspect tools' }] },
})
await harness.waitForFrame(
frame => frame.method === 'session.status'
&& (frame.params as { status?: string } | undefined)?.status === 'idle',
'filtered session idle status',
)
const request = llmServer.requests[0] as { tools?: Array<{ function?: { name?: string } }> }
expect(request.tools?.map(entry => entry.function?.name)).toEqual(['kept'])
} finally {
await harness.dispose()
await rm(storageDir, { recursive: true, force: true })
}
})
it('answers shutdown before exiting 0 exactly once, even against a racing second shutdown', async () => {
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-apply-shutdown-'))
const harness = await mountPlugin(storageDir, { writeDelayMs: 10 })
-41
View File
@@ -14,7 +14,6 @@ import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import SubagentRuntime, { type SubagentResult, type SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent'
import type { JsonRpcTransportPeer } from '@deepseek-ai/dsh-sdk-protocol'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { HarnessSdkJsonRpcServer } from '../src/index.ts'
class FakeTransport implements JsonRpcTransportPeer {
@@ -172,46 +171,6 @@ describe('HarnessSdkJsonRpcServer', () => {
}
})
it('allowlists each root session against current and later global tools', { timeout: 15_000 }, async () => {
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-tool-filter-'))
const llmServer = await mockCompletionServer()
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
vi.stubEnv('DEEPSEEK_BASE_URL', llmServer.url)
const ctx = await makeHarness(storageDir)
const tool = (name: string) => defineTool({
name,
description: name,
parameters: {},
output: {
schema: { type: 'string' as const },
render: (_args, value) => [{ type: 'text' as const, text: value }],
},
execute: async () => name,
})
ctx.tools.register(tool('kept'))
ctx.tools.register(tool('excluded'))
const server = new HarnessSdkJsonRpcServer(ctx, new FakeTransport(), {
toolFilter: { allow: ['kept'] },
})
try {
await server.initialize({ cwd: storageDir, provider: 'deepseek-official', model: 'filtered-model' })
await server.prompt({ sessionId: 'first', contentBlocks: [{ type: 'text', text: 'first' }] })
await vi.waitFor(() => { expect(llmServer.requests).toHaveLength(1) })
ctx.tools.register(tool('future'))
await server.prompt({ sessionId: 'second', contentBlocks: [{ type: 'text', text: 'second' }] })
await vi.waitFor(() => { expect(llmServer.requests).toHaveLength(2) })
expect(llmServer.requests.map((request) => {
const tools = (request as { tools?: Array<{ function?: { name?: string } }> }).tools ?? []
return tools.map(entry => entry.function?.name)
})).toEqual([['kept'], ['kept']])
await server.shutdown()
} finally {
await ctx.fiber.dispose()
await rm(storageDir, { recursive: true, force: true })
}
})
it('queues overlapping prompts for one session without blocking other sessions', async () => {
const mainFollowup = vi.fn<Agent['followup']>()
const mainAgent = ({