diff --git a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml new file mode 100644 index 0000000000..32d76bbc9c --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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/process/2026-07-30-generated-third-party-notices.md +2026-07-30-generated-third-party-notices.md: 5b85abf5716d79213f6937b938d3f0267f7627f3 +2026-07-30-generated-third-party-notices.zh.md: 26b3e6e88ec038d9107c9b78c85a8c45a1916805 diff --git a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md new file mode 100644 index 0000000000..5b85abf571 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md @@ -0,0 +1,47 @@ +# Agent Note: Generated third-party notices + +Status: implemented + +English | [中文](2026-07-30-generated-third-party-notices.zh.md) + +## Problem + +Open-sourcing this repository requires disclosing the third-party software it depends on, with each project's license. The disclosure has to be complete, has to stay true as dependencies change, and has to say something a reader can act on — which of these packages end up on a user's machine, and which only build and test the repository. + +A hand-written inventory answers none of those durably. Roughly a hundred rows of names and license strings derived from manifests drift silently the moment a package is added, removed, or relicensed, and nothing in `doc-sync` would notice. + +## Decision + +[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) is generated by [`scripts/gen-third-party-notices.ts`](../../../../scripts/gen-third-party-notices.ts) from the workspace manifests, `vendor/README.md`, the `pyproject.toml` files, and `pnpm-workspace.yaml`. `pnpm run verify-third-party-notices` runs the generator with `--check` as a `doc-sync` leaf gate, so a dependency change that skips regeneration fails the same way a stale catalog does. The root README pair links the file from its License section. + +The file discloses **direct** dependencies only. The complete npm closure with pinned versions already lives in `pnpm-lock.yaml` (`pnpm licenses list` renders it) and the Python closure in `python/sdk/uv.lock`; re-materializing either as prose would be a second, worse copy. + +**Tiering is by declaring area, not by manifest section.** A package is a runtime dependency when any manifest outside `DEV_ONLY_AREAS` — the root manifest, `packages/support/`, `packages/client/test-runtime/`, `website/`, `examples/`, `native/` — names it under `dependencies` or `optionalDependencies`. Section names alone are wrong in both directions: a test-support package declares `vitest` under `dependencies` without shipping it, and the `bin/dsh` launcher execs through `tsx`, which no manifest declares as a runtime dependency at all (the generator marks it runtime explicitly). + +The runtime tier deliberately covers **every mountable plugin**, not just what the CLI, Web UI, and Python runtime load by default. `scripts/install.sh` installs the repository itself, so a user's `cordis.yml` can mount any plugin package; `@modelcontextprotocol/sdk` and the OpenTelemetry packages reach real users even though no default assembly imports them. Under-disclosure is the costly direction for a legal notice. + +License and repository metadata come from the installed pnpm store, so the generator requires an installed tree and fails loud when a package resolves to neither, rather than emitting an empty cell. `OVERRIDES` carries the packages whose published manifest cannot answer — Rust-built npm bins that omit `license`, and the `modelcontextprotocol/servers` packages whose repository is mid MIT→Apache-2.0 relicensing, so their effective terms are per-contribution. Vendored packages are cross-checked against `vendor/README.md` and rejected if any is not MIT, and `pnpm-workspace.yaml`'s `patchedDependencies` are listed under the runtime table because pnpm applies those patches at install time — shipped artifacts carry modified copies of `@earendil-works/pi-tui` and `node-pty`, and the patch files are the record of what changed. + +## Testing + +[`scripts/gen-third-party-notices.spec.ts`](../../../../scripts/gen-third-party-notices.spec.ts) pins the tiering rule against fixture manifests — including the two cases that motivate it, a `dependencies` entry of a test-support package and a plugin package no app mounts — and pins that the vendored-table parser reads the committed manifest and yields nothing when the table shape changes, which is what makes the generator fail loud rather than emit an empty section. + +## Alternatives considered + +**Keep the hand-written file and review it at release time.** Reviewing a hundred derived rows by eye is exactly the work a generator does correctly, and the file's own claim — that it lists every direct dependency — would be unverified between releases. + +**Enumerate the full transitive closure.** The closure is thousands of packages, already recorded in the lock files with exact versions, and would bury the direct dependencies that a reader actually evaluates. The file points at the lock files and the `pnpm licenses list` renderer instead. + +**Tier by manifest section (`dependencies` vs `devDependencies`).** Mechanically simple and wrong on real data in both directions, as the tiering paragraph above records. + +**Tier by reachability from the shipped assemblies only** (`apps/*` plus `python/sdk-runtime`). This produces a tighter runtime tier, but classifies the MCP client and the OpenTelemetry exporter as development-only even though a user running the installed repository can mount them. It understates the disclosure, which is the wrong direction to err for a legal notice. + +**Emit the notices as a bilingual pair.** Every other root document is paired, but the file is a table of upstream package names, SPDX identifiers, and URLs; the translatable surface is a handful of section blurbs. `scripts/translation-pairing.ts` scopes discovery to `README*`, `.agents/notes/**`, `docs/**`, and `python/**`, so a root non-README file is outside the bilingual corpus by construction, and the README pair carries the bilingual entry points into it. + +## Consequences + +Adding or removing a dependency now requires running `pnpm run gen-third-party-notices` and committing the result; `doc-sync` fails otherwise. That is the intended cost — the disclosure cannot silently go stale. + +The generator needs an installed tree, which makes it heavier than a pure-source generator, and a new package with unusable published metadata needs an `OVERRIDES` entry rather than silently rendering a blank license. Both failures are loud and name the remedy. + +The tiering rule is a policy encoded in one constant. Adding a workspace area that never ships — a second test-infrastructure tier, another site — requires extending `DEV_ONLY_AREAS`, or its dependencies will be disclosed as runtime. diff --git a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md new file mode 100644 index 0000000000..26b3e6e88e --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md @@ -0,0 +1,47 @@ +# Agent Note: Generated third-party notices + +Status: implemented + +[English](2026-07-30-generated-third-party-notices.md) | 中文 + +## Problem + +本仓库开源需要披露所依赖的第三方软件及各自的许可证。这份披露必须完整,必须随依赖变化保持为真,还必须给出读者用得上的信息:哪些包最终会进到用户机器上,哪些只用于构建和测试。 + +手写清单无法长期满足其中任何一条。约一百行从各清单文件推导出来的包名与许可证标识,只要有依赖新增、移除或换用许可证就会悄悄失真,而 `doc-sync` 不会察觉。 + +## Decision + +[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) 由 [`scripts/gen-third-party-notices.ts`](../../../../scripts/gen-third-party-notices.ts) 依据各工作区清单、`vendor/README.md`、`pyproject.toml` 与 `pnpm-workspace.yaml` 生成。`pnpm run verify-third-party-notices` 以 `--check` 运行生成器,作为 `doc-sync` 的叶子门禁;依赖变了却没重新生成,会像目录过期一样直接失败。根 README 双语两侧都从「许可证」一节链到该文件。 + +文件只披露**直接**依赖。完整的 npm 闭包连同锁定版本已记录在 `pnpm-lock.yaml`(`pnpm licenses list` 可渲染),Python 闭包记录在 `python/sdk/uv.lock`;再用散文誊一遍只会得到一份更差的副本。 + +**分层依据是声明方所在区域,而非清单字段名。** 只要 `DEV_ONLY_AREAS` 之外的任一清单——即根清单、`packages/support/`、`packages/client/test-runtime/`、`website/`、`examples/`、`native/` 之外——在 `dependencies` 或 `optionalDependencies` 里点名某个包,它就是运行时依赖。单看字段名在两个方向上都会出错:测试支撑包把 `vitest` 写在 `dependencies` 里却并不交付它;而 `bin/dsh` 启动器 exec 经过的 `tsx`,根本没有任何清单把它声明为运行时依赖,只能由生成器显式标记。 + +运行时层刻意覆盖**所有可挂载的插件**,而不止 CLI、Web UI 与 Python 运行时默认加载的那些。`scripts/install.sh` 安装的就是仓库本身,用户的 `cordis.yml` 可以挂载任何插件包;`@modelcontextprotocol/sdk` 与 OpenTelemetry 系列即使没有任何默认装配引入,也会触达真实用户。对法务披露而言,披露不足才是代价更高的那个方向。 + +许可证与仓库地址取自已安装的 pnpm store,因此生成器要求工作树已安装依赖;某个包两处都解析不到时直接失败,而不是留下空单元格。`OVERRIDES` 收录已发布清单答不上来的包:用 Rust 构建、发布时省略 `license` 字段的 npm 可执行包,以及 `modelcontextprotocol/servers` 系列——该仓库正处在 MIT 向 Apache-2.0 的重新许可过程中,实际条款按贡献逐条而定。被源码收编的包会与 `vendor/README.md` 交叉核对,出现非 MIT 即报错;`pnpm-workspace.yaml` 的 `patchedDependencies` 列在运行时表格之后,因为 pnpm 在安装期就会打上这些补丁——交付产物携带的是改动过的 `@earendil-works/pi-tui` 与 `node-pty`,补丁文件本身就是改动的完整记录。 + +## Testing + +[`scripts/gen-third-party-notices.spec.ts`](../../../../scripts/gen-third-party-notices.spec.ts) 用夹具清单钉住分层规则,覆盖促成该规则的两个场景:测试支撑包的 `dependencies` 条目,以及没有任何应用挂载的插件包。它同时钉住被收编包的表格解析器能读出已提交的清单表,且表格形态一变就解析为空——正是这一点让生成器直接失败,而不是产出一个空章节。 + +## Alternatives considered + +**保留手写文件,发版时人工过一遍。** 用肉眼审阅上百行推导数据,恰恰是生成器能做对的活;而且在两次发版之间,文件自称「列出全部直接依赖」这句话无人验证。 + +**列出完整传递闭包。** 闭包有数千个包,锁文件里已带精确版本,铺开只会淹没读者真正要评估的直接依赖。文件转而指向锁文件与 `pnpm licenses list`。 + +**按清单字段分层(`dependencies` 与 `devDependencies`)。** 机械上最省事,但在真实数据上两个方向都会出错,理由见上文分层段落。 + +**只按已交付装配的可达性分层**(`apps/*` 加 `python/sdk-runtime`)。这样得到的运行时层更紧凑,但会把 MCP 客户端与 OpenTelemetry 导出器判为仅开发用途——而运行已安装仓库的用户完全可以挂载它们。这会低估披露,对法务通告来说错在了更危险的一侧。 + +**把披露文件做成双语对。** 其他根文档都是成对的,但这份文件是上游包名、SPDX 标识与网址构成的表格,可翻译的只有寥寥几段章节导语。`scripts/translation-pairing.ts` 的发现范围限定在 `README*`、`.agents/notes/**`、`docs/**` 与 `python/**`,根目录下的非 README 文件在构造上就不属于双语语料;双语入口由 README 对承担。 + +## Consequences + +此后增删依赖都需要运行 `pnpm run gen-third-party-notices` 并提交结果,否则 `doc-sync` 失败。这正是预期成本——披露不可能再悄悄过期。 + +生成器需要已安装的工作树,因此比纯源码生成器更重;发布元数据不可用的新包需要补一条 `OVERRIDES`,而不是默默渲染出空白许可证。这两类失败都会明确报错并指出补救方式。 + +分层规则是编码在一个常量里的政策。若新增了不参与交付的工作区区域——第二层测试基础设施、另一个站点——就要同步扩展 `DEV_ONLY_AREAS`,否则其依赖会被当作运行时依赖披露出去。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index bfbe5608b0..651133723d 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,8 +1,11 @@ + + # Third-Party Notices DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party open-source software listed below. Each project remains under its own license; nothing in this file changes those terms. -This file lists **direct** dependencies declared by the workspace. The complete transitive closure, with exact pinned versions, is recorded in [`pnpm-lock.yaml`](pnpm-lock.yaml) and can be inspected with `pnpm licenses list`. +This file lists **direct** dependencies declared by the workspace, generated from the workspace manifests by `scripts/gen-third-party-notices.ts` and verified fresh by `pnpm run verify-third-party-notices` (part of `doc-sync`). The complete npm transitive closure, with exact pinned versions, is recorded in [`pnpm-lock.yaml`](pnpm-lock.yaml) (inspect it with `pnpm licenses list`); the Python closure is recorded in [`python/sdk/uv.lock`](python/sdk/uv.lock). ## Vendored source (`vendor/`) @@ -10,19 +13,19 @@ The Cordis framework and its foundation libraries are source-vendored into this | Package | Upstream | License | | --- | --- | --- | -| `cordis` | https://github.com/cordiverse/cordis | MIT | -| `@cordisjs/plugin-loader` | https://github.com/cordiverse/cordis | MIT | -| `@cordisjs/plugin-include` | https://github.com/deepseek-harness/cordis | MIT | -| `@cordisjs/plugin-group` | https://github.com/deepseek-harness/cordis | MIT | -| `@cordisjs/plugin-timer` | https://github.com/deepseek-harness/cordis | MIT | -| `@cordisjs/plugin-hmr` | https://github.com/deepseek-harness/cordis | MIT | -| `@cordisjs/plugin-logger-console` | https://github.com/deepseek-harness/cordis | MIT | -| `cosmokit` | https://github.com/deepseek-harness/cosmokit | MIT | -| `schemastery` | https://github.com/deepseek-harness/schemastery | MIT | +| `cosmokit` | [github.com/deepseek-harness/cosmokit](https://github.com/deepseek-harness/cosmokit) | MIT | +| `schemastery` | [github.com/deepseek-harness/schemastery](https://github.com/deepseek-harness/schemastery) | MIT | +| `cordis` | [github.com/cordiverse/cordis](https://github.com/cordiverse/cordis) | MIT | +| `@cordisjs/plugin-loader` | [github.com/cordiverse/cordis](https://github.com/cordiverse/cordis) | MIT | +| `@cordisjs/plugin-include` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT | +| `@cordisjs/plugin-group` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT | +| `@cordisjs/plugin-timer` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT | +| `@cordisjs/plugin-hmr` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT | +| `@cordisjs/plugin-logger-console` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT | ## Runtime npm dependencies -Direct dependencies that ship in at least one runtime surface (CLI/TUI, Web UI, SDK runtime, or the website at serve time). +External packages that a workspace package resolves at runtime. `scripts/install.sh` installs this repository itself, so the tier covers every plugin a user can mount from `cordis.yml` — not only what the `dsh` CLI/TUI, the Web UI, and the Python SDK runtime load by default. | Package | License | | --- | --- | @@ -42,15 +45,12 @@ Direct dependencies that ship in at least one runtime surface (CLI/TUI, Web UI, | [`@opentelemetry/sdk-logs`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 | | [`@shikijs/langs`](https://github.com/shikijs/shiki) | MIT | | [`@standard-schema/spec`](https://github.com/standard-schema/standard-schema) | MIT | -| [`@testing-library/dom`](https://github.com/testing-library/dom-testing-library) | MIT | -| [`@testing-library/react`](https://github.com/testing-library/react-testing-library) | MIT | | [`anser`](https://github.com/IonicaBizau/anser) | MIT | | [`chokidar`](https://github.com/paulmillr/chokidar) | MIT | | [`clsx`](https://github.com/lukeed/clsx) | MIT | | [`commander`](https://github.com/tj/commander.js) | MIT | | [`diff`](https://github.com/kpdecker/jsdiff) | BSD-3-Clause | | [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT | -| [`execa`](https://github.com/sindresorhus/execa) | MIT | | [`handlebars`](https://github.com/handlebars-lang/handlebars.js) | MIT | | [`immer`](https://github.com/immerjs/immer) | MIT | | [`js-yaml`](https://github.com/nodeca/js-yaml) | MIT | @@ -73,14 +73,18 @@ Direct dependencies that ship in at least one runtime surface (CLI/TUI, Web UI, | [`turndown`](https://github.com/mixmark-io/turndown) | MIT | | [`typescript`](https://github.com/microsoft/TypeScript) | Apache-2.0 | | [`use-sync-external-store`](https://github.com/facebook/react) | MIT | -| [`vitest`](https://github.com/vitest-dev/vitest) | MIT | | [`yaml`](https://github.com/eemeli/yaml) | ISC | | [`zod`](https://github.com/colinhacks/zod) | MIT | | [`zustand`](https://github.com/pmndrs/zustand) | MIT | +pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification: + +- `@earendil-works/pi-tui@0.80.7` — [`patches/@earendil-works__pi-tui@0.80.7.patch`](patches/@earendil-works__pi-tui@0.80.7.patch) +- `node-pty@1.1.0` — [`patches/node-pty@1.1.0.patch`](patches/node-pty@1.1.0.patch) + ## Development-only npm dependencies -Direct dependencies used for building, linting, testing, and generating the documentation site. They are not part of any shipped runtime artifact. +External packages declared only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace. They are not part of any shipped runtime artifact. | Package | License | | --- | --- | @@ -88,7 +92,17 @@ Direct dependencies used for building, linting, testing, and generating the docu | [`@modelcontextprotocol/server-everything`](https://github.com/modelcontextprotocol/servers) | MIT / Apache-2.0 | | [`@modelcontextprotocol/server-filesystem`](https://github.com/modelcontextprotocol/servers) | MIT / Apache-2.0 | | [`@stylistic/eslint-plugin`](https://github.com/eslint-stylistic/eslint-stylistic) | MIT | -| [`@types/*`](https://github.com/DefinitelyTyped/DefinitelyTyped) (babel__code-frame, js-yaml, jsdom, mdast, node, picomatch, react, react-dom, turndown) | MIT | +| [`@testing-library/dom`](https://github.com/testing-library/dom-testing-library) | MIT | +| [`@testing-library/react`](https://github.com/testing-library/react-testing-library) | MIT | +| [`@types/babel__code-frame`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | +| [`@types/js-yaml`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | +| [`@types/jsdom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | +| [`@types/mdast`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | +| [`@types/node`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | +| [`@types/picomatch`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | +| [`@types/react`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | +| [`@types/react-dom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | +| [`@types/turndown`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@typescript-eslint/parser`](https://github.com/typescript-eslint/typescript-eslint) | MIT | | [`@vitejs/plugin-react`](https://github.com/vitejs/vite-plugin-react) | MIT | | [`@vitest/coverage-v8`](https://github.com/vitest-dev/vitest) | MIT | @@ -101,6 +115,7 @@ Direct dependencies used for building, linting, testing, and generating the docu | [`esbuild`](https://github.com/evanw/esbuild) | MIT | | [`eslint`](https://github.com/eslint/eslint) | MIT | | [`eslint-plugin-sonarjs`](https://github.com/SonarSource/SonarJS) | LGPL-3.0-only | +| [`execa`](https://github.com/sindresorhus/execa) | MIT | | [`fast-check`](https://github.com/dubzzz/fast-check) | MIT | | [`jscpd`](https://github.com/kucherenko/jscpd) | MIT | | [`jsdom`](https://github.com/jsdom/jsdom) | MIT | @@ -118,15 +133,18 @@ Direct dependencies used for building, linting, testing, and generating the docu | [`vite-tsconfig-paths`](https://github.com/aleclarson/vite-tsconfig-paths) | MIT | | [`vitepress`](https://github.com/vuejs/vitepress) | MIT | | [`vitepress-plugin-mermaid`](https://github.com/emersonbottero/vitepress-plugin-mermaid) | MIT | +| [`vitest`](https://github.com/vitest-dev/vitest) | MIT | `eslint-plugin-sonarjs` (LGPL-3.0-only) and `lightningcss` (MPL-2.0) run only as development tooling; their code is not linked into or distributed with any DeepSeek Harness artifact. ## Python SDK dependencies (`python/`) +Direct dependencies of the `pyproject.toml` manifests, plus `uv` as the development workflow tool. + | Package | License | Role | | --- | --- | --- | -| [`pydantic`](https://github.com/pydantic/pydantic) | MIT | runtime dependency of `deepseek-harness` | | [`hatchling`](https://github.com/pypa/hatch) | MIT | build backend | +| [`pydantic`](https://github.com/pydantic/pydantic) | MIT | runtime dependency of `deepseek-harness` | | [`pytest`](https://github.com/pytest-dev/pytest) | MIT | test-only | | [`uv`](https://github.com/astral-sh/uv) | MIT / Apache-2.0 | development workflow tool | diff --git a/package.json b/package.json index fc315cb146..535d1f4c52 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,8 @@ "verify-doc-graphs": "tsx scripts/gen-doc-graphs.ts --check", "gen-persistence-catalog": "tsx scripts/gen-persistence-catalog.ts", "verify-persistence-catalog": "tsx scripts/gen-persistence-catalog.ts --check", + "gen-third-party-notices": "tsx scripts/gen-third-party-notices.ts", + "verify-third-party-notices": "tsx scripts/gen-third-party-notices.ts --check", "gen-module-graph": "tsx scripts/gen-module-graph.ts", "gen-scoped-events": "tsx scripts/gen-scoped-events.ts", "verify-scoped-events": "tsx scripts/gen-scoped-events.ts --check", diff --git a/scripts/gen-third-party-notices.spec.ts b/scripts/gen-third-party-notices.spec.ts new file mode 100644 index 0000000000..57c28ae151 --- /dev/null +++ b/scripts/gen-third-party-notices.spec.ts @@ -0,0 +1,69 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' +import { type Manifest, parseVendoredRows, tierExternalDeps } from './gen-third-party-notices.ts' + +const root = resolve(import.meta.dirname, '..') + +/** Build the (manifests, names) pair `tierExternalDeps` consumes. */ +function workspace(entries: Record): { manifests: Map; names: Set } { + const manifests = new Map(Object.entries(entries)) + const names = new Set() + for (const manifest of manifests.values()) { + if (manifest.name !== undefined) names.add(manifest.name) + } + return { manifests, names } +} + +describe('tierExternalDeps', () => { + it('tiers by declaring area, not by the declaring section name', () => { + const { manifests, names } = workspace({ + // Root tooling and test infrastructure never ship, whichever section declares them. + 'package.json': { dependencies: { 'root-runtime-looking': '^1' }, devDependencies: { 'lint-tool': '^1' } }, + 'packages/support/loader-smoke/package.json': { name: '@deepseek-ai/dsh-loader-smoke', dependencies: { 'smoke-helper': '^1' } }, + 'packages/client/test-runtime/package.json': { name: '@deepseek-ai/dsh-client-test-runtime', dependencies: { 'test-lib': '^1' } }, + 'website/package.json': { devDependencies: { 'site-tool': '^1' } }, + // A plugin package's runtime dependency ships even when no app mounts it by default. + 'packages/mcp/mcp-client/package.json': { name: '@deepseek-ai/dsh-mcp-client', dependencies: { 'protocol-sdk': '^1' }, devDependencies: { 'protocol-fixture-server': '^1' } }, + 'apps/cli/package.json': { name: '@deepseek-ai/dsh-cli', dependencies: { 'cli-lib': '^1', '@deepseek-ai/dsh-mcp-client': 'workspace:^' } }, + }) + + expect(tierExternalDeps(manifests, names)).toEqual(new Map([ + ['tsx', true], + ['root-runtime-looking', false], + ['lint-tool', false], + ['smoke-helper', false], + ['test-lib', false], + ['site-tool', false], + ['protocol-sdk', true], + ['protocol-fixture-server', false], + ['cli-lib', true], + ])) + }) + + it('keeps a package runtime when any shipping area declares it, and excludes workspace links', () => { + const { manifests, names } = workspace({ + 'package.json': { devDependencies: { shared: '^1' } }, + 'packages/ui/tui/package.json': { name: '@deepseek-ai/dsh-tui', dependencies: { shared: '^1', '@deepseek-ai/dsh-cli': 'workspace:^' } }, + 'apps/cli/package.json': { name: '@deepseek-ai/dsh-cli' }, + }) + + expect(tierExternalDeps(manifests, names).get('shared')).toBe(true) + expect(tierExternalDeps(manifests, names).has('@deepseek-ai/dsh-cli')).toBe(false) + }) +}) + +describe('parseVendoredRows', () => { + it('reads the committed vendor manifest table', () => { + const rows = parseVendoredRows(readFileSync(resolve(root, 'vendor/README.md'), 'utf8')) + + expect(rows.length).toBeGreaterThan(0) + expect(rows).toContainEqual({ npmName: 'cordis', upstream: 'https://github.com/cordiverse/cordis' }) + // The upstream column carries a trailing package path for some rows; it is not part of the URL. + expect(rows.every(row => /^https:\/\/\S+$/.test(row.upstream))).toBe(true) + }) + + it('yields nothing when the table shape changes, so the generator fails loud', () => { + expect(parseVendoredRows('| `cordis/` | cordis | 4.0.0 | https://example.com | `abc123` |\n')).toEqual([]) + }) +}) diff --git a/scripts/gen-third-party-notices.ts b/scripts/gen-third-party-notices.ts new file mode 100644 index 0000000000..16cbe6153c --- /dev/null +++ b/scripts/gen-third-party-notices.ts @@ -0,0 +1,384 @@ +/** + * Generate `THIRD_PARTY_NOTICES.md` from the workspace manifests: every + * external dependency named by a workspace `package.json`, the vendored-package + * manifest in `vendor/README.md`, the Python `pyproject.toml` files, and the + * pnpm patch list. License and repository metadata come from the installed + * store, so the tree must be installed. `--check` verifies the committed + * artifact. Tier policy and ownership live in + * `.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md`. + */ + +import { existsSync, globSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' +import { resolve } from 'node:path' +import * as yaml from 'js-yaml' + +const root = resolve(import.meta.dirname, '..') +const OUT = 'THIRD_PARTY_NOTICES.md' + +/** Dependency-declaration kinds a consumer resolves at runtime. */ +const RUNTIME_KINDS = ['dependencies', 'optionalDependencies'] as const +/** All manifest sections that name an external package this file must disclose. */ +const ALL_KINDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const + +/** + * Workspace areas that never reach a user: repository tooling and gates (the + * root manifest), test infrastructure, the documentation site, the runnable + * demo leaves, and the native launcher's build workspace. A runtime + * declaration by anything outside these areas is a disclosure-relevant + * runtime dependency, because `scripts/install.sh` installs the repository + * itself and any plugin package can be mounted from a user's `cordis.yml`. + */ +const DEV_ONLY_AREAS = [ + 'package.json', + 'packages/support/', + 'packages/client/test-runtime/', + 'website/', + 'examples/', + 'native/', +] as const + +/** + * First-party packages released from sibling repositories under the project's + * own license: reachable from workspace manifests but not third-party. + */ +const FIRST_PARTY = new Set(['node-addon-landlock-run']) + +/** + * Metadata overrides where the installed manifest is wrong or unreachable. + * Each entry documents why the store cannot answer. + */ +const OVERRIDES: Record = { + // Rust workspaces publishing npm bins without `license` in package.json. + 'oxlint': { license: 'MIT', repo: 'https://github.com/oxc-project/oxc' }, + 'oxlint-tsgolint': { license: 'MIT', repo: 'https://github.com/oxc-project/tsgolint' }, + // `license: SEE LICENSE IN LICENSE`: the servers repo is mid MIT→Apache-2.0 + // relicensing, so the effective terms are per-contribution. + '@modelcontextprotocol/server-everything': { license: 'MIT / Apache-2.0', repo: 'https://github.com/modelcontextprotocol/servers' }, + '@modelcontextprotocol/server-filesystem': { license: 'MIT / Apache-2.0', repo: 'https://github.com/modelcontextprotocol/servers' }, + // No repository field in the published manifest. + 'node-addon-require-builtin': { repo: 'https://www.npmjs.com/package/node-addon-require-builtin' }, +} + +/** + * Python dependencies are few and named directly in `pyproject.toml` files + * without installed metadata to harvest, so license/repo are recorded here and + * the generator fails when a manifest names a package this map misses. + */ +const PYTHON_METADATA: Record = { + pydantic: { license: 'MIT', repo: 'https://github.com/pydantic/pydantic', role: 'runtime dependency of `deepseek-harness`' }, + hatchling: { license: 'MIT', repo: 'https://github.com/pypa/hatch', role: 'build backend' }, + pytest: { license: 'MIT', repo: 'https://github.com/pytest-dev/pytest', role: 'test-only' }, +} + +/** Tools fetched by scripts at build time, keyed by the pin the script owns. */ +const BUILD_TIME_TOOLS = [ + { + name: '@yao-pkg/pkg', + license: 'MIT', + repo: 'https://github.com/yao-pkg/pkg', + role: 'invoked by `scripts/build-exe-for-python-sdk.ts` to assemble the single-file SDK runtime executable', + pinSource: 'scripts/build-exe-for-python-sdk.ts', + }, +] + +/** The `package.json` fields this generator reads. */ +export interface Manifest { + name?: string + private?: boolean + license?: string + dependencies?: Record + devDependencies?: Record + optionalDependencies?: Record + peerDependencies?: Record +} + +/** One disclosed external npm dependency. */ +interface ExternalDep { + name: string + license: string + repo: string + /** True when some shipped workspace consumer reaches it through runtime dependency edges. */ + runtime: boolean +} + +/** Read and parse a workspace-relative `package.json`. */ +function readManifest(rel: string): Manifest { + return JSON.parse(readFileSync(resolve(root, rel), 'utf8')) as Manifest +} + +/** Every workspace manifest, keyed by path, plus the set of workspace package names. */ +function loadWorkspaceManifests(): { manifests: Map; names: Set } { + const patterns = ['package.json', 'vendor/*/package.json', 'packages/*/*/package.json', 'apps/*/package.json', 'website/package.json', 'examples/package.json', 'python/sdk-runtime/package.json', 'native/landlock-run/package.json', 'native/landlock-run/*/package.json'] + const manifests = new Map() + const names = new Set() + for (const pattern of patterns) { + for (const path of globSync(pattern, { cwd: root })) { + const manifest = readManifest(path) + manifests.set(path, manifest) + if (manifest.name !== undefined) names.add(manifest.name) + } + } + if (manifests.size < 100) throw new Error(`gen-third-party-notices: only ${manifests.size} workspace manifests found; the glob set is stale.`) + return { manifests, names } +} + +/** License and repository URL for an installed external package, from the pnpm store. */ +function installedMetadata(name: string): { license: string; repo: string } { + const override = OVERRIDES[name] + let manifest: (Manifest & { license?: string; repository?: string | { url?: string }; homepage?: string }) | undefined + const direct = resolve(root, 'node_modules', name, 'package.json') + if (existsSync(direct)) { + manifest = JSON.parse(readFileSync(direct, 'utf8')) as typeof manifest + } else { + const prefix = `${name.replace('/', '+')}@` + const entry = readdirSync(resolve(root, 'node_modules/.pnpm')).find(dir => dir.startsWith(prefix)) + if (entry !== undefined) { + manifest = JSON.parse(readFileSync(resolve(root, 'node_modules/.pnpm', entry, 'node_modules', name, 'package.json'), 'utf8')) as typeof manifest + } + } + const license = override?.license ?? manifest?.license + const rawRepo = typeof manifest?.repository === 'string' ? manifest.repository : manifest?.repository?.url ?? manifest?.homepage + const repo = override?.repo ?? normalizeRepo(rawRepo) + if (license === undefined || repo === undefined) { + throw new Error(`gen-third-party-notices: cannot resolve ${license === undefined ? 'license' : 'repository'} for ${name}; install the tree or add an OVERRIDES entry.`) + } + return { license, repo } +} + +/** Normalize a manifest repository/homepage value to a browsable https URL. */ +function normalizeRepo(raw: string | undefined): string | undefined { + if (raw === undefined || raw === '') return undefined + let url = raw + .replace(/^git\+ssh:\/\/git@/, 'https://') + .replace(/^git\+/, '') + .replace(/^git:\/\//, 'https://') + .replace(/^github:/, 'https://github.com/') + .replace(/\.git$/, '') + if (!url.startsWith('http')) url = `https://github.com/${url}` + return url +} + +/** + * External npm dependencies, tiered by which workspace area declares them at + * runtime: a package is runtime when any manifest outside `DEV_ONLY_AREAS` + * names it in `dependencies`/`optionalDependencies`. A package declared only + * by tooling, test infrastructure, the website, or the demo leaves — whatever + * the declaring section is called — is development-only. + */ +function collectNpmDeps(): ExternalDep[] { + const { manifests, names } = loadWorkspaceManifests() + return [...tierExternalDeps(manifests, names)] + .filter(([name]) => !FIRST_PARTY.has(name)) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([name, runtime]) => ({ name, ...installedMetadata(name), runtime })) +} + +/** + * Tier every external dependency the workspace declares. + * @param manifests - workspace manifests keyed by repository-relative path. + * @param names - every workspace package name, which never counts as external. + * @returns each external package mapped to whether it is a runtime dependency. + */ +export function tierExternalDeps(manifests: Map, names: Set): Map { + const tiers = new Map() + // `tsx` is runtime by fiat: `bin/dsh` execs the CLI through its ESM hook. + tiers.set('tsx', true) + for (const [path, manifest] of manifests) { + const devOnly = DEV_ONLY_AREAS.some(area => (area.endsWith('/') ? path.startsWith(area) : path === area)) + for (const kind of ALL_KINDS) { + for (const [dep, range] of Object.entries(manifest[kind] ?? {})) { + if (names.has(dep) || range.startsWith('workspace:')) continue + const runtime = !devOnly && (RUNTIME_KINDS as readonly string[]).includes(kind) + tiers.set(dep, (tiers.get(dep) ?? false) || runtime) + } + } + } + return tiers +} + +/** A vendored package row parsed out of the `vendor/README.md` manifest table. */ +export interface VendoredRow { + npmName: string + upstream: string +} + +/** + * Parse the vendored-package manifest table out of `vendor/README.md`. + * @param text - the complete `vendor/README.md` contents. + * @returns one row per manifest-table entry, in table order. + */ +export function parseVendoredRows(text: string): VendoredRow[] { + const rows: VendoredRow[] = [] + for (const line of text.split('\n')) { + const match = /^\| \x60\S+\/\x60 \| \x60([^\x60]+)\x60 \| \S+ \| (https:\/\/\S+?)(?: \([^)]*\))? \| \x60[0-9a-f]+\x60 \|$/.exec(line) + if (match === null) continue + const [, npmName, upstream] = match + if (npmName === undefined || upstream === undefined) continue + rows.push({ npmName, upstream }) + } + return rows +} + +/** Parse the vendored manifest table and confirm every vendored package is MIT. */ +function collectVendored(): VendoredRow[] { + const rows = parseVendoredRows(readFileSync(resolve(root, 'vendor/README.md'), 'utf8')) + if (rows.length === 0) throw new Error('gen-third-party-notices: no vendored rows parsed from vendor/README.md; its table format changed.') + for (const row of rows) { + const manifest = readManifest(`vendor/${vendorDir(row.npmName)}/package.json`) + if (manifest.license !== 'MIT') { + throw new Error(`gen-third-party-notices: vendored ${row.npmName} declares license ${JSON.stringify(manifest.license)}; the vendored section assumes MIT throughout.`) + } + } + return rows +} + +/** The vendor/ directory of a vendored npm name (manifest table order is authoritative for names). */ +function vendorDir(npmName: string): string { + const dirs = readdirSync(resolve(root, 'vendor'), { withFileTypes: true }).filter(entry => entry.isDirectory()).map(entry => entry.name) + for (const dir of dirs) { + const manifest = readManifest(`vendor/${dir}/package.json`) + if (manifest.name === npmName) return dir + } + throw new Error(`gen-third-party-notices: vendored package ${npmName} from vendor/README.md has no vendor/ directory.`) +} + +/** Direct Python dependencies named by the `pyproject.toml` manifests under `python/`. */ +function collectPython(): { name: string; license: string; repo: string; role: string }[] { + const found = new Set() + for (const path of ['python/sdk/pyproject.toml', 'python/sdk-runtime/pyproject.toml']) { + const text = readFileSync(resolve(root, path), 'utf8') + for (const match of text.matchAll(/"([a-zA-Z][a-zA-Z0-9._-]*)\s*(?:>=|==|~=|<|>|\[)/g)) { + const name = match[1] + if (name === undefined || name.startsWith('deepseek')) continue + found.add(name) + } + } + return [...found].sort((a, b) => a.localeCompare(b)).map((name) => { + const metadata = PYTHON_METADATA[name] + if (metadata === undefined) throw new Error(`gen-third-party-notices: python dependency ${name} is missing from PYTHON_METADATA.`) + return { name, ...metadata } + }) +} + +/** pnpm-patched external packages, from `pnpm-workspace.yaml`. */ +function collectPatched(): { spec: string; patch: string }[] { + const workspace = yaml.load(readFileSync(resolve(root, 'pnpm-workspace.yaml'), 'utf8')) as { patchedDependencies?: Record } + return Object.entries(workspace.patchedDependencies ?? {}).map(([spec, patch]) => ({ spec, patch })) +} + +/** Verify each build-time tool pin still appears in its owning script. */ +function verifyBuildTimePins(): void { + for (const tool of BUILD_TIME_TOOLS) { + const text = readFileSync(resolve(root, tool.pinSource), 'utf8') + if (!text.includes(tool.name)) { + throw new Error(`gen-third-party-notices: ${tool.pinSource} no longer references ${tool.name}; update BUILD_TIME_TOOLS.`) + } + } +} + +/** Render one npm dependency table. */ +function renderNpmTable(deps: ExternalDep[]): string { + const lines = ['| Package | License |', '| --- | --- |'] + for (const dep of deps) lines.push(`| [\`${dep.name}\`](${dep.repo}) | ${dep.license} |`) + return lines.join('\n') +} + +/** Render the complete notices document. */ +function render(): string { + verifyBuildTimePins() + const npm = collectNpmDeps() + const runtimeDeps = npm.filter(dep => dep.runtime) + const devDeps = npm.filter(dep => !dep.runtime) + const vendored = collectVendored() + const python = collectPython() + const patched = collectPatched() + + const nonPermissiveDev = devDeps.filter(dep => dep.license.startsWith('LGPL') || dep.license.startsWith('MPL')) + const patchedLines = patched.map(({ spec, patch }) => `- \`${spec}\` — [\`${patch}\`](${patch})`) + + return ` + +# Third-Party Notices + +DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party open-source software listed below. Each project remains under its own license; nothing in this file changes those terms. + +This file lists **direct** dependencies declared by the workspace, generated from the workspace manifests by \`scripts/gen-third-party-notices.ts\` and verified fresh by \`pnpm run verify-third-party-notices\` (part of \`doc-sync\`). The complete npm transitive closure, with exact pinned versions, is recorded in [\`pnpm-lock.yaml\`](pnpm-lock.yaml) (inspect it with \`pnpm licenses list\`); the Python closure is recorded in [\`python/sdk/uv.lock\`](python/sdk/uv.lock). + +## Vendored source (\`vendor/\`) + +The Cordis framework and its foundation libraries are source-vendored into this repository rather than consumed from npm. All are MIT-licensed; each directory preserves its upstream \`LICENSE\` file. Exact upstream commits and local modifications are recorded in [\`vendor/README.md\`](vendor/README.md). + +| Package | Upstream | License | +| --- | --- | --- | +${vendored.map(row => `| \`${row.npmName}\` | [${row.upstream.replace('https://', '')}](${row.upstream}) | MIT |`).join('\n')} + +## Runtime npm dependencies + +External packages that a workspace package resolves at runtime. \`scripts/install.sh\` installs this repository itself, so the tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI/TUI, the Web UI, and the Python SDK runtime load by default. + +${renderNpmTable(runtimeDeps)} + +pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification: + +${patchedLines.join('\n')} + +## Development-only npm dependencies + +External packages declared only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace. They are not part of any shipped runtime artifact. + +${renderNpmTable(devDeps)} + +${nonPermissiveDev.map(dep => `\`${dep.name}\` (${dep.license})`).join(' and ')} run only as development tooling; their code is not linked into or distributed with any DeepSeek Harness artifact. + +## Python SDK dependencies (\`python/\`) + +Direct dependencies of the \`pyproject.toml\` manifests, plus \`uv\` as the development workflow tool. + +| Package | License | Role | +| --- | --- | --- | +${python.map(dep => `| [\`${dep.name}\`](${dep.repo}) | ${dep.license} | ${dep.role} |`).join('\n')} +| [\`uv\`](https://github.com/astral-sh/uv) | MIT / Apache-2.0 | development workflow tool | + +## Fetched at build time + +| Package | License | Role | +| --- | --- | --- | +${BUILD_TIME_TOOLS.map(tool => `| [\`${tool.name}\`](${tool.repo}) | ${tool.license} | ${tool.role} |`).join('\n')} + +## First-party sibling releases + +\`node-addon-landlock-run\` (and its platform packages) is released from a DeepSeek Harness sibling repository under BSD 3-Clause. It is listed here for completeness; it is first-party, not third-party. +` +} + +/** CLI entry: default writes the notices, `--check` fails if the committed copy + * is stale. Guarded behind an entry-point check so importing this module for + * tests neither regenerates the committed file nor calls process.exit. */ +function main(): void { + const content = render() + if (process.argv.includes('--check')) { + let committed: string | null = null + try { + committed = readFileSync(resolve(root, OUT), 'utf8') + } catch { + // Only ENOENT (not yet generated) is expected; a present-but-unreadable + // file is not a state this repo produces, and the remedy is the same. + committed = null + } + if (committed === content) { + console.log(`gen-third-party-notices: ${OUT} is up to date.`) + process.exit(0) + } + console.error(`gen-third-party-notices: ${OUT} is stale. Run \`pnpm run gen-third-party-notices\` and commit ${OUT}.`) + process.exit(1) + } + + writeFileSync(resolve(root, OUT), content) + console.log(`gen-third-party-notices: wrote ${OUT}.`) +} + +// Run only when invoked as a script, not when imported by a test. +if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) { + main() +} diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 167d226ff8..a5c1870a51 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -472,6 +472,7 @@ function docSyncLeafGates(options: { pnpmScript('tool-catalog', 'verify-tool-catalog', { label: 'tool catalog' }), pnpmScript('config-catalog', 'verify-config-catalog', { label: 'config catalog' }), pnpmScript('persistence-catalog', 'verify-persistence-catalog', { label: 'persistence catalog' }), + pnpmScript('third-party-notices', 'verify-third-party-notices', { label: 'third-party notices' }), pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }), pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }), pnpmScript('markdown-wrap', 'verify-md-wrap', { label: 'markdown wrap' }), diff --git a/scripts/snapshots/translation-prompt-v4/request-response.expected.json b/scripts/snapshots/translation-prompt-v4/request-response.expected.json index 78255907c3..e4eb2a3e39 100644 --- a/scripts/snapshots/translation-prompt-v4/request-response.expected.json +++ b/scripts/snapshots/translation-prompt-v4/request-response.expected.json @@ -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## Install\n\nInstall `dsh` with one command:\n\n```sh\ncurl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/master/scripts/install.sh | sh\n```\n\nThe installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, and prompts for a DeepSeek API key.\n\nThe installer keeps every checkout under `~/.dsh/source`: the master clone at `~/.dsh/source/master` and each install's staging checkout as a git worktree `~/.dsh/source/staging-`. The stable symlink `~/.dsh/source/current` points at the active staging worktree, and `dsh` in `~/.local/bin` links to `current/bin/dsh`, so an upgrade repoints one symlink and the `dsh` on PATH never moves. Re-running the command adds a fresh staging worktree from an updated master and repoints `current` at it. See [`scripts/install.sh`](scripts/install.sh) for alternate install locations and other options.\n\n## Use DeepSeek Harness\n\n### Web UI\n\nFor the recommended local interface, build the frontend after installation and after each update, then start the Web UI. Resolve the running checkout from the `dsh` launcher so the command holds regardless of which staging worktree is current (the launcher resolves through the stable `current` symlink):\n\n```sh\ndsh_bin=$(cd \"$(dirname \"$(command -v dsh)\")\" && pwd -P)/$(basename \"$(command -v dsh)\")\nwhile [ -L \"$dsh_bin\" ]; do\n link=$(readlink \"$dsh_bin\")\n case $link in /*) dsh_bin=$link ;; *) dsh_bin=$(cd \"$(dirname \"$dsh_bin\")\" && cd \"$(dirname \"$link\")\" && pwd -P)/$(basename \"$link\") ;; esac\ndone\ndsh_dir=$(cd \"$(dirname \"$dsh_bin\")/..\" && pwd -P)\npnpm --dir \"$dsh_dir\" run build && pnpm --dir \"$dsh_dir\" run build:web\ndsh web\n```\n\nThe Web UI is served at `http://127.0.0.1:3080` by default.\n\n### TUI\n\nStart the full-screen terminal interface:\n\n```sh\ndsh\n```\n\n### Headless\n\nRun one task, print the final answer, and exit:\n\n```sh\ndsh -p \"summarize this workspace\"\n```\n\n## Why DeepSeek Harness\n\nBuilt-in capabilities cover file reading, editing, and search; shell execution; reusable skills; task tracking; subagents and workflows; persistent sessions; and context compaction. The TUI also 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- **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/cordis/tool-cordis/README.md).\n\n## Community\n\nFollow DeepSeek Harness on Twitter for project updates.\n\n## Development\n\n```sh\npnpm install\npnpm run test:coverage\n```\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 pre-release.\n\n## License\n\n[BSD 3-Clause](LICENSE)\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## Install\n\nInstall `dsh` with one command:\n\n```sh\ncurl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/master/scripts/install.sh | sh\n```\n\nThe installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, and prompts for a DeepSeek API key.\n\nThe installer keeps every checkout under `~/.dsh/source`: the master clone at `~/.dsh/source/master` and each install's staging checkout as a git worktree `~/.dsh/source/staging-`. The stable symlink `~/.dsh/source/current` points at the active staging worktree, and `dsh` in `~/.local/bin` links to `current/bin/dsh`, so an upgrade repoints one symlink and the `dsh` on PATH never moves. Re-running the command adds a fresh staging worktree from an updated master and repoints `current` at it. See [`scripts/install.sh`](scripts/install.sh) for alternate install locations and other options.\n\n## Use DeepSeek Harness\n\n### Web UI\n\nFor the recommended local interface, build the frontend after installation and after each update, then start the Web UI. Resolve the running checkout from the `dsh` launcher so the command holds regardless of which staging worktree is current (the launcher resolves through the stable `current` symlink):\n\n```sh\ndsh_bin=$(cd \"$(dirname \"$(command -v dsh)\")\" && pwd -P)/$(basename \"$(command -v dsh)\")\nwhile [ -L \"$dsh_bin\" ]; do\n link=$(readlink \"$dsh_bin\")\n case $link in /*) dsh_bin=$link ;; *) dsh_bin=$(cd \"$(dirname \"$dsh_bin\")\" && cd \"$(dirname \"$link\")\" && pwd -P)/$(basename \"$link\") ;; esac\ndone\ndsh_dir=$(cd \"$(dirname \"$dsh_bin\")/..\" && pwd -P)\npnpm --dir \"$dsh_dir\" run build && pnpm --dir \"$dsh_dir\" run build:web\ndsh web\n```\n\nThe Web UI is served at `http://127.0.0.1:3080` by default.\n\n### TUI\n\nStart the full-screen terminal interface:\n\n```sh\ndsh\n```\n\n### Headless\n\nRun one task, print the final answer, and exit:\n\n```sh\ndsh -p \"summarize this workspace\"\n```\n\n## Why DeepSeek Harness\n\nBuilt-in capabilities cover file reading, editing, and search; shell execution; reusable skills; task tracking; subagents and workflows; persistent sessions; and context compaction. The TUI also 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- **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/cordis/tool-cordis/README.md).\n\n## Community\n\nFollow DeepSeek Harness on Twitter for project updates.\n\n## Development\n\n```sh\npnpm install\npnpm run test:coverage\n```\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 pre-release.\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" }, { "role": "assistant", - "content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent(编程智能体)。\n\n它采用了**一切皆插件**的架构。\n\n## 安装\n\n使用一条命令安装 `dsh`:\n\n```sh\ncurl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/master/scripts/install.sh | sh\n```\n\n安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥。\n\n安装器会把所有检出都放在 `~/.dsh/source` 下:master 克隆位于 `~/.dsh/source/master`,每次安装的 staging 检出是一个 git worktree `~/.dsh/source/staging-<时间戳>`。稳定符号链接 `~/.dsh/source/current` 指向当前生效的 staging worktree,`~/.local/bin` 中的 `dsh` 链接到 `current/bin/dsh`,因此升级只需重指一个符号链接,PATH 上的 `dsh` 从不移动。再次运行该命令会基于更新后的 master 新增一个 staging worktree,并把 `current` 重指到它。其他安装位置和选项见 [`scripts/install.sh`](scripts/install.sh)。\n\n## 使用 DeepSeek Harness\n\n### Web UI\n\n推荐在本地使用 Web UI。安装完成后以及每次更新后,请先构建前端,再启动 Web UI。通过 `dsh` 启动器解析当前运行的检出,这样无论当前是哪个 staging worktree,命令都成立(启动器会经由稳定的 `current` 符号链接解析):\n\n```sh\ndsh_bin=$(cd \"$(dirname \"$(command -v dsh)\")\" && pwd -P)/$(basename \"$(command -v dsh)\")\nwhile [ -L \"$dsh_bin\" ]; do\n link=$(readlink \"$dsh_bin\")\n case $link in /*) dsh_bin=$link ;; *) dsh_bin=$(cd \"$(dirname \"$dsh_bin\")\" && cd \"$(dirname \"$link\")\" && pwd -P)/$(basename \"$link\") ;; esac\ndone\ndsh_dir=$(cd \"$(dirname \"$dsh_bin\")/..\" && pwd -P)\npnpm --dir \"$dsh_dir\" run build && pnpm --dir \"$dsh_dir\" run build:web\ndsh web\n```\n\nWeb UI 默认通过 `http://127.0.0.1:3080` 提供服务。\n\n### TUI\n\n启动全屏终端界面:\n\n```sh\ndsh\n```\n\n### Headless\n\n运行一项任务,打印最终答案后退出:\n\n```sh\ndsh -p \"summarize this workspace\"\n```\n\n## 为什么选择 DeepSeek Harness\n\n内置功能涵盖文件读取、编辑与搜索、shell 执行、可复用 skill(技能)、任务跟踪、subagent 与工作流、持久化会话,以及上下文压缩(context compaction)。TUI 还包含 Plan Mode。\n\n- **一切皆插件。** 模型、工具、策略、存储、上下文管理和界面均可组合为 [Cordis 插件](docs/user/develop/basic/index.md),部署方无需 fork agent loop(智能体循环)即可扩展或替换行为。底层设计见[架构文档](docs/architecture.md)。\n- **Code Mode(需显式启用)。** 它会提供 `run_code` 工具和生成的 TypeScript SDK,只有程序输出会重新进入模型上下文。参见 [Code Mode](packages/core/tools/README.md#code-mode)。\n- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/cordis/tool-cordis/README.md)。\n\n## 社区\n\n扫描二维码,或打开 DeepSeek Harness 微信社区申请页面 申请加入。\n\n

\n \"DeepSeek\n

\n\n## 开发\n\n```sh\npnpm install\npnpm run test:coverage\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" + "content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent(编程智能体)。\n\n它采用了**一切皆插件**的架构。\n\n## 安装\n\n使用一条命令安装 `dsh`:\n\n```sh\ncurl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/master/scripts/install.sh | sh\n```\n\n安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥。\n\n安装器会把所有检出都放在 `~/.dsh/source` 下:master 克隆位于 `~/.dsh/source/master`,每次安装的 staging 检出是一个 git worktree `~/.dsh/source/staging-<时间戳>`。稳定符号链接 `~/.dsh/source/current` 指向当前生效的 staging worktree,`~/.local/bin` 中的 `dsh` 链接到 `current/bin/dsh`,因此升级只需重指一个符号链接,PATH 上的 `dsh` 从不移动。再次运行该命令会基于更新后的 master 新增一个 staging worktree,并把 `current` 重指到它。其他安装位置和选项见 [`scripts/install.sh`](scripts/install.sh)。\n\n## 使用 DeepSeek Harness\n\n### Web UI\n\n推荐在本地使用 Web UI。安装完成后以及每次更新后,请先构建前端,再启动 Web UI。通过 `dsh` 启动器解析当前运行的检出,这样无论当前是哪个 staging worktree,命令都成立(启动器会经由稳定的 `current` 符号链接解析):\n\n```sh\ndsh_bin=$(cd \"$(dirname \"$(command -v dsh)\")\" && pwd -P)/$(basename \"$(command -v dsh)\")\nwhile [ -L \"$dsh_bin\" ]; do\n link=$(readlink \"$dsh_bin\")\n case $link in /*) dsh_bin=$link ;; *) dsh_bin=$(cd \"$(dirname \"$dsh_bin\")\" && cd \"$(dirname \"$link\")\" && pwd -P)/$(basename \"$link\") ;; esac\ndone\ndsh_dir=$(cd \"$(dirname \"$dsh_bin\")/..\" && pwd -P)\npnpm --dir \"$dsh_dir\" run build && pnpm --dir \"$dsh_dir\" run build:web\ndsh web\n```\n\nWeb UI 默认通过 `http://127.0.0.1:3080` 提供服务。\n\n### TUI\n\n启动全屏终端界面:\n\n```sh\ndsh\n```\n\n### Headless\n\n运行一项任务,打印最终答案后退出:\n\n```sh\ndsh -p \"summarize this workspace\"\n```\n\n## 为什么选择 DeepSeek Harness\n\n内置功能涵盖文件读取、编辑与搜索、shell 执行、可复用 skill(技能)、任务跟踪、subagent 与工作流、持久化会话,以及上下文压缩(context compaction)。TUI 还包含 Plan Mode。\n\n- **一切皆插件。** 模型、工具、策略、存储、上下文管理和界面均可组合为 [Cordis 插件](docs/user/develop/basic/index.md),部署方无需 fork agent loop(智能体循环)即可扩展或替换行为。底层设计见[架构文档](docs/architecture.md)。\n- **Code Mode(需显式启用)。** 它会提供 `run_code` 工具和生成的 TypeScript SDK,只有程序输出会重新进入模型上下文。参见 [Code Mode](packages/core/tools/README.md#code-mode)。\n- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/cordis/tool-cordis/README.md)。\n\n## 社区\n\n扫描二维码,或打开 DeepSeek Harness 微信社区申请页面 申请加入。\n\n

\n \"DeepSeek\n

\n\n## 开发\n\n```sh\npnpm install\npnpm run test:coverage\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" }, { "role": "user",