Merge remote-tracking branch 'origin/master' into feat/web-message-feedback-ui

Keep both Remote contributions master and this branch add: the mount loop
now carries commandsRemote, goalsRemote, pluginInventoryRemote, and
messageFeedbackRemote, with both new tsconfig references retained.
This commit is contained in:
Chinesezjc
2026-08-12 16:36:10 +08:00
339 changed files with 7766 additions and 2302 deletions

View File

@@ -131,9 +131,8 @@ function workspaceManifests(): WorkspaceManifest[] {
}
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
// Profile bundles publish their dsh.bundle.patch layer beside the lib;
// dsh-base also ships the win32 shell platform layer the launcher reads.
'@deepseek-ai/dsh-base': ['cordis.patch.yml', 'windows.cordis.patch.yml'],
// Profile bundles publish their dsh.bundle.patch layer beside the lib.
'@deepseek-ai/dsh-base': ['cordis.patch.yml'],
'@deepseek-ai/dsh-web-app': ['cordis.patch.yml'],
'@deepseek-ai/dsh-headless': ['cordis.patch.yml'],
'@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],

View File

@@ -5,7 +5,7 @@ import { existsSync, globSync, mkdirSync, mkdtempSync, readFileSync, realpathSyn
import { tmpdir } from 'node:os'
import { basename, join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { docsPages, type DocsPage } from '../website/docs.ts'
import { docsPages, landingLink, routeLink, sectionSpec, type DocsPage } from '../website/docs.ts'
import {
addProjectionFrontmatter, projectedPageContent, publishableImage, rewriteMarkdown,
} from './project-doc-site.ts'
@@ -364,6 +364,63 @@ describe('docsPages locale routes', () => {
})
})
describe('sidebar ordering', () => {
it('places every section a sidebar collection owns', () => {
for (const page of docsPages) {
if (page.sidebar === null) continue
expect(() => sectionSpec(page.locale, page.section), page.route).not.toThrow()
}
})
it('refuses a section with no declared placement', () => {
expect(() => sectionSpec('root', '数据结构'))
.toThrow('Sidebar section "数据结构" has no placement in the root locale.')
})
it('declares placements per locale rather than in one shared list', () => {
// `SDK` labels a group in both locales, so one shared list would have to
// rank it against `入门` and against `Guide` at the same position.
expect(sectionSpec('root', 'SDK').index).toBeGreaterThan(sectionSpec('root', '入门').index)
expect(sectionSpec('en', 'SDK').index).toBeGreaterThan(sectionSpec('en', 'Guide').index)
expect(() => sectionSpec('en', '入门')).toThrow()
expect(() => sectionSpec('root', 'Guide')).toThrow()
})
it('lands every navigation item on a page the manifest publishes', () => {
// The navigation bar named `/guide/` while the manifest published the guide's
// first page at `guide/quickstart.md`, so the item served a 404.
const collections = [
['root', 'zh-guide'], ['root', 'zh-develop'], ['root', 'zh-reference'],
['en', 'en-guide'], ['en', 'en-develop'], ['en', 'en-reference'],
] as const
const published = new Set(docsPages.map(page => routeLink(page.route)))
for (const [locale, collection] of collections) {
expect(published, `${locale}/${collection}`).toContain(landingLink(locale, collection))
}
})
it('collapses the subsystem groups and leaves the smaller ones open', () => {
expect(sectionSpec('root', '执行与工具').collapsed).toBe(true)
expect(sectionSpec('en', 'Execution and tools').collapsed).toBe(true)
expect(sectionSpec('root', '概念').collapsed).toBeUndefined()
})
it('gives each page its own position within a section', () => {
// Sidebar entries sort by order alone, so a shared value leaves the two
// pages ranked by whichever manifest block happens to be concatenated
// first rather than by an intent the manifest states.
const taken = new Map<string, string>()
const collisions: string[] = []
for (const page of docsPages) {
const slot = `${page.locale}/${String(page.sidebar)}/${page.section}#${page.order}`
const holder = taken.get(slot)
if (holder === undefined) taken.set(slot, page.label)
else collisions.push(`${slot}: ${holder} / ${page.label}`)
}
expect(collisions).toEqual([])
})
})
describe('addProjectionFrontmatter', () => {
it('adds frontmatter to an ordinary Markdown page', () => {
expect(addProjectionFrontmatter('# Guide\n', { source: 'docs/guide.md' })).toBe(
@@ -411,6 +468,25 @@ describe('projectedPageContent', () => {
expect(projectedPageContent(markdown, page('zh-guide'))).toBe(markdown)
})
it('drops the language switcher the navigation bar already offers', () => {
expect(projectedPageContent('# Guide\n\nEnglish | [中文](./en/guide)\n\nBody.\n', page('zh-guide')))
.toBe('# Guide\n\nBody.\n')
expect(projectedPageContent('# 指南\n\n[English](./en/guide) | 中文\n\n正文。\n', page('zh-guide')))
.toBe('# 指南\n\n正文。\n')
})
it('drops the repository badge every page links from its footer', () => {
const badge = '[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness)'
expect(projectedPageContent(`# Guide\n\nBody.\n\n${badge}\n`, page('zh-guide')))
.toBe('# Guide\n\nBody.\n')
})
it('keeps a switcher-shaped line that is not the page header', () => {
// A tutorial showing the convention must still render the example.
const sample = '# Guide\n\nA\n\nB\n\nC\n\nD\n\nE\n\nEnglish | [中文](./x)\n'
expect(projectedPageContent(sample, page('zh-guide'))).toBe(sample)
})
it('rejects a locale home source without frontmatter', () => {
expect(() => projectedPageContent('# Harness\n', page(null)))
.toThrow('locale home source "docs/index.zh.md" must start with YAML frontmatter')

View File

@@ -292,6 +292,37 @@ export function addProjectionFrontmatter(markdown: string, page: Pick<DocsPage,
return `---\n${fields}\n---\n\n${markdown}`
}
/** The switcher line a canonical page carries so its GitHub reader can reach the other language. */
const LANGUAGE_SWITCHER = /^(?:English \| \[中文\]\([^)]*\)|\[English\]\([^)]*\) \| 中文)$/
/** The repository badge a canonical page carries for its GitHub reader. */
const REPOSITORY_BADGE = /^\[!\[[^\]]*\]\(https:\/\/img\.shields\.io\/[^)]*\)\]\([^)]*\)$/
/**
* Drop the lines that address a canonical page's GitHub reader.
*
* The site carries a locale switcher in its navigation bar and links the
* repository from every page, so projecting these lines would repeat both — the
* switcher as the first element under each heading.
*
* @param markdown Rewritten canonical Markdown content.
* @returns The content without the switcher line or the repository badge.
*/
function withoutRepositoryChrome(markdown: string): string {
const lines = markdown.split('\n')
const switcher = lines.findIndex(line => LANGUAGE_SWITCHER.test(line))
// Only the switcher introducing the page qualifies; further down the same
// text is prose or a sample rather than the page's own header.
if (switcher !== -1 && switcher < 8) {
lines.splice(switcher, lines[switcher + 1] === '' ? 2 : 1)
}
const badge = lines.findLastIndex(line => REPOSITORY_BADGE.test(line))
if (badge !== -1) {
lines.splice(lines[badge - 1] === '' ? badge - 1 : badge, lines[badge - 1] === '' ? 2 : 1)
}
return lines.join('\n')
}
/**
* Select the Markdown rendered for one published page.
*
@@ -300,7 +331,7 @@ export function addProjectionFrontmatter(markdown: string, page: Pick<DocsPage,
* @returns Full Markdown for ordinary pages or frontmatter-only Markdown for a locale home page.
*/
export function projectedPageContent(markdown: string, page: DocsPage): string {
if (page.sidebar !== null) return markdown
if (page.sidebar !== null) return withoutRepositoryChrome(markdown)
if (!markdown.startsWith('---\n')) {
throw new Error(`project-doc-site: locale home source ${JSON.stringify(page.source)} must start with YAML frontmatter.`)
}

View File

@@ -99,6 +99,8 @@ const GENERIC_SKIPS: readonly GenericSkip[] = [
// the preset a model mounts, so the scoped name would send the model after an
// id no roster reports.
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', upstream: ['cordis'] },
// The preset-roster loop names the `cordis` preset id, not a package.
{ file: 'apps/cli/tests/windows-shell.spec.ts', upstream: ['cordis'] },
// GROUP_ORDER holds `packages/<group>/` directory names, not package names.
{ file: 'scripts/gen-module-graph.ts', upstream: ['cordis'] },
{ file: 'scripts/gen-doc-graphs.ts', upstream: ['cordis'] },

View File

@@ -8,11 +8,11 @@
},
{
"role": "user",
"content": "# DeepSeek Harness\n\nEnglish | [中文](README.zh.md)\n\nDeepSeek Harness (`dsh`) is an open-source coding agent built on the DeepSeek Harness SDK.\n\nIt uses an architecture where **everything is a plugin**.\n\n## Internal testing notice\n\nDeepSeek Harness is under internal testing. Features and interfaces may change.\n\nThe internal build uploads all Session Logs by default to help diagnose reported problems. Set `DSH_TELEMETRY_DISABLED=1` to disable telemetry. Send feedback through the internal WeChat group.\n\n## Run from source\n\nClone this repo, complete the [dependency and API-key setup](docs/user/guide/quickstart.md#step-1-install-and-configure-the-api-key), then run:\n\n```sh\npnpm dsh web\n```\n\n## Use DeepSeek Harness\n\n### Web UI\n\nStart the recommended local interface from the repository root:\n\n```sh\npnpm dsh web\n```\n\nThe command builds the repository before starting the Web UI, which is served at `http://127.0.0.1:3080` by default.\n\n### Profiles\n\nThe source CLI boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/<name>`:\n\n```sh\npnpm dsh --profile web # the browser UI\npnpm dsh plugin --profile tui add <package> # install a plugin into a custom profile\npnpm dsh --profile tui # boot it\n```\n\nThe [CLI reference](apps/cli/README.md#profiles) describes profile layout, layer semantics, and config dump commands.\n\n### Headless\n\nRun one task, print the final answer, and exit:\n\n```sh\npnpm dsh --profile headless \"summarize this workspace\"\n```\n\n### Automation and SDKs\n\nFrom a source checkout with `DEEPSEEK_API_KEY` in the environment or its root `.env`, start the ACP automation server:\n\n```sh\npnpm run demo:acp\n```\n\nThe [Python SDK](python/README.md) drives a bundled JSON-RPC runtime. The [examples](examples/README.md) cover the runnable headless, ACP, JSON-RPC, Code Mode, and self-referential compositions.\n\n## Why DeepSeek Harness\n\nBuilt-in capabilities cover file reading, editing, and search; shell and persistent PTY execution; reusable skills; task tracking, goals, plans, todos, and background tasks; subagents and workflows; sandboxing and approvals; settings and credentials; persistent, resumable, forkable, and queryable sessions; LSP and web access; context compaction; and telemetry. Each composition selects the subset appropriate to its surface. The Web UI includes Plan Mode.\n\n- **Everything is a plugin.** Models, tools, policies, storage, context management, and interfaces are composable [Cordis plugins](docs/user/develop/basic/index.md), so deployments can extend or replace behavior without forking the agent loop. See the [architecture](docs/architecture.md) for the underlying design.\n- **Runs are reconstructable.** Anything visible to the model is logged in the authoritative session stream; persistence, resume/fork/query, replay, telemetry, and UIs derive from the same events. See the [session-log architecture](docs/architecture.md#session-log).\n- **Code Mode (opt-in).** It exposes a `run_code` tool and a generated TypeScript SDK; only program output re-enters model context. See [Code Mode](packages/core/tools/README.md#code-mode).\n- **Self-referential Cordis tools are opt-in.** They let the agent inspect its live runtime and mount or unmount plugins while it runs. See the [Cordis tools](packages/self-modification/tool-cordis/README.md).\n\n## Community\n\nFollow <a href=\"https://x.com/Deepseekharness\">DeepSeek Harness on Twitter</a> for project updates.\n\n## Development\n\nStart with the [development guide](docs/development.md) and read the [architecture](docs/architecture.md) before changing packages.\n\nFor agents, follow [AGENTS.md](AGENTS.md).\n\nDeepSeek Harness is currently in internal testing.\n\n## License\n\n[BSD 3-Clause](LICENSE)\n\nThird-party dependencies and their licenses are disclosed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).\n\n## Contributing\n\nRead [CONTRIBUTING.md](CONTRIBUTING.md) before contributing to this repository.\n"
"content": "# DeepSeek Harness\n\nEnglish | [中文](README.zh.md)\n\nDeepSeek Harness (`dsh`) is an open-source coding agent built on the DeepSeek Harness SDK.\n\nIt uses an architecture where **everything is a plugin**.\n\n## Internal testing notice\n\nDeepSeek Harness is under internal testing. Features and interfaces may change.\n\nThe internal build uploads all Session Logs by default to help diagnose reported problems. Set `DSH_TELEMETRY_DISABLED=1` to disable telemetry. Send feedback through the internal WeChat group.\n\n## Run\n\nInstall Node.js ^22.19 or >= 24 and pnpm 11, then run the published package:\n\n```sh\nnpx @deepseek-ai/dsh web\n```\n\nThe command initializes the Web profile and prints the Web UI URL, which is `http://127.0.0.1:3080` by default. Open it, add a DeepSeek API key under **Settings → Models**, then start a session. The invoking directory is the default workspace; try `Summarize this repository and identify its main packages.`\n\nContinue with the [Web UI guide](docs/user/guide/index.md).\n\n### Run from source\n\nTo run a repository checkout instead:\n\n```sh\ngit clone https://github.com/deepseek-harness/deepseek-harness.git\ncd deepseek-harness\npnpm install\npnpm dsh web\n```\n\nThe last command builds the repository and opens the same Web UI path.\n\n## Profiles and plugins\n\nA profile is an ordered list of plugin bundles. The shipped `web` profile powers `dsh web`. Manage a profile with `dsh plugin --profile <name> <pnpm args>`, which forwards the remaining arguments to pnpm in that profile's directory:\n\n```sh\nnpx -p @deepseek-ai/dsh dsh plugin --profile web add <package>\nnpx -p @deepseek-ai/dsh dsh plugin --profile web remove <package>\n```\n\n`add`, `remove`, `update`, `why`, and other pnpm commands work unchanged. The command initializes a missing profile before changing its packages and updates its bundle list from installed packages that declare `dsh.bundle`. See the [CLI reference](apps/cli/reference/README.md#plugin-management) for the exact behavior.\n\nThe [CLI reference](apps/cli/README.md) covers headless execution and custom profiles. The [Python SDK](python/README.md) and [examples](examples/README.md) cover programmatic and custom compositions.\n\n## Community\n\nFollow <a href=\"https://x.com/Deepseekharness\">DeepSeek Harness on Twitter</a> for project updates.\n\n## Development\n\nStart with the [development guide](docs/development.md) and read the [architecture](docs/architecture.md) before changing packages.\n\nFor agents, follow [AGENTS.md](AGENTS.md).\n\n## License\n\n[BSD 3-Clause](LICENSE)\n\nThird-party dependencies and their licenses are disclosed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).\n\n## Contributing\n\nRead [CONTRIBUTING.md](CONTRIBUTING.md) before contributing to this repository.\n"
},
{
"role": "assistant",
"content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent智能体。\n\n它采用了**一切皆插件**的架构。\n\n## 内测声明\n\nDeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。\n\n为帮助诊断上报的问题内测版本默认上传所有会话日志。设置 `DSH_TELEMETRY_DISABLED=1` 可关闭遥测。请通过内部企业微信群反馈问题和建议。\n\n## 从源码运行\n\n克隆本仓库,完成[依赖安装和 API 密钥配置](docs/user/guide/quickstart.md#step-1-install-and-configure-the-api-key),然后运行\n\n```sh\npnpm dsh web\n```\n\n## 使用 DeepSeek Harness\n\n### Web UI\n\n请从仓库根目录启动推荐的本地界面\n\n```sh\npnpm dsh web\n```\n\n命令会构建仓库,再启动 Web UI。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。\n\n### Profile\n\n源码 CLI命令行界面会启动 profile按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/<name>` 中的自有覆盖层:\n\n```sh\npnpm dsh --profile web # the browser UI\npnpm dsh plugin --profile tui add <package> # install a plugin into a custom profile\npnpm dsh --profile tui # boot it\n```\n\nprofile 布局、层语义与配置输出命令详见 [CLI命令行界面参考](apps/cli/README.md#profiles)。\n\n### Headless\n\n运行一项任务打印最终答案后退出\n\n```sh\npnpm dsh --profile headless \"summarize this workspace\"\n```\n\n### 自动化与 SDK\n\n在源码检出中通过环境变量或根目录 `.env` 设置 `DEEPSEEK_API_KEY`,然后启动 ACPAgent Client Protocol自动化服务器\n\n```sh\npnpm run demo:acp\n```\n\n[Python SDK](python/README.md) 驱动随附的 JSON-RPC 运行时。[示例](examples/README.md)涵盖可运行的 headless、ACP、JSON-RPC、Code Mode 和自指组合。\n\n## 为什么选择 DeepSeek Harness\n\n内置功能涵盖文件读取、编辑与搜索、shell 和持久 PTY 执行、可复用 skill技能、任务跟踪、目标、计划、待办事项与后台任务、subagent 与工作流、沙箱与审批、设置与凭据、可持久化、恢复、fork 与查询的会话、LSP 与 Web 访问、上下文压缩context compaction以及遥测。每个组合只选用适合其使用方式的能力子集。Web UI 包含 Plan Mode。\n\n- **一切皆插件。** 模型、工具、策略、存储、上下文管理和界面均为可组合的 [Cordis 插件](docs/user/develop/basic/index.md),部署方无需 fork agent loop智能体循环即可扩展或替换行为。底层设计见[架构文档](docs/architecture.md)。\n- **运行可重建。** 凡是模型可见的内容都会记录在权威会话流中持久化、恢复fork查询、回放、遥测和 UI 均从同一组事件派生。参见[会话日志架构](docs/architecture.md#session-log)。\n- **Code Mode需显式启用。** 它会提供 `run_code` 工具和生成的 TypeScript SDK只有程序输出会重新进入模型上下文。参见 [Code Mode](packages/core/tools/README.md#code-mode)。\n- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/self-modification/tool-cordis/README.md)。\n\n## 社区\n\n扫描二维码或打开 <a href=\"https://wj.qq.com/s2/27234598/03eb/\">DeepSeek Harness 微信社区申请页面</a> 申请加入。\n\n<p>\n <img src=\"assets/community-wecom-survey.png\" alt=\"DeepSeek Harness 微信社区二维码\" width=\"240\">\n</p>\n\n## 开发\n\n请先阅读[开发指南](docs/development.md);修改包之前,请阅读[架构文档](docs/architecture.md)。\n\n面向 agent遵循 [AGENTS.md](AGENTS.md)。\n\nDeepSeek Harness 目前处于内测阶段。\n\n## 许可证\n\n[BSD 3-Clause](LICENSE)\n\n第三方依赖及其许可证在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 中披露。\n\n## 参与贡献\n\n向本仓库贡献前请阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。\n"
"content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent智能体。\n\n它采用了**一切皆插件**的架构。\n\n## 内测声明\n\nDeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。\n\n为帮助诊断上报的问题内测版本默认上传所有会话日志。设置 `DSH_TELEMETRY_DISABLED=1` 可关闭遥测。请通过内部企业微信群反馈问题和建议。\n\n## 运行\n\n安装 Node.js ^22.19 或 >= 24 和 pnpm 11然后运行已发布的包\n\n```sh\nnpx @deepseek-ai/dsh web\n```\n\n该命令会初始化 Web profile 并打印 Web UI 地址,默认地址为 `http://127.0.0.1:3080`。打开该地址,在**设置 → 模型**中添加 DeepSeek API 密钥,然后启动一个会话。调用目录是默认工作区;你可以尝试输入 `Summarize this repository and identify its main packages.`。\n\n下一步请阅读 [Web UI 指南](docs/user/guide/index.md)。\n\n### 从源码运行\n\n如需改为运行仓库 checkout\n\n```sh\ngit clone https://github.com/deepseek-harness/deepseek-harness.git\ncd deepseek-harness\npnpm install\npnpm dsh web\n```\n\n最后一条命令会构建仓库,并进入相同的 Web UI 路径。\n\n## Profile 与插件\n\nprofile 是按顺序排列的插件 bundle 列表。随附的 `web` profile 为 `dsh web` 提供功能。使用 `dsh plugin --profile <name> <pnpm args>` 管理 profile该命令会在对应 profile 目录中将剩余参数转发给 pnpm\n\n```sh\nnpx -p @deepseek-ai/dsh dsh plugin --profile web add <package>\nnpx -p @deepseek-ai/dsh dsh plugin --profile web remove <package>\n```\n\n`add`、`remove`、`update`、`why` 等 pnpm 命令均可直接使用。该命令会先初始化不存在的 profile再修改其中的包并根据声明了 `dsh.bundle` 的已安装包更新 bundle 列表。准确行为见 [CLI 参考](apps/cli/reference/README.md#plugin-management)。\n\n[CLI命令行界面参考](apps/cli/README.md)介绍 headless 执行与自定义 profile。[Python SDK](python/README.md) 和[示例](examples/README.md)介绍程序化组合与自定义组合。\n\n## 社区\n\n扫描二维码或打开 <a href=\"https://wj.qq.com/s2/27234598/03eb/\">DeepSeek Harness 微信社区申请页面</a> 申请加入。\n\n<p>\n <img src=\"assets/community-wecom-survey.png\" alt=\"DeepSeek Harness 微信社区二维码\" width=\"240\">\n</p>\n\n## 开发\n\n请先阅读[开发指南](docs/development.md);修改包之前,请阅读[架构文档](docs/architecture.md)。\n\n面向 agent遵循 [AGENTS.md](AGENTS.md)。\n\n## 许可证\n\n[BSD 3-Clause](LICENSE)\n\n第三方依赖及其许可证在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 中披露。\n\n## 参与贡献\n\n向本仓库贡献前请阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。\n"
},
{
"role": "user",

View File

@@ -0,0 +1,39 @@
/**
* The verify-cordis-config metadata contract: `disabled` is the one entry
* metadata field whose `!!js` expression the Loader interpolates; every other
* metadata field must stay static, and a disabled expression must parse.
*/
import { describe, expect, it } from 'vitest'
import { metadataExpressionErrors } from './verify-cordis-config.ts'
describe('verify-cordis-config metadata expressions', () => {
it('accepts a disabled !!js expression', () => {
const problems = metadataExpressionErrors(
{ id: 'tool-bash', name: '@deepseek-ai/dsh-tool-bash', disabled: { __jsExpr: "process.platform === 'win32'" } },
'[0]',
)
expect(problems).toEqual([])
})
it('rejects an expression in a static metadata field', () => {
const problems = metadataExpressionErrors({ id: { __jsExpr: 'process.platform' }, name: 'pkg' }, '[0]')
expect(problems).toContain('[0].id: !!js is not interpolated here')
})
it('rejects an expression nested below disabled (only the field itself interpolates)', () => {
const problems = metadataExpressionErrors(
{ id: 'tool-bash', name: 'pkg', disabled: { when: { __jsExpr: 'process.platform' } } },
'[0]',
)
expect(problems).toContain('[0].disabled.when: !!js is not interpolated here')
})
it('rejects a disabled expression that does not parse (the loader would fail the boot)', () => {
const problems = metadataExpressionErrors(
{ id: 'tool-bash', name: 'pkg', disabled: { __jsExpr: 'process.platform ===' } },
'[0]',
)
expect(problems.some(problem => problem.includes('[0].disabled: disabled expression does not parse'))).toBe(true)
})
})

View File

@@ -1,11 +1,13 @@
/**
* Validate Cordis Loader entry metadata and package resolution.
*
* The Loader interpolates only a plugin entry's `config`; expression objects in
* fields such as `disabled` remain truthy data and silently change composition.
* Example configs and the dsh Web composition resolve named plugins from their
* owning workspace manifests. Local example packages must also be in the root
* TypeScript project graph.
* The Loader interpolates a plugin entry's `config` (after declared injections
* activate, against that plugin context) and the entry `disabled` field (at
* every mount decision, against the loader context). Every other entry
* metadata field stays static, so an expression there remains truthy data and
* silently changes composition. Example configs and the dsh Web composition
* resolve named plugins from their owning workspace manifests. Local example
* packages must also be in the root TypeScript project graph.
*/
import { globSync, readFileSync } from 'node:fs'
@@ -36,7 +38,7 @@ const appOverlayFiles = new Set([
'examples/web-schedule/cordis.yml',
...globSync('examples/mcp-memory/*.cordis.yml', { cwd: root }),
])
const metadataFields = ['id', 'name', 'group', 'disabled', 'inject', 'intercept', 'isolate'] as const
const metadataFields = ['id', 'name', 'group', 'inject', 'intercept', 'isolate'] as const
/** The adaptive directory-picker chooser package (mounts a backend row at boot). */
const CHOOSER_PACKAGE = '@deepseek-ai/dsh-host-directory-picker-auto'
@@ -64,33 +66,36 @@ const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
})
const schema = yaml.JSON_SCHEMA.extend(jsExprType)
const files = cordisConfigFiles(root)
const errors: string[] = []
const pluginReferences: PluginReference[] = []
for (const file of files) {
const document: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'), { schema })
if (!isUnknownArray(document)) {
errors.push(`${file}: root must be a Loader entry array`)
continue
}
for (let index = 0; index < document.length; index++) {
validateEntry(document[index], file, `[${index}]`)
}
}
if (import.meta.main) {
const files = cordisConfigFiles(root)
errors.push(...validateExampleResolution())
errors.push(...validateAppResolution())
errors.push(...validateSourcePlaneResolution())
errors.push(...validatePresetPlaneSeparation())
errors.push(...validateClientHalvesDeclared())
for (const file of files) {
const document: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'), { schema })
if (!isUnknownArray(document)) {
errors.push(`${file}: root must be a Loader entry array`)
continue
}
for (let index = 0; index < document.length; index++) {
validateEntry(document[index], file, `[${index}]`)
}
}
if (errors.length > 0) {
console.error('verify-cordis-config: invalid Loader metadata or plugin package resolution:')
for (const error of errors) console.error(`- ${error}`)
process.exitCode = 1
} else {
console.log(`verify-cordis-config: ${files.length} config files passed.`)
errors.push(...validateExampleResolution())
errors.push(...validateAppResolution())
errors.push(...validateSourcePlaneResolution())
errors.push(...validatePresetPlaneSeparation())
errors.push(...validateClientHalvesDeclared())
if (errors.length > 0) {
console.error('verify-cordis-config: invalid Loader metadata or plugin package resolution:')
for (const error of errors) console.error(`- ${error}`)
process.exitCode = 1
} else {
console.log(`verify-cordis-config: ${files.length} config files passed.`)
}
}
/**
@@ -409,11 +414,60 @@ function packageNameFromSpecifier(specifier: string): string | undefined {
}
function validateMetadata(entry: Record<string, unknown>, file: string, path: string): void {
for (const problem of metadataExpressionErrors(entry, path)) {
errors.push(`${file}${problem}`)
}
}
/**
* Expression-node diagnostics for one entry. `disabled` is the single
* interpolated metadata field: its own `!!js` expression node is allowed and
* must parse, while expressions nested below it stay truthy data; every other
* metadata field must stay fully static.
* @param entry - one loader entry (or patch row).
* @param path - the entry's diagnostic path prefix.
* @returns one diagnostic per offending expression.
*/
export function metadataExpressionErrors(entry: Record<string, unknown>, path: string): string[] {
const problems: string[] = []
for (const field of metadataFields) {
if (!(field in entry)) continue
const expressionPaths: string[] = []
collectExpressionPaths(entry[field], `${path}.${field}`, expressionPaths)
for (const expressionPath of expressionPaths) errors.push(`${file}${expressionPath}: !!js is not interpolated here`)
for (const expressionPath of expressionPaths) problems.push(`${expressionPath}: !!js is not interpolated here`)
}
const disabled = entry.disabled
if (disabled !== undefined) {
if (isJsExpr(disabled)) {
const detail = disabledExpressionProblem(disabled.__jsExpr)
if (detail !== undefined) problems.push(`${path}.disabled${detail}`)
} else {
// A non-expression value gates on Boolean() at mount; an expression
// nested anywhere below it never evaluates, so it must stay literal.
const expressionPaths: string[] = []
collectExpressionPaths(disabled, `${path}.disabled`, expressionPaths)
for (const expressionPath of expressionPaths) problems.push(`${expressionPath}: !!js is not interpolated here`)
}
}
return problems
}
/**
* Parse-only validation of a `disabled` expression: the Loader evaluates it
* at every mount decision, and a syntax error would fail the boot — rejecting
* it here moves that failure to the earliest resolvable point.
* @param expression - the `!!js` expression text.
* @returns the diagnostic suffix, or `undefined` when the expression parses.
*/
function disabledExpressionProblem(expression: string): string | undefined {
try {
// Compilation only — the constructor never executes the body.
// oxlint-disable-next-line typescript/no-implied-eval
new Function(`return (${expression})`)
return undefined
} catch (error) {
const detail = error instanceof Error ? error.message : String(error)
return `: disabled expression does not parse: ${detail}`
}
}

View File

@@ -92,6 +92,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/ui-settings': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-settings-general': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-models': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-plugins': { kind: 'none', reason: 'Browser-side inventory projection; registers nothing model-facing.' },
'packages/client/locale': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/web': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/examples/agent-spine-demo': { kind: 'indirect', reason: 'The bundle only mounts model-facing child plugins.' },
@@ -106,6 +107,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/host/directory-picker-native': { kind: 'none', reason: 'The GUI-host picking backend registers nothing model-facing.' },
'packages/host/webserver': { kind: 'none', reason: 'The HTTP carrier bridges browser and API handler and registers nothing model-facing.' },
'packages/host/frontend-static': { kind: 'none', reason: 'The SPA dist server answers browser asset requests and registers nothing model-facing.' },
'packages/host/plugin-inventory': { kind: 'none', reason: 'Host-side read-only Loader projection; registers nothing model-facing.' },
'packages/bundle/base': { kind: 'indirect', reason: 'The bundle is a patch-list carrier; each inserted row\'s package owns its model-facing behavior.' },
'packages/bundle/headless': { kind: 'none', reason: 'The one-shot runner submits the task as an ordinary user message; prompts and tools belong to the composed base and headless bundles.' },
'packages/llm/llm': { kind: 'none', reason: 'The adapter registry forwards already-assembled requests unchanged.' },