fix(cordis): register Host inspect providers once per process (#4742)

`tool-cordis` registered the four first-party Host inspect providers from
every preset row that mounted it. The Host registry rejects a duplicate
provider id, so a second preset carrying `tool-cordis` — or a user preset
copied from a shipped one — failed to mount in the same process.

- Add `@deepseek-ai/dsh-tool-cordis/host` (`cordis-inspect-providers`),
  the host-plane row that registers the providers; the Web host
  composition mounts it beside `cordis-host-runner`. The per-session
  `tool-cordis` row now only registers the tools and its prompt section.
- Gate the `cordis` preset's `tool-plugin-manager` row on `profileContext`,
  the same condition `dsh-base` applies to the host `plugin-manager`
  service, so a profile-less embedding still mounts the preset.
This commit is contained in:
Turtle authored and GitHub committed 2026-09-21 05:55:09 +00:00
1 parent b6cb6be3da
commit 4aba48ec03
17 files changed
+181 -12

No files matched your search

@@ -28,6 +28,7 @@ import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
import * as ToolJobs from '@deepseek-ai/dsh-tool-jobs'
import CordisHostRunner from '@deepseek-ai/dsh-cordis-host-runner'
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
import * as CordisInspectProviders from '@deepseek-ai/dsh-tool-cordis/host'
/**
* With-key PTC mode proof: a real model receives only `run_code`, composes two
@@ -280,6 +281,7 @@ describe('PTC mode typed values: keyless real-process contracts', () => {
it('uses runtime inspection results directly through PTC', async () => {
ctx = await typedPtcModeHarness()
await ctx.plugin(CordisHostRunner)
await ctx.plugin(CordisInspectProviders)
await ctx.plugin(ToolCordis)
const agent = {
id: SessionId('ptc-cordis'),
+75 -1
View File
@@ -1,5 +1,5 @@
import { randomUUID } from 'node:crypto'
import { mkdir, mkdtemp, readFile, stat, symlink, writeFile } from 'node:fs/promises'
import { cp, mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
@@ -668,6 +668,80 @@ describe('product Bundle and user-preset intersection', () => {
}, 120_000)
})
describe('a user preset copied from a shipped one', () => {
/** The JSON text a `cordis_inspect_*` result renders. */
function resultText(result: { content: ReadonlyArray<{ type: string; text?: string }> }): string {
return result.content.filter(part => part.type === 'text').map(part => part.text ?? '').join('')
}
it('mounts beside the shipped `cordis` preset and reads the shared Host inspect providers', async () => {
// The Host inspect providers are one process-global set registered by the
// host composition (`@deepseek-ai/dsh-tool-cordis/host`); each preset's
// `tool-cordis` row only registers the tools. Before that split the copy
// failed to mount: its row re-registered provider "Service".
const root = await mkdtemp(join(tmpdir(), 'dsh-copied-preset-'))
const userRoot = join(root, 'presets')
await cp(join(SHIPPED_PRESET_ROOT, 'cordis'), join(userRoot, 'cordis-copy'), { recursive: true })
const settingsFile = join(root, 'settings.yaml')
await writeFile(settingsFile, '{}\n')
const copyCtx = await bootWeb(settingsFile, [{
id: 'agent-presets',
config: { default: 'standard', roots: [{ path: userRoot, trust: 'user' }], includeUserRoot: false },
}])
try {
const shipped = await copyCtx.agents.create({
sessionId: SessionId(`preset-cordis-shipped-${randomUUID()}`),
setup: agentCtx => copyCtx.agentPresets.mount(agentCtx, 'cordis').then(() => undefined),
})
const copied = await copyCtx.agents.create({
sessionId: SessionId(`preset-cordis-copy-${randomUUID()}`),
setup: agentCtx => copyCtx.agentPresets.mount(agentCtx, 'cordis-copy').then(() => undefined),
})
try {
for (const handle of [shipped, copied]) {
expect(toolNames(copyCtx, handle.agent)).toEqual(expect.arrayContaining([
'cordis_inspect_list', 'cordis_inspect_query', 'plugin_manager',
]))
}
const signal = new AbortController().signal
const listed = await copyCtx.tools.execute({
callId: ToolCallId('copied-preset-inspect-list'),
name: 'cordis_inspect_list',
arguments: {},
signal,
agent: copied.agent,
})
expect(listed.isError).toBe(false)
const providers = (JSON.parse(resultText(listed)) as { providers: Array<{ id: string; platform: string }> }).providers
expect(providers.filter(provider => provider.platform === 'host').map(provider => provider.id))
.toEqual(['Service', 'Event', 'Builtin', 'Tool'])
// The `Tool` provider is the one built over the host context: it
// answers with the shared registry's view of the REQUESTING agent,
// so the copy sees its own preset's tools, not the host's empty set.
const queried = await copyCtx.tools.execute({
callId: ToolCallId('copied-preset-inspect-tools'),
name: 'cordis_inspect_query',
arguments: { platform: 'host', provider: 'Tool', method: 'listTools' },
signal,
agent: copied.agent,
})
expect(queried.isError).toBe(false)
const tools = (JSON.parse(resultText(queried)) as { data: { tools: Array<{ name: string }> } }).data.tools
expect(tools.map(tool => tool.name)).toEqual(expect.arrayContaining([
'bash', 'cordis_inspect_list', 'cordis_inspect_query', 'plugin_manager',
]))
} finally {
await copied.dispose()
await shipped.dispose()
}
} finally {
await copyCtx.fiber.dispose()
await rm(root, { recursive: true, force: true })
}
}, 120_000)
})
describe('a switch survives the session', () => {
it('records the choice so the log states what the agent runs', async () => {
const handle = await ctx.agents.create({
+6
View File
@@ -125,6 +125,12 @@
- id: cordis-host-runner
name: '@deepseek-ai/dsh-cordis-host-runner'
# The Host inspect providers are process-global: the registry rejects a
# duplicate id, so they register here once and every preset's `tool-cordis`
# row reads them.
- id: cordis-inspect-providers
name: '@deepseek-ai/dsh-tool-cordis/host'
# Ordinary provider for the parsed Web flags. Its plugin-level injection
# waits for cmdlineArgs; no launcher metadata or special row kind is needed.
- id: web-startup
+1
View File
@@ -116,6 +116,7 @@
"@deepseek-ai/dsh-session-stats": "workspace:^",
"@deepseek-ai/dsh-session-turn-outline": "workspace:^",
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-tool-cordis": "workspace:^",
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
"@deepseek-ai/dsh-web-frontend": "workspace:^",
"@deepseek-ai/dsh-workspace": "workspace:^",
@@ -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/extensions/tool-cordis/README.md
README.md: 3536ae2fc39f7a8db425da89df3f4d93e1ca8b4f
README.zh.md: 8127683111f1807b6920d6bb6f56479e1495f281
README.md: 4e7efefd21e3b7cf40e8ba5fe2291d220dfd796f
README.zh.md: 76331c25f0996c053b39e5b227a50afae84e6bb4
+2 -2
View File
@@ -25,7 +25,7 @@ Inspect Host and Client runtime APIs before writing plugin code. Creator mode pr
<a id="use-this-package"></a>
## Use this package
Creator mode includes this toolset. Other compositions mount `@deepseek-ai/dsh-tool-cordis` alongside the host runner that provides `cordisInspect`. Call `cordis_inspect_list` to discover providers, then `cordis_inspect_query` for a provider's exact methods and types. Use [Plugin Manager](../../boot/plugin-manager/README.md) to install bundles containing plugin code or MCP configuration.
Creator mode includes this toolset. Other compositions mount `@deepseek-ai/dsh-tool-cordis/host` once in the host composition beside the host runner that provides `cordisInspect`, and `@deepseek-ai/dsh-tool-cordis` in each agent preset that exposes the tools; a preset row alone registers no Host providers. Call `cordis_inspect_list` to discover providers, then `cordis_inspect_query` for a provider's exact methods and types. Use [Plugin Manager](../../boot/plugin-manager/README.md) to install bundles containing plugin code or MCP configuration.
-----
@@ -35,7 +35,7 @@ Creator mode includes this toolset. Other compositions mount `@deepseek-ai/dsh-t
<details>
<summary>Implementation internals — click to expand</summary>
Host providers combine generated Service/Event catalogs and the requesting agent's tool registry. Client providers synchronize their manifests through the existing inspection registry and answer queries from a connected page. The tool plugin owns its registrations through Cordis effects; disposal removes the two tools and the Host inspect providers. No invariant companion is published because inspection reads its providers directly and maintains no independent runtime projection.
Host providers combine generated Service/Event catalogs and the requesting agent's tool registry. Client providers synchronize their manifests through the existing inspection registry and answer queries from a connected page. The host entry owns the Host provider registrations and the preset row owns the two tools, each through Cordis effects; the registry rejects a duplicate provider id, which is why the providers register once per process rather than per preset. No invariant companion is published because inspection reads its providers directly and maintains no independent runtime projection.
</details>
+2 -2
View File
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
创造模式包含这组工具。其他组合需要同时挂载 `@deepseek-ai/dsh-tool-cordis` 和提供 `cordisInspect` 的 host runner。调用 `cordis_inspect_list` 发现 provider,再用 `cordis_inspect_query` 查询其具体方法和类型。通过 [Plugin Manager](../../boot/plugin-manager/README.zh.md) 安装包含插件代码或 MCP 配置的组合包。
创造模式包含这组工具。其他组合需要在宿主组合里、提供 `cordisInspect` 的 host runner 旁挂载一次 `@deepseek-ai/dsh-tool-cordis/host`,并在每个要暴露这些工具的 agent preset 里挂载 `@deepseek-ai/dsh-tool-cordis`;仅有 preset 行不会注册任何 Host provider。调用 `cordis_inspect_list` 发现 provider,再用 `cordis_inspect_query` 查询其具体方法和类型。通过 [Plugin Manager](../../boot/plugin-manager/README.zh.md) 安装包含插件代码或 MCP 配置的组合包。
-----
@@ -35,7 +35,7 @@ kind: "package-reference"
<details>
<summary>实现细节 — 点击展开</summary>
Host provider 结合生成的 Service/Event 目录与请求 agent 的工具注册表。Client provider 通过现有检查注册表同步清单,并从已连接页面回答查询。工具插件通过 Cordis effect 持有注册;释放时移除两个工具和 Host inspect provider。检查直接读取 provider,不维护独立运行时投影,因此不发布不变式配套插件。
Host provider 结合生成的 Service/Event 目录与请求 agent 的工具注册表。Client provider 通过现有检查注册表同步清单,并从已连接页面回答查询。宿主入口持有 Host provider 的注册,preset 行持有两个工具,都通过 Cordis effect;注册表拒绝重复的 provider id,所以 provider 按进程注册一次而不是按 preset 注册。检查直接读取 provider,不维护独立运行时投影,因此不发布不变式配套插件。
</details>
@@ -18,11 +18,16 @@
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./host": {
"types": "./lib/types/host.d.ts",
"default": "./lib/types/host.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.js",
"lib/types/**/*.d.ts"
],
"license": "MIT",
@@ -36,6 +41,7 @@
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
@@ -0,0 +1,20 @@
/** Host-plane registration of the first-party Cordis inspect providers. */
import type { Context } from '@deepseek-ai/cordis'
import { hostInspectProviders } from './providers.ts'
export const name = 'cordis-inspect-providers'
/** Required services: the Host inspect registry and the Tool registry the `Tool` provider reads. */
export const inject = ['cordisInspect', 'tools']
/**
* Register the Host inspect providers once per process. The registry keys
* providers by id and rejects a duplicate, so this row belongs to the host
* composition beside `cordis-host-runner`; every per-session `tool-cordis`
* row reads the shared set through its tools.
* @param ctx Host composition context.
*/
export function apply(ctx: Context): void {
for (const provider of hostInspectProviders(ctx)) {
ctx.effect(() => ctx.cordisInspect.register(provider), `cordis-inspect-providers: ${provider.manifest.id}`)
}
}
+2 -5
View File
@@ -5,7 +5,6 @@ import type { JsonValue } from '@deepseek-ai/dsh-util-values'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
import { presentInspectListCall, presentInspectQueryCall } from './present.ts'
import { hostInspectProviders } from './providers.ts'
export const name = 'tool-cordis'
export const inject = ['tools', 'cordisInspect']
@@ -15,13 +14,11 @@ function requireAgent(exec: ToolExecution): Agent {
return exec.agent
}
/** Register read-only runtime inspection tools.
/** Register read-only runtime inspection tools over the Host providers that
* `@deepseek-ai/dsh-tool-cordis/host` registers once per process.
* @param ctx Agent-scoped registration context.
*/
export function apply(ctx: Context): void {
for (const provider of hostInspectProviders(ctx)) {
ctx.effect(() => ctx.cordisInspect.register(provider), `tool-cordis: inspect ${provider.manifest.id}`)
}
ctx.tools.register(defineTool({
name: 'cordis_inspect_list',
description:
@@ -0,0 +1,35 @@
import { Context } from '@deepseek-ai/cordis'
import { CordisInspectRegistryService } from '@deepseek-ai/dsh-cordis-host-runner'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import { describe, expect, it } from 'vitest'
import * as CordisInspectProviders from '../src/host.ts'
/**
* The Host inspect providers are one process-global set: the host entry
* registers them beside the registry, and the per-session tool rows only read
* them. Mounting several presets that carry `tool-cordis` over the shared
* registry is covered by `apps/cli/tests/web-agent-presets.e2e.ts`.
*/
async function host(): Promise<Context> {
const ctx = new Context()
// The tool registry injects `systemPrompt`; nothing under test registers a prompt section.
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(CordisInspectRegistryService)
return ctx
}
describe('the cordis-inspect-providers host entry', () => {
it('registers the first-party Host providers and withdraws them on disposal', async () => {
const ctx = await host()
const fiber = ctx.plugin(CordisInspectProviders)
await fiber
expect(ctx.cordisInspect.list().map(provider => provider.id)).toEqual(['Service', 'Event', 'Builtin', 'Tool'])
await fiber.dispose()
expect(ctx.cordisInspect.list()).toEqual([])
})
})
@@ -263,5 +263,8 @@
- id: present
name: '@deepseek-ai/dsh-tool-present'
# Enabled exactly when the host `plugin-manager` service is (`dsh-base` gates
# it on a launched profile), so a profile-less embedding still mounts the preset.
- id: tool-plugin-manager
name: '@deepseek-ai/dsh-plugin-manager/tools'
disabled: !!js "!ctx.get('profileContext')"
@@ -16,6 +16,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include, { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
import { evaluate, isJsExpr } from '@deepseek-ai/cordis-plugin-loader'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import * as yaml from 'js-yaml'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
@@ -153,6 +154,19 @@ describe('the shipped preset root', () => {
}
})
it('enables the cordis plugin-manager tool exactly when a launched profile provides profileContext', async () => {
// Same condition as the host `plugin-manager` row in `dsh-base`: the tool
// injects `pluginManager`, so without that service the row must be
// disabled rather than left waiting and blocking the whole mount.
const disabled = findEntry(await shippedEntries('cordis'), 'tool-plugin-manager')?.disabled
if (!isJsExpr(disabled)) throw new TypeError('cordis tool-plugin-manager must gate on a !!js expression')
const scope = (profileContext: object | undefined) => ({
ctx: { get: (name: string) => (name === 'profileContext' ? profileContext : undefined) },
})
expect(evaluate(scope(undefined), disabled.__jsExpr)).toBe(true)
expect(evaluate(scope({ name: 'web' }), disabled.__jsExpr)).toBe(false)
})
it('disables the ralph tool in every shipped preset that carries it', async () => {
for (const id of ['cordis', 'ptc', 'standard']) {
expect(findEntry(await shippedEntries(id), 'tool-ralph')?.disabled, id).toBe(true)
+6
View File
@@ -2372,6 +2372,9 @@ importers:
'@deepseek-ai/dsh-subprocess':
specifier: workspace:^
version: link:../../subprocess/subprocess
'@deepseek-ai/dsh-tool-cordis':
specifier: workspace:^
version: link:../../extensions/tool-cordis
'@deepseek-ai/dsh-tool-subagent':
specifier: workspace:^
version: link:../../subagent/tool-subagent
@@ -7131,6 +7134,9 @@ importers:
'@deepseek-ai/dsh-cordis-host-runner':
specifier: workspace:^
version: link:../cordis-host-runner
'@deepseek-ai/dsh-system-prompt':
specifier: workspace:^
version: link:../../core/system-prompt
'@deepseek-ai/dsh-tools':
specifier: workspace:^
version: link:../../core/tools
@@ -36,6 +36,8 @@
- insert:
- id: cordis-host-runner
name: '@deepseek-ai/dsh-cordis-host-runner'
- id: cordis-inspect-providers
name: '@deepseek-ai/dsh-tool-cordis/host'
- id: tool-cordis
name: '@deepseek-ai/dsh-tool-cordis'
- id: llm-replay
@@ -33,5 +33,7 @@
- insert:
- id: cordis-host-runner
name: '@deepseek-ai/dsh-cordis-host-runner'
- id: cordis-inspect-providers
name: '@deepseek-ai/dsh-tool-cordis/host'
- id: tool-cordis
name: '@deepseek-ai/dsh-tool-cordis'
+1
View File
@@ -185,6 +185,7 @@
"@deepseek-ai/dsh-plugin-manager/registry": ["./packages/boot/plugin-manager/src/registry.ts"],
"@deepseek-ai/dsh-plugin-manager/operations": ["./packages/boot/plugin-manager/src/operations.ts"],
"@deepseek-ai/dsh-plugin-manager/tools": ["./packages/boot/plugin-manager/src/tools.ts"],
"@deepseek-ai/dsh-tool-cordis/host": ["./packages/extensions/tool-cordis/src/host.ts"],
"@deepseek-ai/dsh-host-plugin-inventory": ["./packages/host/plugin-inventory/src"],
"@deepseek-ai/dsh-host-plugin-inventory/types": ["./packages/host/plugin-inventory/src/types.ts"],
"@deepseek-ai/dsh-client-ui-slots": ["./packages/client/ui-slots/src"],