Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-5-web-ui

# Conflicts:
#	apps/cli/tests/web-agent-presets.e2e.ts
#	packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts
This commit is contained in:
Yichen Jiang
2026-08-09 22:48:47 +08:00
44 changed files with 809 additions and 236 deletions

View File

@@ -234,20 +234,14 @@
# in the user's skill root: it documents THIS deployment's two planes, and a
# preset is the unit that gets copied and edited. `baseUrl` is the preset's
# own directory, so the root resolves wherever the preset is installed.
- id: skills
name: cordis:group
group: true
isolate:
skills: true
# Both rows register into THIS preset's layer of the host skill registry, so
# they need no realm; the agent's merged catalog also carries whatever the
# deployment registered globally (repository plugins).
- id: skill-local
name: '@deepseek-ai/dsh-skill-local'
config:
- id: skill
name: '@deepseek-ai/dsh-skill'
customSkillDirs:
- !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('skills/', baseUrl))"
- id: skill-local
name: '@deepseek-ai/dsh-skill-local'
config:
customSkillDirs:
- !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('skills/', baseUrl))"
- id: tool-skill
name: '@deepseek-ai/dsh-tool-skill'
- id: tool-skill
name: '@deepseek-ai/dsh-tool-skill'

View File

@@ -77,20 +77,16 @@
# ── skills ──────────────────────────────────────────────────────────────────
- id: skills
name: cordis:group
group: true
isolate:
skills: true
config:
- id: skill
name: '@deepseek-ai/dsh-skill'
# The skill REGISTRY lives in the host composition and is layered per scope:
# these rows register into THIS preset's layer of it, so they need no realm.
# `skill-local` contributes local-root discovery for agents on this preset, and
# `tool-skill` gives them the catalog and loader; the merged catalog also
# carries whatever the deployment registered globally (repository plugins).
- id: skill-local
name: '@deepseek-ai/dsh-skill-local'
- id: skill-local
name: '@deepseek-ai/dsh-skill-local'
- id: tool-skill
name: '@deepseek-ai/dsh-tool-skill'
- id: tool-skill
name: '@deepseek-ai/dsh-tool-skill'
# ── goals ───────────────────────────────────────────────────────────────────

View File

@@ -12,6 +12,9 @@
"prepack": "tsc --noEmit && tsdown src/plugin.ts src/mcp-server.ts --no-config --tsconfig tsconfig.json --out-dir lib --platform node --target es2024 --clean && dsh-plugin-prepare"
},
"dsh": {
"skills": [
"../skills"
],
"mcpServers": "./.mcp.json",
"entry": "./lib/plugin.mjs"
},

View File

@@ -0,0 +1,6 @@
---
name: github-source-proof
description: Proves that dsh installed a private repository Plugin from an exact GitHub source.
---
This skill exists only in the GitHub repository source fixture.

View File

@@ -125,11 +125,7 @@ async function startPublishedPackageRegistry(root: string): Promise<PublishedPac
}
describe.skipIf(!enabled)('dsh run GitHub repository Plugin installation', () => {
// The fixture deliberately carries no skill root: this composition's agent
// plane lives behind agent presets, whose per-preset `skills` realm has no
// seam for a deployment-level provider yet — see the repository-plugin
// README's Known Limitations.
it('installs the published prepare dependency, then builds and runs MCP and TypeScript Plugin contributions from a private exact GitHub source', async () => {
it('installs the published prepare dependency, then builds and runs skill, MCP, and TypeScript Plugin contributions from a private exact GitHub source', async () => {
expect(existsSync(dshBin), 'the repository Plugin acceptance must run the built dsh entry').toBe(true)
expect(source, 'DSH_GITHUB_REPOSITORY_PLUGIN_SOURCE is required by this CI lane').toMatch(
/^github:[^/\s#&]+\/[^/\s#&]+#[0-9a-f]{40}&path:\/.*\/\.dsh-plugin$/u,
@@ -205,6 +201,9 @@ describe.skipIf(!enabled)('dsh run GitHub repository Plugin installation', () =>
expect(registry.requests, runtimeDiagnostic).toContain('GET /@deepseek-ai/dsh-repository-plugin/-/dsh-repository-plugin-0.0.1.tgz')
const firstRequest = JSON.stringify(server.requests[0]!.body)
const secondRequest = JSON.stringify(server.requests[1]!.body)
expect(firstRequest, runtimeDiagnostic).toContain(
'Proves that dsh installed a private repository Plugin from an exact GitHub source.',
)
expect(firstRequest, runtimeDiagnostic).toContain('mcp__github_repository__proof')
expect(firstRequest, runtimeDiagnostic).toContain('Proves that an MCP server compiled from the exact GitHub repository package is active.')
expect(secondRequest, runtimeDiagnostic).toContain('MCP_FROM_GITHUB_REPOSITORY')
@@ -222,6 +221,7 @@ describe.skipIf(!enabled)('dsh run GitHub repository Plugin installation', () =>
prepack: 'tsc --noEmit && tsdown src/plugin.ts src/mcp-server.ts --no-config --tsconfig tsconfig.json --out-dir lib --platform node --target es2024 --clean && dsh-plugin-prepare',
},
dsh: {
skills: ['../skills'],
mcpServers: './.mcp.json',
entry: './lib/plugin.mjs',
},
@@ -235,6 +235,8 @@ describe.skipIf(!enabled)('dsh run GitHub repository Plugin installation', () =>
typescript: '6.0.3',
},
})
expect(readFileSync(join(installed, 'dsh-plugin-assets/skills/0/github-source-proof/SKILL.md'), 'utf8'))
.toContain('This skill exists only in the GitHub repository source fixture.')
expect(readFileSync(join(installed, 'dsh-plugin-assets/.mcp.json'), 'utf8')).toContain('lib/mcp-server.mjs')
expect(readFileSync(join(installed, 'lib/plugin.mjs'), 'utf8')).toContain('TS_PLUGIN_FROM_GITHUB_REPOSITORY')
expect(readFileSync(join(installed, 'lib/mcp-server.mjs'), 'utf8')).toContain('MCP_FROM_GITHUB_REPOSITORY')

View File

@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
@@ -10,6 +11,8 @@ import type { PatchOptions } from '@cordisjs/plugin-include'
import { beforeAll, describe, expect, it } from 'vitest'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { resolveSessionPreset, SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-presets'
import { CallId } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-skill'
import type {} from '@deepseek-ai/dsh-tools'
const CONFIG_DIR = fileURLToPath(new URL('../config/', import.meta.url))
@@ -41,6 +44,10 @@ async function bootWeb(settingsFile: string): Promise<Context> {
// Waits for `httpServer`, which the disabled webserver above provides.
{ id: 'web-runtime', disabled: true },
{ id: 'telemetry-otel', disabled: true },
// A deployment-level skill on the host registry's GLOBAL layer — the same
// registration shape a repository plugin's skill root uses. The layered
// skills test below proves it reaches preset-composed agents.
{ id: 'skill-badge', disabled: false },
{ id: 'modules', disabled: true },
{ id: 'connection', disabled: true },
// `api-gateway` stays ENABLED on purpose — the api-proxy is the host row
@@ -176,11 +183,11 @@ describe('the shipped Web composition', () => {
// And it keeps the standard agent's own tools rather than replacing them.
expect(tools).toEqual(expect.arrayContaining(['bash', 'read', 'edit', 'skill']))
// The skill registry sits in this preset's entry-local realm, so it is
// invisible to the host AND to the agent's own scope — only the rows
// inside that group resolve it, which is what makes `tool-skill` the
// agent's own rather than a shared one.
expect(ctx.get('skills')).toBeUndefined()
// The preset's own authoring skill registers into ITS layer of the host
// registry: the cordis agent's view carries it, the global view does not.
const scoped = (await ctx.skills.list({ scope: handle.agent })).map(skill => skill.name)
expect(scoped).toContain('editing-cordis-compositions')
expect((await ctx.skills.list()).map(skill => skill.name)).not.toContain('editing-cordis-compositions')
} finally {
await handle.dispose()
}
@@ -209,6 +216,67 @@ describe('the shipped Web composition', () => {
expect((await readFile(skill, 'utf8')).startsWith('---\nname: editing-cordis-compositions')).toBe(true)
})
it('merges the global skill layer into a preset agent\'s catalog, keeping local discovery preset-side', async () => {
const proj = await mkdtemp(join(tmpdir(), 'dsh-preset-skill-proj-'))
await mkdir(join(proj, '.dsh', 'skills', 'project-proof'), { recursive: true })
await writeFile(join(proj, '.dsh', 'skills', 'project-proof', 'SKILL.md'), [
'---',
'name: project-proof',
'description: Proves the preset layer discovers project skills beside global ones.',
'---',
'',
'Project proof body.',
'',
].join('\n'))
const handle = await ctx.agents.create({
// Unique per run: the composition persists into the ambient DSH home,
// and a fixed id would collide with a log an earlier run left there.
sessionId: SessionId(`preset-skills-standard-${randomUUID()}`),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
try {
// The host (global) view carries the deployment-level provider alone:
// local discovery moved behind the presets with `skill-local`.
expect((await ctx.skills.list({ cwd: proj })).map(skill => skill.name)).toEqual(['dsh-badge'])
// The standard agent's view merges the global layer with its preset's
// own local discovery over the session cwd.
const scoped = (await ctx.skills.list({ cwd: proj, scope: handle.agent })).map(skill => skill.name)
expect(scoped).toContain('dsh-badge')
expect(scoped).toContain('project-proof')
// The preset's own loader tool resolves the global-layer skill.
const loaded = await ctx.tools.execute({
callId: CallId('preset-skills-load'),
name: 'skill',
arguments: { name: 'dsh-badge' },
signal: new AbortController().signal,
agent: handle.agent,
})
expect(loaded.isError).toBe(false)
expect(JSON.stringify(loaded.content)).toContain('powered by dsh')
} finally {
await handle.dispose()
}
})
it('shows a core-web agent the global layer but no loader tool', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId(`preset-skills-core-web-${randomUUID()}`),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'core-web').then(() => undefined),
})
try {
// Layer visibility is the registry's; whether an agent can USE skills
// stays the preset's choice — core-web mounts no `tool-skill`, so its
// tool table has no loader even though the global layer is readable.
expect((await ctx.skills.list({ scope: handle.agent })).map(skill => skill.name)).toContain('dsh-badge')
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'str_replace_editor'])
} finally {
await handle.dispose()
}
})
it('never rewrites the preset file it composed from', async () => {
// The Loader persists a tree whose plugin self-disposed, and tearing an
// agent down disposes its whole subtree. Inherited, that rewrote the