Merge remote-tracking branch 'origin/master' into worktree/python-pypi-release-prep

# Conflicts:
#	docs/user/guide/python-sdk.i18n.yaml
#	docs/user/guide/python-sdk.md
#	docs/user/guide/python-sdk.zh.md
#	python/sdk/README.i18n.yaml
#	python/sdk/README.md
#	python/sdk/README.zh.md
This commit is contained in:
Yichen Jiang
2026-08-12 13:49:15 +08:00
975 changed files with 8778 additions and 4714 deletions
+8 -14
View File
@@ -53,7 +53,9 @@ const releaseMemberDirectory = /^(?:packages\/[^/]+\/[^/]+|apps\/[^/]+|vendor\/[
const localArtifactDirs = new Set(['node_modules'])
const appPackageFiles: Readonly<Record<string, readonly string[]>> = {
'@deepseek-ai/dsh': ['lib/*.js', 'config'],
'@deepseek-ai/dsh-frontend': ['dist'],
// The Web build emits sourcemaps for browser debugging; publishing them is
// what the payload policy forbids, so the bundle ships without them.
'@deepseek-ai/dsh-frontend': ['dist', '!dist/**/*.map'],
}
/** The subset of package.json fields this constraint check cares about. */
@@ -129,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'],
@@ -152,7 +153,6 @@ function sameStringList(actual: readonly string[] | undefined, expected: readonl
function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
const extras = manifest.name ? packageFileExtras[manifest.name] ?? [] : []
const typeRTRemoteNavigation = hasTypeRTRemoteNavigation(manifest)
return [
'lib/index.js',
// Every package publishes its invariant ownership companion as a separate
@@ -185,13 +185,8 @@ function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
...hasExportPair(manifest, './client/typert', './lib/typert.client.d.ts', './lib/typert.client.js')
? ['lib/typert.client.js', 'lib/typert.client.d.ts']
: [],
...typeRTRemoteNavigation
? [
'lib/typert.remote-client.js',
'lib/typert.remote-client.d.ts',
'lib/typert.remote-client.d.ts.map',
'src',
]
...hasTypeRTRemoteNavigation(manifest)
? ['lib/typert.remote-client.js', 'lib/typert.remote-client.d.ts']
: [],
]
}
@@ -270,9 +265,8 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
if (manifest.name?.startsWith('@deepseek-ai/')) {
const allowedSources = publicationSourceAllowlist[manifest.name] ?? []
const publicationPolicy = { typeRTRemoteNavigation: hasTypeRTRemoteNavigation(manifest) }
for (const file of manifest.files ?? []) {
if (isForbiddenPublicationFile(file, publicationPolicy) && !allowedSources.includes(file)) {
if (isForbiddenPublicationFile(file) && !allowedSources.includes(file)) {
errors.push(`${label}: package.json files must not publish ${JSON.stringify(file)}`)
}
}
+2 -2
View File
@@ -436,7 +436,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
pkg: '@deepseek-ai/dsh-tool-subagent',
dir: 'tool-subagent',
source: 'packages/subagent/tool-subagent/src/index.ts',
requires: ['ctx.tools', 'ctx.subagents'],
requires: ['ctx.tools', 'ctx.subagents', 'ctx.systemPrompt'],
writes: ['tool/call', 'tool/result', 'child session events through the chosen provider'],
shippedNames: ['subagent', 'subagent_fork'],
async mount(ctx) {
@@ -445,7 +445,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
await ctx.plugin(ToolSubagent, { provider: 'mock' })
},
note:
'The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped compositions load this package once per subagent backend, so the model additionally sees `subagent_fork` bound to the fork backend. Each instance\'s description and `run_in_background` parameter follow its own `backgroundMode` and `enableRunInBackground`, so the two shipped schemas are not identical: `subagent` is `continuable`, while `subagent_fork` stays `one-shot` — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`.',
'The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped compositions load this package once per subagent backend, so the model additionally sees `subagent_fork` bound to the fork backend. Each instance\'s description, `run_in_background` parameter, and system-prompt policy follow its own `backgroundMode` and `enableRunInBackground`, so the two shipped schemas are not identical: `subagent` is `continuable` and defaults omitted calls to background with automatic settlement delivery, while `subagent_fork` stays `one-shot` and defaults them to foreground — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`.',
},
{
pkg: '@deepseek-ai/dsh-tool-subagent-control',
+7 -4
View File
@@ -53,7 +53,9 @@ describe('Oxlint executable contract', () => {
['host package source', 'packages/fs/fs-policy/src', 'packages/fs/fs-policy/tsconfig.json'],
['host package test', 'packages/fs/fs-policy/tests', 'tsconfig.host.json'],
['client package source', 'packages/client/ui-primitives/src', 'packages/client/ui-primitives/tsconfig.json'],
['client package test', 'packages/client/ui-trajectory/tests', 'tsconfig.client.json'],
// A test under packages/client states its face in the filename, so the
// probe carries the Client suffix to reach the Client aggregate.
['client package test', 'packages/client/ui-trajectory/tests', 'tsconfig.client.json', '.client.ts'],
['example', 'examples/headless-agent/tests', 'tsconfig.host.json'],
['website', 'website', 'tsconfig.host.json'],
] as const
@@ -66,8 +68,8 @@ probePromise()
try {
const paths: Array<readonly [label: string, path: string, tsconfig: string]> = []
for (const [label, parent, tsconfig] of probes) {
const path = join(repositoryRoot, parent, `oxlint-contract-${suffix}.ts`)
for (const [label, parent, tsconfig, extension = '.ts'] of probes) {
const path = join(repositoryRoot, parent, `oxlint-contract-${suffix}${extension}`)
await writeFile(path, source)
paths.push([label, relative(repositoryRoot, path), tsconfig])
}
@@ -98,7 +100,8 @@ probePromise()
expect(output).not.toContain('Unmatched file:')
} finally {
await Promise.all([
...probes.map(([, parent]) => rm(join(repositoryRoot, parent, `oxlint-contract-${suffix}.ts`), { force: true })),
...probes.map(([, parent, , extension = '.ts']) =>
rm(join(repositoryRoot, parent, `oxlint-contract-${suffix}${extension}`), { force: true })),
rm(configPath, { force: true }),
])
}
+13 -15
View File
@@ -29,6 +29,9 @@ describe('publication payload policy', () => {
String.raw`src\index.ts`,
'lib/types/index.d.ts.map',
'./lib/types/index.d.ts.map',
'lib/typert.remote-client.d.ts.map',
'lib/client.js.map',
'./lib/client.js.map',
])('rejects static manifest path %s', (file) => {
expect(isForbiddenPublicationFile(file)).toBe(true)
})
@@ -40,11 +43,19 @@ describe('publication payload policy', () => {
])).toThrow('fixture.tgz publishes source file package/src/index.ts')
})
it('rejects declaration maps in packed tarballs', () => {
it('rejects source maps in packed tarballs', () => {
expect(validateFixtureTarball([
'package/package.json',
'package/lib/types/index.d.ts.map',
])).toThrow('fixture.tgz publishes declaration map package/lib/types/index.d.ts.map')
])).toThrow('fixture.tgz publishes source map package/lib/types/index.d.ts.map')
expect(validateFixtureTarball([
'package/package.json',
'package/lib/typert.remote-client.d.ts.map',
])).toThrow('fixture.tgz publishes source map package/lib/typert.remote-client.d.ts.map')
expect(validateFixtureTarball([
'package/package.json',
'package/lib/client.js.map',
])).toThrow('fixture.tgz publishes source map package/lib/client.js.map')
})
it('accepts a clean packed tarball', () => {
@@ -56,19 +67,6 @@ describe('publication payload policy', () => {
])).not.toThrow()
})
it('allows only the TypeRT declaration map and its navigable source tree when requested', () => {
const policy = { typeRTRemoteNavigation: true }
expect(isForbiddenPublicationFile('src/index.ts', policy)).toBe(false)
expect(isForbiddenPublicationFile('lib/typert.remote-client.d.ts.map', policy)).toBe(false)
expect(isForbiddenPublicationFile('lib/types/index.d.ts.map', policy)).toBe(true)
expect(() => {
validateTarballPayload([
'package/lib/typert.remote-client.d.ts.map',
'package/src/index.ts',
], 'fixture.tgz', policy)
}).not.toThrow()
})
it('recognizes only the canonical Host-for-Client export pair', () => {
expect(hasTypeRTRemoteNavigation({
exports: {
+23 -25
View File
@@ -1,11 +1,10 @@
/** Publication payload policy shared by static manifests and packed tarballs. */
/** Publication exceptions required for TypeRT declaration-map navigation. */
export interface PublicationPayloadPolicy {
readonly typeRTRemoteNavigation?: boolean
}
/** Whether a package manifest exports generated Host-for-Client metadata with source navigation. */
/**
* Whether a package manifest exports generated Host-for-Client metadata.
* @param manifest - parsed package manifest to inspect.
* @returns whether the canonical `./remote` export pair is present.
*/
export function hasTypeRTRemoteNavigation(manifest: unknown): boolean {
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) return false
const exportsField = (manifest as Record<string, unknown>).exports
@@ -23,35 +22,34 @@ function payloadPath(file: string): string {
return normalized.startsWith('package/') ? normalized.slice('package/'.length) : normalized
}
/** Whether a package payload path exposes source or declaration-map intermediates. */
export function isForbiddenPublicationFile(
file: string,
policy: PublicationPayloadPolicy = {},
): boolean {
/**
* Whether a package payload path exposes source or map intermediates. Maps
* serve editor navigation during development, where a workspace consumer
* resolves their source through the package link; a published map resolves
* nothing, so no payload publishes one.
* @param file - manifest path or tarball member to classify.
* @returns whether publishing this path is forbidden.
*/
export function isForbiddenPublicationFile(file: string): boolean {
const normalized = payloadPath(file)
if (policy.typeRTRemoteNavigation === true
&& (normalized === 'src'
|| normalized.startsWith('src/')
|| normalized === 'lib/typert.remote-client.d.ts.map')) {
return false
}
return normalized === 'src'
|| normalized.startsWith('src/')
|| normalized.endsWith('.d.ts.map')
|| normalized.endsWith('.js.map')
}
/** Reject source and declaration-map members in a packed npm tarball. */
export function validateTarballPayload(
files: readonly string[],
context: string,
policy: PublicationPayloadPolicy = {},
): void {
/**
* Reject source and map members in a packed npm tarball.
* @param files - tarball members to validate.
* @param context - tarball identity named in the failure.
*/
export function validateTarballPayload(files: readonly string[], context: string): void {
for (const file of files) {
if (!isForbiddenPublicationFile(file, policy)) continue
if (!isForbiddenPublicationFile(file)) continue
const normalized = payloadPath(file)
if (normalized === 'src' || normalized.startsWith('src/')) {
throw new Error(`${context} publishes source file ${file}`)
}
throw new Error(`${context} publishes declaration map ${file}`)
throw new Error(`${context} publishes source map ${file}`)
}
}
+3 -7
View File
@@ -18,7 +18,7 @@ import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep
import { createInterface } from 'node:readline/promises'
import { pathToFileURL } from 'node:url'
import { parseArgs } from 'node:util'
import { hasTypeRTRemoteNavigation, validateTarballPayload } from './publication-payload.ts'
import { validateTarballPayload } from './publication-payload.ts'
const DEFAULT_REGISTRY = 'https://registry.npm.harnessment.com'
const DEFAULT_OUTPUT_DIRECTORY = '.artifacts/npm-baseline'
@@ -323,9 +323,7 @@ class ReleaseBundle {
throw new Error(`unexpected or duplicate packed package: ${artifact.name}`)
}
if (expected.origin === 'harness') {
validateTarballPayload(artifact.files, tarball, {
typeRTRemoteNavigation: hasTypeRTRemoteNavigation(artifact.manifest),
})
validateTarballPayload(artifact.files, tarball)
}
if (artifact.version !== version) {
throw new Error(`${tarball} has version ${artifact.version}; expected ${version}`)
@@ -401,9 +399,7 @@ class ReleaseBundle {
}
const artifact = inspectTarball(path, runner)
if (pkg.origin === 'harness') {
validateTarballPayload(artifact.files, pkg.tarball, {
typeRTRemoteNavigation: hasTypeRTRemoteNavigation(artifact.manifest),
})
validateTarballPayload(artifact.files, pkg.tarball)
}
if (artifact.name !== pkg.name || artifact.version !== this.manifest.version) {
throw new Error(`tarball identity mismatch: ${pkg.tarball}`)
+2 -4
View File
@@ -11,7 +11,7 @@
import { globSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { hasTypeRTRemoteNavigation, validateTarballPayload } from '../publication-payload.ts'
import { validateTarballPayload } from '../publication-payload.ts'
/** Dependency sections that constrain publish order: a consumer must publish after its dependency. */
const ORDER_SECTIONS = ['dependencies', 'optionalDependencies'] as const
@@ -225,9 +225,7 @@ class DshFamily extends ReleaseFamily {
* @param files - every path inside its tarball.
*/
validatePayload(member: ReleaseMember, files: readonly string[]): void {
validateTarballPayload(files, member.name, {
typeRTRemoteNavigation: hasTypeRTRemoteNavigation(member.manifest),
})
validateTarballPayload(files, member.name)
}
readonly installedEntry = { packageName: '@deepseek-ai/dsh', binPath: 'lib/bin.js' }
+7 -5
View File
@@ -89,9 +89,9 @@ const GENERIC_SKIPS: readonly GenericSkip[] = [
// the creator flow stages and which id the roster reports.
{ file: 'packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/src/client/index.ts', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/tests/apply.spec.ts', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/tests/locales.spec.ts', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/tests/section.spec.tsx', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/tests/apply.client.spec.ts', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/tests/locales.client.spec.ts', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/tests/section.client.spec.tsx', upstream: ['cordis'] },
{ file: 'apps/cli/tests/web-agent-presets.e2e.ts', upstream: ['cordis'] },
{ file: 'apps/web/tests/agent-preset-authoring.e2e.ts', upstream: ['cordis'] },
{ file: 'packages/preset/agent-presets/tests/session.spec.ts', upstream: ['cordis'] },
@@ -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'] },
@@ -126,7 +128,7 @@ const POSTCONDITIONS: readonly PostCondition[] = [
{ file: 'knip.json', text: '@cordisjs', count: 0 },
{ file: 'pnpm-workspace.yaml', text: 'cordis@4.0.0-rc.7', count: 0 },
// The preset ids in this table are product data, not package names.
{ file: 'packages/client/ui-agent-preset/tests/locales.spec.ts', text: '[\'cordis\', \'presetCordisName\'', count: 1 },
{ file: 'packages/client/ui-agent-preset/tests/locales.client.spec.ts', text: '[\'cordis\', \'presetCordisName\'', count: 1 },
// The preset id the shipped composition documents to its own model.
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', text: 'The `cordis` agent preset', count: 1 },
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', text: 'corrupting the `cordis` preset', count: 1 },
@@ -323,7 +325,7 @@ const VENDORED_LIBRARY = /^@deepseek-ai\\/(cosmokit|schemastery)(\\/|$)/
{
// The real package references in files whose other `cordis` strings are preset ids.
id: 'agent-preset-spec-framework-import',
file: 'packages/client/ui-agent-preset/tests/apply.spec.ts',
file: 'packages/client/ui-agent-preset/tests/apply.client.spec.ts',
find: "import { Context } from 'cordis'",
replace: "import { Context } from '@deepseek-ai/cordis'",
expect: 1,
@@ -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/).\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/)。\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",
+3 -3
View File
@@ -304,7 +304,7 @@
{
"doc": "docs/subsystems/commands.md",
"symbol": "CommandInputDescriptor",
"source": "packages/interaction/commands/src/index.ts"
"source": "packages/interaction/commands/src/types.ts"
},
{
"doc": "docs/subsystems/commands.md",
@@ -319,12 +319,12 @@
{
"doc": "docs/subsystems/commands.md",
"symbol": "CommandResult",
"source": "packages/interaction/commands/src/index.ts"
"source": "packages/interaction/commands/src/types.ts"
},
{
"doc": "docs/subsystems/commands.md",
"symbol": "CommandDescriptor",
"source": "packages/interaction/commands/src/index.ts"
"source": "packages/interaction/commands/src/types.ts"
},
{
"doc": "docs/subsystems/commands.md",
+39
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)
})
})
+90 -33
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,20 +38,23 @@ 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'
/**
* The backends the chooser mounts by runtime string (mirror of its exported
* `BACKEND_PACKAGES`), invisible to yml-row scanning: a composition mounting
* the chooser must resolve both, or keyless Linux CI (which only ever
* resolves `browse`) hides a dropped `-native` dependency until a macOS boot.
* The packages the chooser mounts by runtime string (mirror of its exported
* `BACKEND_PACKAGES` and `SURFACE_PACKAGES`), invisible to yml-row scanning: a
* composition mounting the chooser must resolve every one, or keyless Linux CI
* (which only ever resolves `browse`) hides a dropped `-native` dependency
* until a macOS boot.
*/
const CHOOSER_BACKEND_PACKAGES = [
'@deepseek-ai/dsh-host-directory-picker-native',
'@deepseek-ai/dsh-host-directory-picker-browse',
'@deepseek-ai/dsh-client-ui-directory-picker',
'@deepseek-ai/dsh-client-ui-directory-picker-native',
]
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
kind: 'scalar',
@@ -61,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.`)
}
}
/**
@@ -406,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}`
}
}
@@ -85,6 +85,8 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/ui-question': { kind: 'indirect', reason: 'The package mounts dsh-tool-ask-user; that tool owns the model-visible schema and answer rendering.' },
'packages/client/ui-trajectory': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-workspace': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-directory-picker': { kind: 'none', reason: 'Browser-side directory-browsing surface; registers nothing model-facing.' },
'packages/client/ui-directory-picker-native': { kind: 'none', reason: 'Browser-side surface driving the host OS chooser; registers nothing model-facing.' },
'packages/client/ui-theme': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'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.' },