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:
@@ -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'
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
6
apps/cli/tests/fixtures/github-repository-plugin/skills/github-source-proof/SKILL.md
vendored
Normal file
6
apps/cli/tests/fixtures/github-repository-plugin/skills/github-source-proof/SKILL.md
vendored
Normal 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.
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user