From 1d4dcf3b576a913ca3715167aba389d87c03dd3c Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:55:26 +0800 Subject: [PATCH 01/16] refactor(python): remove the private direct-config carrier Delete @deepseek-ai/dsh-sdk-python-runtime and its packaged-bin entry now that Python uses the repository's dsh application launcher. Remove the package's project reference, Knip entry, workspace-policy exception, executable allowlist entry, and README-model classification together so no tooling preserves the old exception. This commit is deliberately mechanical: it removes the obsolete package and gate accommodations without introducing the replacement launch behavior. The following commits add the packaged dsh runtime and Python profile API, keeping the architecture change separate from deletion noise. --- knip.json | 5 -- packages/sdk/python-runtime/README.i18n.yaml | 6 --- packages/sdk/python-runtime/README.md | 34 ------------ packages/sdk/python-runtime/README.zh.md | 34 ------------ packages/sdk/python-runtime/package.json | 48 ----------------- packages/sdk/python-runtime/src/index.ts | 10 ---- packages/sdk/python-runtime/src/invariant.ts | 30 ----------- .../sdk/python-runtime/src/packaged-bin.ts | 12 ----- packages/sdk/python-runtime/src/runner.ts | 54 ------------------- packages/sdk/python-runtime/tsconfig.json | 24 --------- packages/sdk/python-runtime/tsdown.config.ts | 17 ------ scripts/check-workspace-constraints.spec.ts | 24 --------- scripts/check-workspace-constraints.ts | 12 ----- .../verify-application-entrypoints.spec.ts | 10 ++-- scripts/verify-application-entrypoints.ts | 7 ++- .../verify-package-readme-model-experience.ts | 1 - tsconfig.host.json | 1 - 17 files changed, 9 insertions(+), 320 deletions(-) delete mode 100644 packages/sdk/python-runtime/README.i18n.yaml delete mode 100644 packages/sdk/python-runtime/README.md delete mode 100644 packages/sdk/python-runtime/README.zh.md delete mode 100644 packages/sdk/python-runtime/package.json delete mode 100644 packages/sdk/python-runtime/src/index.ts delete mode 100644 packages/sdk/python-runtime/src/invariant.ts delete mode 100644 packages/sdk/python-runtime/src/packaged-bin.ts delete mode 100644 packages/sdk/python-runtime/src/runner.ts delete mode 100644 packages/sdk/python-runtime/tsconfig.json delete mode 100644 packages/sdk/python-runtime/tsdown.config.ts diff --git a/knip.json b/knip.json index b6c4e33254..6e03ebf7da 100644 --- a/knip.json +++ b/knip.json @@ -550,11 +550,6 @@ "zod" ] }, - "packages/sdk/python-runtime": { - "project": [ - "src/**/*.ts" - ] - }, "packages/subagent/subagent-spawn-in-process": { "entry": [ "tests/**/*.spec.ts", diff --git a/packages/sdk/python-runtime/README.i18n.yaml b/packages/sdk/python-runtime/README.i18n.yaml deleted file mode 100644 index d2b2859c7c..0000000000 --- a/packages/sdk/python-runtime/README.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# 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 packages/sdk/python-runtime/README.md -README.md: 291ad3edaff8182007079de8e41f33e91546e5d7 -README.zh.md: 54001ccfffb356db290d7c7b38db41070d8dd661 diff --git a/packages/sdk/python-runtime/README.md b/packages/sdk/python-runtime/README.md deleted file mode 100644 index 291ad3edaf..0000000000 --- a/packages/sdk/python-runtime/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# @deepseek-ai/dsh-sdk-python-runtime - -English | [中文](README.zh.md) - -Private direct-config carrier for the temporarily unchanged Python SDK runtime. Its [`jsonrpc`](../server/README.md) entry serves SDK clients over newline-delimited stdio, while an external `cordis.yml` composes the spine, backends, and serving plugin. This npm package exposes no public bin and is not published; the Python SDK's existing `dsh-jsonrpc-agent-pkg--` [single-executable runtime](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) packages `lib/packaged-bin.js` from the closed deploy tree. Bare plugins resolve from that tree, while relative plugins remain configuration-relative. - -## Config discovery - -The first non-empty channel wins: `$DSH_CORDIS_CONFIG`, then positional `argv[2]`. If neither names an existing file, the packaged entry prints one-line usage to stderr and exits 1; there is no working-directory or built-in fallback. [`dsh-app-boot`](../../boot/app-boot/README.md) makes plugin load failures fatal. This protocol does not use `DSH_SNAPSHOT`. - -A config without `dsh-sdk-jsonrpc-server` is valid and serves nothing; the carrier does not designate a server plugin. - -## Exit lifecycle - -stdin EOF and `SIGTERM` dispose the root to quiescence and exit 0; `SIGINT` exits 130 after the same disposal. EOF may cut off an in-flight turn as documented in the [distribution Agent Note](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md). The `jsonrpc` plugin owns response-before-exit protocol shutdown; both paths are idempotent and safe to race. - -## stdout is the protocol - -stdout carries only JSON-RPC frames. The carrier and boot guards diagnose on stderr, and the config must omit stdout loggers. - -## Model Experience - -Indirectly, through the plugins loaded from the external `cordis.yml`, which own every model-bound prompt, schema, message, and result; this carrier adds none of its own. - -#### KV Cache effect - -No direct invalidation; the named consumer owns any request-prefix changes. - -## Known Limitations and Deferred Work - -- **Temporary direct-config exception** — this private carrier remains outside `dsh --profile sdk` only to preserve the current Python executable and wheel behavior; the later Python runtime migration deletes it and then renames the executable family. -- **The carrier cannot prove that the config serves JSON-RPC** — a valid config with no `dsh-sdk-jsonrpc-server` entry boots successfully and serves nothing. -- **No built-in or default config exists** — every launch must provide `DSH_CORDIS_CONFIG` or a positional path, and deployment owns the complete plugin tree and stdout discipline. -- **stdin EOF cuts off in-flight work** — client disappearance disposes the root immediately; callers that need orderly completion use the protocol-level `shutdown` request. diff --git a/packages/sdk/python-runtime/README.zh.md b/packages/sdk/python-runtime/README.zh.md deleted file mode 100644 index 54001ccfff..0000000000 --- a/packages/sdk/python-runtime/README.zh.md +++ /dev/null @@ -1,34 +0,0 @@ -# @deepseek-ai/dsh-sdk-python-runtime - -[English](README.md) | 中文 - -这是为暂时保持不变的 Python SDK 运行时提供的私有直读配置载体。其 [`jsonrpc`](../server/README.zh.md) 入口通过按换行分隔的 stdio 为 SDK 客户端提供服务,外部 `cordis.yml` 则负责组合主干、后端和服务插件。该 npm 包不公开 bin,也不会发布;Python SDK 既有的 `dsh-jsonrpc-agent-pkg--` [单文件可执行运行时](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)从封闭部署树打包 `lib/packaged-bin.js`。裸插件从该树解析,相对插件仍以配置目录为基准。 - -## 配置发现 - -第一个非空通道生效:先 `$DSH_CORDIS_CONFIG`,再位置参数 `argv[2]`。如果二者都没有指向现有文件,打包入口会向 stderr 打印单行用法并以 1 退出;没有工作目录回退或内置回退。[`dsh-app-boot`](../../boot/app-boot/README.zh.md) 会使插件加载失败成为致命错误。此协议不使用 `DSH_SNAPSHOT`。 - -不含 `dsh-sdk-jsonrpc-server` 的配置仍然有效,只是不提供任何服务;该载体不会指定服务器插件。 - -## 退出生命周期 - -stdin EOF 和 `SIGTERM` 会 dispose(释放资源)根上下文,等待完全停稳后以 0 退出;`SIGINT` 完成同样的 dispose 后以 130 退出。EOF 可能按[分发 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md) 所述截断正在处理的轮次。`jsonrpc` 插件拥有先响应再退出的协议关闭流程;两条路径均幂等,即使发生竞态也安全。 - -## stdout 是协议 - -stdout 只承载 JSON-RPC 帧。该载体和启动守卫在 stderr 上输出诊断,配置必须省略 stdout logger。 - -## 模型体验 - -模型体验由外部 `cordis.yml` 加载的插件间接提供;这些插件负责所有面向模型的提示词、schema、消息和结果,该载体不添加任何内容。 - -#### KV Cache 影响 - -不会直接失效;由上述消费方负责请求前缀的任何变更。 - -## 已知限制与暂缓事项 - -- **临时直读配置例外**:为了保持当前 Python 可执行文件与 wheel 包行为不变,该私有载体暂时不经过 `dsh --profile sdk`;后续 Python 运行时迁移会删除它,之后再重命名可执行文件族。 -- **载体无法证明配置提供 JSON-RPC 服务**:不含 `dsh-sdk-jsonrpc-server` 条目的有效配置也能成功启动,但不会提供任何服务。 -- **不存在内置或默认配置**:每次启动都必须提供 `DSH_CORDIS_CONFIG` 或位置路径;部署方负责完整的插件树和 stdout 纪律。 -- **stdin EOF 会截断正在处理的工作**:客户端消失时立即释放根上下文;需要有序完成的调用方应使用协议级 `shutdown` 请求。 diff --git a/packages/sdk/python-runtime/package.json b/packages/sdk/python-runtime/package.json deleted file mode 100644 index cdae65348f..0000000000 --- a/packages/sdk/python-runtime/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@deepseek-ai/dsh-sdk-python-runtime", - "description": "Private direct-config runtime carrier for the temporarily unchanged Python SDK", - "version": "0.1.1-rc.2", - "private": true, - "repository": { - "type": "git", - "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", - "directory": "packages/sdk/python-runtime" - }, - "type": "module", - "main": "lib/index.js", - "types": "lib/types/index.d.ts", - "exports": { - ".": { - "types": "./lib/types/index.d.ts", - "default": "./lib/index.js" - }, - "./invariant": { - "types": "./lib/types/invariant.d.ts", - "default": "./lib/invariant.js" - }, - "./packaged-bin": { - "types": "./lib/types/packaged-bin.d.ts", - "default": "./lib/packaged-bin.js" - }, - "./src/*": "./src/*", - "./package.json": "./package.json" - }, - "files": [ - "lib/index.js", - "lib/invariant.js", - "lib/packaged-bin.js", - "lib/types/**/*.d.ts" - ], - "license": "MIT", - "dependencies": { - "@deepseek-ai/dsh-app-boot": "workspace:^" - }, - "peerDependencies": { - "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/cordis": "workspace:^" - }, - "devDependencies": { - "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/cordis": "workspace:^" - } -} diff --git a/packages/sdk/python-runtime/src/index.ts b/packages/sdk/python-runtime/src/index.ts deleted file mode 100644 index 74dabd87bf..0000000000 --- a/packages/sdk/python-runtime/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Private Python SDK runtime carrier: its packaged entry discovers an external - * `cordis.yml` and owns process exit. This module exports no composition plugin; - * the config chooses whether to load the - * {@link @deepseek-ai/dsh-sdk-jsonrpc-server} serving plugin. - * - * @module @deepseek-ai/dsh-sdk-python-runtime - */ - -export {} diff --git a/packages/sdk/python-runtime/src/invariant.ts b/packages/sdk/python-runtime/src/invariant.ts deleted file mode 100644 index 79bb5ec1f6..0000000000 --- a/packages/sdk/python-runtime/src/invariant.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Package-owned invariant companion for `@deepseek-ai/dsh-sdk-python-runtime`. - * @module @deepseek-ai/dsh-sdk-python-runtime/invariant - */ - -/* jscpd:ignore-start */ -import type { Context } from '@deepseek-ai/cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' - -const PACKAGE_NAME = '@deepseek-ai/dsh-sdk-python-runtime' - -/** Cordis companion plugin name. */ -export const name = 'sdk-python-runtime-invariant' -/** Service required before the companion can reserve package ownership. */ -export const inject = ['invariants'] - -/** - * No runtime invariant: this composition package owns no independent event stream or mutable data; - * Loader and built-entry tests cover its wiring. - */ -const install: InvariantInstaller = () => {} - -/** - * Register this package's invariant companion. - * @param ctx - Cordis context carrying the invariant service. - * @returns the installed registration's disposer after setup succeeds. - */ -export const apply = (ctx: Context): Promise<() => void> => - Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/sdk/python-runtime/src/packaged-bin.ts b/packages/sdk/python-runtime/src/packaged-bin.ts deleted file mode 100644 index 1c5e5c3e92..0000000000 --- a/packages/sdk/python-runtime/src/packaged-bin.ts +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env node -/** - * Closed-runtime JSON-RPC agent bin. Bare plugins resolve from the installed - * runtime closure while relative plugins remain configuration-relative. - * - * @module @deepseek-ai/dsh-sdk-python-runtime/packaged-bin - */ - -import { runPythonSdkRuntime } from './runner.ts' - -/* v8 ignore next -- exercised through the built Python runtime carriers */ -await runPythonSdkRuntime(import.meta.url) diff --git a/packages/sdk/python-runtime/src/runner.ts b/packages/sdk/python-runtime/src/runner.ts deleted file mode 100644 index 9850c64a0b..0000000000 --- a/packages/sdk/python-runtime/src/runner.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Process lifecycle for the Python SDK's closed direct-config runtime. - * - * @module @deepseek-ai/dsh-sdk-python-runtime/runner - */ - -import { existsSync } from 'node:fs' -import { boot, installFailLoud, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' - -/* v8 ignore start -- composition over tested app-boot/jsonrpc and executable acceptance paths */ -const NAME = 'dsh-jsonrpc-agent' - -/** - * Boot the explicitly selected external configuration and own process exit. - * @param bareModuleBaseUrl - installed-runtime base for bare plugins. - * @returns after process handlers are installed; process lifetime then belongs - * to stdin and signal events. - */ -export async function runPythonSdkRuntime(bareModuleBaseUrl: string): Promise { - installFailLoud(NAME) - loadEnv(NAME) - - // Env wins over argv; empty values are absent. External config defines the deployment. - const fromEnv = process.env['DSH_CORDIS_CONFIG'] - const fromArgv = process.argv[2] - const requested = fromEnv !== undefined && fromEnv !== '' - ? fromEnv - : fromArgv !== undefined && fromArgv !== '' ? fromArgv : undefined - const configPath = requested === undefined ? undefined : resolveConfigPath(requested, undefined) - if (configPath === undefined || !existsSync(configPath)) { - process.stderr.write( - `usage: ${NAME} (or set DSH_CORDIS_CONFIG=, which wins); the config is required — there is no built-in fallback\n`, - ) - process.exit(1) - } - - const ctx = await boot(NAME, configPath, undefined, undefined, bareModuleBaseUrl) - let exiting = false - - async function disposeAndExit(code: number): Promise { - if (exiting) return - exiting = true - try { - await ctx.fiber.dispose() - } finally { - process.exit(code) - } - } - - process.stdin.on('end', () => { void disposeAndExit(0) }) - process.on('SIGTERM', () => { void disposeAndExit(0) }) - process.on('SIGINT', () => { void disposeAndExit(130) }) -} -/* v8 ignore stop */ diff --git a/packages/sdk/python-runtime/tsconfig.json b/packages/sdk/python-runtime/tsconfig.json deleted file mode 100644 index ffd1ce9e41..0000000000 --- a/packages/sdk/python-runtime/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib/types" - }, - "include": [ - "src" - ], - "references": [ - { - "path": "../../../vendor/cordis" - }, - { - "path": "../../../vendor/loader" - }, - { - "path": "../../boot/app-boot" - }, - { - "path": "../../runtime-diagnostics/invariants" - } - ] -} diff --git a/packages/sdk/python-runtime/tsdown.config.ts b/packages/sdk/python-runtime/tsdown.config.ts deleted file mode 100644 index 9b6d5d33a7..0000000000 --- a/packages/sdk/python-runtime/tsdown.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig } from 'tsdown' - -/** Builds each published entry as a self-contained file admitted by the package whitelist. */ -export default defineConfig([ - { - entry: ['lib/types/index.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', - fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, - }, - { - entry: ['lib/types/invariant.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', - fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, - }, - { - entry: ['lib/types/packaged-bin.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', - fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, - }, -]) diff --git a/scripts/check-workspace-constraints.spec.ts b/scripts/check-workspace-constraints.spec.ts index 4bd39ac878..dc3b63227f 100644 --- a/scripts/check-workspace-constraints.spec.ts +++ b/scripts/check-workspace-constraints.spec.ts @@ -1,12 +1,9 @@ /** Experimental-package publication and dependency constraints. */ -import { readFileSync } from 'node:fs' -import { join } from 'node:path' import { describe, expect, it } from 'vitest' import { checkExperimentalDependencyIsolation, checkExperimentalManifest, - checkWorkspaceManifest, type WorkspaceManifest, } from './check-workspace-constraints.ts' @@ -77,24 +74,3 @@ describe('experimental workspace constraints', () => { ]) }) }) - -describe('private Python runtime carrier', () => { - const manifest = JSON.parse( - readFileSync(new URL('../packages/sdk/python-runtime/package.json', import.meta.url), 'utf8'), - ) as WorkspaceManifest['manifest'] - - it('participates in dsh package checks without becoming an npm release member', () => { - expect(checkWorkspaceManifest({ dir: 'packages/sdk/python-runtime', manifest })).toEqual([]) - }) - - it('rejects publication metadata on the private carrier', () => { - const path = join('packages', 'sdk', 'python-runtime', 'package.json') - expect(checkWorkspaceManifest({ - dir: 'packages/sdk/python-runtime', - manifest: { ...manifest, private: false, publishConfig: { access: 'public' } }, - })).toEqual([ - `${path}: @deepseek-ai/dsh-sdk-python-runtime: private carrier must set "private": true`, - `${path}: @deepseek-ai/dsh-sdk-python-runtime: private carrier must omit publishConfig`, - ]) - }) -}) diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index 842e0ca3a7..35e6eb1049 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -54,9 +54,6 @@ const experimentalPackageDirectory = /^packages\/experimental\/[^/]+$/ const experimentalPackageNamePrefix = '@deepseek-ai/dsh-experimental-' /** Directories whose packages this repository publishes: one release member each. */ const releaseMemberDirectory = /^(?:packages\/(?!experimental\/)[^/]+\/[^/]+|apps\/[^/]+|vendor\/[^/]+)$/ -/** Named dsh packages that remain private because another distribution embeds them. */ -const privateCarrierDirectories = new Set(['packages/sdk/python-runtime']) - const localArtifactDirs = new Set(['node_modules']) const appPackageFiles: Readonly> = { '@deepseek-ai/dsh': ['lib/*.js'], @@ -156,8 +153,6 @@ const packageFileExtras: Readonly> = { '@deepseek-ai/dsh-code-runtime-python': ['py/**/*.py'], // The shipped preset compositions travel inside the roster package. '@deepseek-ai/dsh-agent-presets': ['presets'], - // The private Python carrier ships only its closed-resolution entry. - '@deepseek-ai/dsh-sdk-python-runtime': ['lib/packaged-bin.js'], // The argv-prefix runner entry ships beside the lib as its own bundle; // sandbox-local resolves it through the package's ./runner export. tsdown // also shares its generated FFI code through a hashed runtime chunk. @@ -292,13 +287,6 @@ export function checkWorkspaceManifest({ dir, manifest }: WorkspaceManifest): st || manifest.repository.directory !== expectedDirectory) { errors.push(`${label}: published Landlock package repository must use ${repositoryUrl} with directory ${expectedDirectory} for trusted publishing`) } - } else if (privateCarrierDirectories.has(dir)) { - if (manifest.private !== true) { - errors.push(`${label}: private carrier must set "private": true`) - } - if (manifest.publishConfig !== undefined) { - errors.push(`${label}: private carrier must omit publishConfig`) - } } else if (releaseMemberDirectory.test(dir)) { // Release members state that they are publishable: npm refuses a private // package, and the repository field is how a consumer finds the source of diff --git a/scripts/verify-application-entrypoints.spec.ts b/scripts/verify-application-entrypoints.spec.ts index 02ba9a2196..ee590de033 100644 --- a/scripts/verify-application-entrypoints.spec.ts +++ b/scripts/verify-application-entrypoints.spec.ts @@ -74,12 +74,14 @@ describe('application entrypoints', () => { ]) }) - it('accepts the temporary private Python carrier source without an npm bin', () => { + it('rejects a private Python application carrier outside dsh', () => { const root = fixture() - write(root, 'packages/sdk/python-runtime/package.json', JSON.stringify({ private: true })) - write(root, 'packages/sdk/python-runtime/src/packaged-bin.ts', '#!/usr/bin/env node\n') + write(root, 'packages/sdk/rogue-python-runtime/package.json', JSON.stringify({ private: true })) + write(root, 'packages/sdk/rogue-python-runtime/src/bin.ts', '#!/usr/bin/env node\n') - expect(applicationEntrypointViolations(root)).toEqual([]) + expect(applicationEntrypointViolations(root)).toEqual([ + 'packages/sdk/rogue-python-runtime/src/bin.ts: executable source has no application/build/test classification', + ]) }) it('rejects a classified demo wrapper that launches a package entry', () => { diff --git a/scripts/verify-application-entrypoints.ts b/scripts/verify-application-entrypoints.ts index 7dec320e79..b273d6aed6 100644 --- a/scripts/verify-application-entrypoints.ts +++ b/scripts/verify-application-entrypoints.ts @@ -1,7 +1,7 @@ /** * Enforce dsh profiles as the only supported Node application launcher. - * Vendor CLIs, build tools, test tools, and the temporary private Python - * runtime carrier are explicit classifications rather than implicit holes. + * Vendor CLIs, build tools, and test tools are explicit classifications + * rather than implicit holes. */ import { existsSync, globSync, readFileSync } from 'node:fs' @@ -43,7 +43,6 @@ const EXECUTABLE_SOURCE_ALLOWLIST = new Map([ ['packages/experimental/webworker-packer/bin.js', 'private build-only wrapper'], ['packages/experimental/webworker-packer/src/bin.ts', 'private build-only implementation'], ['packages/sdk/client/tests/fake-runtime.ts', 'test-only SDK runtime peer'], - ['packages/sdk/python-runtime/src/packaged-bin.ts', 'temporary private Python runtime carrier'], ['packages/test-support/llm-mock-server/src/bin.ts', 'test-only model server'], ]) @@ -194,6 +193,6 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(p for (const failure of failures) console.error(` ${failure}`) process.exitCode = 1 } else { - console.log('verify-application-entrypoints: dsh is the only supported Node application launcher; the private Python carrier remains the temporary exception.') + console.log('verify-application-entrypoints: dsh is the only supported Node application launcher.') } } diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index 1fb184af82..f83b621965 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -167,7 +167,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/jobs/jobs-local': { kind: 'indirect', reason: 'The registry backend delegates model rendering to producer plugins and dsh-tool-jobs.' }, 'packages/boot/app-boot': { kind: 'indirect', reason: 'Only the loaded plugin tree contributes model context.' }, 'packages/boot/cmdline': { kind: 'none', reason: 'Resolves the process command line before any session exists; configured rows own every model-visible consequence.' }, - 'packages/sdk/python-runtime': { kind: 'indirect', reason: 'Only the externally configured plugin tree contributes model context.' }, 'packages/interaction/permission-presets': { kind: 'indirect', reason: 'The service writes mechanism events rendered by dsh-user-approval and dsh-tool-bash.' }, 'packages/interaction/user-questions': { kind: 'indirect', reason: 'Model-facing consumers render provider answers and seam errors.' }, 'packages/util/timeout': { kind: 'indirect', reason: 'Only timeout consumers render timeout outcomes.' }, diff --git a/tsconfig.host.json b/tsconfig.host.json index b64992cdf1..91fbfd03f7 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -267,7 +267,6 @@ { "path": "./packages/boot/app-boot" }, { "path": "./packages/boot/cmdline" }, { "path": "./packages/sdk/server" }, - { "path": "./packages/sdk/python-runtime" }, { "path": "./packages/test-support/llm-replay" }, { "path": "./packages/typert/generator" }, { "path": "./packages/test-support/acp-snapshot" }, From 809a4c5bade9c97d9a8b61cb4568f9bbfbf9ffa2 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:55:42 +0800 Subject: [PATCH 02/16] fix(app-boot): preserve profile modules inside pkg executables Teach the profile installation fallback to use normal symlinks under Node and real ESM proxy packages under pkg. Each proxy records the source package version, mirrors its explicit runtime subpath exports, and re-exports the virtual /snapshot URLs, so built-in Loader rows and external plugin peers resolve one shared Cordis/module instance from an on-disk profile. Keep proxy healing idempotent, reject foreign real directories, cover root and subpath imports in packaged mode, and expand AggregateError startup diagnostics so concurrent Loader failures retain their individual import causes. This is the reusable packaged-profile mechanism; Python-specific artifact wiring remains in the next commit. --- packages/boot/app-boot/src/index.ts | 6 +- packages/boot/app-boot/src/profile.ts | 146 ++++++++++++++++-- packages/boot/app-boot/tests/app-boot.spec.ts | 22 +++ packages/boot/app-boot/tests/profile.spec.ts | 99 +++++++++++- 4 files changed, 260 insertions(+), 13 deletions(-) diff --git a/packages/boot/app-boot/src/index.ts b/packages/boot/app-boot/src/index.ts index ecfe695f34..9a1d05ffbf 100644 --- a/packages/boot/app-boot/src/index.ts +++ b/packages/boot/app-boot/src/index.ts @@ -1,5 +1,5 @@ /** - * Shared boot glue for `dsh` profiles and the temporarily packaged Python SDK runtime: load the gitignored + * Shared boot glue for `dsh` profiles, including the CLI packaged by the Python runtime wheel: load the gitignored * `.env`, install the fail-loud Loader guards, resolve the config path (snapshot-aware), load the * optional user patch layers from the Harness home (`~/.dsh`), expose its path resolver to * config expressions, and drive the Cordis Loader against a leaf `cordis.yml` until the tree settles. @@ -811,7 +811,9 @@ export async function boot( // original activation error instead of only the wrap chain. let deepest: unknown = cause while (deepest instanceof Error && deepest.cause !== undefined) deepest = deepest.cause - const stack = deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : '' + const stack = deepest instanceof AggregateError + ? `\n${deepest.stack ?? deepest.message}\n${deepest.errors.map(formatActivationError).join('\n')}` + : deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : '' throw new Error(`${binName}: ${stage}: ${detail}${stack}`, { cause }) } } diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts index cbd6074ff9..e886f759b2 100644 --- a/packages/boot/app-boot/src/profile.ts +++ b/packages/boot/app-boot/src/profile.ts @@ -24,9 +24,10 @@ import { createRequire } from 'node:module' import { - existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync, + existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from 'node:fs' import { basename, dirname, join } from 'node:path' +import { pathToFileURL } from 'node:url' import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include' import { resolveDshHome } from '@deepseek-ai/dsh-home-paths' @@ -238,22 +239,140 @@ function ensureSymlink(link: string, target: string): void { } } +interface ModuleProxyManifest { + name: string + version: string + private: true + type: 'module' + exports: Record + dsh: { moduleFallback: { targets: Record } } +} + +interface ModuleProxyRecord { + version?: unknown + dsh?: { moduleFallback?: { targets?: unknown } } +} + +/** Return whether the process reads application modules from pkg's virtual filesystem. */ +function isPackagedExecutable(): boolean { + return (process as NodeJS.Process & { pkg?: unknown }).pkg !== undefined +} + +/** Resolve one package specifier from the dsh installation. */ +function packageEntryFromAnchor(anchor: string, specifier: string): string | undefined { + try { + return createRequire(anchor).resolve(specifier) + } catch { + return undefined + } +} + +/** Resolve every explicit runtime export that an out-of-tree plugin can import. */ +function packageProxySource( + installAnchor: string, + packageName: string, + packageDir: string, +): { version: string; targets: Record } { + const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as { + exports?: unknown + version?: unknown + } + if (typeof manifest.version !== 'string' || manifest.version.length === 0) { + throw new Error(`dsh: installed package ${packageName} must declare a non-empty version`) + } + const declared = manifest.exports + const subpaths = declared !== null && typeof declared === 'object' && !Array.isArray(declared) + && Object.keys(declared).some(key => key.startsWith('.')) + ? Object.keys(declared).filter(key => key === '.' || (key.startsWith('./') && !key.includes('*') && key !== './package.json')) + : ['.'] + const targets: Record = {} + for (const subpath of subpaths) { + const specifier = subpath === '.' ? packageName : packageName + subpath.slice(1) + const entry = packageEntryFromAnchor(installAnchor, specifier) + if (entry !== undefined) targets[subpath] = pathToFileURL(entry).href + } + return { version: manifest.version, targets } +} + +/** + * Materialize a real package proxy whose exports retain pkg's virtual module + * URL. Files outside the executable cannot traverse a symlink into + * `/snapshot`, while an ESM re-export can import that URL and preserves the + * executable's single module instance for out-of-tree plugin peers. + */ +function ensureModuleProxy( + link: string, + packageName: string, + version: string, + targets: Record, +): void { + const proxyExports = Object.fromEntries( + Object.keys(targets).map((subpath, index) => [subpath, `./entry-${index}.js`]), + ) + const manifest: ModuleProxyManifest = { + name: packageName, + version, + private: true, + type: 'module', + exports: proxyExports, + dsh: { moduleFallback: { targets } }, + } + let stat + try { + stat = lstatSync(link) + } catch { + stat = undefined + } + if (stat?.isSymbolicLink()) { + unlinkSync(link) + stat = undefined + } + if (stat !== undefined) { + const marker = join(link, 'package.json') + let existing: ModuleProxyRecord | undefined + try { + existing = JSON.parse(readFileSync(marker, 'utf8')) as ModuleProxyRecord + } catch { + existing = undefined + } + if (existing?.dsh?.moduleFallback?.targets === undefined) { + throw new Error(`dsh: ${link} exists and is not a dsh-managed module proxy; remove it so dsh can manage the installation fallback`) + } + if (existing.version === version + && JSON.stringify(existing.dsh.moduleFallback.targets) === JSON.stringify(targets)) return + rmSync(link, { recursive: true }) + } + mkdirSync(link, { recursive: true }) + writeFileSync(join(link, 'package.json'), JSON.stringify(manifest, undefined, 2) + '\n') + for (const [index, target] of Object.values(targets).entries()) { + const specifier = JSON.stringify(target) + writeFileSync( + join(link, `entry-${index}.js`), + `export * from ${specifier}\nimport * as target from ${specifier}\nexport default target.default\n`, + ) + } +} + /** * Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one - * symlink per package in the dsh app's resolvable dependency CLOSURE (BFS + * entry per package in the dsh app's resolvable dependency CLOSURE (BFS * over `dependencies` from the app manifest), each resolved from its own - * real location. Node's parent-directory walk from any profile finds this + * installation location. Plain Node uses symlinks. A pkg executable writes + * small ESM proxy packages instead because the host filesystem cannot follow + * a symlink into pkg's virtual `/snapshot` tree; the proxy re-exports the + * virtual URL, preserving the executable's single module instance. Node's + * parent-directory walk from any profile finds this * directory after the profile's own `node_modules`, so every in-box plugin * resolves without pnpm ever managing it — the exact "bundles come from the * installation" contract. The closure (not just direct dependencies) is * required for out-of-tree plugins: their peer dependencies name Service * Definition packages (`dsh-compaction`, `dsh-invariants`, ...) that the app - * reaches only through its Service Provider packages. Symlinked packages - * resolve their own dependencies from their real directories (Node's default - * symlink-following), so each package needs only its one flat link. - * Idempotent: correct links are kept and moved installations are - * re-pointed; a stale link to a vanished package stays until its name is - * reused (dangling links are invisible to resolution). + * reaches only through its Service Provider packages. Both a symlink target + * and a proxy's virtual target resolve transitive imports from the original + * package directory, so each package needs one flat fallback entry. + * Idempotent: correct entries are kept and changed installation targets are + * rewritten; under plain Node, a stale dangling link stays until its name is + * reused because resolution cannot discover it. * @param installAnchor - absolute path of the dsh app's package.json. * @param home - the Harness home; defaults to {@link resolveDshHome}. */ @@ -287,7 +406,14 @@ export function healProfilesModuleFallback(installAnchor: string, home: string = for (const [packageName, target] of links) { const link = join(modulesDir, packageName) mkdirSync(dirname(link), { recursive: true }) - ensureSymlink(link, target) + if (isPackagedExecutable()) { + const source = packageProxySource(installAnchor, packageName, target) + if (Object.keys(source.targets).length > 0) { + ensureModuleProxy(link, packageName, source.version, source.targets) + } + } else { + ensureSymlink(link, target) + } } } diff --git a/packages/boot/app-boot/tests/app-boot.spec.ts b/packages/boot/app-boot/tests/app-boot.spec.ts index 8726895c1b..8ae1a21cd2 100644 --- a/packages/boot/app-boot/tests/app-boot.spec.ts +++ b/packages/boot/app-boot/tests/app-boot.spec.ts @@ -771,6 +771,28 @@ describe('boot', () => { ) }) + it('expands a stackless aggregate at the deepest activation cause', async () => { + const dir = tmp() + const aggregate = new AggregateError([ + new Error('first aggregate member'), + 'second aggregate member', + ], 'aggregate activation failure') + delete (aggregate as { stack?: string }).stack + try { + await boot(NAME, join(dir, 'cordis.yml'), undefined, () => { + throw new Error('wrapped aggregate failure', { cause: aggregate }) + }) + expect.fail('boot should reject the aggregate activation failure') + } catch (error) { + expect(error).toBeInstanceOf(Error) + const message = (error as Error).message + expect(message).toContain(`${NAME}: host preparation failed: wrapped aggregate failure`) + expect(message).toContain('aggregate activation failure') + expect(message).toContain('first aggregate member') + expect(message).toContain('second aggregate member') + } + }) + it('reports a pending real Loader fiber and the service unresolved in its own context', async () => { const dir = tmp() writeFileSync(join(dir, 'waiting.mjs'), 'export const inject = ["neverProvided"]\nexport function apply() {}\n') diff --git a/packages/boot/app-boot/tests/profile.spec.ts b/packages/boot/app-boot/tests/profile.spec.ts index 92265d2fdb..3710859380 100644 --- a/packages/boot/app-boot/tests/profile.spec.ts +++ b/packages/boot/app-boot/tests/profile.spec.ts @@ -36,12 +36,18 @@ function stageInstallation(bundles: Record { const fallback = join(home, 'profiles', 'node_modules') expect(lstatSync(join(fallback, 'dsh-app')).isSymbolicLink()).toBe(true) }) + + it('writes real ESM proxies for a packaged executable', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const bundleManifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + bundleManifest.exports = { '.': './index.js', './feature': './feature.js' } + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(bundleManifest)) + writeFileSync(join(bundleDir, 'feature.js'), 'export const feature = "proxied"\n') + const home = tmp() + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + healProfilesModuleFallback(anchor, home) + const fallback = join(home, 'profiles', 'node_modules') + const proxy = join(fallback, 'bundle-a') + expect(lstatSync(proxy).isDirectory()).toBe(true) + const proxyManifest = JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8')) as { + version: unknown + exports: unknown + dsh: { moduleFallback: { targets: Record } } + } + expect(proxyManifest).toMatchObject({ + version: '0.0.0', + exports: { '.': './entry-0.js', './feature': './entry-1.js' }, + }) + expect(proxyManifest.dsh.moduleFallback.targets['.']).toEqual(expect.stringContaining('/bundle-a/index.js')) + await expect(import(join(proxy, 'entry-0.js'))).resolves.toMatchObject({ packageName: 'bundle-a' }) + await expect(import(join(proxy, 'entry-1.js'))).resolves.toMatchObject({ feature: 'proxied' }) + healProfilesModuleFallback(anchor, home) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('requires a package version before writing a packaged proxy', () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + manifest.version = '' + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + expect(() => { healProfilesModuleFallback(anchor, tmp()) }).toThrow( + 'installed package bundle-a must declare a non-empty version', + ) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('replaces plain-node links and stale managed proxies in packaged mode', () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const home = tmp() + healProfilesModuleFallback(anchor, home) + const proxy = join(home, 'profiles', 'node_modules', 'bundle-a') + expect(lstatSync(proxy).isSymbolicLink()).toBe(true) + + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + healProfilesModuleFallback(anchor, home) + expect(lstatSync(proxy).isDirectory()).toBe(true) + const stale = JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8')) as { + version: string + } + stale.version = 'stale' + writeFileSync(join(proxy, 'package.json'), JSON.stringify(stale)) + healProfilesModuleFallback(anchor, home) + expect(JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8'))).toMatchObject({ + version: '0.0.0', + }) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('rejects foreign packaged fallback directories with valid or invalid metadata', () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + for (const metadata of ['{}', '{']) { + const home = tmp() + const proxy = join(home, 'profiles', 'node_modules', 'bundle-a') + mkdirSync(proxy, { recursive: true }) + writeFileSync(join(proxy, 'package.json'), metadata) + expect(() => { healProfilesModuleFallback(anchor, home) }).toThrow( + 'exists and is not a dsh-managed module proxy', + ) + } + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) }) From be7b0645041ef203f3b6516e4623ab63570390cb Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:56:02 +0800 Subject: [PATCH 03/16] feat(python-runtime): package the dsh CLI and profile assets Make the zero-code dsh-python-runtime-closure depend on the real @deepseek-ai/dsh application and every required profile peer, then package apps/cli's built bin instead of the deleted Python carrier. Rename executables to deepseek-harness-sdk-runtime--, update wheel/platform/build workflow discovery, and install a Python dsh console command that requires explicit DSH_HOME before exec. Include profile, bundle, preset, native addon, and shared-library assets needed by the full CLI. Remove the checked-in default cordis.yml and preserve the existing wheel distribution names, Python module names, sidecar validation, and wire identity. Runtime resolution and release tests pin the new artifacts and dev Node carrier. --- .../workflows/build-exe-for-python-sdk.yml | 2 +- .gitignore | 2 +- .gitlab-ci.yml | 2 +- pnpm-lock.yaml | 22 ++----- python/sdk-runtime/hatch_build.py | 4 +- python/sdk-runtime/package.json | 7 ++- python/sdk-runtime/platforms.json | 6 +- python/sdk-runtime/pyproject.toml | 10 +-- .../src/deepseek_harness_runtime/__init__.py | 51 +++++++-------- .../runtime/cordis.yml | 58 ----------------- python/sdk/tests/test_release_version.py | 9 ++- python/sdk/tests/test_runtime_resolution.py | 63 +++++++++++++++---- scripts/build-exe-for-python-sdk.ts | 22 ++++--- scripts/build-python-release.py | 4 +- scripts/translation-pairing.spec.ts | 2 +- scripts/translation-pairing.ts | 4 +- 16 files changed, 125 insertions(+), 143 deletions(-) delete mode 100644 python/sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml diff --git a/.github/workflows/build-exe-for-python-sdk.yml b/.github/workflows/build-exe-for-python-sdk.yml index b9ba5e148e..04501d5deb 100644 --- a/.github/workflows/build-exe-for-python-sdk.yml +++ b/.github/workflows/build-exe-for-python-sdk.yml @@ -235,7 +235,7 @@ jobs: run: | set -euo pipefail platform="${TARGET#node24-}" - exe="$PWD/dist-exe/dsh-jsonrpc-agent-pkg-$platform" + exe="$PWD/dist-exe/deepseek-harness-sdk-runtime-$platform" [ -x "$exe" ] || { echo "::error::$exe missing or not executable"; exit 1; } case "$platform" in linux-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl ;; diff --git a/.gitignore b/.gitignore index 70c355e391..d33bbc8a4e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ tmp/ .idea mise.toml dist-exe/ -python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-* +python/sdk-runtime/src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-* python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/ python/**/__pycache__/ python/**/.pytest_cache/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cf5a093f69..faa9402c42 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,7 +38,7 @@ sdk-wheel: - pnpm install --frozen-lockfile - pnpm run verify-runtime-closure - pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets="$PKG_TARGET" - - EXE="$PWD/dist-exe/dsh-jsonrpc-agent-pkg-$PLATFORM" + - EXE="$PWD/dist-exe/deepseek-harness-sdk-runtime-$PLATFORM" - test -x "$EXE" - uv run --python 3.10 --group test --project python/sdk python scripts/smoke-python-runtime.py --scenario all --exe "$EXE" - python scripts/build-python-release.py --package runtime --tag "$CI_COMMIT_TAG" --platform "$PLATFORM" --runtime-exe "$EXE" --output-dir "release/$PLATFORM" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9aef169a41..9deab88d6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6826,19 +6826,6 @@ importers: specifier: workspace:^ version: link:../../subagent/subagent - packages/sdk/python-runtime: - dependencies: - '@deepseek-ai/dsh-app-boot': - specifier: workspace:^ - version: link:../../boot/app-boot - devDependencies: - '@deepseek-ai/cordis': - specifier: workspace:^ - version: link:../../../vendor/cordis - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../runtime-diagnostics/invariants - packages/sdk/server: dependencies: '@deepseek-ai/schemastery': @@ -9618,6 +9605,9 @@ importers: '@deepseek-ai/cosmokit': specifier: link:../../vendor/cosmokit version: link:../../vendor/cosmokit + '@deepseek-ai/dsh': + specifier: workspace:^ + version: link:../../apps/cli '@deepseek-ai/dsh-acp': specifier: workspace:^ version: link:../../packages/acp/acp @@ -9786,9 +9776,6 @@ importers: '@deepseek-ai/dsh-sdk-protocol': specifier: workspace:^ version: link:../../packages/sdk/protocol - '@deepseek-ai/dsh-sdk-python-runtime': - specifier: workspace:^ - version: link:../../packages/sdk/python-runtime '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../packages/core/session @@ -9819,6 +9806,9 @@ importers: '@deepseek-ai/dsh-session-reference': specifier: workspace:^ version: link:../../packages/context/session-reference + '@deepseek-ai/dsh-session-telemetry': + specifier: workspace:^ + version: link:../../packages/session/session-telemetry '@deepseek-ai/dsh-session-title': specifier: workspace:^ version: link:../../packages/session/session-title diff --git a/python/sdk-runtime/hatch_build.py b/python/sdk-runtime/hatch_build.py index ef5a621f9d..c4083387a6 100644 --- a/python/sdk-runtime/hatch_build.py +++ b/python/sdk-runtime/hatch_build.py @@ -66,7 +66,9 @@ class RuntimeBuildHook(BuildHookInterface): ) expected_executable = matches[0][1] runtime_dir = Path(self.root) / "src" / "deepseek_harness_runtime" / "runtime" - runtime_files = sorted(runtime_dir.glob("dsh-jsonrpc-agent-pkg-*") if runtime_dir.is_dir() else []) + runtime_files = sorted( + runtime_dir.glob("deepseek-harness-sdk-runtime-*") if runtime_dir.is_dir() else [] + ) expected_files = [expected_executable, f"{expected_executable}-rg"] if "-macos-" in expected_executable: expected_files.append(f"{expected_executable}-spawn-helper") diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index dad5db82b7..d5684867e6 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -1,6 +1,6 @@ { - "name": "dsh-sdk-python-runtime-closure", - "description": "Dependency-only deploy root defining the executable and Python runtime closure; pnpm deploy materializes this manifest and node_modules.", + "name": "dsh-python-runtime-closure", + "description": "Dependency-only deploy root defining the dsh executable shipped by the Python runtime wheel.", "version": "0.0.1", "private": true, "type": "module", @@ -10,6 +10,7 @@ "@deepseek-ai/cordis-plugin-loader": "workspace:^", "@deepseek-ai/cordis-plugin-timer": "workspace:^", "@deepseek-ai/cosmokit": "workspace:^", + "@deepseek-ai/dsh": "workspace:^", "@deepseek-ai/dsh-acp": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", @@ -45,7 +46,6 @@ "@deepseek-ai/dsh-hooks-codex": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-sdk-jsonrpc-server": "workspace:^", - "@deepseek-ai/dsh-sdk-python-runtime": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-llm-deepseek": "workspace:^", "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^", @@ -77,6 +77,7 @@ "@deepseek-ai/dsh-session-query": "workspace:^", "@deepseek-ai/dsh-session-query-sqlite": "workspace:^", "@deepseek-ai/dsh-session-reference": "workspace:^", + "@deepseek-ai/dsh-session-telemetry": "workspace:^", "@deepseek-ai/dsh-session-title": "workspace:^", "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", diff --git a/python/sdk-runtime/platforms.json b/python/sdk-runtime/platforms.json index 069378e8cb..e65cd6a735 100644 --- a/python/sdk-runtime/platforms.json +++ b/python/sdk-runtime/platforms.json @@ -1,14 +1,14 @@ { "linux-x64": { "tag": "manylinux_2_28_x86_64", - "executable": "dsh-jsonrpc-agent-pkg-linux-x64" + "executable": "deepseek-harness-sdk-runtime-linux-x64" }, "linux-arm64": { "tag": "manylinux_2_28_aarch64", - "executable": "dsh-jsonrpc-agent-pkg-linux-arm64" + "executable": "deepseek-harness-sdk-runtime-linux-arm64" }, "macos-arm64": { "tag": "macosx_14_0_arm64", - "executable": "dsh-jsonrpc-agent-pkg-macos-arm64" + "executable": "deepseek-harness-sdk-runtime-macos-arm64" } } diff --git a/python/sdk-runtime/pyproject.toml b/python/sdk-runtime/pyproject.toml index 090efc6139..4005314aa0 100644 --- a/python/sdk-runtime/pyproject.toml +++ b/python/sdk-runtime/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "deepseek-harness-runtime-bin" version = "0.0.0.dev0" -description = "Pinned DeepSeek Harness runtime for the Python SDK" +description = "Bundled dsh CLI runtime for the DeepSeek Harness Python SDK" readme = "README.md" requires-python = ">=3.10" license = "MIT" @@ -17,10 +17,12 @@ Documentation = "https://github.com/deepseek-ai/deepseek-harness/blob/master/pyt Issues = "https://github.com/deepseek-ai/deepseek-harness/issues" Source = "https://github.com/deepseek-ai/deepseek-harness" -# Include the injected executable and default config; exclude the dev-only node -# closure from wheels and sdists. +[project.scripts] +dsh = "deepseek_harness_runtime:main" + +# Include the injected dsh executable and sidecars; exclude the dev-only node closure. [tool.hatch.build] -artifacts = ["src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-*"] +artifacts = ["src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-*"] exclude = ["src/deepseek_harness_runtime/runtime/node"] [tool.hatch.build.targets.wheel] diff --git a/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py b/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py index 94aca48d9f..0fc4f416c0 100644 --- a/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py +++ b/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py @@ -1,22 +1,21 @@ -"""Locate the bundled DeepSeek Harness SDK runtime shipped with this package. +"""Locate and execute the bundled dsh CLI shipped with the Python SDK runtime. Two runtime carriers coexist under ``runtime/``, both injected by the repo's ``scripts/build-exe-for-python-sdk.ts`` build (neither is checked into git): - **exe (production)**: single-file Node executables named - ``dsh-jsonrpc-agent-pkg--`` (platform in {linux, macos}, arch in + ``deepseek-harness-sdk-runtime--`` (platform in {linux, macos}, arch in {x64, arm64}) with a sibling ``-rg`` executable; macOS also uses a sibling ``-spawn-helper``. The target machine needs no Node installation. - **node (dev-only)**: the full deploy closure under ``runtime/node/`` (``package.json`` + ``node_modules/``), executed as ``node - runtime/node/node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js`` on a + runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`` on a system Node >= 22.19. It is the current checkout's source build, never selected automatically, and excluded from wheel/sdist distributions. -``runtime/cordis.yml`` IS checked in: it is the default agent configuration -the client SDK injects via ``$DSH_CORDIS_CONFIG`` for zero-config runs — the -runtime itself always requires an explicit config and has no built-in -fallback. +Both carriers execute the same dsh command grammar. The Python SDK selects the +``sdk`` profile and requires an explicit Harness home; the installed ``dsh`` +console command requires ``DSH_HOME`` for the same reason. """ from __future__ import annotations @@ -52,21 +51,6 @@ def bundled_package_dir() -> Path: return root -def bundled_default_config_path() -> Path: - """Path of the checked-in default runtime configuration (``runtime/cordis.yml``). - - The client SDK injects this path via ``$DSH_CORDIS_CONFIG`` when the caller - supplies no config and the launch resolves to the bundled runtime — the - runtime binary itself always demands an explicit config. - """ - path = bundled_package_dir() / "runtime" / "cordis.yml" - if not path.is_file(): - raise FileNotFoundError( - f"deepseek-harness-runtime-bin is missing the default runtime config at {path}" - ) - return path - - def bundled_runtime_path() -> Path: """Absolute path of the bundled single-file runtime executable for the current platform. @@ -78,7 +62,7 @@ def bundled_runtime_path() -> Path: touching callers). """ tag = _current_platform_tag() - path = bundled_package_dir() / "runtime" / f"dsh-jsonrpc-agent-pkg-{tag}" + path = bundled_package_dir() / "runtime" / f"deepseek-harness-sdk-runtime-{tag}" if not path.is_file(): raise FileNotFoundError( f"deepseek-harness-runtime-bin is missing the runtime executable at {path}. " @@ -128,7 +112,7 @@ def _current_platform_tag() -> str: arch = _ARCH_TAGS.get(platform.machine().lower()) if plat is None or arch is None: raise FileNotFoundError( - "no bundled dsh-jsonrpc-agent executable exists for this platform " + "no bundled DeepSeek Harness SDK runtime exists for this platform " f"(sys.platform={sys.platform!r}, machine={platform.machine()!r}); supported: " "linux/macos on x64/arm64. " + _EXE_ACQUISITION_HINT ) @@ -141,9 +125,9 @@ def _node_launch_args() -> tuple[str, str]: node_root / "node_modules" / "@deepseek-ai" - / "dsh-sdk-python-runtime" + / "dsh" / "lib" - / "packaged-bin.js" + / "bin.js" ) if not bin_js.is_file(): raise FileNotFoundError( @@ -161,11 +145,24 @@ def _node_launch_args() -> tuple[str, str]: return (node, str(bin_js)) +def main() -> None: + """Execute the bundled dsh CLI with an explicitly selected Harness home.""" + if not os.environ.get("DSH_HOME", "").strip(): + print( + "dsh: the Python runtime command requires an explicit DSH_HOME; " + "it never uses ~/.dsh implicitly", + file=sys.stderr, + ) + raise SystemExit(2) + argv = (*resolve_bundled_launch_args(), *sys.argv[1:]) + os.execvpe(argv[0], argv, os.environ) + + __all__ = [ "PACKAGE_METADATA_FILENAME", "RUNTIME_MODE_ENV_VAR", - "bundled_default_config_path", "bundled_package_dir", "bundled_runtime_path", + "main", "resolve_bundled_launch_args", ] diff --git a/python/sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml b/python/sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml deleted file mode 100644 index 02f8cac145..0000000000 --- a/python/sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml +++ /dev/null @@ -1,58 +0,0 @@ -# Bundled default config. The runtime still requires an explicit -# $DSH_CORDIS_CONFIG or argv path; the SDK injects this path for bundled -# zero-config launches. SDK-set session-root and cwd variables have manual-run fallbacks. - -# Stdio JSON-RPC server entry; without it the agent has no SDK client. -- id: sdk-jsonrpc-server - name: '@deepseek-ai/dsh-sdk-jsonrpc-server' - -# Agent spine; the SDK server creates agents per sessionId. -- id: agent-core - name: '@deepseek-ai/dsh-agent-spine-demo' - config: - workspaceContext: - maxBytes: 65536 - -# Stock DeepSeek adapters. The adapter resolves DEEPSEEK_API_KEY through the -# credential seam and, with no provider mounted here, from the launching -# environment; DEEPSEEK_BASE_URL follows the same environment ladder. Neither -# is inlined, so this file names no secret and no route. -- id: deepseek-llm-api-extensions - name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' - -- id: session-log-deepseek - name: '@deepseek-ai/dsh-session-log-deepseek' - -- id: plugin-package-inventory-deepseek - name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek' - -- id: llm-deepseek - name: '@deepseek-ai/dsh-llm-deepseek' - -# JSONL persistence; $DSH_SESSION_ROOT wins over ./.sessions in the process cwd. -- id: sessions - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: !!js process.env.DSH_SESSION_ROOT ?? './.sessions' - -# Persistence owns durable storage; this separate policy explicitly selects -# the request, tool-dispatch, and completed-step durability checkpoints. -- id: session-checkpoints - name: '@deepseek-ai/dsh-session-checkpoint-policy' - -# Local bash executor; $DSH_CWD wins over the process cwd. -# Managed child-process groups for the bash executor (spawn/kill/output plumbing). -- id: subprocess - name: '@deepseek-ai/dsh-subprocess-local' - -- id: bash - name: '@deepseek-ai/dsh-bash-local' - config: - cwd: !!js process.env.DSH_CWD ?? process.cwd() - -# Local filesystem provider for workspace instruction loading. This does not -# expose model-facing file tools by itself. -- id: fs-local - name: '@deepseek-ai/dsh-fs-local' - config: - cwd: !!js process.env.DSH_CWD ?? process.cwd() diff --git a/python/sdk/tests/test_release_version.py b/python/sdk/tests/test_release_version.py index b21a485949..deaa65b8a8 100644 --- a/python/sdk/tests/test_release_version.py +++ b/python/sdk/tests/test_release_version.py @@ -59,6 +59,7 @@ def test_pep440_version_spells_a_prerelease_the_python_way() -> None: def test_macos_wheel_tag_does_not_claim_unsupported_node_platforms() -> None: assert build_python_release.PLATFORMS["macos-arm64"][0] == "macosx_14_0_arm64" + assert build_python_release.PLATFORMS["macos-arm64"][1] == "deepseek-harness-sdk-runtime-macos-arm64" def test_platform_manifest_rejects_incomplete_entries(tmp_path: Path) -> None: @@ -88,7 +89,7 @@ def test_stage_sdk_keeps_distribution_module_and_runtime_pin_distinct(tmp_path: def test_stage_runtime_copies_platform_payload( tmp_path: Path, target: str, with_helper: bool ) -> None: - executable = tmp_path / f"dsh-jsonrpc-agent-pkg-{target}" + executable = tmp_path / f"deepseek-harness-sdk-runtime-{target}" executable.write_bytes(b"runtime") executable.chmod(0o755) expected = {executable.name: b"runtime"} @@ -106,10 +107,14 @@ def test_stage_runtime_copies_platform_payload( build_python_release.stage_runtime(destination, "1.2.3", executable, executable.name) runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime" - assert {path.name: path.read_bytes() for path in runtime_dir.glob("dsh-jsonrpc-agent-pkg-*")} == expected + assert { + path.name: path.read_bytes() + for path in runtime_dir.glob("deepseek-harness-sdk-runtime-*") + } == expected pyproject = (destination / "pyproject.toml").read_text() assert 'license = "MIT"' in pyproject assert 'license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md"]' in pyproject + assert 'dsh = "deepseek_harness_runtime:main"' in pyproject assert (destination / "platforms.json").read_bytes() == ( ROOT / "python" / "sdk-runtime" / "platforms.json" ).read_bytes() diff --git a/python/sdk/tests/test_runtime_resolution.py b/python/sdk/tests/test_runtime_resolution.py index 14e90f3283..fc54171b75 100644 --- a/python/sdk/tests/test_runtime_resolution.py +++ b/python/sdk/tests/test_runtime_resolution.py @@ -9,21 +9,12 @@ import pytest from deepseek_harness_runtime import ( RUNTIME_MODE_ENV_VAR, - bundled_default_config_path, bundled_package_dir, + main, resolve_bundled_launch_args, ) -def test_default_config_is_shipped_with_the_package() -> None: - path = bundled_default_config_path() - assert path == bundled_package_dir() / "runtime" / "cordis.yml" - config = path.read_text() - assert "@deepseek-ai/dsh-agent-spine-demo" in config - assert "@deepseek-ai/dsh-session-persistence-jsonl" in config - assert "@deepseek-ai/dsh-session-checkpoint-policy" in config - - def test_unknown_explicit_mode_fails_loud() -> None: with pytest.raises(ValueError, match="expected 'exe' or 'node'"): resolve_bundled_launch_args("bogus") @@ -49,10 +40,10 @@ def test_runtime_requires_spawn_helper_only_on_macos( ) -> None: runtime_dir = tmp_path / "runtime" runtime_dir.mkdir() - linux = runtime_dir / "dsh-jsonrpc-agent-pkg-linux-x64" + linux = runtime_dir / "deepseek-harness-sdk-runtime-linux-x64" linux.touch() Path(f"{linux}-rg").touch() - macos = runtime_dir / "dsh-jsonrpc-agent-pkg-macos-arm64" + macos = runtime_dir / "deepseek-harness-sdk-runtime-macos-arm64" macos.touch() Path(f"{macos}-rg").touch() monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path) @@ -69,9 +60,55 @@ def test_runtime_requires_ripgrep_sidecar( ) -> None: runtime_dir = tmp_path / "runtime" runtime_dir.mkdir() - (runtime_dir / "dsh-jsonrpc-agent-pkg-linux-x64").touch() + (runtime_dir / "deepseek-harness-sdk-runtime-linux-x64").touch() monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path) monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64") with pytest.raises(FileNotFoundError, match="ripgrep sidecar"): runtime.bundled_runtime_path() + + +def test_node_mode_runs_the_deployed_dsh_cli( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + bin_js = tmp_path / "runtime" / "node" / "node_modules" / "@deepseek-ai" / "dsh" / "lib" / "bin.js" + bin_js.parent.mkdir(parents=True) + bin_js.touch() + monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path) + monkeypatch.setattr(runtime.shutil, "which", lambda _name: "/node") + + assert resolve_bundled_launch_args("node") == ("/node", str(bin_js)) + + +def test_python_dsh_command_requires_explicit_home( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + monkeypatch.delenv("DSH_HOME", raising=False) + + with pytest.raises(SystemExit) as excinfo: + main() + + assert excinfo.value.code == 2 + assert "explicit DSH_HOME" in capsys.readouterr().err + + +def test_python_dsh_command_executes_the_bundled_cli( + monkeypatch: pytest.MonkeyPatch +) -> None: + called: dict[str, object] = {} + monkeypatch.setenv("DSH_HOME", "/explicit/home") + monkeypatch.setattr(runtime, "resolve_bundled_launch_args", lambda: ("/runtime",)) + monkeypatch.setattr(runtime.sys, "argv", ["dsh", "plugin", "--profile", "sdk", "list"]) + + def execvpe(file: str, args: tuple[str, ...], env: dict[str, str]) -> None: + called.update(file=file, args=args, home=env.get("DSH_HOME")) + + monkeypatch.setattr(runtime.os, "execvpe", execvpe) + + main() + + assert called == { + "file": "/runtime", + "args": ("/runtime", "plugin", "--profile", "sdk", "list"), + "home": "/explicit/home", + } diff --git a/scripts/build-exe-for-python-sdk.ts b/scripts/build-exe-for-python-sdk.ts index 7516fcabb6..f7d696edfe 100644 --- a/scripts/build-exe-for-python-sdk.ts +++ b/scripts/build-exe-for-python-sdk.ts @@ -1,5 +1,5 @@ /** - * Build the SDK runtime executables and Python node carrier. The fixed + * Build the dsh executables and development Node carrier for the Python runtime wheel. The fixed * `@yao-pkg/pkg --sea` route, deploy flags, and artifact layout are owned by * .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md. * The staged closure is symlink-free, and whole-tree assets cover Cordis's @@ -16,11 +16,11 @@ import { resolveLinuxNodePtyAddon } from './build-exe-for-python-sdk-native-pty. const root = resolve(import.meta.dirname, '..') /** The closure manifest whose dependencies define the executable. */ -const DEPLOY_ROOT_PACKAGE = 'dsh-sdk-python-runtime-closure' -/** The closed-runtime app entry inside the deployed closure. */ -const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js' -/** Stable Python-visible executable basename; rename with the later Python runtime migration. */ -const OUTPUT_BASENAME = 'dsh-jsonrpc-agent-pkg' +const DEPLOY_ROOT_PACKAGE = 'dsh-python-runtime-closure' +/** The sole application launcher inside the deployed closure. */ +const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh/lib/bin.js' +/** Python-visible executable basename. */ +const OUTPUT_BASENAME = 'deepseek-harness-sdk-runtime' /** Default Node major; SEA mode requires at least Node 22. */ const DEFAULT_NODE_RANGE = 'node24' /** Pinned for reproducible builds. */ @@ -47,8 +47,15 @@ const ASSET_GLOBS = [ 'node_modules/**/*.mjs', 'node_modules/**/package.json', 'node_modules/**/*.json', + 'node_modules/**/*.md', + 'node_modules/**/*.dylib', + 'node_modules/**/*.dll', 'node_modules/**/*.node', + 'node_modules/**/*.so', + 'node_modules/**/*.so.*', 'node_modules/**/*.wasm', + 'node_modules/**/*.yaml', + 'node_modules/**/*.yml', ] const PLATFORMS = ['linux', 'macos'] as const @@ -220,8 +227,7 @@ function formatCommand(command: string, args: string[]): string { */ class SingleExeBuild { /** - * The cleared deploy target, pkg input, and Python node-mode carrier. The - * checked-in default `cordis.yml` remains in its parent directory. + * The cleared deploy target, pkg input, and Python node-mode carrier. */ readonly staging = resolve(root, PYTHON_RUNTIME_DIR, PYTHON_NODE_SUBDIR) private readonly outDir = resolve(root, OUT_DIR) diff --git a/scripts/build-python-release.py b/scripts/build-python-release.py index 0b4157f40d..c546c6bb42 100644 --- a/scripts/build-python-release.py +++ b/scripts/build-python-release.py @@ -150,7 +150,7 @@ def copy_package(source: Path, destination: Path) -> None: "*.pyc", "dist", "node_modules", - "dsh-jsonrpc-agent-pkg-*", + "deepseek-harness-sdk-runtime-*", ), ) @@ -246,7 +246,7 @@ def verify_wheel( f"{wheel} has license files {license_files}, expected {expected_license_files}" ) runtime_files = [ - name for name in archive.namelist() if "/runtime/dsh-jsonrpc-agent-pkg-" in name + name for name in archive.namelist() if "/runtime/deepseek-harness-sdk-runtime-" in name ] if package == "runtime": assert platform is not None diff --git a/scripts/translation-pairing.spec.ts b/scripts/translation-pairing.spec.ts index c4dda468fe..e36b19815d 100644 --- a/scripts/translation-pairing.spec.ts +++ b/scripts/translation-pairing.spec.ts @@ -310,7 +310,7 @@ describe('translation scope discovery', () => { 'packages/example/node_modules/dependency/README.md', 'packages/example/lib/README.md', 'coverage/report/README.md', - 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-macos-arm64/README.md', + 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-macos-arm64/README.md', 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/README.md', ])('excludes non-source or non-README path %s', (file) => { expect(isTranslationScopeFile(file)).toBe(false) diff --git a/scripts/translation-pairing.ts b/scripts/translation-pairing.ts index 95b70c48b1..e4c0919a2f 100644 --- a/scripts/translation-pairing.ts +++ b/scripts/translation-pairing.ts @@ -165,7 +165,7 @@ export const TRANSLATION_SCOPE_GLOB_EXCLUDES = [ '**/.pytest_cache/**', 'apps/web/dist/**', '.artifacts/**', - 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-*/**', + 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-*/**', 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/**', 'vendor/**', ] @@ -177,7 +177,7 @@ function isTranslationSourceExcluded(file: string): boolean { || segment.startsWith('.doc-typecheck-') || segment.startsWith('.node-next-types-')) || file.startsWith('apps/web/dist/') - || file.startsWith('python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-') + || file.startsWith('python/sdk-runtime/src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-') || file.startsWith('python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/') } From 56e038b2e33dac3a57bdf14f42992860340ac1b2 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:56:16 +0800 Subject: [PATCH 04/16] feat(python-sdk): launch dsh profiles from explicit homes Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently. Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes. --- python/sdk/src/deepseek_harness/api.py | 32 ++-- python/sdk/src/deepseek_harness/client.py | 82 +++++----- python/sdk/tests/manual_sdk_agent_smoke.py | 30 ++-- python/sdk/tests/test_bundled_runtime.py | 130 ++++++---------- python/sdk/tests/test_client.py | 166 ++++++++++++--------- 5 files changed, 225 insertions(+), 215 deletions(-) diff --git a/python/sdk/src/deepseek_harness/api.py b/python/sdk/src/deepseek_harness/api.py index 9c542c012a..1b33524118 100644 --- a/python/sdk/src/deepseek_harness/api.py +++ b/python/sdk/src/deepseek_harness/api.py @@ -24,11 +24,11 @@ class DeepSeekHarnessConfig: max_tokens: int | None = None cwd: str | None = None runtime_cwd: str | None = None - session_root: str | None = None - cordis: str | None = None + dsh_bin: str | None = None + profile: str = "sdk" + patches: tuple[str, ...] = () + dsh_home: str | None = None env: dict[str, str] = field(default_factory=dict) - runtime_bin: str | None = None - launch_args_override: tuple[str, ...] | None = None request_timeout_seconds: float | None = None shutdown_timeout_seconds: float | None = 1.0 base_url: str | None = None @@ -42,7 +42,6 @@ class RunResult: finish_reason: str | None events: list[JsonObject] notifications: list[Notification] - session_root: str | None = None class DeepSeekHarness: @@ -53,7 +52,13 @@ class DeepSeekHarness: :meth:`close` explicitly when finished, so the subprocess is always reaped. """ - def __init__(self, config: DeepSeekHarnessConfig | None = None, **kwargs: object) -> None: + def __init__( + self, + config: DeepSeekHarnessConfig | None = None, + *, + _launch_args: tuple[str, ...] | None = None, + **kwargs: object, + ) -> None: if config is not None and kwargs: raise TypeError("pass either DeepSeekHarnessConfig or keyword options, not both") self.config = config or DeepSeekHarnessConfig(**kwargs) @@ -61,11 +66,6 @@ class DeepSeekHarness: runtime_cwd = str(Path(self.config.runtime_cwd).resolve()) if self.config.runtime_cwd is not None else cwd self._cwd = cwd env = dict(self.config.env) - if self.config.session_root is not None: - env["DSH_SESSION_ROOT"] = self.config.session_root - if self.config.cordis is not None: - env["DSH_CORDIS_CONFIG"] = self.config.cordis - env["DSH_CWD"] = cwd if self.config.base_url is not None: env["DEEPSEEK_BASE_URL"] = self.config.base_url if self.config.api_key is not None: @@ -73,13 +73,16 @@ class DeepSeekHarness: self._client = HarnessClient( HarnessConfig( - runtime_bin=self.config.runtime_bin, - launch_args_override=self.config.launch_args_override, + dsh_bin=self.config.dsh_bin, + profile=self.config.profile, + patches=self.config.patches, + dsh_home=self.config.dsh_home, cwd=runtime_cwd, env=env, request_timeout_seconds=self.config.request_timeout_seconds, shutdown_timeout_seconds=self.config.shutdown_timeout_seconds, - ) + ), + _launch_args=_launch_args, ) self._initialized = False @@ -179,7 +182,6 @@ class Session: finish_reason=finish_reason(events), events=events, notifications=notifications, - session_root=self.harness.config.session_root, ) diff --git a/python/sdk/src/deepseek_harness/client.py b/python/sdk/src/deepseek_harness/client.py index 5442c7e144..8969bc7901 100644 --- a/python/sdk/src/deepseek_harness/client.py +++ b/python/sdk/src/deepseek_harness/client.py @@ -25,20 +25,28 @@ NotificationFilter: TypeAlias = Callable[[Notification], bool] class HarnessConfig: """Configuration for launching the local DeepSeek Harness SDK runtime.""" - runtime_bin: str | None = None - bridge_bin: str | None = None - launch_args_override: tuple[str, ...] | None = None + dsh_bin: str | None = None + profile: str = "sdk" + patches: tuple[str, ...] = () + dsh_home: str | None = None cwd: str | None = None env: dict[str, str] | None = None request_timeout_seconds: float | None = None shutdown_timeout_seconds: float | None = 1.0 + _launch_args: tuple[str, ...] | None = None class HarnessClient: """Synchronous JSON-RPC client for the DeepSeek Harness SDK runtime over stdio.""" - def __init__(self, config: HarnessConfig | None = None) -> None: + def __init__( + self, + config: HarnessConfig | None = None, + *, + _launch_args: tuple[str, ...] | None = None, + ) -> None: self.config = config or HarnessConfig() + self._launch_args = _launch_args or self.config._launch_args self._proc: subprocess.Popen[str] | None = None self._lock = threading.Lock() self._write_lock = threading.Lock() @@ -65,11 +73,10 @@ class HarnessClient: return with self._lock: self._session_parents.clear() - args = list(self.config.launch_args_override or self._default_launch_args()) env = os.environ.copy() if self.config.env: env.update(self.config.env) - self._inject_bundled_default_config(env) + args = list(self._launch_args or self._default_launch_args(env)) self._proc = subprocess.Popen( args, stdin=subprocess.PIPE, @@ -131,8 +138,15 @@ class HarnessClient: payload["maxTokens"] = max_tokens try: return self.request("initialize", payload, response_model=InitializeResponse) - except BaseException: + except BaseException as error: self.close() + diagnostics = self._runtime_diagnostics() + if isinstance(error, JsonRpcError) and diagnostics: + raise JsonRpcError( + error.code, + f"{error.message}\n{diagnostics}", + error.data, + ) from error raise def session_prompt( @@ -421,37 +435,35 @@ class HarnessClient: parts.append("stderr tail:\n" + "\n".join(self._stderr_lines)) return "\n".join(parts) - def _default_launch_args(self) -> tuple[str, ...]: - if self.config.runtime_bin is not None: - return (self.config.runtime_bin,) - if self.config.bridge_bin is not None: - return (self.config.bridge_bin,) - try: - from deepseek_harness_runtime import resolve_bundled_launch_args - except ImportError as exc: - raise FileNotFoundError( - "Unable to locate the bundled DeepSeek Harness SDK runtime. " - "Install deepseek-harness-runtime-bin or set HarnessConfig.runtime_bin." - ) from exc - return resolve_bundled_launch_args() + def _default_launch_args(self, env: dict[str, str]) -> tuple[str, ...]: + if self.config.dsh_bin is None: + try: + from deepseek_harness_runtime import resolve_bundled_launch_args + except ImportError as exc: + raise FileNotFoundError( + "Unable to locate the bundled DeepSeek Harness dsh runtime. " + "Install deepseek-harness-runtime-bin." + ) from exc + base = resolve_bundled_launch_args() + else: + base = (str(Path(self.config.dsh_bin).expanduser().resolve()),) - def _inject_bundled_default_config(self, env: dict[str, str]) -> None: - """Inject the default config for a bundled launch with no non-empty config. + if self.config.dsh_home is not None: + if not self.config.dsh_home.strip(): + raise ValueError("HarnessConfig requires a non-empty dsh_home") + env["DSH_HOME"] = str(Path(self.config.dsh_home).expanduser().resolve()) + elif not env.get("DSH_HOME", "").strip(): + raise ValueError( + "HarnessConfig requires an explicit dsh_home or non-empty DSH_HOME; " + "the Python SDK never uses ~/.dsh implicitly" + ) - Both bundled carriers require an explicit config. Explicit runtime, - launch-argument, and config channels remain untouched. - """ - uses_bundled_runtime = ( - self.config.launch_args_override is None - and self.config.runtime_bin is None - and self.config.bridge_bin is None + patches = tuple( + argument + for patch in self.config.patches + for argument in ("--patch", str(Path(patch).expanduser().resolve())) ) - if not uses_bundled_runtime or env.get("DSH_CORDIS_CONFIG"): - return - # _default_launch_args already imported the package or raised its install error. - from deepseek_harness_runtime import bundled_default_config_path - - env["DSH_CORDIS_CONFIG"] = str(bundled_default_config_path()) + return (*base, "--profile", self.config.profile, *patches) def _unsubscribe_notifications(self, subscription_id: str) -> None: with self._lock: diff --git a/python/sdk/tests/manual_sdk_agent_smoke.py b/python/sdk/tests/manual_sdk_agent_smoke.py index c0305d9211..acadb495be 100644 --- a/python/sdk/tests/manual_sdk_agent_smoke.py +++ b/python/sdk/tests/manual_sdk_agent_smoke.py @@ -1,4 +1,4 @@ -"""Drive the repo-source JSON-RPC bin through the SDK and a keyless mock SSE server. +"""Drive the repo-source dsh SDK profile through the SDK and a keyless mock SSE server. Requires ``pnpm install`` but no build. This manual test is not collected by pytest; run ``python tests/manual_sdk_agent_smoke.py``. @@ -16,7 +16,6 @@ from pathlib import Path from typing import Any from deepseek_harness import DeepSeekHarness -from deepseek_harness_runtime import bundled_default_config_path class MockCompletionHandler(BaseHTTPRequestHandler): @@ -43,15 +42,16 @@ class MockCompletionHandler(BaseHTTPRequestHandler): def run_smoke(repo_root: Path, keep_sessions: bool) -> None: - session_root = Path(tempfile.mkdtemp(prefix="dsh-sdk-smoke-sessions-")) - runtime_entry = repo_root / "packages/sdk/python-runtime/src/packaged-bin.ts" + dsh_home = Path(tempfile.mkdtemp(prefix="dsh-sdk-smoke-home-")) + session_root = dsh_home / "sessions" + runtime_entry = repo_root / "apps/cli/src/bin.ts" server = ThreadingHTTPServer(("127.0.0.1", 0), MockCompletionHandler) thread = threading.Thread(target=server.serve_forever, name="mock-openai-compatible-server", daemon=True) thread.start() base_url = f"http://127.0.0.1:{server.server_address[1]}" print(f"repo_root={repo_root}") - print(f"session_root={session_root}") + print(f"dsh_home={dsh_home}") print(f"mock_base_url={base_url}") try: @@ -59,10 +59,18 @@ def run_smoke(repo_root: Path, keep_sessions: bool) -> None: model="sdk-smoke-model", cwd=str(repo_root / "python/sdk"), runtime_cwd=str(repo_root), - session_root=str(session_root), - cordis=str(bundled_default_config_path()), - launch_args_override=("node", "--import", "tsx", str(runtime_entry)), + _launch_args=( + "node", + "--import", + "tsx", + str(runtime_entry), + "--profile", + "sdk", + ), env={ + "DSH_HOME": str(dsh_home), + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", "DEEPSEEK_BASE_URL": base_url, "DEEPSEEK_API_KEY": "sdk-smoke-key", }, @@ -92,10 +100,10 @@ def run_smoke(repo_root: Path, keep_sessions: bool) -> None: server.server_close() if keep_sessions: - print(f"kept_session_root={session_root}") + print(f"kept_dsh_home={dsh_home}") else: - shutil.rmtree(session_root) - print("removed temporary session root") + shutil.rmtree(dsh_home) + print("removed temporary dsh home") def main() -> None: diff --git a/python/sdk/tests/test_bundled_runtime.py b/python/sdk/tests/test_bundled_runtime.py index 52d84cd161..3da5247795 100644 --- a/python/sdk/tests/test_bundled_runtime.py +++ b/python/sdk/tests/test_bundled_runtime.py @@ -1,4 +1,4 @@ -"""Keyless boot tests for the production exe and development node carrier. +"""Keyless boot tests for the production exe and development dsh carrier. Each carrier skips independently when absent. The dummy API key only satisfies adapter loading; initialize and shutdown do not call a model. @@ -6,64 +6,39 @@ adapter loading; initialize and shutdown do not call a model. from __future__ import annotations +import json from pathlib import Path import pytest from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig -from deepseek_harness.errors import TransportClosedError -from deepseek_harness_runtime import resolve_bundled_launch_args +from deepseek_harness.errors import JsonRpcError, TransportClosedError +from deepseek_harness_runtime import RUNTIME_MODE_ENV_VAR, resolve_bundled_launch_args _MODES = ("exe", "node") -_REPO_ROOT = Path(__file__).parents[3] -_MINIMAL_CONFIG = _REPO_ROOT / "examples" / "python-sdk-agent" / "minimal.cordis.yml" - -# The config must include the JSON-RPC serving plugin. -_CORDIS_YML = """\ -- id: sdk-jsonrpc-server - name: '@deepseek-ai/dsh-sdk-jsonrpc-server' -- id: agent-core - name: '@deepseek-ai/dsh-agent-spine-demo' - config: - workspaceContext: false -- id: sessions - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: './sessions' -- id: session-checkpoints - name: '@deepseek-ai/dsh-session-checkpoint-policy' -- id: subprocess - name: '@deepseek-ai/dsh-subprocess-local' -- id: bash - name: '@deepseek-ai/dsh-bash-local' - config: - cwd: '.' -- id: todo - name: '@deepseek-ai/dsh-tool-todo' - config: - allowParallelInProgress: true -""" -def _launch_args(mode: str) -> tuple[str, ...]: +def _select_mode(mode: str, monkeypatch: pytest.MonkeyPatch) -> None: try: - return resolve_bundled_launch_args(mode) + resolve_bundled_launch_args(mode) except FileNotFoundError as exc: pytest.skip(f"bundled {mode}-mode runtime unavailable on this machine: {exc}") + monkeypatch.setenv(RUNTIME_MODE_ENV_VAR, mode) -def _client(tmp_path: Path, launch_args: tuple[str, ...]) -> HarnessClient: +def _client(tmp_path: Path, mode: str, monkeypatch: pytest.MonkeyPatch, *patches: Path) -> HarnessClient: + _select_mode(mode, monkeypatch) return HarnessClient( HarnessConfig( - launch_args_override=launch_args, + dsh_home=str(tmp_path / "home"), + patches=tuple(str(patch) for patch in patches), cwd=str(tmp_path), env={ - "DSH_CORDIS_CONFIG": "./cordis.yml", - "DSH_SESSION_ROOT": str(tmp_path / "sessions"), - "DSH_CWD": str(tmp_path), # The lazily mounted adapter requires a key even without a model call. "DEEPSEEK_API_KEY": "sk-dummy-for-boot", "DEEPSEEK_BASE_URL": "http://127.0.0.1:9", + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", }, request_timeout_seconds=120, ) @@ -71,34 +46,39 @@ def _client(tmp_path: Path, launch_args: tuple[str, ...]) -> HarnessClient: @pytest.mark.parametrize("mode", _MODES) -def test_bundled_runtime_boots_a_cordis_config(tmp_path: Path, mode: str) -> None: - launch_args = _launch_args(mode) - (tmp_path / "cordis.yml").write_text(_CORDIS_YML) - - with _client(tmp_path, launch_args) as client: +def test_bundled_runtime_boots_the_sdk_profile( + tmp_path: Path, mode: str, monkeypatch: pytest.MonkeyPatch +) -> None: + with _client(tmp_path, mode, monkeypatch) as client: init = client.initialize(provider="deepseek-official", cwd=str(tmp_path), model="deepseek-v4-pro") assert init.serverInfo is not None assert init.serverInfo.name == "deepseek-harness-sdk-runtime" + profile = json.loads((tmp_path / "home" / "profiles" / "sdk" / "package.json").read_text()) + assert profile["dsh"]["profile"]["bundles"] == [ + "@deepseek-ai/dsh-base", + "@deepseek-ai/dsh-sdk-app", + ] @pytest.mark.parametrize("mode", _MODES) -def test_python_sdk_boots_minimal_jsonrpc_config(tmp_path: Path, mode: str) -> None: - launch_args = _launch_args(mode) - model = "minimal-environment-model" +def test_python_sdk_applies_an_ordered_profile_patch( + tmp_path: Path, mode: str, monkeypatch: pytest.MonkeyPatch +) -> None: + _select_mode(mode, monkeypatch) + patch = tmp_path / "persona.patch.yml" + patch.write_text(json.dumps([{ + "id": "system-prompt", + "config": {"persona": "Python SDK ordered patch marker."}, + }])) harness = DeepSeekHarness( - model=model, + model="deepseek-v4-pro", cwd=str(tmp_path), - session_root=str(tmp_path / "sessions"), - cordis=str(_MINIMAL_CONFIG), - env={ - "DSH_MODEL": model, - "DSH_CONTEXT_WINDOW": "1000000", - "DSH_SYSTEM_PROMPT": "You are the Python SDK minimal boot test agent.", - }, + dsh_home=str(tmp_path / "home"), + patches=(str(patch),), + env={"DSH_PERMISSION_MODE": "danger-full-access"}, api_key="sk-dummy-for-boot", base_url="http://127.0.0.1:9", - launch_args_override=launch_args, request_timeout_seconds=120, ) @@ -107,42 +87,20 @@ def test_python_sdk_boots_minimal_jsonrpc_config(tmp_path: Path, mode: str) -> N @pytest.mark.parametrize("mode", _MODES) -def test_bundled_runtime_surfaces_unbundled_plugin_failure(tmp_path: Path, mode: str) -> None: - launch_args = _launch_args(mode) - (tmp_path / "cordis.yml").write_text( - "- id: missing\n name: '@deepseek-ai/dsh-does-not-exist'\n" - ) +def test_bundled_runtime_surfaces_unbundled_plugin_failure( + tmp_path: Path, mode: str, monkeypatch: pytest.MonkeyPatch +) -> None: + patch = tmp_path / "missing.patch.yml" + patch.write_text(json.dumps([{ + "insert": [{"id": "missing", "name": "@deepseek-ai/dsh-does-not-exist"}], + }])) - client = _client(tmp_path, launch_args) + client = _client(tmp_path, mode, monkeypatch, patch) client.start() try: - with pytest.raises((TransportClosedError, TimeoutError)) as excinfo: + with pytest.raises((JsonRpcError, TransportClosedError, TimeoutError)) as excinfo: client.initialize(provider="deepseek-official", cwd=str(tmp_path), model="deepseek-v4-pro") finally: client.close() assert "@deepseek-ai/dsh-does-not-exist" in str(excinfo.value) - - -@pytest.mark.parametrize("mode", _MODES) -@pytest.mark.parametrize("ambient_config", [None, ""], ids=["unset", "empty-counts-as-absent"]) -def test_zero_config_run_injects_bundled_default_cordis_config( - tmp_path: Path, mode: str, ambient_config: str | None, monkeypatch: pytest.MonkeyPatch -) -> None: - _launch_args(mode) # skip early when this carrier is unavailable - monkeypatch.setenv("DSH_RUNTIME_MODE", mode) - if ambient_config is None: - monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False) - else: - monkeypatch.setenv("DSH_CORDIS_CONFIG", ambient_config) - - harness = DeepSeekHarness( - model="deepseek-v4-pro", - cwd=str(tmp_path), - session_root=str(tmp_path / "sessions"), - api_key="sk-dummy-for-boot", - base_url="http://127.0.0.1:9", - request_timeout_seconds=120, - ) - with harness: - pass diff --git a/python/sdk/tests/test_client.py b/python/sdk/tests/test_client.py index 51c9dacb31..f1190407c9 100644 --- a/python/sdk/tests/test_client.py +++ b/python/sdk/tests/test_client.py @@ -9,7 +9,8 @@ from pathlib import Path import pytest -from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig, Notification, SdkProtocolError +from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig, Notification, RunResult, SdkProtocolError +from deepseek_harness.errors import JsonRpcError def test_high_level_sdk_runs_turn_and_collects_final_response(tmp_path: Path) -> None: @@ -95,9 +96,7 @@ for line in sys.stdin: model="deepseek-v4-flash", max_tokens=4096, cwd=str(tmp_path), - cordis=str(tmp_path / "cordis.yml"), - session_root=str(tmp_path / "sessions"), - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), env={ "ENV_DUMP": str(env_dump), "INIT_DUMP": str(init_dump), @@ -113,9 +112,9 @@ for line in sys.stdin: dumped_env = json.loads(env_dump.read_text()) assert dumped_env["DEEPSEEK_API_KEY"] == "env-key" assert dumped_env["DEEPSEEK_BASE_URL"] == "http://127.0.0.1:4321" - assert dumped_env["DSH_CWD"] == str(tmp_path) - assert dumped_env["DSH_SESSION_ROOT"] == str(tmp_path / "sessions") - assert dumped_env["DSH_CORDIS_CONFIG"] == str(tmp_path / "cordis.yml") + assert dumped_env["DSH_CWD"] is None + assert dumped_env["DSH_SESSION_ROOT"] is None + assert dumped_env["DSH_CORDIS_CONFIG"] is None assert json.loads(init_dump.read_text()) == { "cwd": str(tmp_path), "provider": "deepseek-official", @@ -150,7 +149,7 @@ for line in sys.stdin: seen: list[str] = [] with DeepSeekHarness( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), cwd=str(tmp_path), ) as harness: session = harness.start_session("main") @@ -188,7 +187,7 @@ for line in sys.stdin: ) with DeepSeekHarness( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), cwd=str(tmp_path), ) as harness: with pytest.raises( @@ -224,7 +223,7 @@ for line in sys.stdin: with DeepSeekHarness( cwd=".", runtime_cwd=".", - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), env={"CAPTURE": str(capture)}, ): pass @@ -232,7 +231,7 @@ for line in sys.stdin: expected = str(tmp_path.resolve()) assert json.loads(capture.read_text()) == { "process": expected, - "environment": expected, + "environment": None, "wire": expected, } @@ -263,7 +262,7 @@ for line in sys.stdin: ) with DeepSeekHarness( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), cwd=str(tmp_path), ) as harness: result = harness.run("spawn a helper", session_id="main") @@ -312,7 +311,7 @@ for line in sys.stdin: seen: list[str] = [] with DeepSeekHarness( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), cwd=str(tmp_path), ) as harness: result = harness.run( @@ -367,7 +366,7 @@ for line in sys.stdin: ) with DeepSeekHarness( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), cwd=str(tmp_path), ) as harness: result = harness.run("stay in your lane", session_id="main") @@ -401,7 +400,7 @@ for line in sys.stdin: """.strip() ) - with DeepSeekHarness(launch_args_override=(sys.executable, str(script)), cwd=str(tmp_path)) as harness: + with DeepSeekHarness(_launch_args=(sys.executable, str(script)), cwd=str(tmp_path)) as harness: result = harness.run("one turn", session_id="main") assert harness.client._notifications.qsize() == 0 @@ -441,7 +440,7 @@ for line in sys.stdin: """.strip() ) - with DeepSeekHarness(launch_args_override=(sys.executable, str(script)), cwd=str(tmp_path)) as harness: + with DeepSeekHarness(_launch_args=(sys.executable, str(script)), cwd=str(tmp_path)) as harness: first = harness.run("first turn", session_id="main") second = harness.run("second turn", session_id="main") @@ -473,7 +472,7 @@ for line in sys.stdin: ) with HarnessClient( - HarnessConfig(launch_args_override=(sys.executable, str(script))) + HarnessConfig(_launch_args=(sys.executable, str(script))) ) as client: init = client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent") assert init.serverInfo.name == "fake-dsh" @@ -612,7 +611,7 @@ for line in sys.stdin: def broken_filter(_notification: object) -> bool: raise RuntimeError("bad notification filter") - with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client: + with HarnessClient(HarnessConfig(_launch_args=(sys.executable, str(script)))) as client: client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent") with ( client.subscribe_notifications(broken_filter) as broken, @@ -649,7 +648,7 @@ for line in sys.stdin: """.strip() ) - with HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) as client: + with HarnessClient(HarnessConfig(_launch_args=(sys.executable, str(script)))) as client: client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent") with pytest.raises(ValueError): client.session_prompt("main", [{"type": "text", "text": "fix it"}]) @@ -677,7 +676,7 @@ for line in sys.stdin: ) with HarnessClient( - HarnessConfig(launch_args_override=(sys.executable, str(script))) + HarnessConfig(_launch_args=(sys.executable, str(script))) ) as client: client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent") @@ -711,7 +710,7 @@ for line in sys.stdin: ) with HarnessClient( - HarnessConfig(launch_args_override=(sys.executable, str(script))) + HarnessConfig(_launch_args=(sys.executable, str(script))) ) as client: init = client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent") assert init.serverInfo.name == "fake-dsh" @@ -731,7 +730,7 @@ time.sleep(60) with HarnessClient( HarnessConfig( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), request_timeout_seconds=0.1, ) ) as client: @@ -767,7 +766,7 @@ for line in sys.stdin: client = HarnessClient( HarnessConfig( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), shutdown_timeout_seconds=0.1, ) ) @@ -792,6 +791,7 @@ import sys for line in sys.stdin: msg = json.loads(line) if msg.get("method") == "initialize": + print("initialize diagnostic", file=sys.stderr, flush=True) print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "error": {"code": -32000, "message": "bad initialize"}}), flush=True) elif msg.get("method") == "shutdown": print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True) @@ -799,14 +799,16 @@ for line in sys.stdin: """.strip() ) - client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) + client = HarnessClient(HarnessConfig(_launch_args=(sys.executable, str(script)))) client.start() proc = client._proc assert proc is not None - with pytest.raises(Exception, match="bad initialize"): + with pytest.raises(JsonRpcError, match="bad initialize") as excinfo: client.initialize(provider="deepseek-official", cwd=".", model="dsagent") + assert excinfo.value.code == -32000 + assert "initialize diagnostic" in str(excinfo.value) assert proc.wait(timeout=1) is not None assert client._proc is None @@ -824,6 +826,16 @@ def test_public_signatures_omit_unsupported_wire_parameters() -> None: assert "max_tokens" in inspect.signature(HarnessClient.initialize).parameters assert "client_name" not in HarnessConfig.__dataclass_fields__ assert "client_version" not in HarnessConfig.__dataclass_fields__ + assert {"dsh_bin", "profile", "patches", "dsh_home"} <= set( + DeepSeekHarnessConfig.__dataclass_fields__ + ) + assert {"dsh_bin", "profile", "patches", "dsh_home"} <= set( + HarnessConfig.__dataclass_fields__ + ) + for removed in ("cordis", "session_root", "runtime_bin", "bridge_bin", "launch_args_override"): + assert removed not in DeepSeekHarnessConfig.__dataclass_fields__ + assert removed not in HarnessConfig.__dataclass_fields__ + assert "session_root" not in RunResult.__dataclass_fields__ def test_client_close_is_idempotent_before_and_after_start(tmp_path: Path) -> None: @@ -845,7 +857,7 @@ for line in sys.stdin: """.strip() ) - client = HarnessClient(HarnessConfig(launch_args_override=(sys.executable, str(script)))) + client = HarnessClient(HarnessConfig(_launch_args=(sys.executable, str(script)))) client.start() client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent") client.close() @@ -865,7 +877,7 @@ sys.exit(42) with HarnessClient( HarnessConfig( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), request_timeout_seconds=2, ) ) as client: @@ -897,7 +909,7 @@ with open(os.environ["SEEN"], "w") as seen: with HarnessClient( HarnessConfig( - launch_args_override=(sys.executable, str(script)), + _launch_args=(sys.executable, str(script)), env={"SEEN": str(output)}, ) ) as client: @@ -915,21 +927,22 @@ with open(os.environ["SEEN"], "w") as seen: json.loads(line) -def _install_fake_bundled_runtime( +def _install_fake_bundled_dsh( tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> Path: - """Install a fake runtime package that records config and serves lifecycle calls. - - Returns the fake bundled default config path. - """ - runtime = tmp_path / "dsh-jsonrpc-agent" +) -> None: + """Install a fake runtime package that records dsh argv and serves lifecycle calls.""" + runtime = tmp_path / "dsh.py" runtime.write_text( - """#!/usr/bin/env python3 + """ import json import os import sys -json.dump({"DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG")}, open(os.environ["ENV_DUMP"], "w")) +json.dump({ + "argv": sys.argv[1:], + "DSH_HOME": os.environ.get("DSH_HOME"), + "DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG"), +}, open(os.environ["ENV_DUMP"], "w")) for line in sys.stdin: msg = json.loads(line) if msg.get("method") == "initialize": @@ -939,58 +952,75 @@ for line in sys.stdin: break """.strip() ) - runtime.chmod(0o755) - default_config = tmp_path / "default-cordis.yml" module_dir = tmp_path / "deepseek_harness_runtime" module_dir.mkdir() (module_dir / "__init__.py").write_text( f""" def resolve_bundled_launch_args(mode=None): - return ({str(runtime)!r},) - - -def bundled_default_config_path(): - return {str(default_config)!r} + return ({sys.executable!r}, {str(runtime)!r}) """.strip() ) monkeypatch.syspath_prepend(str(tmp_path)) monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False) - return default_config -@pytest.mark.parametrize("ambient_config", [None, ""], ids=["unset", "empty-counts-as-absent"]) -def test_client_default_launch_uses_bundled_runtime_and_injects_default_config( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ambient_config: str | None -) -> None: - env_dump = tmp_path / "env.json" - default_config = _install_fake_bundled_runtime(tmp_path, monkeypatch) - if ambient_config is None: - monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False) - else: - monkeypatch.setenv("DSH_CORDIS_CONFIG", ambient_config) - - with HarnessClient(HarnessConfig(env={"ENV_DUMP": str(env_dump)})) as client: - init = client.initialize(provider="deepseek-official", cwd="/workspace", model="deepseek-v4-pro") - - assert init.serverInfo.name == "bundled-runtime" - assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == str(default_config) - - -def test_client_respects_explicit_config_over_bundled_default( +def test_client_default_launch_uses_bundled_dsh_sdk_profile_and_explicit_home( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: env_dump = tmp_path / "env.json" - _install_fake_bundled_runtime(tmp_path, monkeypatch) + home = tmp_path / "home" + patch = tmp_path / "sdk.patch.yml" + patch.write_text("[]\n") + _install_fake_bundled_dsh(tmp_path, monkeypatch) + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("DSH_HOME", str(tmp_path / "ambient-home")) monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False) + with HarnessClient(HarnessConfig( + profile="sdk", + patches=("sdk.patch.yml",), + dsh_home=str(home), + env={"ENV_DUMP": str(env_dump), "DSH_HOME": str(tmp_path / "env-home")}, + )) as client: + init = client.initialize(provider="deepseek-official", cwd="/workspace", model="deepseek-v4-pro") + + assert init.serverInfo.name == "bundled-runtime" + assert json.loads(env_dump.read_text()) == { + "argv": ["--profile", "sdk", "--patch", str(patch)], + "DSH_HOME": str(home), + "DSH_CORDIS_CONFIG": None, + } + + +def test_client_accepts_explicit_environment_dsh_home( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + env_dump = tmp_path / "env.json" + home = tmp_path / "environment-home" + _install_fake_bundled_dsh(tmp_path, monkeypatch) + with HarnessClient( - HarnessConfig(env={"ENV_DUMP": str(env_dump), "DSH_CORDIS_CONFIG": "./explicit.yml"}) + HarnessConfig(profile="custom", env={"ENV_DUMP": str(env_dump), "DSH_HOME": str(home)}) ) as client: client.initialize(provider="deepseek-official", cwd="/workspace", model="deepseek-v4-pro") - assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == "./explicit.yml" + assert json.loads(env_dump.read_text()) == { + "argv": ["--profile", "custom"], + "DSH_HOME": str(home), + "DSH_CORDIS_CONFIG": None, + } + + +def test_client_rejects_an_implicit_default_dsh_home( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _install_fake_bundled_dsh(tmp_path, monkeypatch) + monkeypatch.delenv("DSH_HOME", raising=False) + + with pytest.raises(ValueError, match="explicit dsh_home or non-empty DSH_HOME"): + HarnessClient(HarnessConfig(env={})).start() def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.MonkeyPatch) -> None: @@ -998,4 +1028,4 @@ def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.M monkeypatch.setattr(sys, "path", []) with pytest.raises(FileNotFoundError, match="Install deepseek-harness-runtime-bin"): - HarnessClient().start() + HarnessClient(HarnessConfig(dsh_home="/explicit/home")).start() From 01da0437378596c640fd74e34e37fb83e75e8334 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:56:34 +0800 Subject: [PATCH 05/16] test(python): prove installed dsh profile customization Migrate the packaged-runtime smoke inventory from complete Cordis trees to the sdk profile plus ordered patches. Preserve the focused minimal and advanced behaviors, update the generated durable snapshots for explicit permission events and the smaller RunResult, and keep worker, MCP, ripgrep, PTY/editor, direct JSON-RPC, and real-provider coverage. Add an installed-only external bundle scenario that invokes the wheel's dsh plugin command with a local file package, verifies profile manifest reconciliation, imports @deepseek-ai/cordis as a peer, asserts the packaged proxy returns the exact host Context instance, and proves its system-prompt contribution reaches the model. Migrate the repository source e2e and runnable minimal example to the same profile grammar. --- examples/python-sdk-agent/minimal.patch.yml | 68 ++ examples/python-sdk-agent/minimal.py | 19 +- .../tests/keyless-smoke.e2e.ts | 72 +- .../python-sdk-agent/tests/keyless.patch.yml | 38 + scripts/smoke-python-runtime.py | 510 ++++++++--- .../advanced/result.json | 813 ++++++++++-------- .../advanced/session.1.jsonl | 11 +- .../advanced/session.2.jsonl | 11 +- .../advanced/session.jsonl | 46 +- .../restart/requests.json | 8 + .../python-sdk-single-exe/restart/result.json | 94 +- .../restart/session.1.jsonl | 10 +- .../restart/session.2.jsonl | 10 +- 13 files changed, 1101 insertions(+), 609 deletions(-) create mode 100644 examples/python-sdk-agent/minimal.patch.yml create mode 100644 examples/python-sdk-agent/tests/keyless.patch.yml diff --git a/examples/python-sdk-agent/minimal.patch.yml b/examples/python-sdk-agent/minimal.patch.yml new file mode 100644 index 0000000000..be3f292cc0 --- /dev/null +++ b/examples/python-sdk-agent/minimal.patch.yml @@ -0,0 +1,68 @@ +# Minimal Python SDK overlay for `dsh --profile sdk`: keep only persistent +# Bash and the string-replacement editor, with no runtime-context prompt or +# compaction. The profile still owns JSON-RPC serving and persistence. + +- id: system-prompt + config: + includeHarnessIdentity: false + includeRuntimeContext: false + persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.' + +- id: agent-instructions + disabled: true +- id: skill-filesystem + disabled: true +- id: tool-skill + disabled: true +- id: tool-bash + disabled: true +- id: tool-jobs + disabled: true +- id: tool-fs + disabled: true +- id: tool-fs-search + disabled: true +- id: tool-subagent-control + disabled: true +- id: tool-subagent-list-agents + disabled: true +- id: tool-subagent + disabled: true +- id: tool-subagent-fork + disabled: true +- id: tool-subagent-report + disabled: true +- id: tool-workflow + disabled: true +- id: tool-todo + disabled: true +- id: tool-goal + disabled: true +- id: tool-ralph + disabled: true +- id: tool-web + disabled: true +- id: plan-mode + disabled: true +- id: compaction-basic + disabled: true +- id: command-compact + disabled: true +- id: tool-result-pruner + disabled: true + +- id: tool-str-replace-editor + config: + maxOutputChars: 16000 + +- insert: + - id: pty + name: '@deepseek-ai/dsh-terminal' + - id: terminal-bash + name: '@deepseek-ai/dsh-terminal-bash' + config: + timeoutMs: 300000 + - id: persistent-bash + name: '@deepseek-ai/dsh-tool-bash-persistent' + config: + timeoutMs: 300000 diff --git a/examples/python-sdk-agent/minimal.py b/examples/python-sdk-agent/minimal.py index e94b02b7d8..255a0dd3c0 100644 --- a/examples/python-sdk-agent/minimal.py +++ b/examples/python-sdk-agent/minimal.py @@ -10,30 +10,39 @@ from pathlib import Path from deepseek_harness import DeepSeekHarness -CONFIG = Path(__file__).with_name("minimal.cordis.yml") +PATCH = Path(__file__).with_name("minimal.patch.yml") def main() -> None: """Parse one task and print the agent's final response.""" parser = argparse.ArgumentParser() + configured_home = os.environ.get("DSH_HOME", "") parser.add_argument("prompt", help="Task for the minimal agent") parser.add_argument("--workspace", type=Path, default=Path.cwd()) - parser.add_argument("--session-root", type=Path, default=Path(".dsh-sessions")) + parser.add_argument( + "--dsh-home", + type=Path, + default=Path(configured_home) if configured_home.strip() else None, + ) + parser.add_argument("--profile", default="sdk") parser.add_argument("--session-id") parser.add_argument("--provider", default="deepseek-official") parser.add_argument("--model", default=os.environ.get("DSH_MODEL", "deepseek-v4-flash")) parser.add_argument("--max-tokens", type=int) args = parser.parse_args() + if args.dsh_home is None: + parser.error("--dsh-home or a non-empty DSH_HOME is required") workspace = args.workspace.resolve() - session_root = args.session_root.resolve() + dsh_home = args.dsh_home.resolve() with DeepSeekHarness( provider=args.provider, model=args.model, max_tokens=args.max_tokens, cwd=str(workspace), - session_root=str(session_root), - cordis=str(CONFIG.resolve()), + dsh_home=str(dsh_home), + profile=args.profile, + patches=(str(PATCH.resolve()),), ) as harness: result = harness.run(args.prompt, session_id=args.session_id) print(result.final_response) diff --git a/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts b/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts index 99897079ef..c0aabb48b8 100644 --- a/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts +++ b/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts @@ -8,8 +8,8 @@ import { zstdDecompress } from 'node:zlib' import { execa } from 'execa' import { describe, expect, it } from 'vitest' -const binScript = fileURLToPath(new URL('../../../packages/sdk/python-runtime/src/packaged-bin.ts', import.meta.url)) -const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url)) +const binScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url)) +const patchPath = fileURLToPath(new URL('./keyless.patch.yml', import.meta.url)) const repoRoot = fileURLToPath(new URL('../../..', import.meta.url)) const decompress = promisify(zstdDecompress) @@ -45,7 +45,7 @@ function waitForLine( }) } -describe('Python SDK runtime carrier keyless smoke', () => { +describe('Python SDK dsh profile keyless smoke', () => { it.each([ { label: 'reports max-token turns with the default mapping config', envValue: undefined }, { label: 'reports max-token turns with mapping enabled through env', envValue: 'true' }, @@ -73,16 +73,20 @@ describe('Python SDK runtime carrier keyless smoke', () => { // execa owns spawn, the deadline, and exit settlement around it. const child = execa(process.execPath, [ '--import', - 'tsx', + 'tsx/esm', binScript, - configPath, + '--profile', + 'sdk', + '--patch', + patchPath, ], { cwd: repoRoot, env: { + DSH_HOME: join(root, '.dsh'), + DSH_PERMISSION_MODE: 'danger-full-access', + DSH_TELEMETRY_DISABLED: '1', DEEPSEEK_API_KEY: 'keyless-smoke-no-call', DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`, - DSH_CWD: root, - DSH_SESSION_ROOT: join(root, '.sessions'), ...(envValue === undefined ? {} : { DSH_MAX_TOKENS_AS_SUCCESS: envValue }), }, timeout: 35_000, @@ -149,6 +153,7 @@ describe('Python SDK runtime carrier keyless smoke', () => { 'bash', 'edit', 'read', + 'read_image', 'subagent', 'todo_write', 'write', @@ -159,7 +164,7 @@ describe('Python SDK runtime carrier keyless smoke', () => { expect(shutdown).toMatchObject({ jsonrpc: '2.0', id: 3, result: {} }) const exit = await child expect(exit.exitCode, `signal=${String(exit.signal)}; stderr=${stderr}`).toBe(0) - const sessionsRoot = join(root, '.sessions') + const sessionsRoot = join(root, '.dsh', 'sessions') const files = await readdir(sessionsRoot, { recursive: true }) const log = files.find(file => file.endsWith('.jsonl.zstd')) expect(log).toBeDefined() @@ -176,27 +181,36 @@ describe('Python SDK runtime carrier keyless smoke', () => { }, 40_000) it('rejects an invalid max-token success env value', async () => { - const { exitCode, stdout, stderr } = await execa(process.execPath, [ - '--import', - 'tsx', - binScript, - configPath, - ], { - cwd: repoRoot, - env: { - DEEPSEEK_API_KEY: 'keyless-smoke-no-call', - DSH_MAX_TOKENS_AS_SUCCESS: 'sometimes', - }, - stdin: 'ignore', - timeout: 25_000, - killSignal: 'SIGKILL', - reject: false, - }) + const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-runtime-invalid-')) + try { + const { exitCode, stdout, stderr } = await execa(process.execPath, [ + '--import', + 'tsx/esm', + binScript, + '--profile', + 'sdk', + '--patch', + patchPath, + ], { + cwd: repoRoot, + env: { + DSH_HOME: join(root, '.dsh'), + DEEPSEEK_API_KEY: 'keyless-smoke-no-call', + DSH_MAX_TOKENS_AS_SUCCESS: 'sometimes', + }, + stdin: 'ignore', + timeout: 25_000, + killSignal: 'SIGKILL', + reject: false, + }) - expect(exitCode, stderr).toBe(1) - expect(stdout).toBe('') - expect(stderr).toContain('plugin tree failed to load') - expect(stderr).toContain('failed to apply loader entry sdk-jsonrpc-server (@deepseek-ai/dsh-sdk-jsonrpc-server)') - expect(stderr).toContain('sometimes') + expect(exitCode, stderr).toBe(1) + expect(stdout).toBe('') + expect(stderr).toContain('plugin tree failed to load') + expect(stderr).toContain('failed to apply loader entry sdk-jsonrpc-server (@deepseek-ai/dsh-sdk-jsonrpc-server)') + expect(stderr).toContain('sometimes') + } finally { + await rm(root, { recursive: true, force: true }) + } }, 30_000) }) diff --git a/examples/python-sdk-agent/tests/keyless.patch.yml b/examples/python-sdk-agent/tests/keyless.patch.yml new file mode 100644 index 0000000000..71b5ec6ebd --- /dev/null +++ b/examples/python-sdk-agent/tests/keyless.patch.yml @@ -0,0 +1,38 @@ +# Preserve the focused SDK test roster over the shipped sdk profile. + +- id: agent-instructions + disabled: true +- id: tool-jobs + disabled: true +- id: tool-fs-search + disabled: true +- id: skill-filesystem + disabled: true +- id: tool-skill + disabled: true +- id: tool-str-replace-editor + disabled: true +- id: tool-subagent-control + disabled: true +- id: tool-subagent-list-agents + disabled: true +- id: tool-subagent-fork + disabled: true +- id: tool-subagent-report + disabled: true +- id: tool-workflow + disabled: true +- id: tool-goal + disabled: true +- id: plan-mode + disabled: true +- id: tool-ralph + disabled: true +- id: tool-web + disabled: true + +- id: tool-subagent + config: + provider: spawn + toolName: subagent + backgroundMode: one-shot diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 71e7100268..06653c5741 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -12,6 +12,7 @@ import os import queue import subprocess import sys +import sysconfig import tempfile import threading import time @@ -37,14 +38,43 @@ FS_SEARCH_TEXT = "filesystem search smoke ok" FS_SEARCH_MARKER = "PACKAGED_FS_SEARCH_OK" MCP_PROMPT = "Exercise the packaged MCP client with one external stdio server." MCP_TEXT = "MCP client smoke ok" -MINIMAL_CORDIS = ( - Path(__file__).resolve().parent.parent / "examples" / "python-sdk-agent" / "minimal.cordis.yml" -) +PROFILE_PLUGIN_PROMPT = "Verify the Python-installed dsh profile plugin." +PROFILE_PLUGIN_TEXT = "profile plugin smoke ok" +PROFILE_PLUGIN_MARKER = "PYTHON_INSTALLED_DSH_PROFILE_PLUGIN" MINIMAL_BASH_COMMAND = ( "counter=$(( ${counter:-0} + 1 )); export counter; " "printf 'COUNT=%s CWD=%s\\n' \"$counter\" \"$PWD\"; " "if [ \"$counter\" -eq 1 ]; then cd /tmp; fi" ) +MINIMAL_BASH_DESCRIPTION = """Run commands in a bash shell +* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped. +* You don't have access to the internet via this tool. +* You do have access to a mirror of common linux and python packages via apt and pip. +* State is persistent across command calls and discussions with the user. +* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'. +* Please avoid commands that may produce a very large amount of output. +* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.""" +LEGACY_CUSTOM_DISABLED_ROWS = ( + "agent-instructions", + "goal", + "goal-round-driver", + "command-goal", + "plan-mode", + "skill", + "skill-filesystem", + "tool-fs", + "tool-fs-search", + "tool-goal", + "tool-ralph", + "tool-skill", + "tool-str-replace-editor", + "tool-subagent-control", + "tool-subagent-list-agents", + "tool-subagent-fork", + "tool-subagent-report", + "tool-todo", + "tool-web", +) SNAPSHOT_PROMPT = "Run the advanced packaged-runtime snapshot scenario." SNAPSHOT_SESSION_ID = "advanced-executable" SNAPSHOT_DIRECT_CHILD_PROMPT = "Reply with exactly DIRECT_CHILD_OK and nothing else." @@ -95,75 +125,6 @@ RESTART_SNAPSHOT_FILENAMES = ("result.json", "requests.json", "session.1.jsonl", # expected output cannot carry: the same composition emits it on macOS and not on Linux # (deepseek-harness#2488), and the file must replay on both. Everything else is compared. RUNTIME_CONTEXT_PREFIX = "Current runtime context" -CUSTOM_CORDIS = """\ -- id: sdk-jsonrpc-server - name: '@deepseek-ai/dsh-sdk-jsonrpc-server' -- id: deepseek-llm-api-extensions - name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' -- id: session-log-deepseek - name: '@deepseek-ai/dsh-session-log-deepseek' - config: - enabled: true -- id: agent-core - name: '@deepseek-ai/dsh-agent-spine-demo' - config: - workspaceContext: false - skills: - enabled: false - toolBash: false - tools: - mode: both -- id: sessions - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: !!js process.env.DSH_SESSION_ROOT - compression: 'none' -- id: code-runtime - name: '@deepseek-ai/dsh-code-runtime-worker-thread' -- id: subagents - name: '@deepseek-ai/dsh-subagent' -- id: subagent-spawn-in-process - name: '@deepseek-ai/dsh-subagent-spawn-in-process' - config: - providerName: spawn -- id: subagent-tool - name: '@deepseek-ai/dsh-tool-subagent' - config: - provider: spawn -- id: workflow-engine - name: '@deepseek-ai/dsh-workflow-worker-thread' - config: - provider: spawn -- id: workflow-tool - name: '@deepseek-ai/dsh-tool-workflow' -- id: cordis-host-runner - name: '@deepseek-ai/dsh-cordis-host-runner' -- id: cordis-tool - name: '@deepseek-ai/dsh-tool-cordis' -""" -FS_SEARCH_CORDIS = """\ -- id: sdk-jsonrpc-server - name: '@deepseek-ai/dsh-sdk-jsonrpc-server' -- id: agent-core - name: '@deepseek-ai/dsh-agent-spine-demo' - config: - workspaceContext: false - skills: - enabled: false - toolBash: false - toolJobs: false -- id: sessions - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: !!js process.env.DSH_SESSION_ROOT - compression: 'none' -- id: subprocess - name: '@deepseek-ai/dsh-subprocess-local' -- id: fs-search - name: '@deepseek-ai/dsh-tool-fs-search' - config: - sampleOverCapGlobResults: false -""" MCP_SERVER_SCRIPT = """\ import json import os @@ -242,28 +203,29 @@ for line in sys.stdin: """ -def mcp_cordis(server_script: Path) -> str: - """Build an external config that mounts the packaged MCP client.""" - return json.dumps([ +def write_profile_patch( + root: Path, + name: str, + sessions: Path, + patches: list[dict[str, object]], +) -> Path: + """Write one JSON-form dsh profile patch with deterministic persistence.""" + path = root / name + path.write_text(json.dumps([ { - "id": "sdk-jsonrpc-server", - "name": "@deepseek-ai/dsh-sdk-jsonrpc-server", + "id": "session-persistence-jsonl", + "config": {"root": str(sessions), "compression": "none"}, }, - { - "id": "agent-core", - "name": "@deepseek-ai/dsh-agent-spine-demo", - "config": { - "workspaceContext": False, - "skills": {"enabled": False}, - "toolBash": False, - }, - }, - { - "id": "sessions", - "name": "@deepseek-ai/dsh-session-persistence-jsonl", - "config": {"root": "./sessions", "compression": "none"}, - }, - { + {"id": "session-telemetry-otel", "disabled": True}, + *patches, + ], indent=2)) + return path + + +def write_mcp_patch(root: Path, sessions: Path, server_script: Path) -> Path: + """Write a profile patch that mounts the packaged MCP client.""" + return write_profile_patch(root, "mcp.patch.yml", sessions, [{ + "insert": [{ "id": "mcp-fixture", "name": "@deepseek-ai/dsh-mcp-client", "config": { @@ -275,8 +237,8 @@ def mcp_cordis(server_script: Path) -> str: "failOnStartupError": True, "reconnect": {"enabled": False}, }, - }, - ], indent=2) + }], + }]) class MockModelHandler(BaseHTTPRequestHandler): @@ -364,6 +326,7 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]: MCP_PROMPT, RESTART_FIRST_PROMPT, RESTART_SECOND_PROMPT, + PROFILE_PLUGIN_PROMPT, } prompt = next( (candidate for candidate in user_prompts if candidate in scenario_prompts), @@ -430,6 +393,15 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]: "mcp__fixture__add", {"a": 19, "b": 23}, ) + if prompt == PROFILE_PLUGIN_PROMPT: + system_text = "\n".join( + message_text(message.get("content")) + for message in messages + if isinstance(message, dict) and message.get("role") == "system" + ) + if PROFILE_PLUGIN_MARKER not in system_text: + raise AssertionError("external profile plugin contributed no model-visible marker") + return text_chunks(PROFILE_PLUGIN_TEXT) return text_chunks(EXPECTED_TEXT) @@ -710,7 +682,7 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--scenario", - choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-mcp", "sdk-snapshot", "sdk-restart", "sdk-live", "direct"), + choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-mcp", "sdk-snapshot", "sdk-restart", "sdk-profile-plugin", "sdk-live", "direct"), default="all", ) parser.add_argument("--exe", type=Path) @@ -725,6 +697,8 @@ def main() -> None: parser.error("--installed-wheel resolves the wheel's own runtime and cannot be combined with --exe") if args.scenario == "sdk-live" and not args.installed_wheel: parser.error("--scenario sdk-live requires --installed-wheel") + if args.scenario == "sdk-profile-plugin" and not args.installed_wheel: + parser.error("--scenario sdk-profile-plugin requires --installed-wheel") if args.installed_wheel: args.exe = assert_installed_wheel_environment() if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-snapshot", "sdk-restart", "direct"} and args.exe is None: @@ -759,6 +733,8 @@ def main() -> None: if args.scenario in {"all", "sdk-restart"}: assert args.exe is not None smoke_sdk_restart_snapshot(model.url, args.exe.resolve(), args.update_snapshots) + if args.installed_wheel and args.scenario in {"all", "sdk-profile-plugin"}: + smoke_sdk_profile_plugin(model.url) if args.scenario in {"all", "direct"}: assert args.exe is not None smoke_direct(model.url, args.exe.resolve()) @@ -832,7 +808,8 @@ def smoke_sdk_live() -> None: with tempfile.TemporaryDirectory(prefix="dsh-sdk-live-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" + dsh_home = root / "home" + sessions = dsh_home / "sessions" marker = root / "live-api-marker.txt" session_id = "installed-wheel-live-api" create_prompt = ( @@ -847,7 +824,11 @@ def smoke_sdk_live() -> None: provider="deepseek-official", model="deepseek-v4-flash", cwd=str(root), - session_root=str(sessions), + dsh_home=str(dsh_home), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key=api_key, base_url=base_url, request_timeout_seconds=180, @@ -910,18 +891,27 @@ def smoke_sdk_default(base_url: str) -> None: with tempfile.TemporaryDirectory(prefix="dsh-sdk-default-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" + dsh_home = root / "home" + sessions = dsh_home / "sessions" with DeepSeekHarness( provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), + dsh_home=str(dsh_home), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, ) as harness: result = harness.run("reply with the smoke text", session_id="default-smoke") - assert result.final_response == EXPECTED_TEXT, result.final_response + assert result.final_response == EXPECTED_TEXT, ( + f"final={result.final_response!r} finish={result.finish_reason!r} " + f"events={[event.get('type') for event in result.events]!r} " + f"turn_end={safe_turn_end(next((event.get('data', event) for event in reversed(result.events) if event.get('type') == 'turn/end'), {}))!r}" + ) assert_zstd_session_log(sessions) @@ -930,16 +920,44 @@ def smoke_sdk_custom(base_url: str, executable: Path) -> None: with tempfile.TemporaryDirectory(prefix="dsh-sdk-custom-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" - cordis = root / "cordis.yml" - cordis.write_text(CUSTOM_CORDIS) + dsh_home = root / "home" + sessions = dsh_home / "sessions" + patch = write_profile_patch(root, "custom.patch.yml", sessions, [ + {"id": "tools", "config": {"mode": "both"}}, + { + "id": "system-prompt", + "config": { + "persona": "You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.", + }, + }, + {"id": "session-log-deepseek", "config": {"enabled": True}}, + *({"id": row_id, "disabled": True} for row_id in LEGACY_CUSTOM_DISABLED_ROWS), + {"id": "tool-bash", "disabled": True}, + { + "id": "tool-subagent", + "config": { + "provider": "spawn", + "toolName": "subagent", + "backgroundMode": "one-shot", + }, + }, + {"insert": [ + {"id": "code-runtime", "name": "@deepseek-ai/dsh-code-runtime-worker-thread"}, + {"id": "cordis-host-runner", "name": "@deepseek-ai/dsh-cordis-host-runner"}, + {"id": "cordis-tool", "name": "@deepseek-ai/dsh-tool-cordis"}, + ]}, + ]) with DeepSeekHarness( provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), - cordis=str(cordis), - runtime_bin=str(executable), + dsh_bin=str(executable), + dsh_home=str(dsh_home), + patches=(str(patch),), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, @@ -963,14 +981,70 @@ def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) - root = Path(temporary).resolve() editor_path = root / "created.txt" prompt = f"{MINIMAL_PROMPT}\n{MINIMAL_EDITOR_PATH_PREFIX}{editor_path}" - sessions = root / "sessions" + dsh_home = root / "home" + sessions = dsh_home / "sessions" + disabled = [ + "agent-instructions", + "skill-filesystem", + "tool-skill", + "tool-bash", + "tool-jobs", + "tool-fs", + "tool-fs-search", + "tool-subagent-control", + "tool-subagent-list-agents", + "tool-subagent", + "tool-subagent-fork", + "tool-subagent-report", + "tool-workflow", + "tool-todo", + "tool-goal", + "tool-ralph", + "tool-web", + "plan-mode", + "compaction-basic", + "command-compact", + "tool-result-pruner", + ] + patch = write_profile_patch(root, "minimal.patch.yml", sessions, [ + { + "id": "system-prompt", + "config": { + "includeHarnessIdentity": False, + "includeRuntimeContext": False, + "persona": "You are a helpful software engineer assistant.", + }, + }, + *({"id": row_id, "disabled": True} for row_id in disabled), + {"id": "tool-str-replace-editor", "config": {"maxOutputChars": 16000}}, + {"insert": [ + {"id": "pty", "name": "@deepseek-ai/dsh-terminal"}, + { + "id": "terminal-bash", + "name": "@deepseek-ai/dsh-terminal-bash", + "config": {"timeoutMs": 300000}, + }, + { + "id": "persistent-bash", + "name": "@deepseek-ai/dsh-tool-bash-persistent", + "config": { + "timeoutMs": 300000, + "description": MINIMAL_BASH_DESCRIPTION, + }, + }, + ]}, + ]) with DeepSeekHarness( provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), - cordis=str(MINIMAL_CORDIS), - runtime_bin=str(executable), + dsh_bin=str(executable), + dsh_home=str(dsh_home), + patches=(str(patch),), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, @@ -997,16 +1071,23 @@ def smoke_sdk_fs_search(base_url: str, executable: Path) -> None: with tempfile.TemporaryDirectory(prefix="dsh-sdk-fs-search-") as temporary: root = Path(temporary).resolve() (root / "needle.txt").write_text(f"{FS_SEARCH_MARKER}\n") - sessions = root / "sessions" - cordis = root / "cordis.yml" - cordis.write_text(FS_SEARCH_CORDIS) + dsh_home = root / "home" + sessions = dsh_home / "sessions" + patch = write_profile_patch(root, "fs-search.patch.yml", sessions, [ + {"id": "skill-filesystem", "disabled": True}, + {"id": "tool-fs-search", "config": {"sampleOverCapGlobResults": False}}, + ]) with DeepSeekHarness( provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), - cordis=str(cordis), - runtime_bin=str(executable), + dsh_bin=str(executable), + dsh_home=str(dsh_home), + patches=(str(patch),), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, @@ -1023,19 +1104,23 @@ def smoke_sdk_mcp(base_url: str, executable: Path | None) -> None: with tempfile.TemporaryDirectory(prefix="dsh-sdk-mcp-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" + dsh_home = root / "home" + sessions = dsh_home / "sessions" server_script = root / "mcp_server.py" server_script.write_text(MCP_SERVER_SCRIPT) - cordis = root / "cordis.yml" - cordis.write_text(mcp_cordis(server_script)) + patch = write_mcp_patch(root, sessions, server_script) discovery_log = server_script.with_suffix(".log") with DeepSeekHarness( provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), - cordis=str(cordis), - runtime_bin=None if executable is None else str(executable), + dsh_bin=None if executable is None else str(executable), + dsh_home=str(dsh_home), + patches=(str(patch),), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, @@ -1052,22 +1137,131 @@ def smoke_sdk_mcp(base_url: str, executable: Path | None) -> None: assert_session_log(sessions, root, MCP_TEXT, "mcp__fixture__add", "42") +def smoke_sdk_profile_plugin(base_url: str) -> None: + """Install an external bundle through Python's dsh command and load it in the SDK.""" + from deepseek_harness import DeepSeekHarness + + with tempfile.TemporaryDirectory(prefix="dsh-sdk-profile-plugin-") as temporary: + root = Path(temporary).resolve() + dsh_home = root / "home" + plugin = root / "plugin" + plugin.mkdir() + (plugin / "package.json").write_text(json.dumps({ + "name": "dsh-python-blackbox-plugin", + "version": "1.0.0", + "private": True, + "type": "module", + "exports": "./index.js", + "peerDependencies": {"@deepseek-ai/cordis": "*"}, + "dsh": {"bundle": {"patch": "./cordis.patch.yml"}}, + }, indent=2)) + (plugin / "index.js").write_text( + "import { Context } from '@deepseek-ai/cordis'\n" + "export const name = 'python-sdk-blackbox-plugin'\n" + "export const inject = ['systemPrompt']\n" + "export function apply(ctx) {\n" + " if (!(ctx instanceof Context)) throw new Error('external plugin loaded a second Cordis instance')\n" + " ctx.effect(() => ctx.systemPrompt.section({\n" + " name: 'python-sdk:blackbox-plugin',\n" + " order: 10,\n" + f" text: '{PROFILE_PLUGIN_MARKER}',\n" + " }))\n" + "}\n" + ) + (plugin / "cordis.patch.yml").write_text(json.dumps([{ + "insert": [{"id": "python-sdk-blackbox-plugin", "name": "dsh-python-blackbox-plugin"}], + }], indent=2)) + + dsh = Path(sysconfig.get_path("scripts")) / "dsh" + environment = {**os.environ, "DSH_HOME": str(dsh_home)} + installed = subprocess.run( + [str(dsh), "plugin", "--profile", "sdk", "add", f"file:{plugin}"], + cwd=root, + env=environment, + text=True, + capture_output=True, + check=False, + ) + if installed.returncode != 0: + raise AssertionError( + f"Python-installed dsh could not add the external profile plugin: " + f"stdout={installed.stdout!r} stderr={installed.stderr!r}" + ) + manifest = json.loads((dsh_home / "profiles" / "sdk" / "package.json").read_text()) + if "dsh-python-blackbox-plugin" not in manifest.get("dependencies", {}): + raise AssertionError(f"dsh plugin did not record the external dependency: {manifest}") + if "dsh-python-blackbox-plugin" not in manifest["dsh"]["profile"]["bundles"]: + raise AssertionError(f"dsh plugin did not activate the external bundle: {manifest}") + + harness = DeepSeekHarness( + provider="deepseek-official", + model="smoke-model", + cwd=str(root), + dsh_home=str(dsh_home), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, + api_key="sk-keyless-smoke", + base_url=base_url, + request_timeout_seconds=60, + ) + try: + with harness: + result = harness.run(PROFILE_PLUGIN_PROMPT, session_id="profile-plugin-smoke") + except Exception as error: + raise AssertionError( + f"external profile plugin runtime failed: {harness.client._runtime_diagnostics()}" + ) from error + + assert result.final_response == PROFILE_PLUGIN_TEXT, result.final_response + assert_zstd_session_log(dsh_home / "sessions") + + def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool) -> None: """Drive and compare the advanced SDK/executable behavioral snapshot.""" from deepseek_harness import DeepSeekHarness with tempfile.TemporaryDirectory(prefix="dsh-sdk-snapshot-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" - cordis = root / "cordis.yml" - cordis.write_text(CUSTOM_CORDIS) + dsh_home = root / "home" + sessions = dsh_home / "sessions" + patch = write_profile_patch(root, "snapshot.patch.yml", sessions, [ + {"id": "tools", "config": {"mode": "both"}}, + { + "id": "system-prompt", + "config": { + "persona": "You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.", + }, + }, + {"id": "session-log-deepseek", "config": {"enabled": True}}, + *({"id": row_id, "disabled": True} for row_id in LEGACY_CUSTOM_DISABLED_ROWS), + {"id": "tool-bash", "disabled": True}, + { + "id": "tool-subagent", + "config": { + "provider": "spawn", + "toolName": "subagent", + "backgroundMode": "one-shot", + }, + }, + {"insert": [ + {"id": "code-runtime", "name": "@deepseek-ai/dsh-code-runtime-worker-thread"}, + {"id": "cordis-host-runner", "name": "@deepseek-ai/dsh-cordis-host-runner"}, + {"id": "cordis-tool", "name": "@deepseek-ai/dsh-tool-cordis"}, + ]}, + ]) with DeepSeekHarness( provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), - cordis=str(cordis), - runtime_bin=str(executable), + dsh_bin=str(executable), + dsh_home=str(dsh_home), + patches=(str(patch),), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, @@ -1103,9 +1297,33 @@ def smoke_sdk_restart_snapshot(base_url: str, executable: Path, update_snapshots with tempfile.TemporaryDirectory(prefix="dsh-sdk-restart-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" - cordis = root / "cordis.yml" - cordis.write_text(CUSTOM_CORDIS) + dsh_home = root / "home" + sessions = dsh_home / "sessions" + patch = write_profile_patch(root, "restart.patch.yml", sessions, [ + {"id": "tools", "config": {"mode": "both"}}, + { + "id": "system-prompt", + "config": { + "persona": "You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.", + }, + }, + {"id": "session-log-deepseek", "config": {"enabled": True}}, + *({"id": row_id, "disabled": True} for row_id in LEGACY_CUSTOM_DISABLED_ROWS), + {"id": "tool-bash", "disabled": True}, + { + "id": "tool-subagent", + "config": { + "provider": "spawn", + "toolName": "subagent", + "backgroundMode": "one-shot", + }, + }, + {"insert": [ + {"id": "code-runtime", "name": "@deepseek-ai/dsh-code-runtime-worker-thread"}, + {"id": "cordis-host-runner", "name": "@deepseek-ai/dsh-cordis-host-runner"}, + {"id": "cordis-tool", "name": "@deepseek-ai/dsh-tool-cordis"}, + ]}, + ]) first_request = len(MockModelHandler.requests) def run(prompt: str, session_id: str) -> "RunResult": @@ -1113,9 +1331,13 @@ def smoke_sdk_restart_snapshot(base_url: str, executable: Path, update_snapshots provider="deepseek-official", model="smoke-model", cwd=str(root), - session_root=str(sessions), - cordis=str(cordis), - runtime_bin=str(executable), + dsh_bin=str(executable), + dsh_home=str(dsh_home), + patches=(str(patch),), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, @@ -1155,18 +1377,22 @@ def smoke_sdk_restart_snapshot(base_url: str, executable: Path, update_snapshots def smoke_direct(base_url: str, executable: Path) -> None: with tempfile.TemporaryDirectory(prefix="dsh-direct-") as temporary: root = Path(temporary).resolve() - sessions = root / "sessions" - cordis = root / "cordis.yml" - cordis.write_text(CUSTOM_CORDIS) + dsh_home = root / "home" + sessions = dsh_home / "sessions" + patch = write_profile_patch(root, "direct.patch.yml", sessions, []) environment = { **os.environ, - "DSH_CORDIS_CONFIG": str(cordis), - "DSH_SESSION_ROOT": str(sessions), - "DSH_CWD": str(root), + "DSH_HOME": str(dsh_home), + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", "DEEPSEEK_API_KEY": "sk-keyless-smoke", "DEEPSEEK_BASE_URL": base_url, } - peer = RuntimePeer([str(executable)], root, environment) + peer = RuntimePeer( + [str(executable), "--profile", "sdk", "--patch", str(patch)], + root, + environment, + ) try: peer.send({"jsonrpc": "2.0", "id": "initialize", "method": "initialize", "params": {"cwd": str(root), "provider": "deepseek-official", "model": "smoke-model"}}) peer.read_until(lambda message: message.get("id") == "initialize") @@ -1419,7 +1645,6 @@ def build_snapshot_files( {"method": notification.method, "payload": notification.payload} for notification in result.notifications ], - "session_root": result.session_root, } normalized_result = normalize_snapshot_value(result_value, replacements) files = { @@ -1461,7 +1686,6 @@ def build_restart_snapshot_files( "finish_reason": result.finish_reason, "eventTypes": [event.get("type") for event in result.events], "notificationMethods": [notification.method for notification in result.notifications], - "session_root": result.session_root, } for result in (first, second) ] diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/result.json b/scripts/snapshots/python-sdk-single-exe/advanced/result.json index 0889fd6b9f..20aecae839 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json +++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json @@ -4,7 +4,7 @@ "events": [ { "type": "agent/inbox/spliced", - "seq": 0, + "seq": 3, "time": 0, "data": { "target": "next-turn", @@ -28,7 +28,7 @@ }, { "type": "turn/start", - "seq": 1, + "seq": 4, "time": 0, "data": { "turn": 1 @@ -36,7 +36,7 @@ }, { "type": "agent/inbox/spliced", - "seq": 2, + "seq": 5, "time": 0, "data": { "target": "next-turn", @@ -47,7 +47,7 @@ }, { "type": "step/start", - "seq": 3, + "seq": 6, "time": 0, "data": { "turn": 1, @@ -56,7 +56,7 @@ }, { "type": "user/message", - "seq": 4, + "seq": 7, "time": 0, "data": { "content": [ @@ -73,14 +73,45 @@ }, "surfaceOp": "append" }, + { + "type": "user/message", + "seq": 8, + "time": 0, + "data": { + "content": [ + { + "type": "text", + "text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." + } + ], + "source": { + "kind": "plugin", + "plugin": "@deepseek-ai/dsh-system-prompt", + "form": "snapshot", + "sections": [ + { + "name": "sandbox:policy", + "text": "Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations." + }, + { + "name": "approval:policy", + "text": "Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." + } + ] + }, + "role": "user", + "id": "{{messageId}}" + }, + "surfaceOp": "append" + }, { "type": "session/title", - "seq": 5, + "seq": 9, "time": 0, "data": { "title": "Run the advanced packaged-runtime snapsh", "messageSeqs": [ - 4 + 7 ], "source": { "kind": "fallback" @@ -89,7 +120,7 @@ }, { "type": "request/header", - "seq": 6, + "seq": 10, "time": 0, "data": { "header": { @@ -125,7 +156,7 @@ }, { "type": "request/context", - "seq": 7, + "seq": 11, "time": 0, "data": { "provider": "deepseek-official", @@ -135,16 +166,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 8, + "seq": 12, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 7 + "throughSeq": 11 } }, { "type": "assistant/chunk", - "seq": 9, + "seq": 13, "time": 0, "data": { "turn": 1, @@ -158,7 +189,7 @@ }, { "type": "assistant/chunk", - "seq": 10, + "seq": 14, "time": 0, "data": { "turn": 1, @@ -174,7 +205,7 @@ }, { "type": "assistant/chunk", - "seq": 11, + "seq": 15, "time": 0, "data": { "turn": 1, @@ -193,7 +224,7 @@ }, { "type": "assistant/chunk", - "seq": 12, + "seq": 16, "time": 0, "data": { "turn": 1, @@ -209,7 +240,7 @@ }, { "type": "assistant/chunk", - "seq": 13, + "seq": 17, "time": 0, "data": { "turn": 1, @@ -224,7 +255,7 @@ }, { "type": "assistant/message", - "seq": 14, + "seq": 18, "time": 0, "data": { "turn": 1, @@ -252,17 +283,17 @@ } }, "sourceEventSeqs": [ - 9, - 10, - 11, - 12, - 13 + 13, + 14, + 15, + 16, + 17 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 15, + "seq": 19, "time": 0, "data": { "turn": 1, @@ -274,7 +305,7 @@ }, { "type": "tool/result", - "seq": 16, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -306,13 +337,13 @@ } }, "sourceEventSeqs": [ - 15 + 19 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 17, + "seq": 21, "time": 0, "data": { "turn": 1, @@ -321,7 +352,7 @@ }, { "type": "step/start", - "seq": 18, + "seq": 22, "time": 0, "data": { "turn": 1, @@ -330,16 +361,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 19, + "seq": 23, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 18 + "throughSeq": 22 } }, { "type": "assistant/chunk", - "seq": 20, + "seq": 24, "time": 0, "data": { "turn": 1, @@ -353,7 +384,7 @@ }, { "type": "assistant/chunk", - "seq": 21, + "seq": 25, "time": 0, "data": { "turn": 1, @@ -369,7 +400,7 @@ }, { "type": "assistant/chunk", - "seq": 22, + "seq": 26, "time": 0, "data": { "turn": 1, @@ -388,7 +419,7 @@ }, { "type": "assistant/chunk", - "seq": 23, + "seq": 27, "time": 0, "data": { "turn": 1, @@ -404,7 +435,7 @@ }, { "type": "assistant/chunk", - "seq": 24, + "seq": 28, "time": 0, "data": { "turn": 1, @@ -419,7 +450,7 @@ }, { "type": "assistant/message", - "seq": 25, + "seq": 29, "time": 0, "data": { "turn": 1, @@ -447,17 +478,17 @@ } }, "sourceEventSeqs": [ - 20, - 21, - 22, - 23, - 24 + 24, + 25, + 26, + 27, + 28 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 26, + "seq": 30, "time": 0, "data": { "turn": 1, @@ -469,7 +500,7 @@ }, { "type": "tool/result", - "seq": 27, + "seq": 31, "time": 0, "data": { "turn": 1, @@ -502,13 +533,13 @@ } }, "sourceEventSeqs": [ - 26 + 30 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 28, + "seq": 32, "time": 0, "data": { "turn": 1, @@ -517,7 +548,7 @@ }, { "type": "step/start", - "seq": 29, + "seq": 33, "time": 0, "data": { "turn": 1, @@ -526,7 +557,7 @@ }, { "type": "request/header", - "seq": 30, + "seq": 34, "time": 0, "data": { "header": { @@ -563,16 +594,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 31, + "seq": 35, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 30 + "throughSeq": 34 } }, { "type": "assistant/chunk", - "seq": 32, + "seq": 36, "time": 0, "data": { "turn": 1, @@ -586,7 +617,7 @@ }, { "type": "assistant/chunk", - "seq": 33, + "seq": 37, "time": 0, "data": { "turn": 1, @@ -602,7 +633,7 @@ }, { "type": "assistant/chunk", - "seq": 34, + "seq": 38, "time": 0, "data": { "turn": 1, @@ -621,7 +652,7 @@ }, { "type": "assistant/chunk", - "seq": 35, + "seq": 39, "time": 0, "data": { "turn": 1, @@ -637,7 +668,7 @@ }, { "type": "assistant/chunk", - "seq": 36, + "seq": 40, "time": 0, "data": { "turn": 1, @@ -652,7 +683,7 @@ }, { "type": "assistant/message", - "seq": 37, + "seq": 41, "time": 0, "data": { "turn": 1, @@ -680,17 +711,17 @@ } }, "sourceEventSeqs": [ - 32, - 33, - 34, - 35, - 36 + 36, + 37, + 38, + 39, + 40 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 38, + "seq": 42, "time": 0, "data": { "turn": 1, @@ -702,7 +733,7 @@ }, { "type": "tool/code-dispatch-start", - "seq": 39, + "seq": 43, "time": 0, "data": { "rootCallId": "advanced-code", @@ -716,7 +747,7 @@ }, { "type": "tool/code-dispatch", - "seq": 40, + "seq": 44, "time": 0, "data": { "rootCallId": "advanced-code", @@ -737,7 +768,7 @@ }, { "type": "tool/result", - "seq": 41, + "seq": 45, "time": 0, "data": { "turn": 1, @@ -765,13 +796,13 @@ } }, "sourceEventSeqs": [ - 38 + 42 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 42, + "seq": 46, "time": 0, "data": { "turn": 1, @@ -780,7 +811,7 @@ }, { "type": "step/start", - "seq": 43, + "seq": 47, "time": 0, "data": { "turn": 1, @@ -789,16 +820,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 44, + "seq": 48, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 43 + "throughSeq": 47 } }, { "type": "assistant/chunk", - "seq": 45, + "seq": 49, "time": 0, "data": { "turn": 1, @@ -812,7 +843,7 @@ }, { "type": "assistant/chunk", - "seq": 46, + "seq": 50, "time": 0, "data": { "turn": 1, @@ -828,7 +859,7 @@ }, { "type": "assistant/chunk", - "seq": 47, + "seq": 51, "time": 0, "data": { "turn": 1, @@ -847,7 +878,7 @@ }, { "type": "assistant/chunk", - "seq": 48, + "seq": 52, "time": 0, "data": { "turn": 1, @@ -863,7 +894,7 @@ }, { "type": "assistant/chunk", - "seq": 49, + "seq": 53, "time": 0, "data": { "turn": 1, @@ -878,7 +909,7 @@ }, { "type": "assistant/message", - "seq": 50, + "seq": 54, "time": 0, "data": { "turn": 1, @@ -906,17 +937,17 @@ } }, "sourceEventSeqs": [ - 45, - 46, - 47, - 48, - 49 + 49, + 50, + 51, + 52, + 53 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 51, + "seq": 55, "time": 0, "data": { "turn": 1, @@ -928,7 +959,7 @@ }, { "type": "tool/result", - "seq": 52, + "seq": 56, "time": 0, "data": { "turn": 1, @@ -956,13 +987,13 @@ } }, "sourceEventSeqs": [ - 51 + 55 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 53, + "seq": 57, "time": 0, "data": { "turn": 1, @@ -971,7 +1002,7 @@ }, { "type": "step/start", - "seq": 54, + "seq": 58, "time": 0, "data": { "turn": 1, @@ -980,16 +1011,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 55, + "seq": 59, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 54 + "throughSeq": 58 } }, { "type": "assistant/chunk", - "seq": 56, + "seq": 60, "time": 0, "data": { "turn": 1, @@ -1003,7 +1034,7 @@ }, { "type": "assistant/chunk", - "seq": 57, + "seq": 61, "time": 0, "data": { "turn": 1, @@ -1019,7 +1050,7 @@ }, { "type": "assistant/chunk", - "seq": 58, + "seq": 62, "time": 0, "data": { "turn": 1, @@ -1038,7 +1069,7 @@ }, { "type": "assistant/chunk", - "seq": 59, + "seq": 63, "time": 0, "data": { "turn": 1, @@ -1054,7 +1085,7 @@ }, { "type": "assistant/chunk", - "seq": 60, + "seq": 64, "time": 0, "data": { "turn": 1, @@ -1069,7 +1100,7 @@ }, { "type": "assistant/message", - "seq": 61, + "seq": 65, "time": 0, "data": { "turn": 1, @@ -1097,17 +1128,17 @@ } }, "sourceEventSeqs": [ - 56, - 57, - 58, - 59, - 60 + 60, + 61, + 62, + 63, + 64 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 62, + "seq": 66, "time": 0, "data": { "turn": 1, @@ -1119,7 +1150,7 @@ }, { "type": "tool-workflow/run-start", - "seq": 63, + "seq": 67, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1128,7 +1159,7 @@ }, { "type": "tool-workflow/agent-start", - "seq": 64, + "seq": 68, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1140,7 +1171,7 @@ }, { "type": "tool-workflow/agent-end", - "seq": 65, + "seq": 69, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1150,7 +1181,7 @@ }, { "type": "tool-workflow/run-end", - "seq": 66, + "seq": 70, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1159,7 +1190,7 @@ }, { "type": "tool/result", - "seq": 67, + "seq": 71, "time": 0, "data": { "turn": 1, @@ -1187,13 +1218,13 @@ } }, "sourceEventSeqs": [ - 62 + 66 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 68, + "seq": 72, "time": 0, "data": { "turn": 1, @@ -1202,7 +1233,7 @@ }, { "type": "step/start", - "seq": 69, + "seq": 73, "time": 0, "data": { "turn": 1, @@ -1211,16 +1242,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 70, + "seq": 74, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 69 + "throughSeq": 73 } }, { "type": "assistant/chunk", - "seq": 71, + "seq": 75, "time": 0, "data": { "turn": 1, @@ -1234,7 +1265,7 @@ }, { "type": "assistant/chunk", - "seq": 72, + "seq": 76, "time": 0, "data": { "turn": 1, @@ -1250,7 +1281,7 @@ }, { "type": "assistant/chunk", - "seq": 73, + "seq": 77, "time": 0, "data": { "turn": 1, @@ -1269,7 +1300,7 @@ }, { "type": "assistant/chunk", - "seq": 74, + "seq": 78, "time": 0, "data": { "turn": 1, @@ -1285,7 +1316,7 @@ }, { "type": "assistant/chunk", - "seq": 75, + "seq": 79, "time": 0, "data": { "turn": 1, @@ -1300,7 +1331,7 @@ }, { "type": "assistant/message", - "seq": 76, + "seq": 80, "time": 0, "data": { "turn": 1, @@ -1328,17 +1359,17 @@ } }, "sourceEventSeqs": [ - 71, - 72, - 73, - 74, - 75 + 75, + 76, + 77, + 78, + 79 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 77, + "seq": 81, "time": 0, "data": { "turn": 1, @@ -1350,7 +1381,7 @@ }, { "type": "tool/result", - "seq": 78, + "seq": 82, "time": 0, "data": { "turn": 1, @@ -1378,13 +1409,13 @@ } }, "sourceEventSeqs": [ - 77 + 81 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 79, + "seq": 83, "time": 0, "data": { "turn": 1, @@ -1393,7 +1424,7 @@ }, { "type": "step/start", - "seq": 80, + "seq": 84, "time": 0, "data": { "turn": 1, @@ -1402,7 +1433,7 @@ }, { "type": "request/header", - "seq": 81, + "seq": 85, "time": 0, "data": { "header": { @@ -1438,16 +1469,16 @@ }, { "type": "session-log-deepseek/delivery-accepted", - "seq": 82, + "seq": 86, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 81 + "throughSeq": 85 } }, { "type": "assistant/chunk", - "seq": 83, + "seq": 87, "time": 0, "data": { "turn": 1, @@ -1461,7 +1492,7 @@ }, { "type": "assistant/chunk", - "seq": 84, + "seq": 88, "time": 0, "data": { "turn": 1, @@ -1475,7 +1506,7 @@ }, { "type": "assistant/chunk", - "seq": 85, + "seq": 89, "time": 0, "data": { "turn": 1, @@ -1492,7 +1523,7 @@ }, { "type": "assistant/chunk", - "seq": 86, + "seq": 90, "time": 0, "data": { "turn": 1, @@ -1508,7 +1539,7 @@ }, { "type": "assistant/chunk", - "seq": 87, + "seq": 91, "time": 0, "data": { "turn": 1, @@ -1523,7 +1554,7 @@ }, { "type": "assistant/message", - "seq": 88, + "seq": 92, "time": 0, "data": { "turn": 1, @@ -1549,17 +1580,17 @@ } }, "sourceEventSeqs": [ - 83, - 84, - 85, - 86, - 87 + 87, + 88, + 89, + 90, + 91 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 89, + "seq": 93, "time": 0, "data": { "turn": 1, @@ -1568,7 +1599,7 @@ }, { "type": "turn/end", - "seq": 90, + "seq": 94, "time": 0, "data": { "turn": 1, @@ -1585,7 +1616,7 @@ "sessionId": "{{parent}}", "event": { "type": "agent/inbox/spliced", - "seq": 0, + "seq": 3, "time": 0, "data": { "target": "next-turn", @@ -1622,7 +1653,7 @@ "sessionId": "{{parent}}", "event": { "type": "turn/start", - "seq": 1, + "seq": 4, "time": 0, "data": { "turn": 1 @@ -1636,7 +1667,7 @@ "sessionId": "{{parent}}", "event": { "type": "agent/inbox/spliced", - "seq": 2, + "seq": 5, "time": 0, "data": { "target": "next-turn", @@ -1653,7 +1684,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 3, + "seq": 6, "time": 0, "data": { "turn": 1, @@ -1668,7 +1699,7 @@ "sessionId": "{{parent}}", "event": { "type": "user/message", - "seq": 4, + "seq": 7, "time": 0, "data": { "content": [ @@ -1687,18 +1718,55 @@ } } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "user/message", + "seq": 8, + "time": 0, + "data": { + "content": [ + { + "type": "text", + "text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." + } + ], + "source": { + "kind": "plugin", + "plugin": "@deepseek-ai/dsh-system-prompt", + "form": "snapshot", + "sections": [ + { + "name": "sandbox:policy", + "text": "Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations." + }, + { + "name": "approval:policy", + "text": "Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." + } + ] + }, + "role": "user", + "id": "{{messageId}}" + }, + "surfaceOp": "append" + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{parent}}", "event": { "type": "session/title", - "seq": 5, + "seq": 9, "time": 0, "data": { "title": "Run the advanced packaged-runtime snapsh", "messageSeqs": [ - 4 + 7 ], "source": { "kind": "fallback" @@ -1713,7 +1781,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/header", - "seq": 6, + "seq": 10, "time": 0, "data": { "header": { @@ -1755,7 +1823,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/context", - "seq": 7, + "seq": 11, "time": 0, "data": { "provider": "deepseek-official", @@ -1771,11 +1839,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 8, + "seq": 12, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 7 + "throughSeq": 11 } } } @@ -1786,7 +1854,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 9, + "seq": 13, "time": 0, "data": { "turn": 1, @@ -1806,7 +1874,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 10, + "seq": 14, "time": 0, "data": { "turn": 1, @@ -1828,7 +1896,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 11, + "seq": 15, "time": 0, "data": { "turn": 1, @@ -1853,7 +1921,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 12, + "seq": 16, "time": 0, "data": { "turn": 1, @@ -1875,7 +1943,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 13, + "seq": 17, "time": 0, "data": { "turn": 1, @@ -1896,7 +1964,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 14, + "seq": 18, "time": 0, "data": { "turn": 1, @@ -1924,11 +1992,11 @@ } }, "sourceEventSeqs": [ - 9, - 10, - 11, - 12, - 13 + 13, + 14, + 15, + 16, + 17 ], "surfaceOp": "append" } @@ -1940,7 +2008,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 15, + "seq": 19, "time": 0, "data": { "turn": 1, @@ -1958,7 +2026,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 16, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -1990,7 +2058,7 @@ } }, "sourceEventSeqs": [ - 15 + 19 ], "surfaceOp": "append" } @@ -2002,7 +2070,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 17, + "seq": 21, "time": 0, "data": { "turn": 1, @@ -2017,7 +2085,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 18, + "seq": 22, "time": 0, "data": { "turn": 1, @@ -2032,11 +2100,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 19, + "seq": 23, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 18 + "throughSeq": 22 } } } @@ -2047,7 +2115,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 20, + "seq": 24, "time": 0, "data": { "turn": 1, @@ -2067,7 +2135,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 21, + "seq": 25, "time": 0, "data": { "turn": 1, @@ -2089,7 +2157,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 22, + "seq": 26, "time": 0, "data": { "turn": 1, @@ -2114,7 +2182,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 23, + "seq": 27, "time": 0, "data": { "turn": 1, @@ -2136,7 +2204,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 24, + "seq": 28, "time": 0, "data": { "turn": 1, @@ -2157,7 +2225,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 25, + "seq": 29, "time": 0, "data": { "turn": 1, @@ -2185,11 +2253,11 @@ } }, "sourceEventSeqs": [ - 20, - 21, - 22, - 23, - 24 + 24, + 25, + 26, + 27, + 28 ], "surfaceOp": "append" } @@ -2201,7 +2269,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 26, + "seq": 30, "time": 0, "data": { "turn": 1, @@ -2219,7 +2287,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 27, + "seq": 31, "time": 0, "data": { "turn": 1, @@ -2252,7 +2320,7 @@ } }, "sourceEventSeqs": [ - 26 + 30 ], "surfaceOp": "append" } @@ -2264,7 +2332,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 28, + "seq": 32, "time": 0, "data": { "turn": 1, @@ -2279,7 +2347,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 29, + "seq": 33, "time": 0, "data": { "turn": 1, @@ -2294,7 +2362,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/header", - "seq": 30, + "seq": 34, "time": 0, "data": { "header": { @@ -2337,11 +2405,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 31, + "seq": 35, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 30 + "throughSeq": 34 } } } @@ -2352,7 +2420,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 32, + "seq": 36, "time": 0, "data": { "turn": 1, @@ -2372,7 +2440,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 33, + "seq": 37, "time": 0, "data": { "turn": 1, @@ -2394,7 +2462,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 34, + "seq": 38, "time": 0, "data": { "turn": 1, @@ -2419,7 +2487,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 35, + "seq": 39, "time": 0, "data": { "turn": 1, @@ -2441,7 +2509,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 36, + "seq": 40, "time": 0, "data": { "turn": 1, @@ -2462,7 +2530,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 37, + "seq": 41, "time": 0, "data": { "turn": 1, @@ -2490,11 +2558,11 @@ } }, "sourceEventSeqs": [ - 32, - 33, - 34, - 35, - 36 + 36, + 37, + 38, + 39, + 40 ], "surfaceOp": "append" } @@ -2506,7 +2574,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 38, + "seq": 42, "time": 0, "data": { "turn": 1, @@ -2524,7 +2592,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/code-dispatch-start", - "seq": 39, + "seq": 43, "time": 0, "data": { "rootCallId": "advanced-code", @@ -2544,7 +2612,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/code-dispatch", - "seq": 40, + "seq": 44, "time": 0, "data": { "rootCallId": "advanced-code", @@ -2571,7 +2639,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 41, + "seq": 45, "time": 0, "data": { "turn": 1, @@ -2599,7 +2667,7 @@ } }, "sourceEventSeqs": [ - 38 + 42 ], "surfaceOp": "append" } @@ -2611,7 +2679,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 42, + "seq": 46, "time": 0, "data": { "turn": 1, @@ -2626,7 +2694,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 43, + "seq": 47, "time": 0, "data": { "turn": 1, @@ -2641,11 +2709,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 44, + "seq": 48, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 43 + "throughSeq": 47 } } } @@ -2656,7 +2724,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 45, + "seq": 49, "time": 0, "data": { "turn": 1, @@ -2676,7 +2744,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 46, + "seq": 50, "time": 0, "data": { "turn": 1, @@ -2698,7 +2766,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 47, + "seq": 51, "time": 0, "data": { "turn": 1, @@ -2723,7 +2791,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 48, + "seq": 52, "time": 0, "data": { "turn": 1, @@ -2745,7 +2813,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 49, + "seq": 53, "time": 0, "data": { "turn": 1, @@ -2766,7 +2834,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 50, + "seq": 54, "time": 0, "data": { "turn": 1, @@ -2794,11 +2862,11 @@ } }, "sourceEventSeqs": [ - 45, - 46, - 47, - 48, - 49 + 49, + 50, + 51, + 52, + 53 ], "surfaceOp": "append" } @@ -2810,7 +2878,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 51, + "seq": 55, "time": 0, "data": { "turn": 1, @@ -2829,13 +2897,27 @@ "childSessionId": "{{child-1}}" } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{child-1}}", + "event": { + "type": "permission/preset", + "seq": 2, + "time": 0, + "data": { + "preset": "danger-full-access" + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{child-1}}", "event": { "type": "agent/inbox/spliced", - "seq": 0, + "seq": 3, "time": 0, "data": { "target": "next-turn", @@ -2872,7 +2954,7 @@ "sessionId": "{{child-1}}", "event": { "type": "turn/start", - "seq": 1, + "seq": 4, "time": 0, "data": { "turn": 1 @@ -2886,7 +2968,7 @@ "sessionId": "{{child-1}}", "event": { "type": "agent/inbox/spliced", - "seq": 2, + "seq": 5, "time": 0, "data": { "target": "next-turn", @@ -2903,7 +2985,7 @@ "sessionId": "{{child-1}}", "event": { "type": "subagent/descriptor", - "seq": 3, + "seq": 6, "time": 0, "data": { "version": 2, @@ -2920,7 +3002,7 @@ "sessionId": "{{child-1}}", "event": { "type": "step/start", - "seq": 4, + "seq": 7, "time": 0, "data": { "turn": 1, @@ -2935,7 +3017,7 @@ "sessionId": "{{child-1}}", "event": { "type": "user/message", - "seq": 5, + "seq": 8, "time": 0, "data": { "content": [ @@ -2960,13 +3042,13 @@ "sessionId": "{{child-1}}", "event": { "type": "user/message", - "seq": 6, + "seq": 9, "time": 0, "data": { "content": [ { "type": "text", - "text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it." + "text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it." } ], "source": { @@ -2974,6 +3056,14 @@ "plugin": "@deepseek-ai/dsh-system-prompt", "form": "snapshot", "sections": [ + { + "name": "sandbox:policy", + "text": "Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations." + }, + { + "name": "approval:policy", + "text": "Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." + }, { "name": "subagent:delegation", "text": "You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it." @@ -2993,12 +3083,12 @@ "sessionId": "{{child-1}}", "event": { "type": "session/title", - "seq": 7, + "seq": 10, "time": 0, "data": { "title": "Reply with exactly DIRECT_CHILD_OK and", "messageSeqs": [ - 5 + 8 ], "source": { "kind": "fallback" @@ -3013,7 +3103,7 @@ "sessionId": "{{child-1}}", "event": { "type": "request/header", - "seq": 8, + "seq": 11, "time": 0, "data": { "header": { @@ -3056,7 +3146,7 @@ "sessionId": "{{child-1}}", "event": { "type": "request/context", - "seq": 9, + "seq": 12, "time": 0, "data": { "provider": "deepseek-official", @@ -3072,11 +3162,11 @@ "sessionId": "{{child-1}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 10, + "seq": 13, "time": 0, "data": { "sessionId": "{{child-1}}", - "throughSeq": 9 + "throughSeq": 12 } } } @@ -3087,7 +3177,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 11, + "seq": 14, "time": 0, "data": { "turn": 1, @@ -3107,7 +3197,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 12, + "seq": 15, "time": 0, "data": { "turn": 1, @@ -3127,7 +3217,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 13, + "seq": 16, "time": 0, "data": { "turn": 1, @@ -3150,7 +3240,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 14, + "seq": 17, "time": 0, "data": { "turn": 1, @@ -3172,7 +3262,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 15, + "seq": 18, "time": 0, "data": { "turn": 1, @@ -3193,7 +3283,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/message", - "seq": 16, + "seq": 19, "time": 0, "data": { "turn": 1, @@ -3219,11 +3309,11 @@ } }, "sourceEventSeqs": [ - 11, - 12, - 13, 14, - 15 + 15, + 16, + 17, + 18 ], "surfaceOp": "append" } @@ -3235,7 +3325,7 @@ "sessionId": "{{child-1}}", "event": { "type": "step/end", - "seq": 17, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -3250,7 +3340,7 @@ "sessionId": "{{child-1}}", "event": { "type": "turn/end", - "seq": 18, + "seq": 21, "time": 0, "data": { "turn": 1, @@ -3291,7 +3381,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 52, + "seq": 56, "time": 0, "data": { "turn": 1, @@ -3319,7 +3409,7 @@ } }, "sourceEventSeqs": [ - 51 + 55 ], "surfaceOp": "append" } @@ -3331,7 +3421,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 53, + "seq": 57, "time": 0, "data": { "turn": 1, @@ -3346,7 +3436,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 54, + "seq": 58, "time": 0, "data": { "turn": 1, @@ -3361,11 +3451,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 55, + "seq": 59, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 54 + "throughSeq": 58 } } } @@ -3376,7 +3466,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 56, + "seq": 60, "time": 0, "data": { "turn": 1, @@ -3396,7 +3486,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 57, + "seq": 61, "time": 0, "data": { "turn": 1, @@ -3418,7 +3508,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 58, + "seq": 62, "time": 0, "data": { "turn": 1, @@ -3443,7 +3533,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 59, + "seq": 63, "time": 0, "data": { "turn": 1, @@ -3465,7 +3555,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 60, + "seq": 64, "time": 0, "data": { "turn": 1, @@ -3486,7 +3576,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 61, + "seq": 65, "time": 0, "data": { "turn": 1, @@ -3514,11 +3604,11 @@ } }, "sourceEventSeqs": [ - 56, - 57, - 58, - 59, - 60 + 60, + 61, + 62, + 63, + 64 ], "surfaceOp": "append" } @@ -3530,7 +3620,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 62, + "seq": 66, "time": 0, "data": { "turn": 1, @@ -3548,7 +3638,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/run-start", - "seq": 63, + "seq": 67, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -3564,13 +3654,27 @@ "childSessionId": "{{child-2}}" } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{child-2}}", + "event": { + "type": "permission/preset", + "seq": 2, + "time": 0, + "data": { + "preset": "danger-full-access" + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{child-2}}", "event": { "type": "agent/inbox/spliced", - "seq": 0, + "seq": 3, "time": 0, "data": { "target": "next-turn", @@ -3607,7 +3711,7 @@ "sessionId": "{{child-2}}", "event": { "type": "turn/start", - "seq": 1, + "seq": 4, "time": 0, "data": { "turn": 1 @@ -3621,7 +3725,7 @@ "sessionId": "{{child-2}}", "event": { "type": "agent/inbox/spliced", - "seq": 2, + "seq": 5, "time": 0, "data": { "target": "next-turn", @@ -3632,13 +3736,31 @@ } } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "tool-workflow/agent-start", + "seq": 68, + "time": 0, + "data": { + "runId": "{{workflow-run}}", + "seq": 1, + "label": "workflow-child", + "phase": "Delegate", + "childId": "{{child-2}}" + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{child-2}}", "event": { "type": "subagent/descriptor", - "seq": 3, + "seq": 6, "time": 0, "data": { "version": 2, @@ -3654,7 +3776,7 @@ "sessionId": "{{child-2}}", "event": { "type": "step/start", - "seq": 4, + "seq": 7, "time": 0, "data": { "turn": 1, @@ -3669,7 +3791,7 @@ "sessionId": "{{child-2}}", "event": { "type": "user/message", - "seq": 5, + "seq": 8, "time": 0, "data": { "content": [ @@ -3694,13 +3816,13 @@ "sessionId": "{{child-2}}", "event": { "type": "user/message", - "seq": 6, + "seq": 9, "time": 0, "data": { "content": [ { "type": "text", - "text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it." + "text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it." } ], "source": { @@ -3708,6 +3830,14 @@ "plugin": "@deepseek-ai/dsh-system-prompt", "form": "snapshot", "sections": [ + { + "name": "sandbox:policy", + "text": "Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations." + }, + { + "name": "approval:policy", + "text": "Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." + }, { "name": "subagent:delegation", "text": "You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it." @@ -3727,12 +3857,12 @@ "sessionId": "{{child-2}}", "event": { "type": "session/title", - "seq": 7, + "seq": 10, "time": 0, "data": { "title": "Reply with exactly WORKFLOW_CHILD_OK and", "messageSeqs": [ - 5 + 8 ], "source": { "kind": "fallback" @@ -3747,7 +3877,7 @@ "sessionId": "{{child-2}}", "event": { "type": "request/header", - "seq": 8, + "seq": 11, "time": 0, "data": { "header": { @@ -3790,7 +3920,7 @@ "sessionId": "{{child-2}}", "event": { "type": "request/context", - "seq": 9, + "seq": 12, "time": 0, "data": { "provider": "deepseek-official", @@ -3800,35 +3930,17 @@ } } }, - { - "method": "session.event", - "payload": { - "sessionId": "{{parent}}", - "event": { - "type": "tool-workflow/agent-start", - "seq": 64, - "time": 0, - "data": { - "runId": "{{workflow-run}}", - "seq": 1, - "label": "workflow-child", - "phase": "Delegate", - "childId": "{{child-2}}" - } - } - } - }, { "method": "session.event", "payload": { "sessionId": "{{child-2}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 10, + "seq": 13, "time": 0, "data": { "sessionId": "{{child-2}}", - "throughSeq": 9 + "throughSeq": 12 } } } @@ -3839,7 +3951,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 11, + "seq": 14, "time": 0, "data": { "turn": 1, @@ -3859,7 +3971,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 12, + "seq": 15, "time": 0, "data": { "turn": 1, @@ -3879,7 +3991,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 13, + "seq": 16, "time": 0, "data": { "turn": 1, @@ -3902,7 +4014,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 14, + "seq": 17, "time": 0, "data": { "turn": 1, @@ -3924,7 +4036,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 15, + "seq": 18, "time": 0, "data": { "turn": 1, @@ -3945,7 +4057,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/message", - "seq": 16, + "seq": 19, "time": 0, "data": { "turn": 1, @@ -3971,11 +4083,11 @@ } }, "sourceEventSeqs": [ - 11, - 12, - 13, 14, - 15 + 15, + 16, + 17, + 18 ], "surfaceOp": "append" } @@ -3987,7 +4099,7 @@ "sessionId": "{{child-2}}", "event": { "type": "step/end", - "seq": 17, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -4002,7 +4114,7 @@ "sessionId": "{{child-2}}", "event": { "type": "turn/end", - "seq": 18, + "seq": 21, "time": 0, "data": { "turn": 1, @@ -4043,7 +4155,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/agent-end", - "seq": 65, + "seq": 69, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -4059,7 +4171,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/run-end", - "seq": 66, + "seq": 70, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -4074,7 +4186,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 67, + "seq": 71, "time": 0, "data": { "turn": 1, @@ -4102,7 +4214,7 @@ } }, "sourceEventSeqs": [ - 62 + 66 ], "surfaceOp": "append" } @@ -4114,7 +4226,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 68, + "seq": 72, "time": 0, "data": { "turn": 1, @@ -4129,7 +4241,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 69, + "seq": 73, "time": 0, "data": { "turn": 1, @@ -4144,11 +4256,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 70, + "seq": 74, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 69 + "throughSeq": 73 } } } @@ -4159,7 +4271,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 71, + "seq": 75, "time": 0, "data": { "turn": 1, @@ -4179,7 +4291,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 72, + "seq": 76, "time": 0, "data": { "turn": 1, @@ -4201,7 +4313,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 73, + "seq": 77, "time": 0, "data": { "turn": 1, @@ -4226,7 +4338,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 74, + "seq": 78, "time": 0, "data": { "turn": 1, @@ -4248,7 +4360,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 75, + "seq": 79, "time": 0, "data": { "turn": 1, @@ -4269,7 +4381,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 76, + "seq": 80, "time": 0, "data": { "turn": 1, @@ -4297,11 +4409,11 @@ } }, "sourceEventSeqs": [ - 71, - 72, - 73, - 74, - 75 + 75, + 76, + 77, + 78, + 79 ], "surfaceOp": "append" } @@ -4313,7 +4425,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 77, + "seq": 81, "time": 0, "data": { "turn": 1, @@ -4331,7 +4443,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 78, + "seq": 82, "time": 0, "data": { "turn": 1, @@ -4359,7 +4471,7 @@ } }, "sourceEventSeqs": [ - 77 + 81 ], "surfaceOp": "append" } @@ -4371,7 +4483,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 79, + "seq": 83, "time": 0, "data": { "turn": 1, @@ -4386,7 +4498,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 80, + "seq": 84, "time": 0, "data": { "turn": 1, @@ -4401,7 +4513,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/header", - "seq": 81, + "seq": 85, "time": 0, "data": { "header": { @@ -4443,11 +4555,11 @@ "sessionId": "{{parent}}", "event": { "type": "session-log-deepseek/delivery-accepted", - "seq": 82, + "seq": 86, "time": 0, "data": { "sessionId": "{{parent}}", - "throughSeq": 81 + "throughSeq": 85 } } } @@ -4458,7 +4570,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 83, + "seq": 87, "time": 0, "data": { "turn": 1, @@ -4478,7 +4590,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 84, + "seq": 88, "time": 0, "data": { "turn": 1, @@ -4498,7 +4610,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 85, + "seq": 89, "time": 0, "data": { "turn": 1, @@ -4521,7 +4633,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 86, + "seq": 90, "time": 0, "data": { "turn": 1, @@ -4543,7 +4655,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 87, + "seq": 91, "time": 0, "data": { "turn": 1, @@ -4564,7 +4676,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 88, + "seq": 92, "time": 0, "data": { "turn": 1, @@ -4590,11 +4702,11 @@ } }, "sourceEventSeqs": [ - 83, - 84, - 85, - 86, - 87 + 87, + 88, + 89, + 90, + 91 ], "surfaceOp": "append" } @@ -4606,7 +4718,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 89, + "seq": 93, "time": 0, "data": { "turn": 1, @@ -4621,7 +4733,7 @@ "sessionId": "{{parent}}", "event": { "type": "turn/end", - "seq": 90, + "seq": 94, "time": 0, "data": { "turn": 1, @@ -4639,6 +4751,5 @@ "status": "idle" } } - ], - "session_root": "{{cwd}}/sessions" + ] } diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl index 6e2f62bfe1..db7a626752 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl @@ -1,20 +1,23 @@ {"type":"session","version":0,"id":"{{child-1}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","origin":"subagent","delegationDepth":1} +{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}} +{"type":"approval/policy","data":{"policy":"never","source":"delegation"}} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-1}}","throughSeq":9}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-1}}","throughSeq":12}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl index e11cec5548..eed0d049f5 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl @@ -1,20 +1,23 @@ {"type":"session","version":0,"id":"{{child-2}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","origin":"subagent","delegationDepth":1} +{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}} +{"type":"approval/policy","data":{"policy":"never","source":"delegation"}} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn"}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-2}}","throughSeq":9}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-2}}","throughSeq":12}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl index 2e65037b37..a361560a35 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl @@ -1,92 +1,96 @@ {"type":"session","version":0,"id":"{{parent}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[4],"source":{"kind":"fallback"}}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":7}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":11}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-define","name":"cordis_define","argumentsDelta":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[15],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[19],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":18}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":22}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-run","name":"cordis_run","argumentsDelta":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[20,21,22,23,24],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[26],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[30],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"change"}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":30}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":34}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-code","name":"run_code","argumentsDelta":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[36,37,38,39,40],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}} {"type":"tool/code-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21}}} {"type":"tool/code-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}} -{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[38],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[42],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":43}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":47}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-direct-child","name":"subagent","argumentsDelta":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[45,46,47,48,49],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[51],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":54}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":58}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-workflow","name":"workflow","argumentsDelta":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[56,57,58,59,60],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[60,61,62,63,64],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow-run}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow-run}}","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[62],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[66],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"step/start","data":{"turn":1,"step":6}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":69}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":73}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-undefine","name":"cordis_undefine","argumentsDelta":"{\"pluginId\": \"snap-1\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[71,72,73,74,75],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[75,76,77,78,79],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":6,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[77],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[81],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":6}} {"type":"step/start","data":{"turn":1,"step":7}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"change"}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":81}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":85}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_EXECUTABLE_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[83,84,85,86,87],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[87,88,89,90,91],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":7}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/scripts/snapshots/python-sdk-single-exe/restart/requests.json b/scripts/snapshots/python-sdk-single-exe/restart/requests.json index dd5f925294..a7efe0f6ce 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/requests.json +++ b/scripts/snapshots/python-sdk-single-exe/restart/requests.json @@ -9,6 +9,10 @@ { "role": "user", "content": "Complete the first isolated Python SDK process turn." + }, + { + "role": "user", + "content": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." } ], "toolNames": [ @@ -37,6 +41,10 @@ { "role": "user", "content": "Complete the second isolated Python SDK process turn." + }, + { + "role": "user", + "content": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)." } ], "toolNames": [ diff --git a/scripts/snapshots/python-sdk-single-exe/restart/result.json b/scripts/snapshots/python-sdk-single-exe/restart/result.json index 32911ef466..d1aac3e127 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/result.json +++ b/scripts/snapshots/python-sdk-single-exe/restart/result.json @@ -9,51 +9,6 @@ "agent/inbox/spliced", "step/start", "user/message", - "session/title", - "request/header", - "request/context", - "session-log-deepseek/delivery-accepted", - "assistant/chunk", - "assistant/chunk", - "assistant/chunk", - "assistant/chunk", - "assistant/chunk", - "assistant/message", - "step/end", - "turn/end" - ], - "notificationMethods": [ - "session.event", - "session.status", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.event", - "session.status" - ], - "session_root": "{{sessions}}" - }, - { - "session_id": "{{session-2}}", - "final_response": "PROCESS_TWO_OK", - "finish_reason": "completed", - "eventTypes": [ - "agent/inbox/spliced", - "turn/start", - "agent/inbox/spliced", - "step/start", "user/message", "session/title", "request/header", @@ -87,8 +42,55 @@ "session.event", "session.event", "session.event", + "session.event", "session.status" + ] + }, + { + "session_id": "{{session-2}}", + "final_response": "PROCESS_TWO_OK", + "finish_reason": "completed", + "eventTypes": [ + "agent/inbox/spliced", + "turn/start", + "agent/inbox/spliced", + "step/start", + "user/message", + "user/message", + "session/title", + "request/header", + "request/context", + "session-log-deepseek/delivery-accepted", + "assistant/chunk", + "assistant/chunk", + "assistant/chunk", + "assistant/chunk", + "assistant/chunk", + "assistant/message", + "step/end", + "turn/end" ], - "session_root": "{{sessions}}" + "notificationMethods": [ + "session.event", + "session.status", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.event", + "session.status" + ] } ] diff --git a/scripts/snapshots/python-sdk-single-exe/restart/session.1.jsonl b/scripts/snapshots/python-sdk-single-exe/restart/session.1.jsonl index 38e5f98ff6..babaec2193 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/session.1.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/restart/session.1.jsonl @@ -1,18 +1,22 @@ {"type":"session","version":0,"id":"{{session-1}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Complete the first isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Complete the first isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"session/title","data":{"title":"Complete the first isolated Python","messageSeqs":[4],"source":{"kind":"fallback"}}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Complete the first isolated Python","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-1}}","throughSeq":7}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-1}}","throughSeq":11}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"PROCESS_ONE_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PROCESS_ONE_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_ONE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_ONE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/scripts/snapshots/python-sdk-single-exe/restart/session.2.jsonl b/scripts/snapshots/python-sdk-single-exe/restart/session.2.jsonl index 523f784dfa..afd9753b4a 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/session.2.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/restart/session.2.jsonl @@ -1,18 +1,22 @@ {"type":"session","version":0,"id":"{{session-2}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Complete the second isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"Complete the second isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"session/title","data":{"title":"Complete the second isolated Python","messageSeqs":[4],"source":{"kind":"fallback"}}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Complete the second isolated Python","messageSeqs":[7],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-2}}","throughSeq":7}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-2}}","throughSeq":11}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"PROCESS_TWO_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PROCESS_TWO_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_TWO_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_TWO_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} From f0f9b294ddee7a0ced10bb19aa2c8af4b91c2a35 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:56:55 +0800 Subject: [PATCH 06/16] docs(python): make the dsh profile runtime current Document dsh as the only application launcher across architecture, CLI, SDK, app-boot, Python package, contributor, tutorial, and example references. Explain explicit home selection, profile and patch precedence, persistent external plugin installation, the Node-free runtime path, and the absence of complete-config or ~/.dsh fallbacks. Record the Python profile-runtime decision and update the active naming, installed-wheel, and SEA packaging notes with precise supersession. Regenerate the configuration catalog and module graph after deleting the carrier, update both reviewed languages and pairing records, and classify the retained standalone Cordis files as lower-level test fixtures rather than launch interfaces. --- ...cutable-sdk-runtime-distribution.i18n.yaml | 4 +- ...ile-executable-sdk-runtime-distribution.md | 8 +- ...-executable-sdk-runtime-distribution.zh.md | 8 +- ...aming-contract-and-rename-ledger.i18n.yaml | 4 +- ...itory-naming-contract-and-rename-ledger.md | 8 +- ...ry-naming-contract-and-rename-ledger.zh.md | 8 +- ...3-python-sdk-dsh-profile-runtime.i18n.yaml | 6 ++ ...26-08-23-python-sdk-dsh-profile-runtime.md | 53 ++++++++++++ ...08-23-python-sdk-dsh-profile-runtime.zh.md | 53 ++++++++++++ ...talled-python-wheel-black-box-ci.i18n.yaml | 4 +- ...-23-installed-python-wheel-black-box-ci.md | 4 +- ...-installed-python-wheel-black-box-ci.zh.md | 4 +- AGENTS.md | 2 +- apps/cli/README.i18n.yaml | 4 +- apps/cli/README.md | 2 +- apps/cli/README.zh.md | 2 +- apps/cli/reference/README.i18n.yaml | 4 +- apps/cli/reference/README.md | 2 +- apps/cli/reference/README.zh.md | 2 +- docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 2 +- docs/architecture.zh.md | 2 +- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 1 - docs/config-catalog.zh.md | 1 - docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 3 - docs/module-graph.zh.md | 3 - docs/user/guide/python-sdk.i18n.yaml | 4 +- docs/user/guide/python-sdk.md | 66 ++++++++------- docs/user/guide/python-sdk.zh.md | 84 ++++++++++--------- examples/python-sdk-agent/README.i18n.yaml | 4 +- examples/python-sdk-agent/README.md | 54 ++++++------ examples/python-sdk-agent/README.zh.md | 52 ++++++------ examples/python-sdk-agent/cordis.snapshot.yml | 7 +- examples/python-sdk-agent/cordis.yml | 5 +- examples/python-sdk-agent/minimal.cordis.yml | 7 +- packages/README.i18n.yaml | 4 +- packages/README.md | 2 +- packages/README.zh.md | 2 +- packages/boot/app-boot/README.i18n.yaml | 4 +- packages/boot/app-boot/README.md | 4 +- packages/boot/app-boot/README.zh.md | 4 +- packages/sdk/README.i18n.yaml | 4 +- packages/sdk/README.md | 3 +- packages/sdk/README.zh.md | 3 +- packages/sdk/server/README.i18n.yaml | 4 +- packages/sdk/server/README.md | 8 +- packages/sdk/server/README.zh.md | 8 +- python/README.i18n.yaml | 4 +- python/README.md | 4 +- python/README.zh.md | 4 +- python/development.i18n.yaml | 4 +- python/development.md | 14 ++-- python/development.zh.md | 14 ++-- python/sdk-runtime/README.i18n.yaml | 4 +- python/sdk-runtime/README.md | 41 +++++---- python/sdk-runtime/README.zh.md | 41 +++++---- python/sdk/README.i18n.yaml | 4 +- python/sdk/README.md | 67 +++++++++------ python/sdk/README.zh.md | 64 ++++++++------ 61 files changed, 480 insertions(+), 324 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md create mode 100644 .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index be7aedb4bd..29b7caeb50 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml @@ -2,5 +2,5 @@ # 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/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md -2026-07-10-single-file-executable-sdk-runtime-distribution.md: e5438fc01fdd634cd4d8535dfa37a1de356da04d -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 218133f077bed7584b6a26fea5f939b64d4fd670 +2026-07-10-single-file-executable-sdk-runtime-distribution.md: 50e63bbc4f664f9450f01acf45888991a27517d2 +2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: cf92f42f508f005c64f09aa0127b28ed162cede5 diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md index e5438fc01f..50e63bbc4f 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md @@ -23,14 +23,14 @@ The exe is packaged with the **`--sea` (enhanced SEA) mode** of [@yao-pkg/pkg](h Terminology reminder: pkg's `/snapshot` VFS has nothing to do with this repo's testing-system "snapshot" (ACP replay expected outputs, `$DSH_SNAPSHOT`); this document says "VFS" for the former. -### The serving interface is a plugin: the two packages sdk/server + sdk/python-runtime +### The serving interface is a plugin inside the dsh application -The deterministic protocol implementation (`server.ts` / `transport.ts`) lands as two packages on the existing `acp/acp` + `examples/acp-demo` pattern — the serving surface is itself a plugin: +The deterministic serving surface is a plugin selected by the packaged `dsh` application: - [`packages/sdk/server`](../../../../packages/sdk/server/README.md) (`@deepseek-ai/dsh-sdk-jsonrpc-server`): the pure protocol plugin; on apply it mounts `HarnessSdkJsonRpcServer` plus a line-delimited JSON-RPC transport on the process stdio, with disposal through `ctx.effect()`. Whether to serve is decided by `cordis.yml`; a yml that does not mount it is a legitimate process that does not serve. Protocol-level exit belongs to the plugin (after answering and flushing the `shutdown` response it disposes the root runtime so persistence drains, then `exit(0)`; an HMR-style unload only stops the service without exiting the process). -- [`packages/sdk/python-runtime`](../../../../packages/sdk/python-runtime/README.md) (`@deepseek-ai/dsh-sdk-python-runtime`): a private packaged entry — `installFailLoud` + `loadEnv` + config discovery + `boot()` from [`dsh-app-boot`](../../../../packages/boot/app-boot/src/index.ts), done once boot completes; the server is brought up by the `dsh-sdk-jsonrpc-server` entry in the yml. Its only dependency is app-boot. Process-level exit belongs to the packaged entry (stdin EOF/SIGTERM → dispose then 0, SIGINT → 130). +- [`apps/cli`](../../../../apps/cli/README.md) (`@deepseek-ai/dsh`): the packaged application entry; its `sdk` profile mounts `dsh-sdk-jsonrpc-server`, and the CLI owns environment layering, profile composition, stdin/signal shutdown, and process exit. -Config discovery has two channels and fails loudly when both are missing: the `DSH_CORDIS_CONFIG` environment variable first (the SDK client convention), then an argv positional argument; no default path and no built-in fallback whatsoever — "the plugins actually booted are decided by an external cordis.yml" is a hard semantic. +The Python client supplies an explicit Harness home and selects the `sdk` profile plus ordered patch files. A missing home, profile, bundle, or server row fails loudly; there is no external complete-config fallback. The [Python profile-runtime decision](2026-08-23-python-sdk-dsh-profile-runtime.md) owns this application surface. ### Plugin resolution: the VFS holds a real package tree, the closure manifest IS the deploy root diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md index 218133f077..cf92f42f50 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md @@ -23,14 +23,14 @@ exe 使用 [@yao-pkg/pkg](https://github.com/yao-pkg/pkg)(vercel/pkg 归档后 术语提醒:pkg 的 `/snapshot` VFS 与本仓库测试体系的「快照」(ACP(Agent Client Protocol)回放预期输出、`$DSH_SNAPSHOT`)无关,本文用「VFS」指前者。 -### 对外服务接口也是插件:sdk/server + sdk/python-runtime 两个包 +### 对外服务接口是 dsh 应用中的插件 -确定性协议实现(`server.ts` / `transport.ts`)按 `acp/acp` + `examples/acp-demo` 的既有模式落为两包——对外服务接口本身也是插件: +确定性服务接口由打包后的 `dsh` 应用选择为插件: - [`packages/sdk/server`](../../../../packages/sdk/server/README.zh.md)(`@deepseek-ai/dsh-sdk-jsonrpc-server`):纯协议插件;执行 `apply` 时,在进程 stdio 上挂载 `HarnessSdkJsonRpcServer` 与按行分隔的 JSON-RPC 传输层,资源释放走 `ctx.effect()`。是否提供服务由 `cordis.yml` 决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答并确保 `shutdown` 响应发送完毕后,对根运行时执行 dispose(资源释放),让待处理的持久化操作完成,再调用 `exit(0)`;HMR(热模块替换)式卸载只停止服务,不退出进程)。 -- [`packages/sdk/python-runtime`](../../../../packages/sdk/python-runtime/README.zh.md)(`@deepseek-ai/dsh-sdk-python-runtime`):私有打包入口——`installFailLoud` + `loadEnv` + 配置发现 + [`dsh-app-boot`](../../../../packages/boot/app-boot/src/index.ts) 的 `boot()`;`boot()` 完成后入口即完成,服务器由 `cordis.yml` 中的 `dsh-sdk-jsonrpc-server` 条目启动。它只依赖 `app-boot`。进程级退出归打包入口所有(stdin EOF/SIGTERM → dispose 后返回 0,SIGINT → 130)。 +- [`apps/cli`](../../../../apps/cli/README.zh.md)(`@deepseek-ai/dsh`):打包后的应用入口;其 `sdk` profile 挂载 `dsh-sdk-jsonrpc-server`,CLI 负责环境分层、profile 组合、stdin/signal 关闭与进程退出。 -配置发现有两个通道,均缺失时立即报错:优先使用 `DSH_CORDIS_CONFIG` 环境变量(SDK 客户端约定),其次使用 argv 位置参数;没有默认路径或内置回退——「实际启动的插件由外部 `cordis.yml` 决定」是硬语义。 +Python 客户端提供显式 Harness home,并选择 `sdk` profile 与有序 patch 文件。缺失 home、profile、bundle 或 server 配置项都会明确失败;不存在外部完整配置回退。[Python profile 运行时决策](2026-08-23-python-sdk-dsh-profile-runtime.zh.md)负责该应用接口。 ### 插件解析:VFS 装载真实包树,闭包 manifest(元数据清单)就是部署根目录 diff --git a/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.i18n.yaml index 6f7a67dd4d..d3d02c9f70 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.i18n.yaml @@ -2,5 +2,5 @@ # 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/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md -2026-08-11-repository-naming-contract-and-rename-ledger.md: 2de88df5f1a5037d32daa9a8ee9cd1edfc60528a -2026-08-11-repository-naming-contract-and-rename-ledger.zh.md: 0cbbbee561358614da0b20e49c610fbee1d4e537 +2026-08-11-repository-naming-contract-and-rename-ledger.md: 403a7288bb3af0eff09b229e7d399b94492ae6c0 +2026-08-11-repository-naming-contract-and-rename-ledger.zh.md: 644e1c0bebfc43414e9e67def8d6c42acddf4c72 diff --git a/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md b/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md index 2de88df5f1..403a7288bb 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md +++ b/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md @@ -274,11 +274,11 @@ Keep MCP, Todo, and the Plan Mode package, key, events, and tool names. This dec | `E2BSandboxService` | `E2BRuntime` | The class creates, reuses, and disposes the E2B execution environment used by filesystem and subprocess adapters. It is broader than one sandbox handle and narrower than a generic owner. Keep `@deepseek-ai/dsh-e2b`, `ctx.e2b`, and the `e2b/` group. | | `@deepseek-ai/dsh-frontend-static` | `@deepseek-ai/dsh-host-frontend-static` | The package is the Host plugin that serves the frontend assets. The prefix distinguishes it from frontend application code. | | `PluginInventoryService` | `PluginInventoryGateway` | The class is a Remote-only adapter from the live Loader tree to the `pluginInventory/list` RPC. It owns no same-process service, cache, history, or mutation path. `Gateway` states the role that exists. | -| `@deepseek-ai/dsh-jsonrpc-demo`, `@deepseek-ai/dsh-sdk-jsonrpc-demo` | `@deepseek-ai/dsh-sdk-python-runtime` | The private package carries only the temporarily separate Python SDK runtime; the [single dsh launcher decision](2026-08-22-single-dsh-application-launcher.md) owns its application-boundary change. | -| `packages/examples/jsonrpc-demo/` | `packages/sdk/python-runtime/` | The carrier is production packaging infrastructure for the Python SDK, not a demo bundle. | -| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | The direct-config runnable example belongs specifically to the Python SDK exception. | +| `@deepseek-ai/dsh-jsonrpc-demo`, `@deepseek-ai/dsh-sdk-jsonrpc-demo`, `@deepseek-ai/dsh-sdk-python-runtime` | removed | The Python runtime packages the existing `@deepseek-ai/dsh` CLI and its `sdk` profile; a private application package would recreate a second launcher. | +| `packages/examples/jsonrpc-demo/`, `packages/sdk/python-runtime/` | removed | The Python runtime wheel's closure manifest owns packaging without a separate application package. | +| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | The example demonstrates Python use of the `sdk` profile and ordered patches. | | `@deepseek-ai/dsh-acp-demo` | `@deepseek-ai/dsh-acp-app` | The package is the ACP profile's application bundle, not a standalone demo bin. | -| Deploy-root manifest `dsh-jsonrpc-agent-pkg` | `dsh-sdk-python-runtime-closure` | The manifest defines the private Python runtime dependency closure. The Python-visible executable basename remains fixed until its documented profile migration. | +| Deploy-root manifests `dsh-jsonrpc-agent-pkg`, `dsh-sdk-python-runtime-closure` | `dsh-python-runtime-closure` | The zero-code manifest defines the Python runtime wheel's complete `dsh` dependency closure without naming a separate SDK application. | | `@deepseek-ai/dsh-frontend` | `@deepseek-ai/dsh-web-frontend` | The application is the web frontend. Keep its physical `apps/web/` folder. | Keep atomic-write, brand, native-command, timeout utility, directory-picker, `dsh-base`, `dsh-web-app`, `dsh-sdk-app`, `dsh-acp-app`, app boot, CLI names, and the `headless` package, bundle, and example identity. `headless` is the intended product essence and may later support more than one-shot execution. diff --git a/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.zh.md b/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.zh.md index 0cbbbee561..644e1c0beb 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.zh.md @@ -274,11 +274,11 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui` | `E2BSandboxService` | `E2BRuntime` | 该类创建、复用和释放文件系统与子进程适配器所使用的 E2B 执行环境。它比单个沙箱句柄的职责更广,又比通用所有者更具体。保留 `@deepseek-ai/dsh-e2b`、`ctx.e2b` 和 `e2b/` 组。 | | `@deepseek-ai/dsh-frontend-static` | `@deepseek-ai/dsh-host-frontend-static` | 该包是提供前端资源的 Host 插件。此前缀可将它与前端应用代码区分开。 | | `PluginInventoryService` | `PluginInventoryGateway` | 该类只负责把实时 Loader 树适配到 `pluginInventory/list` RPC。它不拥有同进程服务、缓存、历史或修改路径。`Gateway` 准确说明现有角色。 | -| `@deepseek-ai/dsh-jsonrpc-demo`、`@deepseek-ai/dsh-sdk-jsonrpc-demo` | `@deepseek-ai/dsh-sdk-python-runtime` | 该私有包只承载暂时独立的 Python SDK 运行时;其应用边界变更由[单一 dsh 启动器决策](2026-08-22-single-dsh-application-launcher.zh.md)负责。 | -| `packages/examples/jsonrpc-demo/` | `packages/sdk/python-runtime/` | 该载体是 Python SDK 的生产打包基础设施,不是演示组合包。 | -| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | 直读配置的可运行示例专属于 Python SDK 例外。 | +| `@deepseek-ai/dsh-jsonrpc-demo`、`@deepseek-ai/dsh-sdk-jsonrpc-demo`、`@deepseek-ai/dsh-sdk-python-runtime` | 已删除 | Python 运行时打包现有 `@deepseek-ai/dsh` CLI 与其 `sdk` profile;私有应用包会重新产生第二个启动器。 | +| `packages/examples/jsonrpc-demo/`、`packages/sdk/python-runtime/` | 已删除 | Python 运行时 wheel 的闭包 manifest 负责打包,无需独立应用包。 | +| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | 该示例演示 Python 使用 `sdk` profile 与有序 patch。 | | `@deepseek-ai/dsh-acp-demo` | `@deepseek-ai/dsh-acp-app` | 该包是 ACP profile 的应用组合包,不是独立 demo bin。 | -| 部署根 manifest `dsh-jsonrpc-agent-pkg` | `dsh-sdk-python-runtime-closure` | 该 manifest 定义私有 Python 运行时依赖闭包。面向 Python 的可执行文件基本名称保持不变,直至完成已记录的 profile 迁移。 | +| 部署根 manifest `dsh-jsonrpc-agent-pkg`、`dsh-sdk-python-runtime-closure` | `dsh-python-runtime-closure` | 该零代码 manifest 定义 Python 运行时 wheel 的完整 `dsh` 依赖闭包,不再命名独立 SDK 应用。 | | `@deepseek-ai/dsh-frontend` | `@deepseek-ai/dsh-web-frontend` | 该应用是 Web 前端。保留其物理目录 `apps/web/`。 | 保留 atomic-write、brand、native-command、timeout 实用工具、目录选择器、`dsh-base`、`dsh-web-app`、`dsh-sdk-app`、`dsh-acp-app`、应用启动、CLI(命令行界面)名称,以及 `headless` 包、组合包和示例身份。`headless` 是预期的产品本质,未来也可以支持不止一次性执行。 diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml new file mode 100644 index 0000000000..2e98550ed0 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.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/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md +2026-08-23-python-sdk-dsh-profile-runtime.md: 400915155460598e754c2f81d60b26e693ff438a +2026-08-23-python-sdk-dsh-profile-runtime.zh.md: a3c9074817d1c1e9b3f59fbe5fb02910d20e02f4 diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md new file mode 100644 index 0000000000..4009151554 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md @@ -0,0 +1,53 @@ +# Agent Note: Python SDK runtime through the dsh profile launcher + +Status: implemented + +English | [中文](2026-08-23-python-sdk-dsh-profile-runtime.zh.md) + +## Problem + +The Python SDK distributed a private Node application that booted a complete external `cordis.yml`, while every other supported application entered through `dsh` profiles. That exception duplicated environment loading, configuration ownership, plugin resolution, shutdown, artifact names, and test paths. It also made SDK customization an all-or-nothing application tree: a caller replacing one plugin had to own the JSON-RPC server and every unrelated deployment row. + +A normal profile cannot be adopted only at the Python wrapper. The runtime executable must contain the `dsh` CLI, shipped profile and bundle files, native libraries, and a module-resolution path that works when profile files and external plugins live outside pkg's virtual filesystem. + +## Decision + +### One application launcher + +The runtime executable packages `@deepseek-ai/dsh` and runs its ordinary command grammar. The Python client selects `--profile sdk` by default, forwards ordered absolute `--patch` paths, and may select another `dsh` executable or profile. The private `@deepseek-ai/dsh-sdk-python-runtime` application package and checked-in runtime `cordis.yml` do not exist. JSON-RPC serving remains the `@deepseek-ai/dsh-sdk-app` bundle and `@deepseek-ai/dsh-sdk-jsonrpc-server` plugin, not a Python-owned boot path. + +The public Python configuration is `dsh_bin`, `profile`, ordered `patches`, `dsh_home`, process cwd/environment, provider/model/token selection, and timeouts. It does not expose a complete Cordis tree or arbitrary launch argv. `RunResult` reports the protocol-owned run values and does not duplicate the profile's persistence path. + +Every Python launch requires either explicit `dsh_home` or a non-empty `DSH_HOME` in the child environment. The SDK never discovers `~/.dsh`. The selected home consistently owns profiles, external plugins, credentials, settings, and sessions. + +### Plugin customization + +Persistent SDK customization uses the same profile interfaces as direct CLI use. `dsh plugin --profile sdk ...` manages external dependencies and bundle order, `$DSH_HOME/profiles/sdk/cordis.patch.yml` owns persistent row changes, the home patch applies machine-local changes across profiles, and Python `patches` supplies invocation-specific overlays. A different profile is valid only when it retains an SDK server row. Missing profiles, bundles, server rows, and invalid patches fail without a complete-config fallback. + +The runtime wheel installs a `dsh` console command. Ordinary profile and SDK execution remains Node-free; external package management requires a caller-installed `pnpm`. + +### Executable packaging + +The zero-code deployment manifest is `dsh-python-runtime-closure`. It packages `node_modules/@deepseek-ai/dsh/lib/bin.js` and profile, bundle, preset, native-addon, and shared-library assets into `deepseek-harness-sdk-runtime--`. The wheel distribution names, Python import modules, JSON-RPC messages, and wire-stable `serverInfo.name = deepseek-harness-sdk-runtime` remain unchanged. + +Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy mirrors the source package's explicit runtime exports and identity and re-exports its virtual module URLs. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module. + +The published target set is Linux x64, Linux arm64, and macOS arm64. Installed-wheel black-box CI owns artifact provenance, default and patched profiles, external bundle installation, native tools, MCP, direct JSON-RPC, snapshots, and trusted real-provider turns on every target. + +## Existing decisions and supersession + +This decision implements and supersedes the Python exception and deferred-migration sections of [the single dsh application launcher](2026-08-22-single-dsh-application-launcher.md). It supersedes the private application, external complete-config, artifact-name, and customization facts in [the single-file Python SDK runtime distribution](2026-07-10-single-file-executable-sdk-runtime-distribution.md), which remains authoritative for pkg/SEA, wheel construction, native target validation, and publication. No active note is fully superseded, so none is archived. + +## Alternatives considered + +**Keep complete `cordis.yml` as an advanced escape hatch.** Rejected because it preserves a second application assembly and lets a caller bypass profile environment, plugin, and shutdown ownership. + +**Silently use `~/.dsh` for compatibility.** Rejected because an SDK process must not inherit a person's plugins, credentials, settings, or sessions without an explicit choice. + +**Copy the virtual dependency tree into every home.** Rejected because it duplicates hundreds of megabytes and loads a second Cordis instance. Export-preserving proxies are small and retain module identity. + +**Bundle pnpm and Node package management into every SDK launch.** Rejected because installed plugins are deployment state, not per-turn runtime work. Only `dsh plugin` needs the external package manager. + +## Consequences + +Python callers configure the same profile vocabulary as TypeScript and direct CLI users, and arbitrary external bundles can extend an SDK profile without replacing the application tree. Homes must now be selected explicitly, complete-config and `session_root` parameters are unavailable, and the executable includes shared-library assets plus profile-module proxies. The stronger installed-wheel CI makes those package, profile, native, and provider paths release requirements rather than source-only assumptions. diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md new file mode 100644 index 0000000000..a3c9074817 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md @@ -0,0 +1,53 @@ +# Agent Note: 通过 dsh profile 启动器运行 Python SDK 运行时 + +Status: implemented + +[English](2026-08-23-python-sdk-dsh-profile-runtime.md) | 中文 + +## 问题 + +Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml`;其他所有受支持应用都从 `dsh` profile 进入。该例外重复了环境加载、配置所有权、插件解析、关闭流程、产物命名与测试路径。它还把 SDK 自定义变成全量替换应用树:只想替换一个插件的调用方也必须拥有 JSON-RPC server 和所有无关 deployment 配置项。 + +仅修改 Python wrapper 无法采用普通 profile。运行时可执行程序必须包含 `dsh` CLI、随附 profile 与 bundle 文件、原生库,以及在 profile 文件和外部插件位于 pkg 虚拟文件系统之外时仍可工作的模块解析路径。 + +## 决策 + +### 一个应用启动器 + +运行时可执行程序打包 `@deepseek-ai/dsh` 并运行其普通命令语法。Python 客户端默认选择 `--profile sdk`,转发有序绝对 `--patch` 路径,也可以选择另一个 `dsh` 可执行程序或 profile。私有 `@deepseek-ai/dsh-sdk-python-runtime` 应用包和检入的运行时 `cordis.yml` 均不存在。JSON-RPC 服务仍由 `@deepseek-ai/dsh-sdk-app` bundle 与 `@deepseek-ai/dsh-sdk-jsonrpc-server` 插件提供,而不是 Python 自有启动路径。 + +公开 Python 配置包括 `dsh_bin`、`profile`、有序 `patches`、`dsh_home`、进程 cwd/环境、provider/model/token 选择与 timeout。它不暴露完整 Cordis 树或任意启动 argv。`RunResult` 报告协议所有的运行值,不重复 profile 的持久化路径。 + +每次 Python 启动都要求显式 `dsh_home`,或子进程环境中的非空 `DSH_HOME`。SDK 绝不会发现 `~/.dsh`。所选 home 统一拥有 profile、外部插件、凭据、设置与会话。 + +### 插件自定义 + +持久 SDK 自定义使用与直接 CLI 相同的 profile 接口。`dsh plugin --profile sdk ...` 管理外部依赖与 bundle 顺序,`$DSH_HOME/profiles/sdk/cordis.patch.yml` 负责持久配置项变更,home patch 对所有 profile 应用机器本地变更,Python `patches` 则提供单次启动 overlay。另一个 profile 只有保留 SDK server 配置项时才有效。缺失 profile、bundle、server 配置项或非法 patch 都会直接失败,不存在完整配置回退。 + +运行时 wheel 安装 `dsh` 控制台命令。普通 profile 与 SDK 运行仍不需要 Node;外部包管理要求调用方自行安装 `pnpm`。 + +### 可执行程序打包 + +零代码部署 manifest 是 `dsh-python-runtime-closure`。它把 `node_modules/@deepseek-ai/dsh/lib/bin.js` 以及 profile、bundle、preset、原生 addon 与共享库资源打包进 `deepseek-harness-sdk-runtime--`。Wheel distribution 名称、Python import 模块、JSON-RPC 消息和协议稳定的 `serverInfo.name = deepseek-harness-sdk-runtime` 保持不变。 + +普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理镜像源包的显式运行时 exports 与身份,并重新导出其虚拟模块 URL。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。 + +已发布目标集合是 Linux x64、Linux arm64 与 macOS arm64。Installed-wheel 黑盒 CI 在每个目标上负责产物来源、默认及 patched profile、外部 bundle 安装、原生工具、MCP、直接 JSON-RPC、快照,以及可信真实提供方轮次。 + +## 既有决策与取代关系 + +本决策实现并取代[单一 dsh 应用启动器](2026-08-22-single-dsh-application-launcher.zh.md)中的 Python 例外与延后迁移章节。它取代[单文件 Python SDK 运行时分发](2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)中的私有应用、外部完整配置、产物名称与自定义事实;后者继续负责 pkg/SEA、wheel 构建、原生目标验证与发布。没有任何 active note 被完全取代,因此无需归档。 + +## 考虑过的替代方案 + +**把完整 `cordis.yml` 保留为高级逃生口。** 不予采用,因为它会保留第二套应用组装,并允许调用方绕过 profile 的环境、插件与关闭所有权。 + +**为兼容性静默使用 `~/.dsh`。** 不予采用,因为 SDK 进程不应在没有显式选择时继承个人插件、凭据、设置或会话。 + +**把虚拟依赖树复制到每个 home。** 不予采用,因为这会重复数百 MB,并加载第二个 Cordis 实例。保留 exports 的代理很小,而且维持模块身份。 + +**把 pnpm 与 Node 包管理纳入每次 SDK 启动。** 不予采用,因为已安装插件属于 deployment 状态,而不是逐轮运行时工作。只有 `dsh plugin` 需要外部包管理器。 + +## 结果 + +Python 调用方使用与 TypeScript 和直接 CLI 用户相同的 profile 词汇,任意外部 bundle 可以扩展 SDK profile,而无需替换应用树。调用方现在必须显式选择 home,完整配置与 `session_root` 参数不可用,可执行程序则包含共享库资源和 profile 模块代理。更强的 installed-wheel CI 将包、profile、原生与提供方路径变成发布要求,而不是仅在源码中成立的假设。 diff --git a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.i18n.yaml b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.i18n.yaml index 93a20e387f..635b5df7af 100644 --- a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.i18n.yaml @@ -2,5 +2,5 @@ # 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/testing/2026-08-23-installed-python-wheel-black-box-ci.md -2026-08-23-installed-python-wheel-black-box-ci.md: f2b5bd0edeb02a5d72e8010c62c3cfb59ee95c5d -2026-08-23-installed-python-wheel-black-box-ci.zh.md: fb0f5fb2da676f1a24a2630bd45f005f46a3095e +2026-08-23-installed-python-wheel-black-box-ci.md: a2fd4134d7bff0e74aa2d1afc3590e9cdd90809e +2026-08-23-installed-python-wheel-black-box-ci.zh.md: 203440ed0f6bbe84257474dd69400a64bc480cd3 diff --git a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md index f2b5bd0ede..a2fd4134d7 100644 --- a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md +++ b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md @@ -18,7 +18,7 @@ The black-box harness rejects a non-venv process, repository-relative working di ### Keyless behavior -Every target runs the complete packaged-runtime scenario set after installation. A local SSE model keeps outputs deterministic while the public SDK exercises the default configuration, an external complete configuration, persistent PTY and editor behavior, worker-thread code and workflow execution, ripgrep-backed search, external stdio MCP discovery and execution, model-visible and durable snapshots, JSONL/Zstandard persistence, direct JSON-RPC, and shutdown. A restart snapshot launches two complete SDK runtime processes against one persistence root and pins their isolated model histories, high-level results, and separate durable logs. The installed run replaces the source-SDK pre-wheel run; the executable and wheel are tested together once rather than maintaining two behavior inventories. +Every target runs the complete packaged-runtime scenario set after installation. A local SSE model keeps outputs deterministic while the public SDK exercises the default SDK profile, ordered patch overlays, external bundle installation through `dsh plugin`, persistent PTY and editor behavior, worker-thread code and workflow execution, ripgrep-backed search, external stdio MCP discovery and execution, model-visible and durable snapshots, Zstandard persistence, direct JSON-RPC, and shutdown. A restart snapshot launches two complete SDK runtime processes against one persistence root and pins their isolated model histories, high-level results, and separate durable logs. The installed run replaces the source-SDK pre-wheel run; the executable and wheel are tested together once rather than maintaining two behavior inventories. Linux additionally retains its manylinux 2.28 clean-install smoke and GLIBC checks. macOS retains deployment-target and native helper checks. These platform constraints supplement the common black-box behavior rather than substituting for it. @@ -34,7 +34,7 @@ The pull-request `python-runtime` job calls the reusable builder for Linux x64, ## Existing decisions and supersession -This decision supersedes the single-target topology in the archived [required Python runtime pull-request validation](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md) while retaining its requirement that the real executable, snapshots, wheels, and clean installation meet before merge. The [single-file Python SDK runtime distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) remains authoritative for SEA packaging, the closed dependency set, native sidecars, wheel tags, and release artifacts. +This decision supersedes the single-target topology in the archived [required Python runtime pull-request validation](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md) while retaining its requirement that the real executable, snapshots, wheels, and clean installation meet before merge. The [Python SDK dsh profile runtime](../architecture/2026-08-23-python-sdk-dsh-profile-runtime.md) owns the launched application and customization surface; the [single-file Python SDK runtime distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) remains authoritative for SEA packaging, native sidecars, wheel tags, and release artifacts. ## Alternatives considered diff --git a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md index fb0f5fb2da..203440ed0f 100644 --- a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md +++ b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md @@ -18,7 +18,7 @@ Python SDK 单元测试驱动 fake peer,而打包运行时工作流可以在 ### Keyless 行为 -每个目标都会在安装后运行完整的打包运行时场景。一个本地 SSE mock 模型提供确定性输出,公开 SDK 则覆盖默认配置、外部完整配置、持久 PTY 与 editor 行为、worker thread 代码与 workflow 执行、基于 ripgrep 的搜索、外部 stdio MCP 发现与执行、模型可见及持久化快照、JSONL/Zstandard 持久化、直接 JSON-RPC 与关闭。Restart 快照针对同一持久化根目录启动两个完整 SDK 运行时进程,并固定其彼此隔离的模型历史、高层结果与独立持久日志。安装后运行取代 wheel 构建前的源码 SDK 运行,因此可执行文件与 wheel 包共同接受一次验证,而不是维护两套行为清单。 +每个目标都会在安装后运行完整的打包运行时场景。一个本地 SSE mock 模型提供确定性输出,公开 SDK 则覆盖默认 SDK profile、有序 patch overlay、通过 `dsh plugin` 安装外部 bundle、持久 PTY 与 editor 行为、worker thread 代码与 workflow 执行、基于 ripgrep 的搜索、外部 stdio MCP 发现与执行、模型可见及持久化快照、Zstandard 持久化、直接 JSON-RPC 与关闭。Restart 快照针对同一持久化根目录启动两个完整 SDK 运行时进程,并固定其彼此隔离的模型历史、高层结果与独立持久日志。安装后运行取代 wheel 构建前的源码 SDK 运行,因此可执行文件与 wheel 包共同接受一次验证,而不是维护两套行为清单。 Linux 另外保留 manylinux 2.28 干净安装冒烟测试与 GLIBC 检查。macOS 保留部署目标与原生 helper 检查。这些平台约束补充共同黑盒行为,不能替代它。 @@ -34,7 +34,7 @@ Fork 与 Dependabot 拉取请求永远不会获得仓库密钥。它们的原生 ## Existing decisions and supersession -本决策取代已归档的[必需 Python 运行时拉取请求验证](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md)中的单目标拓扑,同时保留真实可执行文件、快照、wheel 包与干净安装必须在合并前相遇的要求。[单文件 Python SDK 运行时 distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)仍负责 SEA 打包、封闭依赖集合、原生 sidecar、wheel 包标签与发布产物。 +本决策取代已归档的[必需 Python 运行时拉取请求验证](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md)中的单目标拓扑,同时保留真实可执行文件、快照、wheel 包与干净安装必须在合并前相遇的要求。[Python SDK dsh profile 运行时](../architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md)负责启动应用与自定义接口;[单文件 Python SDK 运行时 distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)继续负责 SEA 打包、原生 sidecar、wheel 包标签与发布产物。 ## Alternatives considered diff --git a/AGENTS.md b/AGENTS.md index 3fa1ce77e4..1fd35bb6c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ packages/ @deepseek-ai/dsh- workspaces at packages/// acp/ automation-only Agent Client Protocol server interaction/ approval/interaction capabilities, permission, commands, ask-user boot/ shared profile/application boot glue - sdk/ JSON-RPC protocol, server, TypeScript client, and private Python carrier + sdk/ JSON-RPC protocol, server, and TypeScript client examples/ reusable demo bundles (agent-spine) experimental/ private prototypes excluded from official releases support/ dev/test infrastructure diff --git a/apps/cli/README.i18n.yaml b/apps/cli/README.i18n.yaml index 977871533e..c23418172b 100644 --- a/apps/cli/README.i18n.yaml +++ b/apps/cli/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 apps/cli/README.md -README.md: d59b8264093dafb0160893c942371a4daa942c14 -README.zh.md: 6a209f5ad64b38c138ae1712a05ed9e840d9a74f +README.md: b64861ce68b4c6de8edc3142fb95d83f1d395d15 +README.zh.md: a42a1b0539703dc03adf0ac53e8b5819940429a2 diff --git a/apps/cli/README.md b/apps/cli/README.md index d59b826409..b64861ce68 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The `dsh` command is the sole supported Node application launcher: profiles are ordered stacks of plugin-bundle patch layers under the user's own overrides. SDK and ACP are profiles, not separate public bins. [`src/args.ts`](src/args.ts) owns the command grammar, and [`src/bin.ts`](src/bin.ts) loads only the selected runner. Invalid commands, options from another mode, configuration errors, and boot failures exit nonzero. +The `dsh` command is the sole supported Node application launcher: profiles are ordered stacks of plugin-bundle patch layers under the user's own overrides. SDK and ACP are profiles, not separate public bins. The Python runtime wheel packages this same command and launches its `sdk` profile. [`src/args.ts`](src/args.ts) owns the command grammar, and [`src/bin.ts`](src/bin.ts) loads only the selected runner. Invalid commands, options from another mode, configuration errors, and boot failures exit nonzero. ## Entry modes diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md index 6a209f5ad6..a42a1b0539 100644 --- a/apps/cli/README.zh.md +++ b/apps/cli/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -`dsh` 是唯一受支持的 Node 应用启动器;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。SDK 与 ACP 都是 profile,而不是独立的公开 bin。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。 +`dsh` 是唯一受支持的 Node 应用启动器;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。SDK 与 ACP 都是 profile,而不是独立的公开 bin。Python 运行时 wheel 会打包同一个命令,并启动其 `sdk` profile。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。 ## 入口模式 diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index 5ed7b32789..f432cc953b 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 apps/cli/reference/README.md -README.md: 0f407afa3b06d144681550d5096bf96c498e6451 -README.zh.md: bf4dc4ca9f49c1801d108234411123de459c0444 +README.md: 2692731ecdfb4a3f798ca0506954289f2c9bdca8 +README.zh.md: 0a2c0a14151f7b774ed00aab808c7313716c131a diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index 0f407afa3b..2692731ecd 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -8,7 +8,7 @@ This reference defines the profile, web-alias, plugin-management, and config-dum `dsh --profile ` boots the profile at `$DSH_HOME/profiles/`. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's `dsh.profile.bundles` list, the profile's own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml` (machine-local preferences shared by every profile, so it outranks the per-profile layer), and each `--patch ` overlay in argv order. Later layers win per row; a patch replaces the targeted row's complete `config` value rather than deep-merging keys, and may insert new rows. `dsh.profile.patchReload` selects `live` patch-file watching or `startup` one-time loading; omission defaults a custom profile to `live`. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit. -Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-acp-app`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent-walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch). +Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-acp-app`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules`. Plain Node installations place one healed symlink there per dependency-closure package. A pkg executable instead places a real ESM proxy that mirrors explicit exports and re-exports the virtual package URL, because operating-system symlinks cannot enter pkg's `/snapshot` filesystem. The `web`, `headless`, `sdk`, and `acp` profiles auto-initialize from shipped templates on first use (`web`: base + web-app with live patches; `headless`: base + headless with startup-only patches; `sdk`: base + sdk-app with startup-only patches; `acp`: base + acp-app with startup-only patches). Any other missing profile fails loud with a hint to run `dsh plugin --profile add `. diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index bf4dc4ca9f..0a2c0a1415 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -8,7 +8,7 @@ `dsh --profile ` 启动位于 `$DSH_HOME/profiles/` 的 profile。生效配置树以空根节点为起点,依次叠加 profile manifest(元数据清单)的 `dsh.profile.bundles` 列表中指定的各组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(这是各 profile 共享的机器本地偏好,因此优先于逐 profile 配置层),以及按 argv 顺序指定的各个 `--patch ` 覆盖层。对同一配置行,后应用的层优先。patch 会替换目标行的整个 `config` 值,而不是深度合并其中的键;patch 也可以插入新行。`dsh.profile.patchReload` 可选择 `live` patch 文件监视或 `startup` 单次加载;自定义 profile 省略该值时默认使用 `live`。配置解析、schema 校验、模块解析或插件启动失败时,系统会报告错误并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。 -组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。该目录为 dsh 安装中的应用和组合包所依赖的每个包各维护一个符号链接,并在每次启动时修复这些链接。 +组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。普通 Node 安装会为依赖闭包中的每个包放置并修复一个符号链接。pkg 可执行程序则放置真实 ESM 代理,镜像显式 exports 并重新导出虚拟包 URL,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统。 `web`、`headless`、`sdk` 和 `acp` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app,实时应用 patch;`headless`:base + headless,只在启动时应用 patch;`sdk`:base + sdk-app,只在启动时应用 patch;`acp`:base + acp-app,只在启动时应用 patch)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile add `。 diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 7f2bc49362..fe84ec6adf 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/architecture.md -architecture.md: e37f2321377242803fb89f0a2258682e6c52801c -architecture.zh.md: 69abbcdf52654ecbce6549d25ee089b937228123 +architecture.md: 362a3758fabdb67ea883bcf84ea5df4cf4e77b0f +architecture.zh.md: 421c38ad5b90db7b7ac1b5fe6bb10ccc0793feb5 diff --git a/docs/architecture.md b/docs/architecture.md index e37f232137..362a3758fa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -44,7 +44,7 @@ Every supported Node application starts at the `dsh` CLI with a named profile. T Vendored CLIs, build-only and test-only executables, direct in-process plugin mounting, and the private browser WebWorker preview are not Harness application launchers. [`verify-application-entrypoints`](../scripts/verify-application-entrypoints.ts) keeps every package bin, executable source, and root demo in an explicit class and rejects a Node application path that bypasses `dsh`. -The packaged Python SDK runtime is the sole temporary application exception. Its private [`dsh-sdk-python-runtime`](../packages/sdk/python-runtime/README.md) carrier and `dsh-sdk-python-runtime-closure` deploy manifest preserve the current Python API, wire, default `cordis.yml`, environment variables, wheel names, `dsh-jsonrpc-agent-pkg--` executables, sidecars, and platform set. A later Python migration will launch `dsh --profile sdk`, delete the private direct-config carrier, and then rename that executable family to `deepseek-harness-sdk-runtime--`. +The Python SDK follows the same application architecture. Its runtime wheel packages the normal `dsh` CLI as `deepseek-harness-sdk-runtime--`, and the client launches `dsh --profile sdk` with an explicit Harness home. Python exposes profile selection and ordered patch files rather than a complete Cordis tree; persistent external plugins are installed through `dsh plugin`. The removed private direct-config carrier has no compatibility bin or fallback parser. ## Core packages diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 69abbcdf52..421c38ad5b 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -44,7 +44,7 @@ dsh --profile web --dump-config Vendored CLI、仅用于构建和测试的可执行文件、进程内直接挂载插件以及私有浏览器 WebWorker 预览都不属于 Harness 应用启动器。[`verify-application-entrypoints`](../scripts/verify-application-entrypoints.ts)将每个包 bin、可执行源码与根 demo 归入显式类别,并拒绝任何绕过 `dsh` 的 Node 应用路径。 -打包后的 Python SDK 运行时是唯一的临时应用例外。其私有 [`dsh-sdk-python-runtime`](../packages/sdk/python-runtime/README.zh.md) 载体与 `dsh-sdk-python-runtime-closure` 部署 manifest 保持当前 Python API、协议格式、默认 `cordis.yml`、环境变量、wheel 包名称、`dsh-jsonrpc-agent-pkg--` 可执行文件、伴随文件及平台集合不变。后续 Python 迁移会改为启动 `dsh --profile sdk`、删除私有直读配置载体,然后把该可执行文件族重命名为 `deepseek-harness-sdk-runtime--`。 +Python SDK 遵循相同的应用架构。其运行时 wheel 把普通 `dsh` CLI 打包为 `deepseek-harness-sdk-runtime--`,客户端以显式 Harness home 启动 `dsh --profile sdk`。Python 暴露 profile 选择与有序 patch 文件,而不是完整 Cordis 树;持久外部插件通过 `dsh plugin` 安装。已删除的私有直读配置载体没有兼容 bin 或回退 parser。 ## 核心包 diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index de34a153cf..9dc39c909c 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/config-catalog.md -config-catalog.md: 46228907ea4241e69a4439c2dc1c1786de94eaff -config-catalog.zh.md: ed09a3d44f93815ab667ef4613249a33306d9899 +config-catalog.md: acd32b7e76180f13956ec8a54ca16d0ef99bf203 +config-catalog.zh.md: eacca1575191cff6dde93cf80e5ac576e47f485c diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 46228907ea..acd32b7e76 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3389,7 +3389,6 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. - `@deepseek-ai/dsh-scope` ([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts)) - `@deepseek-ai/dsh-sdk-client` ([`packages/sdk/client/src/index.ts`](../packages/sdk/client/src/index.ts)) - `@deepseek-ai/dsh-sdk-protocol` ([`packages/sdk/protocol/src/index.ts`](../packages/sdk/protocol/src/index.ts)) -- `@deepseek-ai/dsh-sdk-python-runtime` ([`packages/sdk/python-runtime/src/index.ts`](../packages/sdk/python-runtime/src/index.ts)) - `@deepseek-ai/dsh-session-telemetry` ([`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts)) - `@deepseek-ai/dsh-session-title-llm` ([`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts)) - `@deepseek-ai/dsh-subagent-in-process-driver` ([`packages/subagent/subagent-in-process-driver/src/index.ts`](../packages/subagent/subagent-in-process-driver/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index ed09a3d44f..eacca15751 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3390,7 +3390,6 @@ export interface Config { - `@deepseek-ai/dsh-scope`([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts)) - `@deepseek-ai/dsh-sdk-client`([`packages/sdk/client/src/index.ts`](../packages/sdk/client/src/index.ts)) - `@deepseek-ai/dsh-sdk-protocol`([`packages/sdk/protocol/src/index.ts`](../packages/sdk/protocol/src/index.ts)) -- `@deepseek-ai/dsh-sdk-python-runtime`([`packages/sdk/python-runtime/src/index.ts`](../packages/sdk/python-runtime/src/index.ts)) - `@deepseek-ai/dsh-session-telemetry`([`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts)) - `@deepseek-ai/dsh-session-title-llm`([`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts)) - `@deepseek-ai/dsh-subagent-in-process-driver`([`packages/subagent/subagent-in-process-driver/src/index.ts`](../packages/subagent/subagent-in-process-driver/src/index.ts)) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 8ba88343ec..e5f37e4a8c 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/module-graph.md -module-graph.md: aeb35195f3a095b9de694f164dc1111acf5c8197 -module-graph.zh.md: ef3bc3fbce9cad37542eeea6adf936dd70e6581b +module-graph.md: d401df81c59de27a0171750d92031c40939c6564 +module-graph.zh.md: 8186217711a2ca320017dc0473227f7a9cf59b1a diff --git a/docs/module-graph.md b/docs/module-graph.md index aeb35195f3..d401df81c5 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -275,7 +275,6 @@ flowchart TD pkg_sdk_client["sdk-client"] pkg_sdk_jsonrpc_server["sdk-jsonrpc-server"] pkg_sdk_protocol["sdk-protocol"] - pkg_sdk_python_runtime["sdk-python-runtime"] end subgraph group_session["packages/session"] pkg_session_checkpoint_policy["session-checkpoint-policy"] @@ -381,7 +380,6 @@ flowchart TD pkg_host_directory_picker_native --> pkg_invariants pkg_host_webserver --> pkg_invariants pkg_sandbox_windows_acl --> pkg_invariants - pkg_sdk_python_runtime --> pkg_invariants pkg_storage --> pkg_invariants pkg_subprocess --> pkg_invariants pkg_win32_process --> pkg_invariants @@ -1672,7 +1670,6 @@ flowchart TD | [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-webserver`](../packages/host/webserver) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants) | -| [`sdk-python-runtime`](../packages/sdk/python-runtime) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`storage`](../packages/storage/storage) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`win32-process`](../packages/subprocess/win32-process) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index ef3bc3fbce..8186217711 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -277,7 +277,6 @@ flowchart TD pkg_sdk_client["sdk-client"] pkg_sdk_jsonrpc_server["sdk-jsonrpc-server"] pkg_sdk_protocol["sdk-protocol"] - pkg_sdk_python_runtime["sdk-python-runtime"] end subgraph group_session["packages/session"] pkg_session_checkpoint_policy["session-checkpoint-policy"] @@ -383,7 +382,6 @@ flowchart TD pkg_host_directory_picker_native --> pkg_invariants pkg_host_webserver --> pkg_invariants pkg_sandbox_windows_acl --> pkg_invariants - pkg_sdk_python_runtime --> pkg_invariants pkg_storage --> pkg_invariants pkg_subprocess --> pkg_invariants pkg_win32_process --> pkg_invariants @@ -1674,7 +1672,6 @@ flowchart TD | [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-webserver`](../packages/host/webserver) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants) | -| [`sdk-python-runtime`](../packages/sdk/python-runtime) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`storage`](../packages/storage/storage) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`win32-process`](../packages/subprocess/win32-process) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants) | diff --git a/docs/user/guide/python-sdk.i18n.yaml b/docs/user/guide/python-sdk.i18n.yaml index f2299b6512..bbb6cff7ec 100644 --- a/docs/user/guide/python-sdk.i18n.yaml +++ b/docs/user/guide/python-sdk.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/user/guide/python-sdk.md -python-sdk.md: 21c7a908a8524b16baf8f98453746f59b5d5efc8 -python-sdk.zh.md: bb883f6f3799225bda590a80883063ecb29e15b4 +python-sdk.md: 4fbc6b343714b232b90a7954b35bf6863716281b +python-sdk.zh.md: 51637ed986d298d46633891077bf2a9d27c35bdd diff --git a/docs/user/guide/python-sdk.md b/docs/user/guide/python-sdk.md index 21c7a908a8..4fbc6b3437 100644 --- a/docs/user/guide/python-sdk.md +++ b/docs/user/guide/python-sdk.md @@ -2,7 +2,7 @@ English | [中文](python-sdk.zh.md) -This tutorial is the programmatic alternative to the Web UI. It installs the published Python SDK, runs a checked-in agent composition, and shows how to call the same API from your own program. +This tutorial installs the published Python SDK, runs the checked-in minimal profile overlay, and shows how to customize the same `dsh` profile from your own program. ## Prerequisites @@ -10,12 +10,10 @@ This tutorial is the programmatic alternative to the Web UI. It installs the pub - Git - Linux x64, Linux arm64, or macOS 14 or newer on arm64 - A DeepSeek-compatible API endpoint and credential -- An isolated workspace that the agent may modify +- An isolated workspace and an isolated Harness home ## Install the SDK -Clone the repository for its runnable example, create a virtual environment, and install the SDK with its same-version bundled runtime: - ```sh git clone https://github.com/deepseek-ai/deepseek-harness.git cd deepseek-harness @@ -24,51 +22,48 @@ python -m venv .venv python -m pip install deepseek-harness-sdk ``` -The installed runtime needs no system Node.js. Repository contributors who need to build the runtime or wheels from source should use the [Python contributor workflows](../../../python/development.md). +The installation includes a matching native runtime wheel and the `dsh` command. Normal SDK execution needs no system Node.js. Repository contributors who build the artifacts should use the [Python contributor workflow](../../../python/development.md). ## Run the checked-in example -Set the credential in the environment. Set `DEEPSEEK_BASE_URL` as well when the model is served by an OpenAI-compatible proxy rather than the default DeepSeek endpoint. +Export the credential and, when needed, a compatible proxy endpoint: ```sh export DEEPSEEK_API_KEY=sk-your-key-here # export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1 -# export DSH_MODEL=deepseek-v4-flash -# export DSH_SYSTEM_PROMPT='You are a helpful software engineer assistant.' ``` -Run one task against an isolated workspace and session directory: +Run one task with explicit workspace and home paths: ```sh python examples/python-sdk-agent/minimal.py \ - --workspace /absolute/path/to/workspace \ - --session-root /absolute/path/to/sessions \ + --workspace /absolute/path/to/disposable-workspace \ + --dsh-home /absolute/path/to/example-dsh-home \ --session-id example-001 \ "Inspect the repository and fix the failing tests." ``` -The script prints the final assistant response. The session directory receives a JSONL log containing the assembled model requests and tool calls. +The script prints the final assistant response. The selected home receives the generated `sdk` profile, settings, credentials if you add them, installed plugins, and Zstandard session logs under `sessions/`. The example and SDK never silently read `~/.dsh`. -## Use the SDK in your own program - -The checked-in example is a thin wrapper around this SDK call: +## Use the SDK in your program ```python from pathlib import Path from deepseek_harness import DeepSeekHarness -config = Path("examples/python-sdk-agent/minimal.cordis.yml").resolve() -workspace = Path("/absolute/path/to/workspace").resolve() -sessions = Path("/absolute/path/to/sessions").resolve() +workspace = Path("/absolute/path/to/disposable-workspace").resolve() +dsh_home = Path("/absolute/path/to/example-dsh-home").resolve() +patch = Path("examples/python-sdk-agent/minimal.patch.yml").resolve() with DeepSeekHarness( provider="deepseek-official", model="deepseek-v4-flash", max_tokens=49_152, cwd=str(workspace), - session_root=str(sessions), - cordis=str(config), + dsh_home=str(dsh_home), + profile="sdk", + patches=(str(patch),), ) as harness: result = harness.run( "Inspect the repository and fix the failing tests.", @@ -78,9 +73,23 @@ with DeepSeekHarness( print(result.final_response) ``` -`DeepSeekHarness` starts the bundled runtime lazily and reuses it until the context manager exits. Reusing the same harness and session id preserves the session-owned Bash process, including its working directory, exported variables, and shell functions. Use a fresh session id for an independent task; reuse an id only when the next call should continue the same durable conversation. +The SDK starts the bundled `dsh --profile sdk` process lazily and reuses it until context-manager exit. The profile, its persistent patch, the home patch, and the ordered `patches` tuple form the application configuration. There is no separate Python runtime bin or complete-config option. -## Understand the example composition +## Install or define plugins + +Use `dsh plugin` for dependencies and bundle layers that should persist in this home: + +```sh +export DSH_HOME=/absolute/path/to/example-dsh-home +dsh --profile sdk --dump-default-config >/dev/null +dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle +``` + +The first command initializes the shipped SDK profile. The second forwards package management to `pnpm`, then records any installed package that exports a `dsh.bundle` layer. Install `pnpm` only for this management command; launching the installed SDK does not need it. Edit `$DSH_HOME/profiles/sdk/cordis.patch.yml` for persistent row changes, or pass patch files from Python for per-launch changes. + +Another `profile` is valid when it includes `@deepseek-ai/dsh-sdk-app` or another JSON-RPC server row. Missing server rows, unresolved plugins, and invalid patches fail during startup instead of falling back to another composition. + +## Understand the minimal overlay | Property | Value | |---|---| @@ -90,15 +99,10 @@ print(result.final_response) | Bash timeout | 300 seconds | | Editor output limit | 16,000 characters | | Context compaction | Disabled | -| Filesystem | Bare local backend; absolute editor paths may address any path visible to the runtime process | -| Session persistence | Uncompressed JSONL under `DSH_SESSION_ROOT` | +| Session persistence | Zstandard JSONL under `/sessions` | -The composition omits harness identity, workspace prompt text, skills, one-shot Bash, task tools, compaction, and every other model-facing plugin. Sandbox-policy facts are logged as runtime user context rather than appended to the system prompt. +The overlay removes runtime-context prompt messages and most default tools while retaining the SDK application's protocol and persistence. Persistent Bash and the editor can modify any path visible to the runtime, so use a disposable checkout or container. The PTY implementation makes this example POSIX-only. -## Choose workspace and session IDs +Use a fresh home when profiles, plugins, credentials, settings, and sessions must be isolated. Use a fresh session id for independent work; reuse a harness, home, and id only to continue the same durable conversation and session-owned resources. -`cwd` selects the workspace available to the agent, while `session_root` stores session logs and state. Use a fresh session id for an independent task; reuse an id only when the next call should continue the same conversation and persistent shell state. - -The composition uses `danger-full-access`. Run it only inside a disposable checkout or container: Bash and the editor can modify any path allowed to the runtime process. The persistent PTY backend requires a POSIX terminal substrate, so this composition does not support Windows agents. - -The [`python-sdk-agent` example reference](../../../examples/python-sdk-agent/README.md) owns the exact composition. The [Python SDK reference](../../../python/sdk/README.md) covers lifecycle, results, notifications, runtime selection, and configuration; the [Cordis primer](../../cordis-primer.md) covers composition syntax. +The [example reference](../../../examples/python-sdk-agent/README.md) owns the checked-in overlay. The [Python SDK reference](../../../python/sdk/README.md) covers lifecycle, results, notifications, and low-level behavior; the [dsh CLI reference](../../../apps/cli/reference/README.md) covers profile layering. diff --git a/docs/user/guide/python-sdk.zh.md b/docs/user/guide/python-sdk.zh.md index bb883f6f37..51637ed986 100644 --- a/docs/user/guide/python-sdk.zh.md +++ b/docs/user/guide/python-sdk.zh.md @@ -1,21 +1,19 @@ -# Python SDK 快速上手 +# Python SDK 入门 [English](python-sdk.md) | 中文 -本教程介绍 Web UI 之外的程序化使用方式:安装已发布的 Python SDK、运行仓库内置的 agent(智能体)组合,并在自己的程序中调用同一套 API。 +本教程安装已发布的 Python SDK,运行检入的极简 profile overlay,并说明如何从自己的程序自定义同一个 `dsh` profile。 -## 前置要求 +## 前置条件 - Python 3.10 或更高版本 - Git -- Linux x64、Linux arm64 或 macOS 14 或更高版本的 arm64 -- DeepSeek 兼容的 API 端点与凭据 -- agent 可以修改的隔离 workspace +- Linux x64、Linux arm64,或 arm64 上的 macOS 14 或更高版本 +- DeepSeek 兼容的 API endpoint 与凭据 +- 隔离的 workspace 与隔离的 Harness home ## 安装 SDK -克隆仓库以使用其中的可运行示例,创建虚拟环境,并安装 SDK 及其同版本内置运行时: - ```sh git clone https://github.com/deepseek-ai/deepseek-harness.git cd deepseek-harness @@ -24,51 +22,48 @@ python -m venv .venv python -m pip install deepseek-harness-sdk ``` -安装后的运行时不需要系统提供 Node.js。需要从源码构建运行时或 wheel 包的仓库贡献者应使用 [Python 贡献者工作流](../../../python/development.zh.md)。 +安装内容包含匹配的原生运行时 wheel 与 `dsh` 命令。普通 SDK 运行不需要系统 Node.js。需要构建产物的仓库贡献者应使用 [Python 贡献者工作流](../../../python/development.zh.md)。 -## 运行仓库内置示例 +## 运行检入示例 -请在环境中设置凭据。如果模型不是由默认 DeepSeek 端点提供,而是通过 OpenAI 兼容代理提供,还需要设置 `DEEPSEEK_BASE_URL`。 +导出凭据;使用兼容代理时再设置 endpoint: ```sh export DEEPSEEK_API_KEY=sk-your-key-here # export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1 -# export DSH_MODEL=deepseek-v4-flash -# export DSH_SYSTEM_PROMPT='You are a helpful software engineer assistant.' ``` -针对隔离的 workspace 和会话目录运行一个任务: +使用显式 workspace 与 home 路径运行一个任务: ```sh python examples/python-sdk-agent/minimal.py \ - --workspace /absolute/path/to/workspace \ - --session-root /absolute/path/to/sessions \ + --workspace /absolute/path/to/disposable-workspace \ + --dsh-home /absolute/path/to/example-dsh-home \ --session-id example-001 \ "Inspect the repository and fix the failing tests." ``` -脚本会打印 assistant 的最终回复。会话目录会收到 JSONL 日志,其中包含组装后的模型请求与工具调用。 +脚本会打印最终 assistant 响应。所选 home 会保存生成的 `sdk` profile、设置、调用方添加的凭据、已安装插件,以及 `sessions/` 下的 Zstandard 会话日志。示例与 SDK 绝不会静默读取 `~/.dsh`。 -## 在自己的程序中使用 SDK - -仓库内置示例是以下 SDK 调用的轻量包装: +## 在程序中使用 SDK ```python from pathlib import Path from deepseek_harness import DeepSeekHarness -config = Path("examples/python-sdk-agent/minimal.cordis.yml").resolve() -workspace = Path("/absolute/path/to/workspace").resolve() -sessions = Path("/absolute/path/to/sessions").resolve() +workspace = Path("/absolute/path/to/disposable-workspace").resolve() +dsh_home = Path("/absolute/path/to/example-dsh-home").resolve() +patch = Path("examples/python-sdk-agent/minimal.patch.yml").resolve() with DeepSeekHarness( provider="deepseek-official", model="deepseek-v4-flash", max_tokens=49_152, cwd=str(workspace), - session_root=str(sessions), - cordis=str(config), + dsh_home=str(dsh_home), + profile="sdk", + patches=(str(patch),), ) as harness: result = harness.run( "Inspect the repository and fix the failing tests.", @@ -78,27 +73,36 @@ with DeepSeekHarness( print(result.final_response) ``` -`DeepSeekHarness` 会延迟启动内置运行时,并持续复用,直至退出上下文管理器。复用同一个 harness 与 session id 会保留该会话拥有的 Bash 进程,包括其工作目录、已导出的变量与 shell 函数。独立任务应使用新的 session id;只有下一次调用需要延续同一段持久化对话时,才复用原有 id。 +SDK 会延迟启动内置的 `dsh --profile sdk` 进程,并复用到上下文管理器退出。Profile、其持久 patch、home patch 与有序 `patches` tuple 共同组成应用配置。不存在独立 Python 运行时 bin 或完整配置选项。 -## 了解示例组合 +## 安装或定义插件 + +需要在该 home 中持久保存依赖与 bundle 层时,使用 `dsh plugin`: + +```sh +export DSH_HOME=/absolute/path/to/example-dsh-home +dsh --profile sdk --dump-default-config >/dev/null +dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle +``` + +第一个命令初始化随附的 SDK profile。第二个命令把包管理转发给 `pnpm`,然后记录所有导出 `dsh.bundle` 层的已安装包。只有执行此管理命令时才需要安装 `pnpm`;启动已安装 SDK 不需要它。持久配置项变更应编辑 `$DSH_HOME/profiles/sdk/cordis.patch.yml`;单次启动变更则从 Python 传入 patch 文件。 + +另一个 `profile` 只有包含 `@deepseek-ai/dsh-sdk-app` 或另一个 JSON-RPC server 配置项时才有效。缺失 server 配置项、无法解析的插件和非法 patch 会在启动时失败,不会回退到其他组合。 + +## 理解极简 overlay | 属性 | 值 | |---|---| -| 系统提示词 | `DSH_SYSTEM_PROMPT`;未设置时使用 `You are a helpful software engineer assistant.` | -| `minimal.py` 使用的模型 | `--model`,其次为 `DSH_MODEL`,最后为 `deepseek-v4-flash` | +| 系统提示词 | `DSH_SYSTEM_PROMPT`,未设置时为 `You are a helpful software engineer assistant.` | +| `minimal.py` 的模型 | `--model`,然后是 `DSH_MODEL`,最后是 `deepseek-v4-flash` | | 面向模型的工具 | 仅持久 `bash` 与 `str_replace_editor` | | Bash 超时 | 300 秒 | -| 编辑器输出上限 | 16,000 个字符 | -| 上下文压缩 | 已关闭 | -| 文件系统 | 裸本地后端;编辑器使用绝对路径,可以访问运行时进程可见的任何路径 | -| 会话持久化 | `DSH_SESSION_ROOT` 下未压缩的 JSONL | +| Editor 输出上限 | 16,000 字符 | +| 上下文压缩 | 禁用 | +| 会话持久化 | `/sessions` 下的 Zstandard JSONL | -该组合省略了 harness 身份、workspace 提示词文本、skill(技能)、一次性 Bash、任务工具、上下文压缩和其他所有面向模型的插件。沙箱策略事实记录为运行时用户上下文,而不会追加到系统提示词中。 +该 overlay 会移除运行时上下文提示消息与大多数默认工具,但保留 SDK 应用的协议与持久化。持久 Bash 与 editor 可以修改运行时可见的任何路径,因此应使用一次性 checkout 或容器。由于采用 PTY 实现,本示例只支持 POSIX。 -## 选择 workspace 与 session id +需要隔离 profile、插件、凭据、设置与会话时,应使用新的 home。独立工作应使用新的 session id;只有继续同一段持久对话和会话资源时,才同时复用 harness、home 与 id。 -`cwd` 用于选择 agent 可访问的 workspace,`session_root` 用于保存会话日志和状态。独立任务应使用新的 session id;只有下一次调用需要延续同一段对话和持久 shell 状态时,才复用原有 id。 - -该组合使用 `danger-full-access`。只能在可丢弃的 checkout 或容器内运行:Bash 与编辑器可以修改运行时进程有权访问的任何路径。持久 PTY 后端需要 POSIX 终端环境,因此该组合不支持 Windows agent。 - -准确的组合内容归 [`python-sdk-agent` 示例参考](../../../examples/python-sdk-agent/README.zh.md)所有。[Python SDK 参考](../../../python/sdk/README.zh.md)介绍生命周期、结果、通知、运行时选择和配置;[Cordis primer](../../cordis-primer.zh.md)介绍组合语法。 +[示例参考](../../../examples/python-sdk-agent/README.zh.md)定义检入 overlay。[Python SDK 参考](../../../python/sdk/README.zh.md)介绍生命周期、结果、通知与底层行为;[dsh CLI 参考](../../../apps/cli/reference/README.zh.md)介绍 profile 分层。 diff --git a/examples/python-sdk-agent/README.i18n.yaml b/examples/python-sdk-agent/README.i18n.yaml index d217628a89..1f9b897250 100644 --- a/examples/python-sdk-agent/README.i18n.yaml +++ b/examples/python-sdk-agent/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 examples/python-sdk-agent/README.md -README.md: 279aa5bcf0e988168cc936fbd6d96b69e74a5873 -README.zh.md: d843d8be719349b24e0369a2177748f2b09e40d6 +README.md: e46765b0419b0b614163fa507cadef0e5c5a53b9 +README.zh.md: 13c297bdbf01b612369ded5909ec8b7f71bab26c diff --git a/examples/python-sdk-agent/README.md b/examples/python-sdk-agent/README.md index 279aa5bcf0..e46765b041 100644 --- a/examples/python-sdk-agent/README.md +++ b/examples/python-sdk-agent/README.md @@ -2,39 +2,41 @@ English | [中文](README.zh.md) -The unattended coding-agent composition for the Python SDK's bundled JSON-RPC runtime. It intentionally loads no terminal UI, console logger, approval UI, or user-questions tool because stdout belongs to the SDK protocol and turns are driven by the SDK. +Runnable Python SDK example over the sole application launcher, `dsh --profile sdk`. The Python client owns JSON-RPC stdio; the profile owns the agent composition, persistence, permissions, and plugins. -The model-facing tools are: +## Run the minimal agent -- `bash`, foreground only -- `read`, `write`, and `edit` -- `subagent`, using one foreground in-process spawn provider -- `todo_write` +Install `deepseek-harness-sdk`, export a model credential, then supply an isolated Harness home and workspace: -The surrounding runtime also loads JSONL session persistence and automatic context compaction. `maxTokensAsSuccess` keeps a token-limited model turn as an accepted evaluation result while preserving its `max-tokens` reason. +```sh +export DEEPSEEK_API_KEY=sk-your-key-here +python examples/python-sdk-agent/minimal.py \ + --dsh-home /absolute/path/to/example-dsh-home \ + --workspace /absolute/path/to/disposable-workspace \ + --session-id example-001 \ + "Inspect the repository and fix the failing tests." +``` -## Runtime environment +Set `DEEPSEEK_BASE_URL` for a compatible proxy, `DSH_MODEL` for the default model, or `DSH_SYSTEM_PROMPT` for the deployment persona. `--model` and `--profile` override their script defaults. The selected home stores the generated profile and Zstandard session logs under `sessions/`; the script never reads `~/.dsh` implicitly. -| Variable | Purpose | -|---|---| -| `DEEPSEEK_API_KEY` | Credential passed to the OpenAI-compatible host endpoint | -| `DEEPSEEK_BASE_URL` | Host endpoint used by `dsh-llm-deepseek` | -| `DSH_CWD` | Agent workspace for bash and filesystem tools | -| `DSH_CONTEXT_WINDOW` | Context capacity recorded for the `DSH_MODEL` catalog entry in the minimal variant | -| `DSH_MAX_TOKENS_AS_SUCCESS` | `true` (default) accepts token-limited results; `false` reports them as errors | -| `DSH_MODEL` | Default model used by `minimal.py`; `--model` takes precedence | -| `DSH_SESSION_ROOT` | JSONL session directory | -| `DSH_SYSTEM_PROMPT` | Deployment-provided coding persona | - -Pass the config path through the Python SDK's `cordis` option or `DSH_CORDIS_CONFIG`. The bundled executable already carries every plugin named by this file; the target machine does not need Node.js. - -## Minimal variant - -[`minimal.cordis.yml`](minimal.cordis.yml) is the complete standalone counterpart of the Web `minimal` preset. `DSH_SYSTEM_PROMPT` selects its system prompt, with `You are a helpful software engineer assistant.` as the fallback. It suppresses every system-prompt runtime-context contribution for fresh sessions and mounts no context-compaction plugin. Its model-facing tools are exactly: +[`minimal.patch.yml`](minimal.patch.yml) is an ordered overlay on the shipped SDK profile. It preserves the SDK application bundle but narrows model-visible behavior to: - owner-scoped persistent `bash` - `str_replace_editor` with `view`, `create`, `str_replace`, and `insert` -It composes the local PTY, bare `fs-local` backend, danger-full-access policy for persistent Bash, and uncompressed JSONL persistence needed by the bundled runtime. Bash and absolute editor paths can modify any path available to the runtime process, so run this variant only against a disposable checkout or container. The persistent PTY requires a POSIX terminal environment and is not a Windows agent interface. +The patch omits Harness identity and runtime-context messages, local instruction discovery, skills, compaction, plan/goal/task/web/subagent/workflow tools, and the profile's one-shot Bash. It inserts the local PTY and persistent Bash providers and sets the editor output limit to 16,000 characters. -[`minimal.py`](minimal.py) runs the composition through the Python SDK and uses `DSH_MODEL` as its default model. The [Python SDK tutorial](../../docs/user/guide/python-sdk.md) covers installation, execution, workspace selection, and session identity; the [SDK reference](../../python/sdk/README.md) owns runtime lifecycle and result semantics. +This variant is intentionally POSIX-only. Its persistent PTY and editor can modify any path available to the runtime process, so use a disposable checkout or container. + +## Add plugins + +Use the runtime wheel's `dsh` command against the same explicit home for persistent profile changes: + +```sh +export DSH_HOME=/absolute/path/to/example-dsh-home +dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle +``` + +The Python call can also pass additional absolute patch paths in `patches=(...)`; later files win. A selected profile must retain `@deepseek-ai/dsh-sdk-app` or another JSON-RPC server row. Complete standalone Cordis files in this directory remain test fixtures for lower-level composition coverage; they are not Python SDK launch interfaces. + +See the [Python SDK tutorial](../../docs/user/guide/python-sdk.md) and [SDK reference](../../python/sdk/README.md). diff --git a/examples/python-sdk-agent/README.zh.md b/examples/python-sdk-agent/README.zh.md index d843d8be71..13c297bdbf 100644 --- a/examples/python-sdk-agent/README.zh.md +++ b/examples/python-sdk-agent/README.zh.md @@ -2,39 +2,41 @@ [English](README.md) | 中文 -面向 Python SDK 内置 JSON-RPC 运行时的无人值守编码 agent(智能体)组合。它有意不加载终端 UI、控制台日志记录器、批准界面或用户交互工具,因为 stdout 属于 SDK 协议,轮次由 SDK 驱动。 +基于唯一应用启动器 `dsh --profile sdk` 的可运行 Python SDK 示例。Python 客户端负责 JSON-RPC stdio;profile 负责 agent 组合、持久化、权限与插件。 -面向模型的工具为: +## 运行极简 agent -- `bash`,仅前台 -- `read`、`write` 和 `edit` -- `subagent`,使用一个在进程内以前台方式运行的 spawn 提供方 -- `todo_write` +安装 `deepseek-harness-sdk`、导出模型凭据,然后提供隔离的 Harness home 与 workspace: -周边运行时还加载 JSONL 会话持久化和自动上下文压缩(context compaction)。`maxTokensAsSuccess` 将受 token 上限限制的模型轮次保留为已接受的评估结果,同时保留其 `max-tokens` 原因。 +```sh +export DEEPSEEK_API_KEY=sk-your-key-here +python examples/python-sdk-agent/minimal.py \ + --dsh-home /absolute/path/to/example-dsh-home \ + --workspace /absolute/path/to/disposable-workspace \ + --session-id example-001 \ + "Inspect the repository and fix the failing tests." +``` -## 运行时环境 +兼容代理使用 `DEEPSEEK_BASE_URL`,默认模型使用 `DSH_MODEL`,deployment persona 使用 `DSH_SYSTEM_PROMPT`。`--model` 与 `--profile` 会覆盖脚本默认值。所选 home 保存生成的 profile,并在 `sessions/` 下保存 Zstandard 会话日志;脚本绝不会隐式读取 `~/.dsh`。 -| 变量 | 用途 | -|---|---| -| `DEEPSEEK_API_KEY` | 传给 OpenAI 兼容宿主端点的凭据 | -| `DEEPSEEK_BASE_URL` | `dsh-llm-deepseek` 使用的宿主端点 | -| `DSH_CWD` | bash 和文件系统工具使用的 agent workspace | -| `DSH_CONTEXT_WINDOW` | 极简变体中为 `DSH_MODEL` 目录项记录的上下文容量 | -| `DSH_MAX_TOKENS_AS_SUCCESS` | `true`(默认)接受受 token 上限限制的结果;`false` 将其报告为错误 | -| `DSH_MODEL` | `minimal.py` 使用的默认模型;`--model` 优先 | -| `DSH_SESSION_ROOT` | JSONL 会话目录 | -| `DSH_SYSTEM_PROMPT` | 由部署提供的编码人格 | +[`minimal.patch.yml`](minimal.patch.yml) 是随附 SDK profile 上的有序 overlay。它保留 SDK 应用 bundle,但将模型可见行为收窄为: -通过 Python SDK 的 `cordis` 选项或 `DSH_CORDIS_CONFIG` 传入配置路径。内置可执行文件已携带此文件中指定的每个插件;目标机器无需 Node.js。 +- agent 所有的持久 `bash` +- 支持 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor` -## 极简变体 +该 patch 会省略 Harness 身份与运行时上下文消息、本地指令发现、skill、compaction,以及 plan/goal/task/web/subagent/workflow 工具和 profile 的单次 Bash。它插入本地 PTY 与持久 Bash provider,并把 editor 输出上限设为 16,000 字符。 -[`minimal.cordis.yml`](minimal.cordis.yml) 是 Web `minimal` preset 的完整独立版本。`DSH_SYSTEM_PROMPT` 选择它的系统提示词,未设置时使用 `You are a helpful software engineer assistant.`。它为新建会话抑制每个 system-prompt runtime-context 贡献,且不挂载上下文压缩插件。面向模型的工具严格只有: +此变体刻意只支持 POSIX。其持久 PTY 与 editor 可以修改运行时进程可访问的任何路径,因此只应在一次性 checkout 或容器中使用。 -- 所有者作用域内持久化的 `bash` -- 提供 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor` +## 添加插件 -它组合了内置运行时所需的本地 PTY、裸 `fs-local` 后端、供持久 Bash 使用的 danger-full-access 策略,以及未压缩的 JSONL 持久化。Bash 和编辑器绝对路径可以修改运行时进程有权访问的任何路径,因此只能针对可丢弃的 checkout 或容器运行该变体。持久 PTY 需要 POSIX 终端环境,因此不适用于 Windows agent 接口。 +对同一个显式 home 使用运行时 wheel 提供的 `dsh` 命令,以进行持久 profile 变更: -[`minimal.py`](minimal.py)通过 Python SDK 运行该组合,并把 `DSH_MODEL` 作为默认模型。[Python SDK 教程](../../docs/user/guide/python-sdk.zh.md)介绍安装、运行、workspace 选择与 session 标识;[SDK 参考](../../python/sdk/README.zh.md)归属运行时生命周期与结果语义。 +```sh +export DSH_HOME=/absolute/path/to/example-dsh-home +dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle +``` + +Python 调用也可以在 `patches=(...)` 中传入更多绝对 patch 路径;后面的文件优先。所选 profile 必须保留 `@deepseek-ai/dsh-sdk-app` 或另一个 JSON-RPC server 配置项。本目录中的完整独立 Cordis 文件仍作为底层组合测试 fixture;它们不是 Python SDK 启动接口。 + +另见 [Python SDK 教程](../../docs/user/guide/python-sdk.zh.md)与 [SDK 参考](../../python/sdk/README.zh.md)。 diff --git a/examples/python-sdk-agent/cordis.snapshot.yml b/examples/python-sdk-agent/cordis.snapshot.yml index 23bc8c5402..a054ddbe4e 100644 --- a/examples/python-sdk-agent/cordis.snapshot.yml +++ b/examples/python-sdk-agent/cordis.snapshot.yml @@ -3,10 +3,9 @@ # key or network; every other entry remains shared. The replay provider # catalog claims the `deepseek-official` provider so the SDK server's `initialize` # finds it owned and never mounts the real-adapter fallback. The SDK snapshot -# suite passes this path explicitly through `DSH_CORDIS_CONFIG` (the -# jsonrpc-demo bin performs no DSH_SNAPSHOT config swap of its own), and -# `llm-replay` reads `DSH_SNAPSHOT_FILE` / `DSH_SNAPSHOT_CHILD_FILES` from the -# harness. Stdout remains reserved for JSON-RPC frames. +# suite selects this complete-config fixture explicitly, and `llm-replay` +# reads `DSH_SNAPSHOT_FILE` / `DSH_SNAPSHOT_CHILD_FILES` from the harness. +# It is not a Python launch interface. Stdout remains reserved for JSON-RPC. - id: base name: '@deepseek-ai/cordis-plugin-include' config: diff --git a/examples/python-sdk-agent/cordis.yml b/examples/python-sdk-agent/cordis.yml index 40878c58b2..6a04f5f42d 100644 --- a/examples/python-sdk-agent/cordis.yml +++ b/examples/python-sdk-agent/cordis.yml @@ -1,5 +1,6 @@ -# Unattended coding-agent deployment for the bundled dsh-jsonrpc-agent runtime. -# stdout is reserved for JSON-RPC; do not add a console logger or terminal UI. +# Complete JSON-RPC composition fixture for lower-level Loader and SDK tests. +# Python users launch `dsh --profile sdk` and apply patches instead. +# Stdout is reserved for JSON-RPC; do not add a console logger or terminal UI. - id: sdk-jsonrpc-server name: '@deepseek-ai/dsh-sdk-jsonrpc-server' diff --git a/examples/python-sdk-agent/minimal.cordis.yml b/examples/python-sdk-agent/minimal.cordis.yml index fdf3a18e7a..f849b798a0 100644 --- a/examples/python-sdk-agent/minimal.cordis.yml +++ b/examples/python-sdk-agent/minimal.cordis.yml @@ -1,7 +1,6 @@ -# Complete unattended minimal-agent composition for the Python SDK. The model -# sees one deployment-selected system prompt and only the owner-scoped -# persistent Bash and string-replace editor tools. Runtime-context injection and -# context compaction are absent. +# Complete minimal-agent composition fixture for lower-level snapshot tests. +# Python users apply `minimal.patch.yml` to `dsh --profile sdk`. This fixture +# retains the same two-tool behavior without defining a supported launch path. - id: sdk-jsonrpc-server name: '@deepseek-ai/dsh-sdk-jsonrpc-server' diff --git a/packages/README.i18n.yaml b/packages/README.i18n.yaml index 9478bfcb2a..4dc1bb405a 100644 --- a/packages/README.i18n.yaml +++ b/packages/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/README.md -README.md: 5e44b3821d1272923f1545e697d91a434374d24a -README.zh.md: 1bf06b993c0379ecebf8246a50e1fc5fe5f4fce1 +README.md: 6d1add5baa7c73033dadc4247a03b546e6ff9dfd +README.zh.md: da2606b93cae1a6a642a6ee15ac5f0cfc8ce6457 diff --git a/packages/README.md b/packages/README.md index 5e44b3821d..6d1add5baa 100644 --- a/packages/README.md +++ b/packages/README.md @@ -50,7 +50,7 @@ Groups hold `packages///`; names stay `@deepseek-ai/dsh-`. **Gr | [`credentials/`](credentials/README.md) | Credential reference/record seam + env-over-`.env` provider + authorization flows | Product — stable API | | [`storage/`](storage/README.md) | Non-session storage hub + backends + domain form | Product — stable API | | [`workspace/`](workspace/README.md) | Workspace entity | Product — stable API | -| [`sdk/`](sdk/README.md) | Out-of-process SDK: JSON-RPC protocol, TypeScript client/server, and private Python carrier | Product — stable API | +| [`sdk/`](sdk/README.md) | Out-of-process SDK: JSON-RPC protocol and TypeScript client/server | Product — stable API | | [`acp/`](acp/README.md) | Automation-only Agent Client Protocol server | Product — stable API | | [`interaction/`](interaction/README.md) | Human-collaboration plane: approval/interaction seams, permission preset, commands, ask-user tool | Product — stable API | | [`boot/`](boot/README.md) | Shared app-bin boot glue | Product — stable API | diff --git a/packages/README.zh.md b/packages/README.zh.md index 1bf06b993c..da2606b93c 100644 --- a/packages/README.zh.md +++ b/packages/README.zh.md @@ -50,7 +50,7 @@ npm scope 为 `@deepseek-ai/dsh-*`;Cordis `Service` 子类和函数插件通 | [`credentials/`](credentials/README.zh.md) | 凭据引用/记录 seam + 环境变量优先于 `.env` 的提供方 + 授权 flow | 产品:稳定 API | | [`storage/`](storage/README.zh.md) | 非会话存储中枢 + 后端 + 领域形式 | 产品:稳定 API | | [`workspace/`](workspace/README.zh.md) | Workspace 实体 | 产品:稳定 API | -| [`sdk/`](sdk/README.zh.md) | 进程外 SDK:JSON-RPC 协议、TypeScript 客户端/服务器和私有 Python 载体 | 产品:稳定 API | +| [`sdk/`](sdk/README.zh.md) | 进程外 SDK:JSON-RPC 协议与 TypeScript 客户端/服务器 | 产品:稳定 API | | [`acp/`](acp/README.zh.md) | 仅面向自动化的 ACP(Agent Client Protocol)服务器 | 产品:稳定 API | | [`interaction/`](interaction/README.zh.md) | 人机协作平面:批准/交互 seam、权限预设、命令、询问用户的工具 | 产品:稳定 API | | [`boot/`](boot/README.zh.md) | 共享的 app bin 启动粘合层 | 产品:稳定 API | diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml index bcdd303a0d..177b740547 100644 --- a/packages/boot/app-boot/README.i18n.yaml +++ b/packages/boot/app-boot/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/boot/app-boot/README.md -README.md: 9965d6d57f4ec6cd9a93650bbd0096b059fb010b -README.zh.md: 436b19e4b2a05f4462f7636fccd3911b8ed41548 +README.md: 81c53abb799a91a2f7601f7506defbe9dc465820 +README.zh.md: 0d9e0707e0e809f4b2db0643a04619573107941a diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md index 9965d6d57f..81c53abb79 100644 --- a/packages/boot/app-boot/README.md +++ b/packages/boot/app-boot/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Shared Loader boot glue for [`dsh`](../../../apps/cli/README.md) profiles and the [temporarily packaged Python SDK runtime](../../../python/README.md). The product launcher owns profile composition and process lifecycle; the direct-config helpers remain only for that held-back runtime until its later migration. +Shared Loader boot glue for [`dsh`](../../../apps/cli/README.md) profiles, including the CLI packaged by the [Python runtime wheel](../../../python/README.md). The product launcher owns profile composition and process lifecycle. Direct-config helpers serve lower-level embedders and tests; they do not define another supported application entrypoint. | Export | Role | |---|---| @@ -35,7 +35,7 @@ This package carries no loader hooks and no dev-mode surface. The [`dsh` app](.. ## Profiles -A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory — one symlink per package the installation's app and bundles depend on — so bare plugin names in any profile resolve through Node's ordinary parent-walk without pnpm managing in-box packages. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged. +A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable writes real ESM proxy packages that mirror explicit exports and re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged. User-level machine-local preferences also live in the Harness home: diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md index 436b19e4b2..0d9e0707e0 100644 --- a/packages/boot/app-boot/README.zh.md +++ b/packages/boot/app-boot/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -供 [`dsh`](../../../apps/cli/README.zh.md) profile 与[暂时打包的 Python SDK runtime](../../../python/README.zh.md) 共用的 Loader 启动粘合层。产品启动器负责 profile 组合与进程生命周期;直接配置 helper 只为暂缓迁移的 runtime 保留,直至后续迁移。 +供 [`dsh`](../../../apps/cli/README.zh.md) profile 共用的 Loader 启动粘合层,也用于 [Python 运行时 wheel](../../../python/README.zh.md)打包的 CLI。产品启动器负责 profile 组合与进程生命周期。直接配置 helper 服务于底层 embedder 与测试,不会定义另一个受支持的应用入口。 | 导出 | 职责 | |---|---| @@ -35,7 +35,7 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面 ## Profiles -profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录(安装目录的应用与各组合包依赖的每个包对应一个符号链接),使任意 profile 中的裸插件名都能经 Node 常规的逐级向上查找解析,而无需由 pnpm 管理随安装内置的包。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。 +profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则写入真实 ESM 代理包,镜像显式 exports 并重新导出虚拟模块 URL,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。 用户级的机器本地偏好同样位于 harness home 中: diff --git a/packages/sdk/README.i18n.yaml b/packages/sdk/README.i18n.yaml index 05788b05a5..e46b9b6698 100644 --- a/packages/sdk/README.i18n.yaml +++ b/packages/sdk/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/sdk/README.md -README.md: 663fa95dc56ecd71c7a2639b6edb45caacbab860 -README.zh.md: 0343ad477f9fa522666cd8d4440bfe645f309c2e +README.md: 7227e9c2c93b3f870d0affc74471d7801f37d4ef +README.zh.md: 3b6f5556cb539abafd1a1e8c866f09a44a2d2f1a diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 663fa95dc5..7227e9c2c9 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -2,11 +2,10 @@ English | [中文](README.zh.md) -This group contains the protocol stack for driving a Harness runtime from another process. The TypeScript client launches the matching `dsh` CLI with a named profile and ordered patches; the private Python carrier preserves the current packaged direct-config runtime until Python moves through the same profile path. The [TypeScript SDK decision](../../.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md) owns the client contract, and the [toolchain removal](../../.agents/notes/implemented/simplification/2026-08-11-remove-sdk-project-toolchain.md) owns the product boundary. +This group contains the protocol stack for driving a Harness runtime from another process. The TypeScript and Python clients both launch `dsh` with a named profile and ordered patches; no package in this group defines a separate application. The [TypeScript SDK decision](../../.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md) owns the client contract, and the [Python profile-runtime decision](../../.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md) owns the packaged Python launch. | Package | Role | |---|---| | [`protocol/`](protocol/README.md) | Defines the SDK runtime wire protocol | | [`client/`](client/README.md) | Drives a Harness runtime through the TypeScript client API | | [`server/`](server/README.md) | Serves out-of-process SDK clients over stdio JSON-RPC | -| [`python-runtime/`](python-runtime/README.md) | Private direct-config carrier for the temporarily unchanged Python SDK runtime | diff --git a/packages/sdk/README.zh.md b/packages/sdk/README.zh.md index 0343ad477f..3b6f5556cb 100644 --- a/packages/sdk/README.zh.md +++ b/packages/sdk/README.zh.md @@ -2,11 +2,10 @@ [English](README.md) | 中文 -本组包含用于从另一进程驱动 Harness 运行时的协议栈。TypeScript 客户端通过具名 profile 与有序 patch 启动匹配版本的 `dsh` CLI;私有 Python 载体在 Python 迁移到同一 profile 路径之前,保留当前打包后的直读配置运行时。[TypeScript SDK 决策](../../.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md)负责客户端约定,[工具链移除](../../.agents/notes/implemented/simplification/2026-08-11-remove-sdk-project-toolchain.zh.md)负责产品边界。 +本组包含用于从另一进程驱动 Harness 运行时的协议栈。TypeScript 与 Python 客户端都通过具名 profile 与有序 patch 启动 `dsh`;本组没有任何包定义独立应用。[TypeScript SDK 决策](../../.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md)负责客户端约定,[Python profile 运行时决策](../../.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md)负责打包后的 Python 启动。 | 包 | 职责 | |---|---| | [`protocol/`](protocol/README.zh.md) | 定义 SDK 运行时通信协议 | | [`client/`](client/README.zh.md) | 通过 TypeScript 客户端 API 驱动 Harness 运行时 | | [`server/`](server/README.zh.md) | 通过 stdio JSON-RPC 为进程外 SDK 客户端提供服务 | -| [`python-runtime/`](python-runtime/README.zh.md) | 为暂时保持不变的 Python SDK 运行时提供私有直读配置载体 | diff --git a/packages/sdk/server/README.i18n.yaml b/packages/sdk/server/README.i18n.yaml index adf84f76c4..5301b4b19e 100644 --- a/packages/sdk/server/README.i18n.yaml +++ b/packages/sdk/server/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/sdk/server/README.md -README.md: e3deaa5288eb94d64a7f5a8425a829e3eed8896e -README.zh.md: a7273e9b4efbfae8e849205ea86b4c0c90588f7c +README.md: 547836ddab99d5c5a5c39c013c38b4bb25ba7e7d +README.zh.md: 97a7e9897fbf338a79c2cb33a8bab809c1cd3899 diff --git a/packages/sdk/server/README.md b/packages/sdk/server/README.md index e3deaa5288..547836ddab 100644 --- a/packages/sdk/server/README.md +++ b/packages/sdk/server/README.md @@ -2,11 +2,11 @@ English | [中文](README.zh.md) -The `jsonrpc` plugin serves newline-delimited JSON-RPC over stdio so out-of-process SDK clients can drive harness agents. [`HarnessSdkJsonRpcServer`](src/server.ts) owns the protocol methods and notifications; the transport and the named wire types live in [`dsh-sdk-protocol`](../protocol/README.md), shared with the client SDKs. The TypeScript client receives this server through `dsh --profile sdk`; the private [Python runtime carrier](../python-runtime/README.md) temporarily supplies a direct-config application around it. +The `jsonrpc` plugin serves newline-delimited JSON-RPC over stdio so out-of-process SDK clients can drive harness agents. [`HarnessSdkJsonRpcServer`](src/server.ts) owns the protocol methods and notifications; the transport and the named wire types live in [`dsh-sdk-protocol`](../protocol/README.md), shared with the client SDKs. TypeScript and Python clients receive this server through `dsh --profile sdk` or another profile that mounts the same row. ## Wiring -`inject: ['agents']`. The server gets or creates one agent per `sessionId`. It forwards subagent completions only when the service-snapshotted lifecycle `local` flag is true; provider names, child ids, and durable lineage never establish locality. A registered adapter wins, an unowned `deepseek-official` route mounts `dsh-llm-deepseek`, and any other unowned provider fails initialization. Other capabilities come from the surrounding `cordis.yml`. +`inject: ['agents']`. The server gets or creates one agent per `sessionId`. It forwards subagent completions only when the service-snapshotted lifecycle `local` flag is true; provider names, child ids, and durable lineage never establish locality. A registered adapter wins, an unowned `deepseek-official` route mounts `dsh-llm-deepseek`, and any other unowned provider fails initialization. Other capabilities come from the surrounding Loader composition. ## Config @@ -22,7 +22,7 @@ The plugin answers `shutdown`, flushes the response, disposes the root context s ## Wire notes -`initialize` is the runtime-readiness boundary: when the server is mounted by a Loader composition, it waits for the current plugin tree to settle before replying, so async sibling capabilities such as initial MCP tool discovery are visible to the first prompt. Hand-built contexts without Loader remain immediately usable. `initialize.serverInfo.name` is the wire-stable `deepseek-harness-sdk-runtime`. An optional positive `initialize.maxTokens` becomes the request output cap of each SDK-created agent and its in-process descendants; invalid values reject initialization, while omission sends no SDK cap and allows the selected adapter or provider route default to apply. `session/prompt` queues one identified user message and immediately returns `{ messageId }`. The server streams every durable fact as `session.event` and every whole-agent lifecycle transition as `session.status`; it does not assign an assistant message or `turn/end` to that prompt. Independent requests may enqueue more work on the same session. Persistence roots and persona come from `cordis.yml`. +`initialize` is the runtime-readiness boundary: when the server is mounted by a Loader composition, it waits for the current plugin tree to settle before replying, so async sibling capabilities such as initial MCP tool discovery are visible to the first prompt. Hand-built contexts without Loader remain immediately usable. `initialize.serverInfo.name` is the wire-stable `deepseek-harness-sdk-runtime`. An optional positive `initialize.maxTokens` becomes the request output cap of each SDK-created agent and its in-process descendants; invalid values reject initialization, while omission sends no SDK cap and allows the selected adapter or provider route default to apply. `session/prompt` queues one identified user message and immediately returns `{ messageId }`. The server streams every durable fact as `session.event` and every whole-agent lifecycle transition as `session.status`; it does not assign an assistant message or `turn/end` to that prompt. Independent requests may enqueue more work on the same session. Persistence roots and persona come from the surrounding composition. ## Model Experience @@ -30,7 +30,7 @@ The plugin answers `shutdown`, flushes the response, disposes the root context s #### What the model sees -For each accepted `session/prompt`, the conversation model receives the caller-supplied `contentBlocks` verbatim as one user message in that SDK session. This package adds no system-prompt prose or tool schema; those come from the plugins in the surrounding `cordis.yml`. +For each accepted `session/prompt`, the conversation model receives the caller-supplied `contentBlocks` verbatim as one user message in that SDK session. This package adds no system-prompt prose or tool schema; those come from the other plugins in the composition. #### Token effect diff --git a/packages/sdk/server/README.zh.md b/packages/sdk/server/README.zh.md index a7273e9b4e..97a7e9897f 100644 --- a/packages/sdk/server/README.zh.md +++ b/packages/sdk/server/README.zh.md @@ -2,11 +2,11 @@ [English](README.md) | 中文 -`jsonrpc` 插件通过 stdio 提供以换行符分隔的 JSON-RPC,使进程外 SDK 客户端能够驱动 harness agent(智能体)。[`HarnessSdkJsonRpcServer`](src/server.ts) 负责协议方法和通知;传输与具名协议类型位于 [`dsh-sdk-protocol`](../protocol/README.zh.md),与客户端 SDK 共享。TypeScript 客户端通过 `dsh --profile sdk` 获得该服务器;私有 [Python 运行时载体](../python-runtime/README.zh.md)暂时为其提供直读配置应用。 +`jsonrpc` 插件通过 stdio 提供以换行符分隔的 JSON-RPC,使进程外 SDK 客户端能够驱动 harness agent(智能体)。[`HarnessSdkJsonRpcServer`](src/server.ts) 负责协议方法和通知;传输与具名协议类型位于 [`dsh-sdk-protocol`](../protocol/README.zh.md),与客户端 SDK 共享。TypeScript 与 Python 客户端都通过 `dsh --profile sdk` 或挂载同一配置项的其他 profile 获得该服务器。 ## 组装 -`inject: ['agents']`。服务器按 `sessionId` 获取或创建一个 agent。只有服务对生命周期建立快照时记录的 `local` 标志为 true,服务器才会转发 subagent 完成事件;提供方名称、子级 id 和持久化谱系均不能证明本地性。已注册的适配器优先;尚无适配器负责的 `deepseek-official` 路由会挂载 `dsh-llm-deepseek`,任何其他尚无适配器负责的提供方都会导致初始化失败。其他能力由外围 `cordis.yml` 提供。 +`inject: ['agents']`。服务器按 `sessionId` 获取或创建一个 agent。只有服务对生命周期建立快照时记录的 `local` 标志为 true,服务器才会转发 subagent 完成事件;提供方名称、子级 id 和持久化谱系均不能证明本地性。已注册的适配器优先;尚无适配器负责的 `deepseek-official` 路由会挂载 `dsh-llm-deepseek`,任何其他尚无适配器负责的提供方都会导致初始化失败。其他能力由外围 Loader 组合提供。 ## 配置 @@ -22,7 +22,7 @@ Stdout 只承载 JSON-RPC 帧。部署不得组合 stdout logger;诊断应写 ## 协议说明 -`initialize` 是运行时就绪边界:服务器由 Loader 组合挂载时,会等待当前插件树完成所有加载任务后再响应,因此首次提示词能够看到 MCP 初始工具发现等异步同级能力。没有 Loader 的手工组装上下文仍可立即使用。`initialize.serverInfo.name` 的协议稳定值为 `deepseek-harness-sdk-runtime`。可选的正整数 `initialize.maxTokens` 会成为每个 SDK 创建的 agent 及其进程内后代的请求输出上限;非法值会使初始化失败,省略时则不发送 SDK 上限,并应用所选适配器或提供方路由的默认值。`session/prompt` 将一条带标识的用户消息排入队列,并立即返回 `{ messageId }`。服务器将每个持久事实作为 `session.event` 流式发出,并将整个 agent 生命周期的每次状态转换作为 `session.status` 发出;它不会把某条助手消息或 `turn/end` 归属于该提示词。同一会话上的独立请求可以继续排入更多工作。持久化根目录和 persona 由 `cordis.yml` 提供。 +`initialize` 是运行时就绪边界:服务器由 Loader 组合挂载时,会等待当前插件树完成所有加载任务后再响应,因此首次提示词能够看到 MCP 初始工具发现等异步同级能力。没有 Loader 的手工组装上下文仍可立即使用。`initialize.serverInfo.name` 的协议稳定值为 `deepseek-harness-sdk-runtime`。可选的正整数 `initialize.maxTokens` 会成为每个 SDK 创建的 agent 及其进程内后代的请求输出上限;非法值会使初始化失败,省略时则不发送 SDK 上限,并应用所选适配器或提供方路由的默认值。`session/prompt` 将一条带标识的用户消息排入队列,并立即返回 `{ messageId }`。服务器将每个持久事实作为 `session.event` 流式发出,并将整个 agent 生命周期的每次状态转换作为 `session.status` 发出;它不会把某条助手消息或 `turn/end` 归属于该提示词。同一会话上的独立请求可以继续排入更多工作。持久化根目录和 persona 由外围组合提供。 ## 模型体验 @@ -30,7 +30,7 @@ Stdout 只承载 JSON-RPC 帧。部署不得组合 stdout logger;诊断应写 #### 模型看到的内容 -对于每个已接受的 `session/prompt`,对话模型会将调用方提供的 `contentBlocks` 原样作为该 SDK 会话中的一条用户消息接收。此包不会添加系统提示词文本或工具 schema;这些内容来自外围 `cordis.yml` 中的插件。 +对于每个已接受的 `session/prompt`,对话模型会将调用方提供的 `contentBlocks` 原样作为该 SDK 会话中的一条用户消息接收。此包不会添加系统提示词文本或工具 schema;这些内容来自组合中的其他插件。 #### Token 影响 diff --git a/python/README.i18n.yaml b/python/README.i18n.yaml index 8c5a23e37b..fee9373a2d 100644 --- a/python/README.i18n.yaml +++ b/python/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 python/README.md -README.md: 75276a915eb4b63f84e0876de46e6d8d63540b59 -README.zh.md: f41822d66520869e5235a6fedd895b83a52639c9 +README.md: 7cb822e475acaa36a2a1637b0b4830e7dcabcb7a +README.zh.md: e571b936dad5d14db834fcabed363b5b7c199807 diff --git a/python/README.md b/python/README.md index 75276a915e..7cb822e475 100644 --- a/python/README.md +++ b/python/README.md @@ -9,11 +9,11 @@ Python packages for driving DeepSeek Harness as a subprocess. The client SDK com | Directory | Dist / module | Role | |---|---|---| | [sdk](sdk/README.md) | `deepseek-harness-sdk` / `deepseek_harness` | High-level turns API and lower-level JSON-RPC client | -| [sdk-runtime](sdk-runtime/README.md) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | Bundled runtime binaries and default agent configuration | +| [sdk-runtime](sdk-runtime/README.md) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | Bundled `dsh` CLI executable and native sidecars | ## Behavior -The SDK starts the matching bundled runtime unless the caller selects an explicit channel. The client selects the channel and supplies default configuration; the runtime itself always requires an explicit configuration. The [SDK reference](sdk/README.md) and [runtime carrier reference](sdk-runtime/README.md) own the complete runtime-selection and configuration contracts. +The SDK starts the matching bundled `dsh --profile sdk` runtime unless the caller selects another `dsh` executable or profile. Every launch requires an explicitly selected Harness home; Python never silently reads `~/.dsh`. The [SDK reference](sdk/README.md) and [runtime carrier reference](sdk-runtime/README.md) own runtime selection, profiles, patches, and external plugin management. ## Contributor workflows diff --git a/python/README.zh.md b/python/README.zh.md index f41822d665..e571b936da 100644 --- a/python/README.zh.md +++ b/python/README.zh.md @@ -9,11 +9,11 @@ | 目录 | 分发名/模块 | 职责 | |---|---|---| | [sdk](sdk/README.zh.md) | `deepseek-harness-sdk` / `deepseek_harness` | 高层轮次 API 与低层 JSON-RPC 客户端 | -| [sdk-runtime](sdk-runtime/README.zh.md) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | 内置运行时二进制与默认 agent(智能体)配置 | +| [sdk-runtime](sdk-runtime/README.zh.md) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | 内置 `dsh` CLI 可执行程序与原生伴随文件 | ## 行为 -除非调用方选择显式通道,否则 SDK 会启动匹配的内置运行时。客户端选择通道并提供默认配置;运行时本身始终要求显式配置。[SDK 参考](sdk/README.zh.md)和[运行时载体参考](sdk-runtime/README.zh.md)定义完整的运行时选择与配置约定。 +除非调用方选择另一个 `dsh` 可执行程序或 profile,否则 SDK 会启动匹配的内置 `dsh --profile sdk` 运行时。每次启动都要求显式选择 Harness home;Python 绝不会静默读取 `~/.dsh`。[SDK 参考](sdk/README.zh.md)和[运行时载体参考](sdk-runtime/README.zh.md)定义运行时选择、profile、patch 与外部插件管理约定。 ## 贡献者工作流 diff --git a/python/development.i18n.yaml b/python/development.i18n.yaml index 75c657c153..bff1468133 100644 --- a/python/development.i18n.yaml +++ b/python/development.i18n.yaml @@ -2,5 +2,5 @@ # 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 python/development.md -development.md: e96be7af10e0008cc0fe5dea4ca51529f102fd7b -development.zh.md: e4ca1c980c36fdea381e9b8a6c615276e279b4d2 +development.md: 145f958a9df28e76456fb16a68b36fe318afa323 +development.zh.md: 557d6ba1f766dca61ab7bc73bf71ced2eb79afbe diff --git a/python/development.md b/python/development.md index e96be7af10..145f958a9d 100644 --- a/python/development.md +++ b/python/development.md @@ -31,7 +31,7 @@ That suite drives fake runtime peers. `scripts/smoke-python-runtime.py` drives t ```sh uv run --project python/sdk python scripts/smoke-python-runtime.py \ - --scenario sdk-minimal --exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 + --scenario sdk-minimal --exe dist-exe/deepseek-harness-sdk-runtime-macos-arm64 ``` Three scenarios compare committed expected output under `scripts/snapshots/python-sdk-single-exe/`. `minimal/model-visible.json` pins the checked-in minimal composition's assembled system prompts, advertised tool schemas, and model-visible messages, so a plugin that contributes an unintended system section or user message fails the job; it drops the dynamic runtime-context snapshot, which the same composition emits on macOS and not on Linux ([#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488)). `advanced/` pins one complex process's SDK result and parent/child session logs. `restart/` launches two complete SDK runtime processes against one persistence root and snapshots their isolated model histories, high-level results, and separate durable logs. Rerun the owning scenario with `--update-snapshots` and review that diff before committing it. @@ -43,18 +43,20 @@ An interactive smoke test needs `DEEPSEEK_API_KEY` in the environment or reposit ```python from deepseek_harness import DeepSeekHarness -with DeepSeekHarness() as harness: +with DeepSeekHarness(dsh_home="/absolute/path/to/test-dsh-home") as harness: print(harness.run("say hi").final_response) ``` +Alternatively export a non-empty `DSH_HOME`. The SDK rejects a launch that would silently use `~/.dsh`. + ## Run against Node source -Repository contributors can select either development carrier: +Repository contributors can select either development route; both execute the normal `dsh --profile sdk` launcher: - Set `DSH_RUNTIME_MODE=node` to use the built Node carrier on system Node `>=22.19`. The build script refreshes this carrier, but distributions never include or auto-select it. -- Set `launch_args_override=("./node_modules/.bin/tsx", "packages/sdk/python-runtime/src/packaged-bin.ts")` with the repository root as `cwd` to run the private carrier's unbuilt TypeScript source. Supply `cordis=...` when the default configuration is not suitable. +- Set `dsh_bin` to the absolute built `apps/cli/lib/bin.js` path to exercise the checkout's CLI directly. Supply an explicit `dsh_home`, plus `profile` and ordered `patches` as needed. -See `python/sdk/tests/manual_sdk_agent_smoke.py` for a complete source-mode invocation. +`python/sdk/tests/manual_sdk_agent_smoke.py` uses the internal `_launch_args` test adapter to exercise the unbuilt TypeScript CLI under tsx. Arbitrary argv replacement is intentionally absent from the public SDK. ## Build distributions @@ -71,7 +73,7 @@ print(release["pep440_version"](release["repository_version"]())) PY )" python scripts/build-python-release.py --package sdk --output-dir dist-python -python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python +python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/deepseek-harness-sdk-runtime-macos-arm64 --output-dir dist-python pip install \ "dist-python/deepseek_harness_sdk-$version-py3-none-any.whl" \ "dist-python/deepseek_harness_runtime_bin-$version-py3-none-macosx_14_0_arm64.whl" diff --git a/python/development.zh.md b/python/development.zh.md index e4ca1c980c..557d6ba1f7 100644 --- a/python/development.zh.md +++ b/python/development.zh.md @@ -31,7 +31,7 @@ uv run --project python/sdk pytest ```sh uv run --project python/sdk python scripts/smoke-python-runtime.py \ - --scenario sdk-minimal --exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 + --scenario sdk-minimal --exe dist-exe/deepseek-harness-sdk-runtime-macos-arm64 ``` 其中三个场景会比对 `scripts/snapshots/python-sdk-single-exe/` 下已提交的期望输出。`minimal/model-visible.json` 固定了签入的极简组合所组装的系统提示词、对外公布的工具 schema 以及模型可见消息,因此插件一旦贡献出计划外的系统分段或 user 消息,该任务即失败;它会丢弃动态运行时上下文快照——同一组合在 macOS 上会发出它,在 Linux 上不会([#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488))。`advanced/` 固定一个复杂进程的 SDK 结果及父/子会话日志。`restart/` 针对同一持久化根目录启动两个完整 SDK 运行时进程,并固定其彼此隔离的模型历史、高层结果与独立持久日志。重新运行对应场景时加上 `--update-snapshots`,并在提交前审阅该差异。 @@ -43,18 +43,20 @@ uv run --project python/sdk python scripts/smoke-python-runtime.py \ ```python from deepseek_harness import DeepSeekHarness -with DeepSeekHarness() as harness: +with DeepSeekHarness(dsh_home="/absolute/path/to/test-dsh-home") as harness: print(harness.run("say hi").final_response) ``` +也可以导出非空 `DSH_HOME`。SDK 会拒绝可能静默使用 `~/.dsh` 的启动。 + ## 针对 Node 源码运行 -仓库贡献者可以选择以下任一开发载体: +仓库贡献者可以选择以下任一开发路径;两者都执行普通的 `dsh --profile sdk` 启动器: - 设置 `DSH_RUNTIME_MODE=node`,在系统 Node `>=22.19` 上使用已构建的 Node 载体。构建脚本会刷新该载体,但分发物绝不会包含或自动选择它。 -- 将仓库根目录设为 `cwd`,并设置 `launch_args_override=("./node_modules/.bin/tsx", "packages/sdk/python-runtime/src/packaged-bin.ts")`,以运行私有载体未构建的 TypeScript 源码。默认配置不合适时,请提供 `cordis=...`。 +- 将 `dsh_bin` 设置为已构建 `apps/cli/lib/bin.js` 的绝对路径,直接验证当前 checkout 的 CLI。请显式提供 `dsh_home`,并按需提供 `profile` 与有序 `patches`。 -完整的源码模式调用见 `python/sdk/tests/manual_sdk_agent_smoke.py`。 +`python/sdk/tests/manual_sdk_agent_smoke.py` 使用内部 `_launch_args` 测试适配器,通过 tsx 验证未构建的 TypeScript CLI。公开 SDK 刻意不提供任意 argv 替换。 ## 构建分发包 @@ -71,7 +73,7 @@ print(release["pep440_version"](release["repository_version"]())) PY )" python scripts/build-python-release.py --package sdk --output-dir dist-python -python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python +python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/deepseek-harness-sdk-runtime-macos-arm64 --output-dir dist-python pip install \ "dist-python/deepseek_harness_sdk-$version-py3-none-any.whl" \ "dist-python/deepseek_harness_runtime_bin-$version-py3-none-macosx_14_0_arm64.whl" diff --git a/python/sdk-runtime/README.i18n.yaml b/python/sdk-runtime/README.i18n.yaml index c4ccb18bf1..ddcef99f9e 100644 --- a/python/sdk-runtime/README.i18n.yaml +++ b/python/sdk-runtime/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 python/sdk-runtime/README.md -README.md: 67d3842a9255250f66f22ff1f9422b26c3cf3282 -README.zh.md: 47c94b29d68eb915fae5303274fe2888274c1f82 +README.md: 52103b538be0ebc79572d8477e407f324e419ce6 +README.zh.md: e4fe7673a510102504454d5a6113ec7ec6ffd086 diff --git a/python/sdk-runtime/README.md b/python/sdk-runtime/README.md index 67d3842a92..52103b538b 100644 --- a/python/sdk-runtime/README.md +++ b/python/sdk-runtime/README.md @@ -1,31 +1,36 @@ -# DeepSeek Harness Runtime Wheel +# deepseek-harness-runtime-bin -English | [中文](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/README.zh.md) +English | [中文](README.zh.md) -Runtime carrier package for the Python SDK (dist `deepseek-harness-runtime-bin`, module `deepseek_harness_runtime`): it locates the bundled runtime binaries the `deepseek-harness-sdk` client spawns, and ships the default configuration behind zero-config runs. +Platform runtime wheel for the DeepSeek Harness Python SDK. It packages the normal `dsh` CLI and its closed Node dependency tree into a native executable, so SDK use requires no system Node.js. This package publishes wheels only. -## Runtime carriers +## Installed commands and artifacts -Two carriers coexist under `src/deepseek_harness_runtime/runtime/`, both injected by the repo's `scripts/build-exe-for-python-sdk.ts` build and both gitignored: +The wheel installs a `dsh` console command and the `deepseek_harness_runtime` Python module. `dsh` forwards its arguments to the bundled executable and requires a non-empty `DSH_HOME`; it never falls back to `~/.dsh`. -- **exe (production)** — a single-file Node executable `dsh-jsonrpc-agent-pkg--` (platform: `linux`/`macos`; arch: `x64`/`arm64`) with a target-native ripgrep `-rg` sidecar. macOS builds also ship the native `-spawn-helper` sibling that `node-pty` uses there. No Node installation is needed on the target machine. This is the only carrier that ships in wheel distributions; this package does not publish sdists. -- **node (dev-only)** — the full deploy closure under `runtime/node/` (`package.json` + `node_modules/`), executed as `node runtime/node/node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js` on a system Node >= 22.19. It is the current checkout's source build, meant for repo-local development and verification only; it is never selected automatically and is excluded from distributions. +Production executables are named `deepseek-harness-sdk-runtime--` under the module's `runtime/` directory. Linux and macOS wheels include a target-native `-rg` sidecar; macOS also includes `-spawn-helper` for `node-pty`. Published targets are Linux x64, Linux arm64, and macOS arm64. The wheel tag and payload must match exactly. -Both carriers hold the same content, defined once: the [package.json](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/package.json) at this package's root is the private `dsh-sdk-python-runtime-closure` deploy root of the single-exe pipeline — a pure dependency manifest (no code of its own) whose dependency closure IS both the plugin set compiled into the exe and the tree materialized into `runtime/node/`. Adding a plugin to the distribution means adding one dependency line there and rebuilding. +Repository builds also materialize a dev-only `runtime/node/` carrier. It runs `node runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. It is never selected automatically and is excluded from wheels and sdists. -The bundled plugin set includes `@deepseek-ai/dsh-mcp-client`, so an external Cordis config can connect to stdio or Streamable HTTP MCP servers and expose their tools to the model. The wheel does not bundle MCP server programs or credentials: a stdio config supplies its executable and arguments, while a Streamable HTTP config supplies its URL and headers. The bridge supports MCP tools; MCP Resources and Prompts remain unsupported. +Both carriers execute the same `dsh` grammar and profiles. The private `dsh-python-runtime-closure` manifest defines the packaged dependency closure; there is no Python-specific Node application or checked-in default `cordis.yml`. -A missing exe raises `FileNotFoundError` naming both acquisition routes: build via `scripts/build-exe-for-python-sdk.ts` in a deepseek-harness checkout, or install the matching platform runtime wheel produced by the `build-exe-for-python-sdk` CI workflow. A missing dev-only node carrier names its sole route, the build script. The workflow retains wheels rather than standalone executable archives. Acquisition strategy is deliberately separate from the lookup interface, so an on-demand download can replace it later without touching callers. +## Python module API -Each wheel contains exactly one runtime executable and its matching ripgrep `-rg` sidecar. The macOS wheel also contains its matching native spawn helper; any missing sidecar makes that installation incomplete and is a hard startup error, even for a selected Cordis composition that does not use filesystem-search or PTY tools. Linux wheels contain no spawn helper because `node-pty` uses the staged `pty.node` addon directly. The fixed tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_14_0_arm64`; the macOS tag conservatively matches the bundled Node 24 executable's macOS 13.5 deployment target. This package's `platforms.json` owns the fixed tag and executable-name pairs used by both the repository release builder and the isolated build hook. The build hook rejects `py3-none-any`, absent or multiple runtime executables, missing or extra sidecars, non-executable files, and unsupported platform tags. The repository root `package.json` supplies the shared version for this package and the SDK, and a `python-v` release tag must match it. +- `bundled_package_dir() -> Path` returns the installed module-data root and verifies its release metadata. +- `bundled_runtime_path() -> Path` returns the current platform executable and verifies required sidecars. +- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]` returns the executable argv by default. Explicit `mode="node"` or `DSH_RUNTIME_MODE=node` selects the repo-only Node carrier. +- `main()` implements the installed `dsh` console command and rejects an absent or blank `DSH_HOME` before replacing the Python process. -## Resolution API +Unsupported platforms and missing executables or sidecars raise `FileNotFoundError` with the build and installation routes. Unknown runtime modes raise `ValueError`. -- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]` — the argv tuple that launches the bundled runtime: `(exe_path,)` in exe mode, `(node_path, bin_js_path)` in node mode. Mode selection: explicit argument > `DSH_RUNTIME_MODE` env var (`exe` | `node`) > automatic. Automatic resolution finds the production exe ONLY — the dev-only node carrier must be opted into explicitly so a production deployment can never silently ride on a source build. -- `bundled_runtime_path() -> Path` — the platform exe path (exe carrier only); it validates the required sibling `-rg` sidecar on every platform and the `-spawn-helper` sidecar on macOS. The node carrier has no single-path equivalent and launches via the argv tuple above. -- `bundled_default_config_path() -> Path` — the checked-in default config (see below). -- `bundled_package_dir() -> Path` — the installed package data root. +## Packaged profile resolution -## Zero-config design +`dsh` initializes shipped profiles under the explicit home, composes their bundle patches, and loads bundled plugins from the executable's virtual filesystem. Because operating-system symlinks cannot enter that filesystem, packaged launches maintain small real ESM proxy packages under `$DSH_HOME/profiles/node_modules`. Each proxy mirrors explicit runtime exports, records the original package identity, and re-exports the virtual module URL. Built-in rows and external plugin peers therefore share one Cordis/module instance. Native shared libraries are packaged with native addons, while ripgrep and the macOS PTY helper remain executable sidecars. -The runtime binary always demands an explicit config (`$DSH_CORDIS_CONFIG`, or a config path as an argv positional argument) and exits loudly without one — that hard semantic is part of the runtime's design and this package does not soften it. The bin (`dsh-jsonrpc-agent`) boots only the plugins the config lists; the serving interface (the stdio JSON-RPC server) is itself one of its entries (`@deepseek-ai/dsh-sdk-jsonrpc-server`), and without it the booted agent has no channel to the outside. This package checks in `runtime/cordis.yml` with the JSON-RPC serving entry, agent core, a preloaded DeepSeek adapter, JSONL persistence, the explicitly composed semantic checkpoint policy, local bash, and a local filesystem provider for bounded workspace-instruction loading. The persistence backend owns durable storage while the separate policy selects request-, tool-dispatch-, and completed-step checkpoints. The adapter reads `DEEPSEEK_API_KEY` and `DEEPSEEK_BASE_URL`, while persistence, bash, and the filesystem provider use `DSH_SESSION_ROOT` and `DSH_CWD` with manual-run fallbacks. When the caller uses no explicit config channel, the `deepseek_harness` client injects that file's path via `DSH_CORDIS_CONFIG` (injection conditions: [sdk README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk/README.md)). Zero-config is thus an explicit, visible parameter pass in the wrapper, not a hidden fallback in the runtime. +External profile management uses `dsh plugin --profile ...`. That command requires `pnpm` on `PATH`; ordinary SDK/profile execution does not. + +## Build and distribution + +From the repository root, `pnpm exec tsx scripts/build-exe-for-python-sdk.ts` verifies the closure, builds packages, deploys a symlink-free tree, packages the selected target, and syncs the executable and sidecars into this module. `scripts/build-python-release.py` stages release-shaped wheels at the root repository version and pins `deepseek-harness-sdk` to the exact runtime version. + +The installed-wheel smoke creates a clean virtual environment outside the checkout, proves distribution and executable provenance, then exercises default and customized SDK profiles, external plugins, MCP, native tools, direct JSON-RPC, committed snapshots, and the real provider on trusted runs. See the [Python contributor workflow](../development.md) and [installed-wheel testing decision](../../.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md). diff --git a/python/sdk-runtime/README.zh.md b/python/sdk-runtime/README.zh.md index 47c94b29d6..e4fe7673a5 100644 --- a/python/sdk-runtime/README.zh.md +++ b/python/sdk-runtime/README.zh.md @@ -1,31 +1,36 @@ -# DeepSeek Harness 运行时 wheel 包 +# deepseek-harness-runtime-bin -[English](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/README.md) | 中文 +[English](README.md) | 中文 -Python SDK 的运行时载体包(分发名 `deepseek-harness-runtime-bin`,模块名 `deepseek_harness_runtime`):它定位 `deepseek-harness-sdk` 客户端要 spawn 的内置运行时二进制,并附带支撑零配置运行的默认配置。 +DeepSeek Harness Python SDK 的平台运行时 wheel。它把普通 `dsh` CLI 及其封闭的 Node 依赖树打包成原生可执行程序,因此使用 SDK 不需要系统 Node.js。本包只发布 wheel。 -## 运行时载体 +## 安装命令与产物 -两种载体并存于 `src/deepseek_harness_runtime/runtime/` 之下,均由仓库的 `scripts/build-exe-for-python-sdk.ts` 构建注入,且均被 git 忽略: +Wheel 会安装 `dsh` 控制台命令和 `deepseek_harness_runtime` Python 模块。`dsh` 将参数转发给内置可执行程序,并要求非空 `DSH_HOME`;它不会回退到 `~/.dsh`。 -- **exe(生产)**——单文件 Node 可执行程序 `dsh-jsonrpc-agent-pkg--`(platform:`linux`/`macos`;arch:`x64`/`arm64`),以及匹配目标平台的 ripgrep `-rg` 伴随文件。macOS 构建还会随附 `node-pty` 在该平台使用的原生 `-spawn-helper` 伴随文件。目标机器无需安装 Node。这是唯一随 wheel 包分发的载体;本包不发布 sdist。 -- **node(仅限开发)**——`runtime/node/` 下的完整部署闭包(`package.json` + `node_modules/`),在系统 Node >= 22.19 上以 `node runtime/node/node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js` 执行。它是当前检出的源码构建,仅用于仓库本地的开发与验证;不会被自动选中,也不进入分发物。 +生产可执行程序位于模块的 `runtime/` 目录,命名为 `deepseek-harness-sdk-runtime--`。Linux 与 macOS wheel 包含目标平台原生的 `-rg` 伴随程序;macOS 还包含 `node-pty` 使用的 `-spawn-helper`。已发布目标是 Linux x64、Linux arm64 与 macOS arm64。Wheel tag 必须与载荷严格匹配。 -两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/package.json) 是 single-exe 流水线的私有 `dsh-sdk-python-runtime-closure` 部署根目录——一份零代码的纯依赖 manifest,其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。 +仓库构建还会物化仅限开发的 `runtime/node/` 载体。它在系统 Node 22.19 或更高版本上运行 `node runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。系统不会自动选择它,而且 wheel 与 sdist 均不包含它。 -内置插件集合包含 `@deepseek-ai/dsh-mcp-client`,因此外部 Cordis 配置可以连接 stdio 或 Streamable HTTP MCP server,并向模型提供这些 server 的工具。wheel 包不包含 MCP server 程序或凭据:stdio 配置需要提供可执行程序及其参数,Streamable HTTP 配置需要提供 URL 和请求头。该桥接仅支持 MCP 工具,尚不支持 MCP Resources 与 Prompts。 +两种载体执行相同的 `dsh` 语法和 profile。私有 `dsh-python-runtime-closure` manifest 定义打包依赖闭包;不存在 Python 专用 Node 应用或检入的默认 `cordis.yml`。 -exe 缺失时抛出 `FileNotFoundError`,并写明两种获取途径:在 deepseek-harness 检出中经 `scripts/build-exe-for-python-sdk.ts` 构建,或安装 `build-exe-for-python-sdk` CI 工作流生成的对应平台运行时 wheel 包。仅限开发的 node 载体缺失时只提示构建脚本这一条途径。该工作流只保留 wheel 包,不保留独立 exe 归档。获取策略与查找接口刻意分离,之后可以换成按需下载而不改动任何调用方。 +## Python 模块 API -每个 wheel 包只包含一个运行时可执行文件及其匹配的 ripgrep `-rg` 伴随文件。macOS wheel 包还包含与其匹配的原生 spawn helper;缺少任一伴随文件都意味着该安装不完整,并会在启动时硬失败,即使所选 Cordis 组合不使用文件系统搜索或 PTY 工具也是如此。Linux wheel 包不包含 spawn helper,因为 `node-pty` 直接使用暂存的 `pty.node` 原生插件。固定标签为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 与 `py3-none-macosx_14_0_arm64`;macOS 标签保守匹配内置 Node 24 可执行文件的 macOS 13.5 部署目标。本包的 `platforms.json` 统一定义仓库发行构建器与隔离构建钩子使用的固定标签和可执行文件名。构建钩子会拒绝 `py3-none-any`、不存在或存在多个运行时可执行文件、缺失或多余的伴随文件、文件不可执行以及不支持的平台标签。仓库根目录的 `package.json` 为本包和 SDK 提供共同版本,`python-v` 发布标签必须与其匹配。 +- `bundled_package_dir() -> Path` 返回已安装模块数据根目录,并校验发布元数据。 +- `bundled_runtime_path() -> Path` 返回当前平台可执行程序,并校验必需伴随文件。 +- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]` 默认返回可执行程序 argv。显式 `mode="node"` 或 `DSH_RUNTIME_MODE=node` 会选择仅限仓库使用的 Node 载体。 +- `main()` 实现已安装的 `dsh` 控制台命令,并在替换 Python 进程前拒绝缺失或空白的 `DSH_HOME`。 -## 解析 API +不支持的平台以及缺失的可执行程序或伴随文件会抛出 `FileNotFoundError`,并指出构建与安装路径。未知运行时模式会抛出 `ValueError`。 -- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]`——启动内置运行时的 argv 元组:exe 模式下为 `(exe_path,)`,node 模式下为 `(node_path, bin_js_path)`。模式选择:显式参数 > `DSH_RUNTIME_MODE` 环境变量(`exe` | `node`)> 自动。自动解析只找生产 exe——仅限开发的 node 载体必须显式选用,从而生产部署绝不会悄悄跑在源码构建上。 -- `bundled_runtime_path() -> Path`——平台 exe 路径(仅 exe 载体);它会在所有平台校验必要的 `-rg` 伴随文件,并在 macOS 上额外校验 `-spawn-helper` 伴随文件。node 载体没有单一路径的等价物,经由上面的 argv 元组启动。 -- `bundled_default_config_path() -> Path`——检入的默认配置(见下文)。 -- `bundled_package_dir() -> Path`——已安装包的数据根目录。 +## 打包后的 profile 解析 -## 零配置设计 +`dsh` 在显式 home 下初始化随附 profile、组合其 bundle patch,并从可执行程序的虚拟文件系统加载内置插件。操作系统符号链接无法进入该文件系统,因此打包运行会在 `$DSH_HOME/profiles/node_modules` 下维护小型真实 ESM 代理包。每个代理镜像显式运行时 exports、记录原包身份,并重新导出虚拟模块 URL。因此,内置配置项与外部插件 peer 会共享同一个 Cordis/模块实例。原生共享库与原生 addon 一同打包;ripgrep 与 macOS PTY helper 仍是可执行伴随程序。 -运行时二进制始终要求显式配置(`$DSH_CORDIS_CONFIG`,或作为 argv 位置参数的配置路径),缺了就报错退出——这一强制语义是运行时设计的一部分,本包不会弱化它。bin(`dsh-jsonrpc-agent`)只启动配置里列出的插件;对外服务接口(stdio JSON-RPC 服务器)也是其中一个条目(`@deepseek-ai/dsh-sdk-jsonrpc-server`),缺了它,启动出的 agent(智能体)就没有对外通道。本包检入的 `runtime/cordis.yml` 包含 JSON-RPC 服务条目、agent 核心、预载的 DeepSeek 适配器、JSONL 持久化、显式组合的语义检查点策略、本地 bash,以及用于有界加载工作区指令的本地文件系统提供方。持久化后端负责持久存储,独立的策略则选择请求、工具分发和已完成步骤的检查点。DeepSeek 适配器读取 `DEEPSEEK_API_KEY` 与 `DEEPSEEK_BASE_URL`,持久化、bash 和文件系统提供方则使用 `DSH_SESSION_ROOT` 和 `DSH_CWD`,并为手动运行提供回退值。调用方未使用任何显式配置通道时,`deepseek_harness` 客户端把该文件路径注入 `DSH_CORDIS_CONFIG`(注入条件见 [sdk README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk/README.md))。因此,零配置是包装层中一次显式、可见的参数传递,而不是运行时中的隐藏回退。 +外部 profile 管理使用 `dsh plugin --profile ...`。该命令要求 `PATH` 中存在 `pnpm`;普通 SDK/profile 运行不需要它。 + +## 构建与分发 + +在仓库根目录运行 `pnpm exec tsx scripts/build-exe-for-python-sdk.ts`,会校验闭包、构建包、部署无符号链接的文件树、打包所选目标,并把可执行程序及伴随文件同步到本模块。`scripts/build-python-release.py` 按仓库根版本暂存发布形态的 wheel,并将 `deepseek-harness-sdk` 固定到完全相同的运行时版本。 + +Installed-wheel smoke 会在 checkout 外创建干净虚拟环境,证明 distribution 与可执行程序来源,然后覆盖默认及自定义 SDK profile、外部插件、MCP、原生工具、直接 JSON-RPC、检入快照,以及可信运行中的真实提供方。另见 [Python 贡献者工作流](../development.zh.md)与 [installed-wheel 测试决策](../../.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)。 diff --git a/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index 4b93a8d04e..7a8ef7c0fd 100644 --- a/python/sdk/README.i18n.yaml +++ b/python/sdk/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 python/sdk/README.md -README.md: 99515c52e6314dc29788a324338699d75c0a4451 -README.zh.md: 6ec268545107478ce9f347cdfb7f17d4a8afd151 +README.md: edf5ced63f181bb17895d81cc060577916554305 +README.zh.md: e03a19e5b8f0cef2e2f9d86f8d91b8808e268ff4 diff --git a/python/sdk/README.md b/python/sdk/README.md index 99515c52e6..edf5ced63f 100644 --- a/python/sdk/README.md +++ b/python/sdk/README.md @@ -1,51 +1,64 @@ # DeepSeek Harness Python SDK -English | [中文](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk/README.zh.md) +English | [中文](README.zh.md) -Python subprocess SDK for driving DeepSeek Harness over JSON-RPC stdio. The -runtime inherits normal DeepSeek Harness environment variables such as -`DEEPSEEK_BASE_URL` and `DEEPSEEK_API_KEY`, so callers can use real model -endpoints directly or point those variables at a local proxy. - -Install the `deepseek-harness-sdk` distribution from PyPI; the import module remains `deepseek_harness`: +Python subprocess SDK for driving DeepSeek Harness over newline-delimited JSON-RPC on stdio. Install `deepseek-harness-sdk`; it installs the exact same-version `deepseek-harness-runtime-bin` wheel for the current platform. ```sh python -m pip install deepseek-harness-sdk ``` -Installing `deepseek-harness-sdk` installs the exact same-version `deepseek-harness-runtime-bin` platform wheel. The normal entry point therefore needs no executable argument: +## Start a runtime -```py -from deepseek_harness import DeepSeekHarness +The Python SDK has no separate application entrypoint. It launches the bundled `dsh` CLI with `--profile sdk`; the selected profile owns the JSON-RPC server, agent composition, credentials, persistence, tools, and shutdown behavior. -with DeepSeekHarness() as harness: - result = harness.run("Say hi.") -``` - -`DeepSeekHarness` keeps its lazily started runtime subprocess for reuse across calls. Use it as a context manager, as above, or call `close()` explicitly when finished. - -By default, the SDK launches the bundled single-file `dsh-jsonrpc-agent` executable from the `deepseek-harness-runtime-bin` package and injects that package's default configuration (the stdio JSON-RPC server, agent core, preloaded DeepSeek adapter, JSONL session persistence with an explicitly composed semantic checkpoint policy, local bash) via `DSH_CORDIS_CONFIG`. To run a plugin composition of your own, keep the `@deepseek-ai/dsh-sdk-jsonrpc-server` entry in the config and pass the Cordis config path. +Every launch requires an explicit Harness home. Pass `dsh_home` or provide a non-empty `DSH_HOME` in the child environment. The SDK deliberately never discovers `~/.dsh`. ```py from deepseek_harness import DeepSeekHarness with DeepSeekHarness( - provider="deepseek-official", - model="deepseek-v4-flash", - max_tokens=49_152, - cordis="examples/python-sdk-agent/cordis.yml", + dsh_home="/absolute/path/to/isolated-dsh-home", + cwd="/absolute/path/to/workspace", +) as harness: + result = harness.run("Say hi.", session_id="example-001") + +print(result.final_response) +``` + +`DeepSeekHarness` starts lazily and reuses its runtime until `close()` or context-manager exit. `cwd` is the agent workspace; `runtime_cwd` independently selects the subprocess working directory. Both become absolute before launch. `provider`, `model`, and optional positive `max_tokens` are sent during JSON-RPC initialization. `base_url` and `api_key` explicitly override `DEEPSEEK_BASE_URL` and `DEEPSEEK_API_KEY` in the child environment. + +## Customize plugins + +Persistent customization belongs to a `dsh` profile. Initialize the shipped SDK profile and install an external bundle with the runtime wheel's `dsh` command: + +```sh +export DSH_HOME=/absolute/path/to/isolated-dsh-home +dsh --profile sdk --dump-default-config >/dev/null +dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle +``` + +The `file:` form installs the local bundle into the profile package tree, where its peer imports reach the bundled installation fallback. The profile manifest records installed dependencies and ordered bundle layers; its `$DSH_HOME/profiles/sdk/cordis.patch.yml` is the persistent user patch. `dsh plugin` needs `pnpm` only when managing external packages. Running the SDK does not require system Node.js. + +For an invocation-specific change, pass one or more patch files. They become absolute and are forwarded in order after the profile and home patch layers: + +```py +with DeepSeekHarness( + dsh_home="/absolute/path/to/isolated-dsh-home", + profile="sdk", + patches=("/absolute/path/to/first.patch.yml", "/absolute/path/to/last.patch.yml"), ) as harness: result = harness.run("Make the requested code change.") ``` -`provider` selects a provider route registered by the chosen Cordis composition; `model` is the model id resolved by that adapter. `max_tokens` is an optional positive per-request output-token cap for the root agent and its in-process descendants; omission leaves the provider default in control. Compaction summaries keep the separate limit configured by their compaction plugin. The bundled default composition registers `deepseek-official`. A custom composition can mount `llm-pi-ai`, configure provider-specific credentials/endpoints there, and select any provider/model present in pi-ai's installed catalog. +`profile` may select another existing profile, but that composition must retain `@deepseek-ai/dsh-sdk-app` or another `@deepseek-ai/dsh-sdk-jsonrpc-server` row. Misconfiguration fails during CLI boot or SDK initialization; there is no complete-config fallback. `dsh_bin` may select another `dsh` executable while preserving the same profile grammar. Arbitrary argv replacement remains an internal fake-runtime test adapter, not public API. -The [Python SDK tutorial](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/python-sdk.md) provides an ordered installation and first-run path without the Web UI. The [`python-sdk-agent` example](https://github.com/deepseek-ai/deepseek-harness/blob/master/examples/python-sdk-agent/README.md) owns the complete standalone Cordis file used there. +## Results and notifications -`Session.run()` owns an activity interval from its prompt's durable inbox receipt through the next whole-agent idle and returns `RunResult(session_id, final_response, finish_reason, events, notifications, session_root)`. `final_response` is the last committed root-session assistant text in the interval. `finish_reason` is the `kind` of the last root-session `turn/end` in the interval, such as `completed`, `max-tokens`, or `error`, and is `None` when no turn ended. A `turn/end` without a string `data.reason.kind` violates the runtime protocol and raises `SdkProtocolError`. Both result fields describe the owned interval rather than an output or ending causally assigned to the prompt. Steering, injected context, and other queued work may contribute before idle. +`Session.run()` owns an activity interval from its prompt's durable inbox receipt through the next whole-agent idle and returns `RunResult(session_id, final_response, finish_reason, events, notifications)`. `final_response` is the last committed root-session assistant text in the interval. `finish_reason` is the `kind` of the last root-session `turn/end`, such as `completed`, `max-tokens`, or `error`, and is `None` when no turn ended. A `turn/end` without a string `data.reason.kind` violates the protocol and raises `SdkProtocolError`. -`HarnessClient` retains discovered subagent ancestry for the lifetime of the runtime process. During each `Session.run()`, `RunResult.notifications` and `on_notification` receive the root session and all known descendant notifications in wire order, including nested subagent lifecycle and session events. `RunResult.events` contains root-session events only, so descendant messages cannot replace the root response. The low-level `session_prompt()` returns the queued `MessageId` immediately; callers that bypass `Session.run()` own any later activity boundary themselves. +`HarnessClient` retains discovered subagent ancestry for the runtime process lifetime. During `Session.run()`, `RunResult.notifications` and `on_notification` receive the root session and known descendants in wire order. `RunResult.events` contains root-session events only, so descendant output cannot replace the root response. The low-level `session_prompt()` returns the queued message id immediately; callers that bypass `Session.run()` own the later activity boundary. -The same behavior can be selected for the runtime subprocess with `DSH_CORDIS_CONFIG`. The injection lives in `HarnessClient.start()`, so the low-level client's default launch gets it too: when the launch resolves to the bundled runtime and neither `cordis` nor a non-empty `DSH_CORDIS_CONFIG` is set (the runtime treats an empty value as absent, and so does the injection check), the bundled default configuration is used; an explicit `runtime_bin`, `bridge_bin`, or `launch_args_override` disables the injection entirely. See the [sdk-runtime README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/README.md) for the runtime carriers (production exe vs dev-only node closure) and how to obtain them. +The selected home stores profiles, plugins, credentials, settings, and sessions. Use a fresh home when those resources must be isolated, and a fresh session id for independent work. Reusing both a harness and session id continues the durable conversation and session-owned resources. -`cwd` and `runtime_cwd` are resolved to absolute paths before subprocess launch, environment injection, and the wire handshake. The public API exposes only applied options: deployment persona and persistence belong in `cordis.yml`, while `session_root` remains the high-level convenience that sets `DSH_SESSION_ROOT`. +See the [Python tutorial](../../docs/user/guide/python-sdk.md), [`python-sdk-agent` example](../../examples/python-sdk-agent/README.md), and [runtime wheel reference](../sdk-runtime/README.md). diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index 6ec2685451..e03a19e5b8 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -1,48 +1,64 @@ # DeepSeek Harness Python SDK -[English](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk/README.md) | 中文 +[English](README.md) | 中文 -通过 JSON-RPC stdio 驱动 DeepSeek Harness 的 Python 子进程 SDK。运行时继承常规的 DeepSeek Harness 环境变量(如 `DEEPSEEK_BASE_URL` 与 `DEEPSEEK_API_KEY`),调用方可以直接使用真实模型端点,也可以把这些变量指向本地代理。 - -请从 PyPI 安装 `deepseek-harness-sdk` 分发包;导入模块仍为 `deepseek_harness`: +用于通过 stdio 上按行分隔的 JSON-RPC 驱动 DeepSeek Harness 的 Python 子进程 SDK。安装 `deepseek-harness-sdk` 时,会同时安装当前平台上版本完全相同的 `deepseek-harness-runtime-bin` wheel。 ```sh python -m pip install deepseek-harness-sdk ``` -安装 `deepseek-harness-sdk` 会同时安装版本完全相同的 `deepseek-harness-runtime-bin` 平台 wheel 包。因此常规入口不需要传可执行文件参数: +## 启动运行时 -```py -from deepseek_harness import DeepSeekHarness +Python SDK 没有独立的应用入口。它以 `--profile sdk` 启动内置的 `dsh` CLI;所选 profile 负责 JSON-RPC 服务器、agent 组合、凭据、持久化、工具和关闭流程。 -with DeepSeekHarness() as harness: - result = harness.run("Say hi.") -``` - -`DeepSeekHarness` 会保留其按需启动的运行时子进程,以便在多次调用之间复用。请像上例一样将其用作上下文管理器,或在使用完毕后显式调用 `close()`。 - -默认情况下,SDK 会启动 `deepseek-harness-runtime-bin` 包内置的单文件可执行程序 `dsh-jsonrpc-agent`,并通过 `DSH_CORDIS_CONFIG` 注入该包的默认配置,其中包括 stdio JSON-RPC 服务器、agent core(智能体核心)、预载的 DeepSeek 适配器、采用显式组合语义检查点策略的 JSONL 会话持久化,以及本地 bash。要运行自己的插件组合,请在配置中保留 `@deepseek-ai/dsh-sdk-jsonrpc-server` 配置项,并传入 Cordis 配置文件路径。 +每次启动都必须显式指定 Harness home。请传入 `dsh_home`,或在子进程环境中提供非空的 `DSH_HOME`。SDK 刻意不会发现 `~/.dsh`。 ```py from deepseek_harness import DeepSeekHarness with DeepSeekHarness( - provider="deepseek-official", - model="deepseek-v4-flash", - max_tokens=49_152, - cordis="examples/python-sdk-agent/cordis.yml", + dsh_home="/absolute/path/to/isolated-dsh-home", + cwd="/absolute/path/to/workspace", +) as harness: + result = harness.run("Say hi.", session_id="example-001") + +print(result.final_response) +``` + +`DeepSeekHarness` 延迟启动运行时,并在调用 `close()` 或退出上下文管理器前复用该进程。`cwd` 是 agent workspace;`runtime_cwd` 独立选择子进程工作目录。两者都会在启动前转成绝对路径。`provider`、`model` 和可选的正整数 `max_tokens` 通过 JSON-RPC 初始化发送。`base_url` 与 `api_key` 会显式覆盖子进程环境中的 `DEEPSEEK_BASE_URL` 与 `DEEPSEEK_API_KEY`。 + +## 自定义插件 + +持久自定义属于 `dsh` profile。使用运行时 wheel 提供的 `dsh` 命令初始化随附的 SDK profile,并安装外部 bundle: + +```sh +export DSH_HOME=/absolute/path/to/isolated-dsh-home +dsh --profile sdk --dump-default-config >/dev/null +dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle +``` + +`file:` 形式会把本地 bundle 安装到 profile 包树中,使其 peer import 可以到达内置安装后备。Profile manifest 会记录已安装依赖与有序 bundle 层;`$DSH_HOME/profiles/sdk/cordis.patch.yml` 是持久用户 patch。只有管理外部包时,`dsh plugin` 才需要 `pnpm`。运行 SDK 不需要系统 Node.js。 + +对于单次调用的变更,可传入一个或多个 patch 文件。它们会转成绝对路径,并在 profile 层与 home patch 层之后按顺序传给 CLI: + +```py +with DeepSeekHarness( + dsh_home="/absolute/path/to/isolated-dsh-home", + profile="sdk", + patches=("/absolute/path/to/first.patch.yml", "/absolute/path/to/last.patch.yml"), ) as harness: result = harness.run("Make the requested code change.") ``` -`provider` 选择指定 Cordis 组合所注册的提供方路由;`model` 是该适配器解析出的模型 ID。`max_tokens` 是一个可选的正整数,用于限制根 agent 及其进程内后代在每次请求中输出的 token 数量;省略该参数时,由提供方的默认行为决定输出上限。压缩摘要继续使用压缩插件单独配置的上限。内置默认组合注册 `deepseek-official`。自定义组合可以挂载 `llm-pi-ai`,在其中配置各提供方专属的凭据和端点,并选择 pi-ai 已安装 catalog 中存在的任意提供方/模型组合。 +`profile` 可以选择另一个已存在的 profile,但该组合必须保留 `@deepseek-ai/dsh-sdk-app` 或另一个 `@deepseek-ai/dsh-sdk-jsonrpc-server` 配置项。配置错误会在 CLI 启动或 SDK 初始化时失败;不存在完整配置回退。`dsh_bin` 可以选择另一个 `dsh` 可执行程序,同时保持相同的 profile 语法。任意 argv 替换仅是内部 fake-runtime 测试适配器,不属于公开 API。 -[Python SDK 教程](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/python-sdk.md)提供一套无需使用 Web UI、按步骤完成安装和首次运行的流程。该教程所用的完整独立 Cordis 配置文件位于 [`python-sdk-agent` 示例](https://github.com/deepseek-ai/deepseek-harness/blob/master/examples/python-sdk-agent/README.md)中。 +## 结果与通知 -`Session.run()` 的活动区间从其提示词被持久 inbox 接收时开始,到整个 agent 下一次进入空闲状态时结束,并返回 `RunResult(session_id, final_response, finish_reason, events, notifications, session_root)`。`final_response` 是该区间内根会话最后提交的助手文本。`finish_reason` 是该区间内根会话最后一个 `turn/end` 的 `kind`,例如 `completed`、`max-tokens` 或 `error`;没有轮次结束时为 `None`。缺少字符串 `data.reason.kind` 的 `turn/end` 违反运行时协议,并会抛出 `SdkProtocolError`。这两个结果字段描述的是 `Session.run()` 所界定的活动区间,并不表示某项输出或结束原因在因果上归属于该提示词。steering(中途引导)、注入的上下文和其他排队工作,也可能在 agent 进入空闲状态前参与这段活动。 +`Session.run()` 的活动区间从提示词被持久 inbox 接收时开始,到整个 agent 下一次进入 idle 时结束,并返回 `RunResult(session_id, final_response, finish_reason, events, notifications)`。`final_response` 是该区间内根会话最后提交的 assistant 文本。`finish_reason` 是最后一个根会话 `turn/end` 的 `kind`,例如 `completed`、`max-tokens` 或 `error`;没有轮次结束时为 `None`。缺少字符串 `data.reason.kind` 的 `turn/end` 违反协议,并会抛出 `SdkProtocolError`。 -`HarnessClient` 会在运行时进程的整个生命周期内保留已发现的 subagent 谱系。每次执行 `Session.run()` 时,`RunResult.notifications` 与 `on_notification` 会按协议传输顺序收到根会话及所有已知后代的通知,其中包括嵌套 subagent 的生命周期事件与会话事件。`RunResult.events` 只包含根会话事件,因此后代消息不会覆盖根会话回复。底层 `session_prompt()` 会立即返回已排队消息的 `MessageId`;绕过 `Session.run()` 的调用方必须自行负责后续的活动边界。 +`HarnessClient` 会在运行时进程的整个生命周期内保留已发现的子 agent 祖先关系。在 `Session.run()` 期间,`RunResult.notifications` 与 `on_notification` 按协议顺序接收根会话和已知后代的通知。`RunResult.events` 只包含根会话事件,因此后代输出不会替换根响应。底层 `session_prompt()` 会立即返回已排队消息的 id;绕过 `Session.run()` 的调用方自行负责后续活动边界。 -也可以通过 `DSH_CORDIS_CONFIG` 为运行时子进程指定配置。注入逻辑位于 `HarnessClient.start()`,因此底层客户端按默认方式启动时也具有该行为:如果启动方式最终解析为内置运行时,且既没有设置 `cordis`,也没有设置非空的 `DSH_CORDIS_CONFIG`(运行时将空值视为未设置,注入检查也是如此),系统就会使用内置默认配置;显式指定 `runtime_bin`、`bridge_bin` 或 `launch_args_override` 时,则会完全禁用该注入。运行时载体(生产用 exe 与仅限开发的 `node` 闭包)及其获取方式见 [sdk-runtime README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/README.md)。 +所选 home 保存 profile、插件、凭据、设置和会话。需要隔离这些资源时应使用新的 home;独立工作应使用新的 session id。同时复用 harness 与 session id 会延续持久对话和会话资源。 -`cwd` 与 `runtime_cwd` 会在启动子进程、注入环境变量和协议握手前解析为绝对路径。公开 API 只暴露由 SDK 直接应用的选项:部署 persona 和持久化配置应在 `cordis.yml` 中定义;`session_root` 则保留为设置 `DSH_SESSION_ROOT` 的高层便捷参数。 +另见 [Python 教程](../../docs/user/guide/python-sdk.zh.md)、[`python-sdk-agent` 示例](../../examples/python-sdk-agent/README.zh.md)和[运行时 wheel 参考](../sdk-runtime/README.zh.md)。 From 9edf1b9f108e4e78f6bf2db7356e4318308afe37 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:04:19 +0800 Subject: [PATCH 07/16] fix(python-sdk): harden profile runtime startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve packaged profile proxies with Node ESM import conditions from each package installation, and fail loud when an explicit runtime export or legacy main entry is missing. Serialize the shared profile fallback under the existing cross-process writer lock so concurrent dsh processes cannot observe partial proxies; either carrier now replaces the other carrier’s managed entry without manual cleanup. Give Python initialize its own 10-second default bound and name the selected profile in timeout diagnostics, while leaving ordinary agent turns unbounded by default. Package the dynamically resolved web frontend and skill-badge assets so the runtime wheel’s normal dsh profiles do not depend on pkg static-discovery accidents. Rewrite the root launch rule and every active stale SDK-runtime note to the shipped dsh profile architecture in both languages. Focused tests prove import-only and transitive package exports, lock contention, cross-carrier transitions, missing-entry failures, asset inventory, and bounded initialization. --- ...cutable-sdk-runtime-distribution.i18n.yaml | 4 +- ...ile-executable-sdk-runtime-distribution.md | 10 +- ...-executable-sdk-runtime-distribution.zh.md | 10 +- ...-single-dsh-application-launcher.i18n.yaml | 4 +- ...6-08-22-single-dsh-application-launcher.md | 18 +- ...8-22-single-dsh-application-launcher.zh.md | 18 +- ...3-python-sdk-dsh-profile-runtime.i18n.yaml | 4 +- ...26-08-23-python-sdk-dsh-profile-runtime.md | 6 +- ...08-23-python-sdk-dsh-profile-runtime.zh.md | 6 +- ...ipt-sdk-and-sdk-subagent-backend.i18n.yaml | 4 +- ...typescript-sdk-and-sdk-subagent-backend.md | 2 +- ...escript-sdk-and-sdk-subagent-backend.zh.md | 2 +- AGENTS.md | 2 +- THIRD_PARTY_NOTICES.md | 1 + apps/cli/src/bin.ts | 2 +- apps/cli/src/dump-config.ts | 5 +- apps/cli/src/profile-boot.ts | 12 +- apps/cli/tests/web-agent-presets.e2e.ts | 2 +- apps/web/tests/scaffold.ts | 2 +- packages/boot/app-boot/README.i18n.yaml | 4 +- packages/boot/app-boot/README.md | 2 +- packages/boot/app-boot/README.zh.md | 2 +- packages/boot/app-boot/package.json | 2 + packages/boot/app-boot/src/profile.ts | 92 ++++++---- packages/boot/app-boot/tests/profile.spec.ts | 164 +++++++++++++++--- packages/boot/app-boot/tsconfig.json | 3 + pnpm-lock.yaml | 6 + python/sdk/README.i18n.yaml | 4 +- python/sdk/README.md | 2 +- python/sdk/README.zh.md | 2 +- python/sdk/src/deepseek_harness/api.py | 2 + python/sdk/src/deepseek_harness/client.py | 11 +- python/sdk/tests/test_client.py | 6 +- .../build-exe-for-python-sdk-assets.spec.ts | 27 +++ scripts/build-exe-for-python-sdk.ts | 5 + 35 files changed, 321 insertions(+), 127 deletions(-) create mode 100644 scripts/build-exe-for-python-sdk-assets.spec.ts diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index 29b7caeb50..64c9153ca6 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml @@ -2,5 +2,5 @@ # 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/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md -2026-07-10-single-file-executable-sdk-runtime-distribution.md: 50e63bbc4f664f9450f01acf45888991a27517d2 -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: cf92f42f508f005c64f09aa0127b28ed162cede5 +2026-07-10-single-file-executable-sdk-runtime-distribution.md: e46dbbc119e2078e44632d81b333c8be5ab9d6d7 +2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: d7a1e3f445ea1c1f03df2b349a4391534a5502c3 diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md index 50e63bbc4f..e46dbbc119 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md @@ -36,27 +36,27 @@ The Python client supplies an explicit Harness home and selects the `sdk` profil Inside the exe's VFS sits a **real package tree in build-artifact form** (each package's `lib/` plus a real `node_modules`). The packaged JSON-RPC entry supplies its installed harness base to app-boot's root Include: relative plugin specifiers resolve from the external configuration directory, while bare package names resolve from the VFS, so a configuration inside another Node project cannot shadow the packaged plugin set. The ordinary development bin leaves bare packages configuration-owned. Bare specifiers in the packaged entry resolve upward along `node_modules` from the entry's position inside the VFS and land inside the VFS naturally. The closed set needs no allowlist code — the set is whatever the VFS has installed, and importing a name outside the set fails. -The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json) (`dsh-sdk-python-runtime-closure`, a pnpm workspace member and a zero-code pure dependency manifest) — the unified source of truth for "which plugins the exe ships" and "what the Python runtime distributes". Adding a plugin to the exe = adding one dependency line to the manifest and repackaging. [`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) reads every shipped `packages/preset/agent-presets/presets/*/agent.cordis.yml`, evaluates `disabled` conditions that compare `process.platform` for every target in `python/sdk-runtime/platforms.json`, and requires each active workspace plugin at the runtime root through an explicit `workspace:` dependency. It also traverses every workspace package covered by that manifest and requires every non-optional workspace peer, reporting the complete preset or referencing-package → missing-dependency chain; unknown platform conditions remain active so a plugin cannot be omitted by an unsupported expression. `pnpm run hygiene`, CI static, and the single-exe build run it before packaging. Deploy also packs by each package's `files`, so the shared chunks tsdown splits out must be covered by `files`. +The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json) (`dsh-python-runtime-closure`, a pnpm workspace member and a zero-code pure dependency manifest) — the unified source of truth for "which plugins the exe ships" and "what the Python runtime distributes". Adding a plugin to the exe = adding one dependency line to the manifest and repackaging. [`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) reads every shipped `packages/preset/agent-presets/presets/*/agent.cordis.yml`, evaluates `disabled` conditions that compare `process.platform` for every target in `python/sdk-runtime/platforms.json`, and requires each active workspace plugin at the runtime root through an explicit `workspace:` dependency. It also traverses every workspace package covered by that manifest and requires every non-optional workspace peer, reporting the complete preset or referencing-package → missing-dependency chain; unknown platform conditions remain active so a plugin cannot be omitted by an unsupported expression. `pnpm run hygiene`, CI static, and the single-exe build run it before packaging. Deploy also packs by each package's `files`, so the shared chunks tsdown splits out must be covered by `files`. The deploy root includes `@deepseek-ai/dsh-mcp-client` as an explicitly supported custom-configuration plugin even though no shipped preset mounts it. An external config can therefore connect to user-supplied stdio and Streamable HTTP MCP servers and register their tools; the distribution does not carry those servers or extend the bridge to MCP Resources and Prompts. The executable and installed-wheel smokes start a temporary stdio server, discover its tool, and complete one model-requested call. ### Build pipeline and artifacts -[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-sdk-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → restore any direct workspace package that legacy deploy hoisted back under the source manifest's `node_modules`, omitting its package-local dependency tree and rejecting any remaining manifest gap → replace every staged dependency symlink with its target bytes, remove package-manager `.bin` links, and fail if any symlink remains → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → stage the target `node-pty` addon → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg--` land in `dist-exe/` and are copied back into the runtime directory. Linux installs build `pty.node` from source; CI rebuilds that addon inside the matching manylinux 2.28 container before packaging, and the builder copies it from the root install into the staged closure because legacy deploy omits that side-effect directory. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. CI treats these products as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. +[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → inject pkg configuration whose bin is `node_modules/@deepseek-ai/dsh/lib/bin.js` and whose assets cover dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime--` under `dist-exe/` and copy it into the runtime directory. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) is called for all three targets by the [installed-wheel Python runtime pull-request validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) and the [public publication workflow](../process/2026-08-11-python-publication-workflow.md); `workflow_dispatch` and the `build-exe` label can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, proves their package and executable provenance, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests additionally run a real DeepSeek two-turn tool smoke on every target; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS verifies that the executable's deployment target fits the wheel tag. A full three-target run retains four artifacts, each containing one release file: the platform-independent SDK wheel and three native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts `python-v` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and three native runtime wheels, then a single serialized job checks and publishes all four to the project PyPI registry. Windows is a non-goal. ### Python SDK distribution: two carriers, exe for production, node for development -The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds the checked-in default `runtime/cordis.yml`, the build-injected platform exe with its required `-rg` sidecar and optional macOS helper, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions. +The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` is the client and `python/sdk-runtime` is the runtime carrier package. The runtime package's data directory holds the build-injected platform executable with its required `-rg` sidecar and optional macOS helper, plus the build-injected `runtime/node/` closure tree for repository development. `resolve_bundled_launch_args()` selects the executable by default; explicit `DSH_RUNTIME_MODE=node` runs `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. The node carrier never enters wheel distributions, and neither carrier uses a checked-in complete `cordis.yml`. [`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative `X.Y.Z` or prerelease version from the repository root `package.json`, converts prereleases to their PEP 440 spelling, and stages both packages at that wheel version, with `deepseek-harness-sdk` depending exactly on the matching `deepseek-harness-runtime-bin`. An optional `python-v` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. Staging also carries the repository license into both wheels and the third-party notices into the bundled runtime wheel. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe and its architecture-matched `-rg` sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or the conservative `py3-none-macosx_14_0_arm64` tag for the Node 24 executable's macOS 13.5 deployment target; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms. -The exe's "must be explicitly configured" hard semantic is unchanged; the zero-config experience is restored by the wrapper: when the caller gave no `cordis`, named no explicit runtime, and the environment has no `DSH_CORDIS_CONFIG`, the client explicitly injects the checked-in default `cordis.yml` (agent-core + preloaded llm-deepseek + JSONL persistence + bash-local + the `dsh-sdk-jsonrpc-server` serving entry, with `!!js` environment-variable fallbacks) via `DSH_CORDIS_CONFIG`. +The Python client launches the packaged `dsh` command with the selected profile (`sdk` by default), ordered patch files, and an explicit Harness home. The profile owns JSON-RPC serving and application composition; missing homes, profiles, bundles, patches, and server rows fail without an external complete-config fallback. ### Naming lineage -`@deepseek-ai/dsh-sdk-python-runtime` (the private carrier) → `dsh-sdk-python-runtime-closure` (the deploy manifest; no scope prefix, so it is not a dsh release package) → `dsh-jsonrpc-agent-pkg--` (the exe artifacts). The wire `serverInfo.name` stays `deepseek-harness-sdk-runtime` (a protocol-stable value); the Python distribution names are `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`, while the import modules remain `deepseek_harness` / `deepseek_harness_runtime`. +`dsh-python-runtime-closure` is the private deploy manifest and `deepseek-harness-sdk-runtime--` is the executable family. The wire `serverInfo.name` is `deepseek-harness-sdk-runtime`; the Python distribution names are `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`, while the import modules are `deepseek_harness` / `deepseek_harness_runtime`. ## Disposition of worker-style plugins diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md index cf92f42f50..d7a1e3f445 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md @@ -36,27 +36,27 @@ Python 客户端提供显式 Harness home,并选择 `sdk` profile 与有序 pa exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真实 `node_modules`)。打包专用 JSON-RPC 入口会向 app-boot 的根 Include 提供自身已安装 harness 的基准位置:相对插件说明符从外部配置目录解析,裸包名则从 VFS 解析,因此位于另一个 Node 项目内的配置无法遮蔽已打包的插件集合。普通开发 bin 仍由配置项目提供裸包。打包入口中的裸包名从该入口在 VFS 内的位置沿 `node_modules` 向上解析,自然落在 VFS 内。封闭集不需要白名单代码——VFS 中安装了什么,集合中就有什么;`import()` 集合外的名称会失败。 -部署根目录是 [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json)(`dsh-sdk-python-runtime-closure`,pnpm 工作区成员、零代码纯依赖 manifest),也是「exe 安装哪些插件」与「Python 运行时分发什么」的统一真源。向 exe 添加插件,就是在 manifest 中增加一行依赖后重新打包。[`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) 读取每个已发布的 `packages/preset/agent-presets/presets/*/agent.cordis.yml`,针对 `python/sdk-runtime/platforms.json` 中的每个目标解析比较 `process.platform` 的 `disabled` 条件,并要求该目标启用的每个工作区插件都通过显式的 `workspace:` 依赖列在运行时根目录。它还遍历该 manifest 覆盖的全部工作区包,要求每个非可选的工作区对等依赖(peer dependency)都显式列出,并报告“preset 或引用包 → 缺失依赖”的完整链路;无法识别的平台条件会保持启用,避免因不支持的表达式遗漏插件。`pnpm run hygiene`、CI 静态检查与 single-exe 构建都会在打包前运行该门禁。部署还会依据各包的 `files` 字段打包,因此 tsdown 拆出的共享分片必须被 `files` 覆盖。 +部署根目录是 [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json)(`dsh-python-runtime-closure`,pnpm 工作区成员、零代码纯依赖 manifest),也是「exe 安装哪些插件」与「Python 运行时分发什么」的统一真源。向 exe 添加插件,就是在 manifest 中增加一行依赖后重新打包。[`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) 读取每个已发布的 `packages/preset/agent-presets/presets/*/agent.cordis.yml`,针对 `python/sdk-runtime/platforms.json` 中的每个目标解析比较 `process.platform` 的 `disabled` 条件,并要求该目标启用的每个工作区插件都通过显式的 `workspace:` 依赖列在运行时根目录。它还遍历该 manifest 覆盖的全部工作区包,要求每个非可选的工作区对等依赖(peer dependency)都显式列出,并报告“preset 或引用包 → 缺失依赖”的完整链路;无法识别的平台条件会保持启用,避免因不支持的表达式遗漏插件。`pnpm run hygiene`、CI 静态检查与 single-exe 构建都会在打包前运行该门禁。部署还会依据各包的 `files` 字段打包,因此 tsdown 拆出的共享分片必须被 `files` 覆盖。 部署根目录显式包含 `@deepseek-ai/dsh-mcp-client`,将其作为自定义配置可用的插件,即使随附 preset 均未挂载该插件。外部配置因此可以连接由用户提供的 stdio 与 Streamable HTTP MCP server 并注册其工具;分发物不包含这些 server,也不将桥接范围扩展到 MCP Resources 和 Prompts。可执行程序与已安装 wheel 包的冒烟测试会启动临时 stdio server,发现其工具,并完成一次由模型请求的调用。 ### 构建流水线与产物 -[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-sdk-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 恢复被 legacy deploy 提升回源 manifest 的 `node_modules` 下的任何直接工作区包,同时省略其包内依赖树,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的每个符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js`;`assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg--` 写入 `dist-exe/`,并拷回运行时目录。Linux 安装会从源码构建 `pty.node`;CI 会在打包前进入匹配架构的 manylinux 2.28 容器重新构建该 addon,而 `--legacy` 部署会省略这一副作用目录,因此构建器会把它从根安装目录复制到暂存闭包。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 +[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置,其中 bin 为 `node_modules/@deepseek-ai/dsh/lib/bin.js`,assets 覆盖动态读取的 profile、bundle、前端、preset、原生库与配置文件 → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime--` 写入 `dist-exe/` 并拷回运行时目录。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel Python 运行时拉取请求验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)与[公开发布工作流](../process/2026-08-11-python-publication-workflow.zh.md)都会调用它构建全部三个目标;`workflow_dispatch` 与 `build-exe` 标签仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)和 macos-arm64 三个平台分别进行原生构建,并缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求还会在每个目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则验证可执行文件的部署目标符合 wheel 包标签。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。 ### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发 -Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 及其必需的 `-rg` 伴随文件和可选的 macOS helper,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**;`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。 +Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件和可选的 macOS helper,以及供仓库开发使用的构建注入 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 默认选择可执行文件;显式设置 `DSH_RUNTIME_MODE=node` 会在系统 Node 22.19 或更高版本上运行 `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。node 载体从不进入 wheel 分发,两种载体都不使用检入的完整 `cordis.yml`。 [`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的 `X.Y.Z` 或预发布版本,把预发布版本转换为 PEP 440 写法,并以该 wheel 包版本暂存两个包,让 `deepseek-harness-sdk` 精确依赖匹配版本的 `deepseek-harness-runtime-bin`。可选的 `python-v` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。暂存过程还会把仓库许可证放入两个 wheel 包,并把第三方声明放入内置运行时 wheel 包。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe 及其架构匹配的 `-rg` 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`,或针对 Node 24 可执行文件 macOS 13.5 部署目标而保守选择的 `py3-none-macosx_14_0_arm64` 标签;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。 -exe「必须显式配置」的硬语义不变;零配置体验由包装层恢复:调用方没有提供 `cordis`、没有显式指定运行时,且环境中没有 `DSH_CORDIS_CONFIG` 时,客户端将检入的默认 `cordis.yml`(`agent-core` + 预载的 `llm-deepseek` + JSONL 持久化 + `bash-local` + `dsh-sdk-jsonrpc-server` 对外服务条目,并通过 `!!js` 使用环境变量兜底)显式注入 `DSH_CORDIS_CONFIG`。 +Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和显式 Harness home 启动打包后的 `dsh` 命令。Profile 负责 JSON-RPC 服务和应用组合;缺失 home、profile、bundle、patch 或 server 配置项都会失败,不存在外部完整配置回退。 ### 命名血统 -`@deepseek-ai/dsh-sdk-python-runtime`(私有载体)→ `dsh-sdk-python-runtime-closure`(部署 manifest;没有作用域前缀,因此不属于 dsh 发布包)→ `dsh-jsonrpc-agent-pkg--`(exe 产物)。协议字段 `serverInfo.name` 保持为 `deepseek-harness-sdk-runtime`(协议稳定值);Python 分发包名为 `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`,导入模块名仍为 `deepseek_harness` / `deepseek_harness_runtime`。 +`dsh-python-runtime-closure` 是私有部署 manifest,`deepseek-harness-sdk-runtime--` 是可执行文件族。协议字段 `serverInfo.name` 是 `deepseek-harness-sdk-runtime`;Python 分发包名是 `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`,导入模块名是 `deepseek_harness` / `deepseek_harness_runtime`。 ## 工作线程插件 diff --git a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml index 1e9ba16226..de2009b1ce 100644 --- a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml @@ -2,5 +2,5 @@ # 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/architecture/2026-08-22-single-dsh-application-launcher.md -2026-08-22-single-dsh-application-launcher.md: 69188e806d9192d230d1f1b52daf27a3b30481be -2026-08-22-single-dsh-application-launcher.zh.md: a6bb1909b019ea0d386bd2f4a1bb8f5199ef1974 +2026-08-22-single-dsh-application-launcher.md: 27ec766dc62a203a3054ec67828b8897ab5ad245 +2026-08-22-single-dsh-application-launcher.zh.md: 5fb7fb4234b093a6f69446e0d9b1d7fbb7f46cf3 diff --git a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md index 69188e806d..27ec766dc6 100644 --- a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md +++ b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md @@ -8,7 +8,7 @@ English | [中文](2026-08-22-single-dsh-application-launcher.zh.md) DeepSeek Harness application processes need one owner for composition, plugin resolution, environment discovery, shutdown, and user customization. A dedicated app bin with a complete `cordis.yml` creates a second lifecycle beside profile launch: plugins installed into a profile do not reach it, behavior drifts from `dsh-base`, and SDK callers learn arbitrary process argv instead of the product's composition model. -The Python SDK distributes a native executable and three platform wheels whose embedded direct-config runtime cannot change launch architecture without rebuilding and validating the complete VFS closure. That distribution needs an explicit temporary exception, not a second general Node application pattern. +The Python SDK distributes a native executable and three platform wheels. Its packaged process must use the same profile launcher while preserving the closed VFS dependency tree, native sidecars, and installed-wheel evidence. ## Decision @@ -43,19 +43,15 @@ SDK users customize plugins through profiles. `dsh plugin --profile ...` Direct SDK use follows normal Harness-home resolution: explicit `dshHome`, inherited `DSH_HOME`, then `~/.dsh`. `subagent-dsh-sdk` instead requires an explicit absolute home, so a nested runtime cannot discover a person's profiles, installed plugins, credentials, or sessions through the operating-system home. DSH-specific ACP child examples also pass an isolated home; the ACP backend itself remains generic for non-DSH agents. -### Python exception and names +### Python runtime -The Python SDK's direct-config application lives in the private `packages/sdk/python-runtime` package named `@deepseek-ai/dsh-sdk-python-runtime`. Its only packaged executable entry is `lib/packaged-bin.js`, consumed by the private `dsh-sdk-python-runtime-closure` deploy root. It has no public npm bin. The runnable direct Python example is `examples/python-sdk-agent`. +The Python runtime wheel packages the ordinary `@deepseek-ai/dsh` CLI from `node_modules/@deepseek-ai/dsh/lib/bin.js` through the private `dsh-python-runtime-closure` deploy manifest. The Python client selects `dsh --profile sdk`, ordered patch files, and an explicit Harness home; the installed `dsh` console command exposes the same profile grammar. The runnable Python example is `examples/python-sdk-agent`. -Python-observable behavior remains fixed: Python API, SDK wire, default `cordis.yml`, environment variables, wheel distribution names, packaged executable names, sidecar names, explicit runtime options, zero-config behavior, and supported platforms. The stable SDK family remains `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, `@deepseek-ai/dsh-sdk-jsonrpc-server`, and wire identity `deepseek-harness-sdk-runtime`; `@deepseek-ai/dsh-acp` remains the ACP protocol plugin. There is no compatibility package, forwarding executable, fallback parser, or SDK/ACP launcher alias. +The executable family is `deepseek-harness-sdk-runtime--`. The SDK wire, wheel and import distribution names, sidecar names, and wire identity `deepseek-harness-sdk-runtime` remain stable. The SDK package family is `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and `@deepseek-ai/dsh-sdk-jsonrpc-server`; `@deepseek-ai/dsh-acp` remains the ACP protocol plugin. There is no Python-specific Node application, checked-in complete config, compatibility package, forwarding executable, fallback parser, or SDK/ACP launcher alias. ### Enforcement -`verify-application-entrypoints` scans application/package manifests, executable sources, and root demo scripts. The allowlist classifies the `dsh` product bin, vendor-excluded scope, the private WebWorker build tool, test support, and the private Python carrier. An unclassified shebang, a new package bin, or a demo wrapper that bypasses `apps/cli/src/bin.ts` fails hygiene and the primary/static CI aggregates. - -## Deferred Python migration - -The Python runtime follow-up must move the packaged process through `dsh --profile sdk`, preserve the wheel's closed dependency and native sidecar behavior, and delete `@deepseek-ai/dsh-sdk-python-runtime`. Only after those conditions pass on Linux x64, Linux arm64, and macOS arm64 does the executable family change from `dsh-jsonrpc-agent-pkg--` to `deepseek-harness-sdk-runtime--`. The temporary carrier and current artifact names make that obligation visible without weakening current Python compatibility. +`verify-application-entrypoints` scans application/package manifests, executable sources, and root demo scripts. The allowlist classifies the `dsh` product bin, vendor-excluded scope, the private WebWorker build tool, and test support. An unclassified shebang, a new package bin, or a demo wrapper that bypasses `apps/cli/src/bin.ts` fails hygiene and the primary/static CI aggregates. ## Existing decisions and supersession @@ -88,7 +84,7 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o - Focused unit suites cover profile launch resolution, initialization bounds, SDK retries, server readiness, and nested isolated homes with 100% coverage on the changed runtime sources. - Keyless ACP and SDK snapshots boot real `dsh` profiles and pin protocol output plus persisted logs; the nested SDK composition boots a second real profile runtime. - The real-API workflow caps file parallelism at four because one profile e2e file can own several complete `dsh` subprocess trees; workflow tests pin that resource bound. -- The Python suite exercises exe and node carriers; all packaged-runtime scenarios, native macOS executable construction, both wheels, and clean-wheel default/MCP smokes retain the existing artifact names. +- The Python suite exercises exe and node carriers; packaged-runtime scenarios, native macOS executable construction, both wheels, and clean-wheel default/MCP smokes pin the `deepseek-harness-sdk-runtime-*` artifacts and profile launch. - `verify-application-entrypoints` includes invalid fixtures for package bins, executable sources, package-launching demo wrappers, and unclassified demos. ## Consequences @@ -98,4 +94,4 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o - SDK and ACP share the complete base application and one set of policy and tools; snapshots present intentional assembled differences explicitly. - Adding `@deepseek-ai/dsh` increases the TypeScript client's install size in exchange for a deterministic same-version runtime. - Trusted user patches can add a plugin that writes to stdout and corrupt their own protocol stream; shipped profiles guarantee purity, not arbitrary third-party composition. -- Python keeps a visibly private, narrowly allowed direct-config carrier until its platform artifact migration is independently proven. +- Python packages the ordinary `dsh` profile launcher while retaining a closed native runtime and no system-Node requirement for wheel users. diff --git a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md index a6bb1909b0..5fb7fb4234 100644 --- a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md @@ -8,7 +8,7 @@ Status: implemented DeepSeek Harness 应用进程需要由同一个机制负责组合、插件解析、环境发现、关闭和用户自定义。带完整 `cordis.yml` 的专用应用 bin 会在 profile 启动之外形成第二套生命周期:安装到 profile 的插件无法到达它,行为会与 `dsh-base` 偏离,SDK 调用方还需要学习任意进程 argv,而不是产品的组合模型。 -Python SDK 分发一个原生可执行文件和三个平台 wheel 包;其中嵌入的直读配置运行时只有在重建并验证完整 VFS 闭包后才能改变启动架构。该分发需要一个明确的临时例外,而不是另一种通用 Node 应用模式。 +Python SDK 分发一个原生可执行文件和三个平台 wheel 包。其打包进程必须使用同一 profile 启动器,同时保留封闭的 VFS 依赖树、原生伴随文件与 installed-wheel 证据。 ## Decision @@ -43,19 +43,15 @@ SDK 用户通过 profile 自定义插件。`dsh plugin --profile ...` 管 直接使用 SDK 时遵循普通 Harness home 解析:显式 `dshHome`、继承的 `DSH_HOME`,最后是 `~/.dsh`。`subagent-dsh-sdk` 则要求显式绝对 home,因此嵌套运行时不会通过操作系统 home 发现个人 profile、已安装插件、凭据或会话。DSH 专用 ACP 子进程示例同样传入隔离 home;ACP 后端自身继续适用于非 DSH agent。 -### Python 例外与命名 +### Python 运行时 -Python SDK 的直读配置应用位于私有 `packages/sdk/python-runtime` 包,名称是 `@deepseek-ai/dsh-sdk-python-runtime`。它唯一的打包可执行入口是 `lib/packaged-bin.js`,由私有 `dsh-sdk-python-runtime-closure` 部署根消费。它没有公开 npm bin。可运行的直启 Python 示例是 `examples/python-sdk-agent`。 +Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest,打包来自 `node_modules/@deepseek-ai/dsh/lib/bin.js` 的普通 `@deepseek-ai/dsh` CLI。Python 客户端选择 `dsh --profile sdk`、有序 patch 文件和显式 Harness home;安装的 `dsh` 控制台命令暴露相同 profile 语法。可运行的 Python 示例是 `examples/python-sdk-agent`。 -Python 可观察行为保持不变:Python API、SDK 协议格式、默认 `cordis.yml`、环境变量、wheel 包分发名称、打包可执行文件名称、伴随文件名称、显式运行时选项、零配置行为与支持平台。稳定 SDK 包族继续是 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol`、`@deepseek-ai/dsh-sdk-jsonrpc-server`,协议 identity 继续是 `deepseek-harness-sdk-runtime`;`@deepseek-ai/dsh-acp` 继续作为 ACP 协议插件。仓库不保留兼容包、转发可执行文件、后备解析器或 SDK/ACP 启动别名。 +可执行文件族是 `deepseek-harness-sdk-runtime--`。SDK 协议格式、wheel 与 import 分发名称、伴随文件名称,以及协议 identity `deepseek-harness-sdk-runtime` 保持稳定。SDK 包族是 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol` 与 `@deepseek-ai/dsh-sdk-jsonrpc-server`;`@deepseek-ai/dsh-acp` 继续作为 ACP 协议插件。仓库不保留 Python 专用 Node 应用、检入的完整配置、兼容包、转发可执行文件、后备解析器或 SDK/ACP 启动别名。 ### 强制校验 -`verify-application-entrypoints` 扫描应用/包 manifest、可执行源码和根 demo 脚本。允许清单对 `dsh` 产品 bin、排除的 vendor 范围、私有 WebWorker 构建工具、测试支持以及私有 Python 载体进行分类。未分类的 shebang、新包 bin 或绕过 `apps/cli/src/bin.ts` 的 demo wrapper 都会使 hygiene 与 primary/static CI 聚合失败。 - -## 暂缓的 Python 迁移 - -Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk`,保持 wheel 包的封闭依赖与原生伴随文件行为,并删除 `@deepseek-ai/dsh-sdk-python-runtime`。只有这些条件在 Linux x64、Linux arm64 与 macOS arm64 全部通过后,可执行文件族才会从 `dsh-jsonrpc-agent-pkg--` 改名为 `deepseek-harness-sdk-runtime--`。临时载体与当前产物名称使这项义务清晰可见,同时不削弱当前 Python 兼容性。 +`verify-application-entrypoints` 扫描应用/包 manifest、可执行源码和根 demo 脚本。允许清单对 `dsh` 产品 bin、排除的 vendor 范围、私有 WebWorker 构建工具和测试支持进行分类。未分类的 shebang、新包 bin 或绕过 `apps/cli/src/bin.ts` 的 demo wrapper 都会使 hygiene 与 primary/static CI 聚合失败。 ## 既有决策与取代关系 @@ -88,7 +84,7 @@ Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk` - 聚焦单元套件覆盖 profile 启动解析、初始化时限、SDK 重试、服务器就绪和嵌套隔离 home,并对变更后的运行时源码实现 100% 覆盖率。 - 免密钥 ACP 与 SDK 快照启动真实 `dsh` profile,并钉住协议输出与持久化日志;嵌套 SDK 组合会启动第二个真实 profile 运行时。 - 真实 API 工作流把文件并行度限制为 4,因为一个 profile e2e 文件可能拥有多个完整 `dsh` 子进程树;工作流测试会钉住该资源上限。 -- Python 套件同时测试 exe 与 node 载体;全部打包运行时场景、原生 macOS 可执行文件构建、两个 wheel 包以及干净 wheel 默认/MCP 冒烟测试都保留既有产物名称。 +- Python 套件同时测试 exe 与 node 载体;打包运行时场景、原生 macOS 可执行文件构建、两个 wheel 包以及干净 wheel 默认/MCP 冒烟测试会钉住 `deepseek-harness-sdk-runtime-*` 产物与 profile 启动。 - `verify-application-entrypoints` 包含包 bin、可执行源码、直启包的 demo wrapper 与未分类 demo 等非法 fixture(测试前置数据)。 ## 影响 @@ -98,4 +94,4 @@ Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk` - SDK 与 ACP 共享完整 base 应用和同一份策略与工具;快照以显式差异呈现刻意采用的组装变化。 - 增加 `@deepseek-ai/dsh` 会扩大 TypeScript 客户端的安装体积,换来确定的同版本运行时。 - 受信任用户 patch 可以增加写入 stdout 的插件并破坏自己的协议流;随附 profile 保证纯净,不为任意第三方组合提供保证。 -- Python 保留一个清晰可见的私有直读配置载体,直到其平台产物迁移得到独立证明。 +- Python 打包普通 `dsh` profile 启动器,同时保留封闭原生运行时,wheel 用户无需系统 Node。 diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml index 2e98550ed0..17e40be1db 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml @@ -2,5 +2,5 @@ # 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/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md -2026-08-23-python-sdk-dsh-profile-runtime.md: 400915155460598e754c2f81d60b26e693ff438a -2026-08-23-python-sdk-dsh-profile-runtime.zh.md: a3c9074817d1c1e9b3f59fbe5fb02910d20e02f4 +2026-08-23-python-sdk-dsh-profile-runtime.md: 14c58c85923aac1b54362c72f61f7f1e1c145124 +2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 9f584ca9bfcc7907caede70ef7e9781f2bc37aab diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md index 4009151554..14c58c8592 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md @@ -16,13 +16,13 @@ A normal profile cannot be adopted only at the Python wrapper. The runtime execu The runtime executable packages `@deepseek-ai/dsh` and runs its ordinary command grammar. The Python client selects `--profile sdk` by default, forwards ordered absolute `--patch` paths, and may select another `dsh` executable or profile. The private `@deepseek-ai/dsh-sdk-python-runtime` application package and checked-in runtime `cordis.yml` do not exist. JSON-RPC serving remains the `@deepseek-ai/dsh-sdk-app` bundle and `@deepseek-ai/dsh-sdk-jsonrpc-server` plugin, not a Python-owned boot path. -The public Python configuration is `dsh_bin`, `profile`, ordered `patches`, `dsh_home`, process cwd/environment, provider/model/token selection, and timeouts. It does not expose a complete Cordis tree or arbitrary launch argv. `RunResult` reports the protocol-owned run values and does not duplicate the profile's persistence path. +The public Python configuration is `dsh_bin`, `profile`, ordered `patches`, `dsh_home`, process cwd/environment, provider/model/token selection, a bounded initialization timeout, and optional turn/shutdown timeouts. It does not expose a complete Cordis tree or arbitrary launch argv. `RunResult` reports the protocol-owned run values and does not duplicate the profile's persistence path. Every Python launch requires either explicit `dsh_home` or a non-empty `DSH_HOME` in the child environment. The SDK never discovers `~/.dsh`. The selected home consistently owns profiles, external plugins, credentials, settings, and sessions. ### Plugin customization -Persistent SDK customization uses the same profile interfaces as direct CLI use. `dsh plugin --profile sdk ...` manages external dependencies and bundle order, `$DSH_HOME/profiles/sdk/cordis.patch.yml` owns persistent row changes, the home patch applies machine-local changes across profiles, and Python `patches` supplies invocation-specific overlays. A different profile is valid only when it retains an SDK server row. Missing profiles, bundles, server rows, and invalid patches fail without a complete-config fallback. +Persistent SDK customization uses the same profile interfaces as direct CLI use. `dsh plugin --profile sdk ...` manages external dependencies and bundle order, `$DSH_HOME/profiles/sdk/cordis.patch.yml` owns persistent row changes, the home patch applies machine-local changes across profiles, and Python `patches` supplies invocation-specific overlays. A different profile is valid only when it retains an SDK server row. Missing profiles, bundles, server rows, and invalid patches fail without a complete-config fallback; a profile that remains alive without serving JSON-RPC fails the independently bounded initialization handshake with a diagnostic naming that profile. The runtime wheel installs a `dsh` console command. Ordinary profile and SDK execution remains Node-free; external package management requires a caller-installed `pnpm`. @@ -30,7 +30,7 @@ The runtime wheel installs a `dsh` console command. Ordinary profile and SDK exe The zero-code deployment manifest is `dsh-python-runtime-closure`. It packages `node_modules/@deepseek-ai/dsh/lib/bin.js` and profile, bundle, preset, native-addon, and shared-library assets into `deepseek-harness-sdk-runtime--`. The wheel distribution names, Python import modules, JSON-RPC messages, and wire-stable `serverInfo.name = deepseek-harness-sdk-runtime` remain unchanged. -Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy mirrors the source package's explicit runtime exports and identity and re-exports its virtual module URLs. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module. +Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit runtime exports under ESM import conditions and re-exports its virtual module URLs. One cross-process writer lock serializes fallback healing, preventing partial proxy visibility and allowing either carrier to replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module. The published target set is Linux x64, Linux arm64, and macOS arm64. Installed-wheel black-box CI owns artifact provenance, default and patched profiles, external bundle installation, native tools, MCP, direct JSON-RPC, snapshots, and trusted real-provider turns on every target. diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md index a3c9074817..9f584ca9bf 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md @@ -16,13 +16,13 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml 运行时可执行程序打包 `@deepseek-ai/dsh` 并运行其普通命令语法。Python 客户端默认选择 `--profile sdk`,转发有序绝对 `--patch` 路径,也可以选择另一个 `dsh` 可执行程序或 profile。私有 `@deepseek-ai/dsh-sdk-python-runtime` 应用包和检入的运行时 `cordis.yml` 均不存在。JSON-RPC 服务仍由 `@deepseek-ai/dsh-sdk-app` bundle 与 `@deepseek-ai/dsh-sdk-jsonrpc-server` 插件提供,而不是 Python 自有启动路径。 -公开 Python 配置包括 `dsh_bin`、`profile`、有序 `patches`、`dsh_home`、进程 cwd/环境、provider/model/token 选择与 timeout。它不暴露完整 Cordis 树或任意启动 argv。`RunResult` 报告协议所有的运行值,不重复 profile 的持久化路径。 +公开 Python 配置包括 `dsh_bin`、`profile`、有序 `patches`、`dsh_home`、进程 cwd/环境、provider/model/token 选择、有界初始化 timeout,以及可选的轮次/关闭 timeout。它不暴露完整 Cordis 树或任意启动 argv。`RunResult` 报告协议所有的运行值,不重复 profile 的持久化路径。 每次 Python 启动都要求显式 `dsh_home`,或子进程环境中的非空 `DSH_HOME`。SDK 绝不会发现 `~/.dsh`。所选 home 统一拥有 profile、外部插件、凭据、设置与会话。 ### 插件自定义 -持久 SDK 自定义使用与直接 CLI 相同的 profile 接口。`dsh plugin --profile sdk ...` 管理外部依赖与 bundle 顺序,`$DSH_HOME/profiles/sdk/cordis.patch.yml` 负责持久配置项变更,home patch 对所有 profile 应用机器本地变更,Python `patches` 则提供单次启动 overlay。另一个 profile 只有保留 SDK server 配置项时才有效。缺失 profile、bundle、server 配置项或非法 patch 都会直接失败,不存在完整配置回退。 +持久 SDK 自定义使用与直接 CLI 相同的 profile 接口。`dsh plugin --profile sdk ...` 管理外部依赖与 bundle 顺序,`$DSH_HOME/profiles/sdk/cordis.patch.yml` 负责持久配置项变更,home patch 对所有 profile 应用机器本地变更,Python `patches` 则提供单次启动 overlay。另一个 profile 只有保留 SDK server 配置项时才有效。缺失 profile、bundle、server 配置项或非法 patch 都会直接失败,不存在完整配置回退;保持运行却不提供 JSON-RPC 服务的 profile 会在独立有界的初始化握手中失败,诊断会指明该 profile。 运行时 wheel 安装 `dsh` 控制台命令。普通 profile 与 SDK 运行仍不需要 Node;外部包管理要求调用方自行安装 `pnpm`。 @@ -30,7 +30,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml 零代码部署 manifest 是 `dsh-python-runtime-closure`。它把 `node_modules/@deepseek-ai/dsh/lib/bin.js` 以及 profile、bundle、preset、原生 addon 与共享库资源打包进 `deepseek-harness-sdk-runtime--`。Wheel distribution 名称、Python import 模块、JSON-RPC 消息和协议稳定的 `serverInfo.name = deepseek-harness-sdk-runtime` 保持不变。 -普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理镜像源包的显式运行时 exports 与身份,并重新导出其虚拟模块 URL。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。 +普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理按 ESM import 条件解析源包的显式运行时 exports,并重新导出其虚拟模块 URL。一把跨进程写入锁会串行执行后备修复,避免暴露未完整写入的代理,并允许任一载体替换另一载体留下的受管条目。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。 已发布目标集合是 Linux x64、Linux arm64 与 macOS arm64。Installed-wheel 黑盒 CI 在每个目标上负责产物来源、默认及 patched profile、外部 bundle 安装、原生工具、MCP、直接 JSON-RPC、快照,以及可信真实提供方轮次。 diff --git a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.i18n.yaml b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.i18n.yaml index 1d9c28595a..06e2503aa4 100644 --- a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.i18n.yaml @@ -2,5 +2,5 @@ # 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/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md -2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: eda9d3a3de91944a298070d6cc22f632294f7a28 -2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 1a72c6b1cdb7453b8468d6e6be37aec76323f714 +2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: 37c341e964b556c7ab5fdd9081416883066b97d1 +2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: e97e951028de3bcda9fe11be0351072481c72dd9 diff --git a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md index eda9d3a3de..37c341e964 100644 --- a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md +++ b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md @@ -17,7 +17,7 @@ Three packages, layered exactly like the existing Python stack, plus one Service - **`@deepseek-ai/dsh-subagent-dsh-sdk`** (`packages/subagent/subagent-dsh-sdk/`) — the second out-of-process `SubagentProvider`, structured as `subagent-acp`'s sibling: same all-false capabilities and `inheritsParentContext: false`, same publish-after-handshake ownership transaction, same result-never-rejects flattening through an `onError` sink, same parent-namespace run id. The child answer is read from streamed `session.event`s — the last complete `assistant/message`, else accumulated `text-delta` chunks, so partial answers survive cancellation. Stop reasons map from the child's structured `TurnEndReason` (`completed`/`max-tokens`/`aborted` pass through; everything else, including a settled-without-turn child, is `error`). Its `dshBin`/profile/patch/home config selects an isolated SDK application, `provider`/`model` feeds the child's `initialize`, and `env` supplies explicit child-only values such as its API key. - **The subagent seam grows `out-of-process.ts`**: the provider-side vocabulary both out-of-process backends share — `NO_START_CAPABILITIES`, timing-bound validation, child cwd resolution (config override, else the delegating parent session's workspace), the never-reject `settleRunResult`, and the `subprocessRunHandle` publication. Process mechanics (spawn, env scrub, tree-scoped teardown) live in the `dsh-subprocess` seam; `subagent-acp` spawns through `ctx.subprocess`, while this backend spawns through the SDK client (the subprocess README's documented exception for SDK-managed transports) and applies the seam's `scrubbedParentEnv()` itself. -`dsh-sdk-jsonrpc-server` keeps serving unchanged (the wire is byte-identical); the private `@deepseek-ai/dsh-sdk-python-runtime` carrier consumes the shared protocol through its packaged closure. +`dsh-sdk-jsonrpc-server` keeps serving unchanged (the wire is byte-identical). TypeScript and Python clients both consume the shared protocol through `dsh --profile sdk`; the Python wheel packages that CLI and its closed dependency tree. ## Testing diff --git a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md index 1a72c6b1cd..e97e951028 100644 --- a/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md +++ b/.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md @@ -17,7 +17,7 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-sdk-jsonrpc-server`,见[ - **`@deepseek-ai/dsh-subagent-dsh-sdk`**(`packages/subagent/subagent-dsh-sdk/`)—— 第二个进程外 `SubagentProvider`,采用与 `subagent-acp` 对等的结构:同样的全 false 能力与 `inheritsParentContext: false`,同样的握手后发布所有权事务,同样通过 `onError` sink 将结果归一为绝不拒绝,同样的父命名空间 run id。子答案从流式 `session.event` 读取——最后一条完整 `assistant/message`,否则累积的 `text-delta` 块,部分答案在取消时得以保留。停止原因由子进程的结构化 `TurnEndReason` 映射(`completed`/`max-tokens`/`aborted` 直通;其余一切、包括未运行任何轮次便已结束的子进程,都是 `error`)。其 `dshBin`/profile/patch/home 配置选择隔离的 SDK 应用,`provider`/`model` 写入子进程 `initialize`,`env` 则提供子进程专用的显式值,例如其 API key。 - **subagent seam 新增 `out-of-process.ts`**:两个进程外后端共享的 provider 侧词汇——`NO_START_CAPABILITIES`、时限校验、子进程 cwd 解析(配置覆盖、否则发起委托的父会话工作区)、绝不拒绝的 `settleRunResult`、以及 `subprocessRunHandle` 发布。进程机制(spawn、环境清理、进程树清理)属于 `dsh-subprocess` seam;`subagent-acp` 经 `ctx.subprocess` spawn 子进程,本后端则经 SDK 客户端 spawn 子进程(subprocess README 记载的 SDK 托管传输例外)并自行应用该 seam 的 `scrubbedParentEnv()`。 -`dsh-sdk-jsonrpc-server` 的服务不变(协议字节完全一致);私有 `@deepseek-ai/dsh-sdk-python-runtime` 载体通过其打包闭包消费共享协议。 +`dsh-sdk-jsonrpc-server` 的服务不变(协议字节完全一致)。TypeScript 与 Python 客户端都通过 `dsh --profile sdk` 消费共享协议;Python wheel 会打包该 CLI 及其封闭依赖树。 ## 测试 diff --git a/AGENTS.md b/AGENTS.md index 1fd35bb6c9..fa9515604b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ DeepSeek Harness is an all-plugin agent harness on vendored Cordis. Read [docs/a ## Application launch -Node apps launch only through `dsh` profiles; application-package bins, demos, and SDK argv escape hatches are forbidden. The private Python runtime is the sole temporary exception. [Architecture](docs/architecture.md#application-launch) owns scope and deferred artifact rename; `pnpm run verify-application-entrypoints` enforces it. +Supported Node applications launch only through `dsh` profiles; application-package bins, demos, and public SDK argv escape hatches are forbidden. [Architecture](docs/architecture.md#application-launch) owns the launch set; `pnpm run verify-application-entrypoints` enforces it. ## Repository layout diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index f2637fe699..347f3b035e 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -66,6 +66,7 @@ External packages that a workspace package resolves at runtime. The tier covers | [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT | | [`fflate`](https://github.com/101arrowz/fflate) | MIT | | [`immer`](https://github.com/immerjs/immer) | MIT | +| [`import-meta-resolve`](https://github.com/wooorm/import-meta-resolve) | MIT | | [`js-yaml`](https://github.com/nodeca/js-yaml) | MIT | | [`katex`](https://github.com/KaTeX/KaTeX) | MIT | | [`koffi`](https://github.com/Koromix/koffi) | MIT | diff --git a/apps/cli/src/bin.ts b/apps/cli/src/bin.ts index 321849f2d9..9e7c600c8d 100644 --- a/apps/cli/src/bin.ts +++ b/apps/cli/src/bin.ts @@ -41,7 +41,7 @@ switch (invocation.mode) { } case 'dump-config': { const { runDumpConfig } = await import('./dump-config.ts') - runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches) + await runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches) break } default: diff --git a/apps/cli/src/dump-config.ts b/apps/cli/src/dump-config.ts index 1754eb4efd..dc2089e7c8 100644 --- a/apps/cli/src/dump-config.ts +++ b/apps/cli/src/dump-config.ts @@ -26,9 +26,10 @@ const NAME = 'dsh' * (the recovery diagnostic for a broken `cordis.patch.yml`, which is then * never parsed). * @param patches - `--patch` overlay paths, in argv order. + * @returns settlement after the profile is healed and the dump is written. */ -export function runDumpConfig(profile: string, defaultOnly: boolean, patches: readonly string[]): void { - const loaded = prepareProfile(profile, !defaultOnly) +export async function runDumpConfig(profile: string, defaultOnly: boolean, patches: readonly string[]): Promise { + const loaded = await prepareProfile(profile, !defaultOnly) const layers: ConfigDumpLayer[] = loaded.layers.map(layer => ({ label: layer.packageName, patches: layer.patches, diff --git a/apps/cli/src/profile-boot.ts b/apps/cli/src/profile-boot.ts index 68c66a8e34..1c77b1ff8a 100644 --- a/apps/cli/src/profile-boot.ts +++ b/apps/cli/src/profile-boot.ts @@ -115,8 +115,8 @@ export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: b * @param userLayer - `false` skips parsing `cordis.patch.yml` (the default dump). * @returns the loaded profile. */ -export function prepareProfile(name: string, userLayer = true): Profile { - healProfilesModuleFallback(INSTALL_ANCHOR) +export async function prepareProfile(name: string, userLayer = true): Promise { + await healProfilesModuleFallback(INSTALL_ANCHOR) const profile = loadProfile(NAME, name, INSTALL_ANCHOR, undefined, { userLayer }) writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG) return profile @@ -154,11 +154,11 @@ function allPatches(composed: ComposedProfile): PatchOptions[] { * @param patchFiles - `--patch` overlay paths, in argv order. * @returns the profile and its patch layers. */ -function composeProfile( +async function composeProfile( name: string, patchFiles: readonly string[], -): ComposedProfile { - const profile = prepareProfile(name) +): Promise { + const profile = await prepareProfile(name) const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? [] const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file))) const bundlePatches = profile.layers.flatMap(layer => layer.patches) @@ -207,7 +207,7 @@ function suppressShutdownError(ctx: Context, signal: AbortSignal, error: unknown * @returns the settled root context and the shutdown controller. */ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Context; shutdown: ProcessShutdown }> { - const composed = composeProfile(options.profile, options.patchFiles) + const composed = await composeProfile(options.profile, options.patchFiles) const app: { current?: Context } = {} const appReady = createAppReady() const shutdown = createProcessShutdown(async () => { await app.current?.fiber.dispose() }) diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index 1dc66b2f08..a4e048b5fd 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -107,7 +107,7 @@ async function bootWeb( // upward walk. The flat fallback the preset boot maintains is what makes // them resolvable — the same mechanism, not a test-only shim. const home = dirname(settingsFile) - healProfilesModuleFallback(INSTALL_ANCHOR, home) + await healProfilesModuleFallback(INSTALL_ANCHOR, home) const profileDir = join(home, 'profiles', 'spec') await mkdir(profileDir, { recursive: true }) // Product Bundles are installed into the Profile, not the dsh app. Model diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 51ae01105f..ec30bc1e0c 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -519,7 +519,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise/profiles. - healProfilesModuleFallback(INSTALL_ANCHOR, harnessHome) + await healProfilesModuleFallback(INSTALL_ANCHOR, harnessHome) const profileDir = join(harnessHome, 'profiles', 'scaffold') await mkdir(profileDir, { recursive: true }) const rootConfig = join(profileDir, 'cordis.yml') diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml index 177b740547..6bd721e291 100644 --- a/packages/boot/app-boot/README.i18n.yaml +++ b/packages/boot/app-boot/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/boot/app-boot/README.md -README.md: 81c53abb799a91a2f7601f7506defbe9dc465820 -README.zh.md: 0d9e0707e0e809f4b2db0643a04619573107941a +README.md: f48abfa445e4ac604b5e1bc694f8bae395b566e7 +README.zh.md: e903c74fb12fee0fca1d201d217dd28be8b7e08d diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md index 81c53abb79..f48abfa445 100644 --- a/packages/boot/app-boot/README.md +++ b/packages/boot/app-boot/README.md @@ -35,7 +35,7 @@ This package carries no loader hooks and no dev-mode surface. The [`dsh` app](.. ## Profiles -A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable writes real ESM proxy packages that mirror explicit exports and re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged. +A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory under a cross-process writer lock. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable resolves exports with Node ESM import conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. The lock prevents concurrent launchers from observing partial proxies, and either carrier replaces the other carrier's managed entry. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged. User-level machine-local preferences also live in the Harness home: diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md index 0d9e0707e0..e903c74fb1 100644 --- a/packages/boot/app-boot/README.zh.md +++ b/packages/boot/app-boot/README.zh.md @@ -35,7 +35,7 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面 ## Profiles -profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则写入真实 ESM 代理包,镜像显式 exports 并重新导出虚拟模块 URL,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。 +profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 在跨进程写入锁下维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则按 Node ESM import 条件解析 exports,并写入重新导出虚拟模块 URL 的真实代理包,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。该锁防止并发启动器观察到未完整写入的代理,而两种载体都会替换另一种载体留下的受管条目。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。 用户级的机器本地偏好同样位于 harness home 中: diff --git a/packages/boot/app-boot/package.json b/packages/boot/app-boot/package.json index 0b4b1d7a74..96594a9117 100644 --- a/packages/boot/app-boot/package.json +++ b/packages/boot/app-boot/package.json @@ -32,6 +32,8 @@ ], "license": "MIT", "dependencies": { + "@deepseek-ai/dsh-atomic-write": "workspace:^", + "import-meta-resolve": "^4.2.0", "js-yaml": "^4.2.0" }, "peerDependencies": { diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts index e886f759b2..3f2a3d11c0 100644 --- a/packages/boot/app-boot/src/profile.ts +++ b/packages/boot/app-boot/src/profile.ts @@ -27,10 +27,12 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from 'node:fs' import { basename, dirname, join } from 'node:path' -import { pathToFileURL } from 'node:url' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { withFileLock } from '@deepseek-ai/dsh-atomic-write' import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include' import { resolveDshHome } from '@deepseek-ai/dsh-home-paths' +import { resolve as resolveImport } from 'import-meta-resolve' import { loadOverlayPatches } from './index.ts' /** Directory under the Harness home holding every profile. */ @@ -205,7 +207,16 @@ export function initProfile( if (!existsSync(workspacePath)) writeFileSync(workspacePath, PROFILE_PNPM_WORKSPACE) } -/** Ensure `link` is a symlink to `target`, replacing a wrong or dangling link; a real directory throws. */ +function readModuleProxyRecord(link: string): ModuleProxyRecord | undefined { + try { + return JSON.parse(readFileSync(join(link, 'package.json'), 'utf8')) as ModuleProxyRecord + } catch { + // Missing or invalid metadata is not managed state; callers reject it. + return undefined + } +} + +/** Ensure `link` is a symlink to `target`, replacing a wrong link or a dsh-managed packaged proxy. */ function ensureSymlink(link: string, target: string): void { let stat try { @@ -217,12 +228,19 @@ function ensureSymlink(link: string, target: string): void { } if (stat !== undefined) { if (!stat.isSymbolicLink()) { - throw new Error(`dsh: ${link} exists and is not a symlink; remove it so dsh can manage the installation fallback`) + const existing = stat.isDirectory() ? readModuleProxyRecord(link) : undefined + if (existing?.dsh?.moduleFallback?.targets === undefined) { + throw new Error(`dsh: ${link} exists and is not a symlink or dsh-managed module proxy; remove it so dsh can manage the installation fallback`) + } + rmSync(link, { recursive: true }) + stat = undefined + } + if (stat !== undefined) { + if (readlinkSync(link) === target) return + // unlink deletes the reparse point itself on Windows too; rmSync treats a + // junction as a directory and throws EISDIR unless recursive. + unlinkSync(link) } - if (readlinkSync(link) === target) return - // unlink deletes the reparse point itself on Windows too; rmSync treats a - // junction as a directory and throws EISDIR unless recursive. - unlinkSync(link) } try { symlinkSync(target, link, 'junction') @@ -258,29 +276,40 @@ function isPackagedExecutable(): boolean { return (process as NodeJS.Process & { pkg?: unknown }).pkg !== undefined } -/** Resolve one package specifier from the dsh installation. */ -function packageEntryFromAnchor(anchor: string, specifier: string): string | undefined { +/** Resolve one package export with the ESM conditions used by the generated proxy. */ +function packageEntryFromPackage(packageName: string, packageDir: string, specifier: string): string { try { - return createRequire(anchor).resolve(specifier) - } catch { - return undefined + const resolved = resolveImport(specifier, pathToFileURL(join(packageDir, 'package.json')).href) + if (!resolved.startsWith('file:') || !existsSync(fileURLToPath(resolved))) { + throw new Error(`resolved to missing or non-file URL ${resolved}`) + } + return resolved + } catch (error) { + throw new Error(`dsh: cannot resolve ESM export ${specifier} from installed package ${packageName}`, { cause: error }) } } /** Resolve every explicit runtime export that an out-of-tree plugin can import. */ function packageProxySource( - installAnchor: string, packageName: string, packageDir: string, ): { version: string; targets: Record } { const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as { exports?: unknown + main?: unknown version?: unknown } if (typeof manifest.version !== 'string' || manifest.version.length === 0) { throw new Error(`dsh: installed package ${packageName} must declare a non-empty version`) } const declared = manifest.exports + if (declared === undefined) { + const entry = join(packageDir, typeof manifest.main === 'string' ? manifest.main : 'index.js') + if (!existsSync(entry)) { + throw new Error(`dsh: installed package ${packageName} main entry is missing at ${entry}`) + } + return { version: manifest.version, targets: { '.': pathToFileURL(entry).href } } + } const subpaths = declared !== null && typeof declared === 'object' && !Array.isArray(declared) && Object.keys(declared).some(key => key.startsWith('.')) ? Object.keys(declared).filter(key => key === '.' || (key.startsWith('./') && !key.includes('*') && key !== './package.json')) @@ -288,8 +317,7 @@ function packageProxySource( const targets: Record = {} for (const subpath of subpaths) { const specifier = subpath === '.' ? packageName : packageName + subpath.slice(1) - const entry = packageEntryFromAnchor(installAnchor, specifier) - if (entry !== undefined) targets[subpath] = pathToFileURL(entry).href + targets[subpath] = packageEntryFromPackage(packageName, packageDir, specifier) } return { version: manifest.version, targets } } @@ -328,18 +356,13 @@ function ensureModuleProxy( stat = undefined } if (stat !== undefined) { - const marker = join(link, 'package.json') - let existing: ModuleProxyRecord | undefined - try { - existing = JSON.parse(readFileSync(marker, 'utf8')) as ModuleProxyRecord - } catch { - existing = undefined - } + const existing = readModuleProxyRecord(link) if (existing?.dsh?.moduleFallback?.targets === undefined) { throw new Error(`dsh: ${link} exists and is not a dsh-managed module proxy; remove it so dsh can manage the installation fallback`) } if (existing.version === version - && JSON.stringify(existing.dsh.moduleFallback.targets) === JSON.stringify(targets)) return + && JSON.stringify(existing.dsh.moduleFallback.targets) === JSON.stringify(targets) + && Object.keys(targets).every((_, index) => existsSync(join(link, `entry-${index}.js`)))) return rmSync(link, { recursive: true }) } mkdirSync(link, { recursive: true }) @@ -357,10 +380,12 @@ function ensureModuleProxy( * Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one * entry per package in the dsh app's resolvable dependency CLOSURE (BFS * over `dependencies` from the app manifest), each resolved from its own - * installation location. Plain Node uses symlinks. A pkg executable writes - * small ESM proxy packages instead because the host filesystem cannot follow - * a symlink into pkg's virtual `/snapshot` tree; the proxy re-exports the - * virtual URL, preserving the executable's single module instance. Node's + * installation location. Plain Node uses symlinks. A pkg executable resolves + * exports under ESM import conditions and writes small proxy packages because + * the host filesystem cannot follow a symlink into pkg's virtual `/snapshot` + * tree; the proxy re-exports the virtual URL, preserving the executable's + * single module instance. One cross-process writer lock prevents partial + * proxies and serializes carrier transitions. Node's * parent-directory walk from any profile finds this * directory after the profile's own `node_modules`, so every in-box plugin * resolves without pnpm ever managing it — the exact "bundles come from the @@ -375,11 +400,20 @@ function ensureModuleProxy( * reused because resolution cannot discover it. * @param installAnchor - absolute path of the dsh app's package.json. * @param home - the Harness home; defaults to {@link resolveDshHome}. + * @returns settlement after the locked fallback generation is complete. */ -export function healProfilesModuleFallback(installAnchor: string, home: string = resolveDshHome()): void { +export async function healProfilesModuleFallback(installAnchor: string, home: string = resolveDshHome()): Promise { const profilesDir = join(home, PROFILES_DIR) const modulesDir = join(profilesDir, 'node_modules') mkdirSync(modulesDir, { recursive: true }) + await withFileLock(modulesDir, () => { + healProfilesModuleFallbackLocked(installAnchor, modulesDir) + return Promise.resolve() + }) +} + +/** Heal one module-fallback generation while the cross-process writer lock is held. */ +function healProfilesModuleFallbackLocked(installAnchor: string, modulesDir: string): void { const appManifest = JSON.parse(readFileSync(installAnchor, 'utf8')) as ProfileManifest const links = new Map() /* v8 ignore next -- a real app manifest always declares its name */ @@ -407,7 +441,7 @@ export function healProfilesModuleFallback(installAnchor: string, home: string = const link = join(modulesDir, packageName) mkdirSync(dirname(link), { recursive: true }) if (isPackagedExecutable()) { - const source = packageProxySource(installAnchor, packageName, target) + const source = packageProxySource(packageName, target) if (Object.keys(source.targets).length > 0) { ensureModuleProxy(link, packageName, source.version, source.targets) } diff --git a/packages/boot/app-boot/tests/profile.spec.ts b/packages/boot/app-boot/tests/profile.spec.ts index 3710859380..6eb37bc65a 100644 --- a/packages/boot/app-boot/tests/profile.spec.ts +++ b/packages/boot/app-boot/tests/profile.spec.ts @@ -4,9 +4,10 @@ * empty-root composition, and the installation module-fallback healing. */ -import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { withFileLock } from '@deepseek-ai/dsh-atomic-write' import { describe, expect, it } from 'vitest' import { composeEntries, @@ -271,7 +272,7 @@ describe('composeEntries', () => { }) describe('healProfilesModuleFallback', () => { - it('links the app and bundle dependency surface flat under profiles/node_modules', () => { + it('links the app and bundle dependency surface flat under profiles/node_modules', async () => { const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n', deps: { 'dep-of-a': '0.0.0', 'ghost-dep': '0.0.0' } }, 'plain-lib': {}, @@ -285,7 +286,7 @@ describe('healProfilesModuleFallback', () => { mkdirSync(join(modules, 'dep-of-a'), { recursive: true }) writeFileSync(join(modules, 'dep-of-a', 'package.json'), JSON.stringify({ name: 'dep-of-a', version: '0.0.0' })) const home = tmp() - healProfilesModuleFallback(anchor, home) + await healProfilesModuleFallback(anchor, home) const fallback = join(home, 'profiles', 'node_modules') // App deps, the bundle's own deps, and the bundle itself are linked; the // plain library is linked as an app dep (harmless), the app itself too. @@ -293,42 +294,67 @@ describe('healProfilesModuleFallback', () => { expect(lstatSync(join(fallback, name)).isSymbolicLink(), name).toBe(true) } // Idempotent, and a moved target is re-pointed. - healProfilesModuleFallback(anchor, home) + await healProfilesModuleFallback(anchor, home) const before = readlinkSync(join(fallback, 'dep-of-a')) expect(before).toContain('dep-of-a') }) - it('throws when a fallback entry is a real directory', () => { + it('throws when a fallback entry is a foreign file or directory', async () => { const anchor = stageInstallation({}) - const home = tmp() - mkdirSync(join(home, 'profiles', 'node_modules', 'dsh-app'), { recursive: true }) - expect(() => { healProfilesModuleFallback(anchor, home) }).toThrow('is not a symlink') + for (const kind of ['file', 'directory']) { + const home = tmp() + const entry = join(home, 'profiles', 'node_modules', 'dsh-app') + mkdirSync(join(entry, '..'), { recursive: true }) + if (kind === 'directory') mkdirSync(entry) + else writeFileSync(entry, '') + await expect(healProfilesModuleFallback(anchor, home)).rejects.toThrow('is not a symlink') + } }) - it('replaces a wrong symlink', () => { + it('replaces a wrong symlink', async () => { const anchor = stageInstallation({}) const home = tmp() const fallback = join(home, 'profiles', 'node_modules') mkdirSync(fallback, { recursive: true }) symlinkSync(tmp(), join(fallback, 'dsh-app'), 'junction') - healProfilesModuleFallback(anchor, home) + await healProfilesModuleFallback(anchor, home) expect(readlinkSync(join(fallback, 'dsh-app'))).toContain('app') }) - it('tolerates losing the concurrent-heal race to an identical link and rejects a different one', () => { - // The EEXIST arm: a second process wrote the link between our lstat miss - // and symlinkSync. Simulated by pre-creating the correct link and calling - // the internal path through a stale-lstat shim is not possible from - // outside, so probe the observable contract: healing twice concurrently - // is a no-op, and a foreign REAL directory still fails loud. + it('serializes concurrent healers and retains the identical link', async () => { const anchor = stageInstallation({}) const home = tmp() - healProfilesModuleFallback(anchor, home) - healProfilesModuleFallback(anchor, home) // second healer sees the correct link + await Promise.all([ + healProfilesModuleFallback(anchor, home), + healProfilesModuleFallback(anchor, home), + ]) const fallback = join(home, 'profiles', 'node_modules') expect(lstatSync(join(fallback, 'dsh-app')).isSymbolicLink()).toBe(true) }) + it('waits for the module-fallback writer lock before publishing entries', async () => { + const anchor = stageInstallation({}) + const home = tmp() + const modules = join(home, 'profiles', 'node_modules') + mkdirSync(modules, { recursive: true }) + let releaseLock: (() => void) | undefined + let reportLock: (() => void) | undefined + const lockHeld = new Promise((resolve) => { reportLock = resolve }) + const release = new Promise((resolve) => { releaseLock = resolve }) + const holder = withFileLock(modules, async () => { + reportLock?.() + await release + }) + await lockHeld + + const healer = healProfilesModuleFallback(anchor, home) + await new Promise(resolve => setTimeout(resolve, 20)) + expect(existsSync(join(modules, 'dsh-app'))).toBe(false) + releaseLock?.() + await Promise.all([holder, healer]) + expect(lstatSync(join(modules, 'dsh-app')).isSymbolicLink()).toBe(true) + }) + it('writes real ESM proxies for a packaged executable', async () => { const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') @@ -339,7 +365,7 @@ describe('healProfilesModuleFallback', () => { const home = tmp() Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) try { - healProfilesModuleFallback(anchor, home) + await healProfilesModuleFallback(anchor, home) const fallback = join(home, 'profiles', 'node_modules') const proxy = join(fallback, 'bundle-a') expect(lstatSync(proxy).isDirectory()).toBe(true) @@ -355,13 +381,79 @@ describe('healProfilesModuleFallback', () => { expect(proxyManifest.dsh.moduleFallback.targets['.']).toEqual(expect.stringContaining('/bundle-a/index.js')) await expect(import(join(proxy, 'entry-0.js'))).resolves.toMatchObject({ packageName: 'bundle-a' }) await expect(import(join(proxy, 'entry-1.js'))).resolves.toMatchObject({ feature: 'proxied' }) - healProfilesModuleFallback(anchor, home) + await healProfilesModuleFallback(anchor, home) } finally { delete (process as NodeJS.Process & { pkg?: unknown }).pkg } }) - it('requires a package version before writing a packaged proxy', () => { + it('resolves import-only exports from each package installation', async () => { + const anchor = stageInstallation({ + 'bundle-a': { patch: '[]\n', deps: { 'nested-esm': '0.0.0' } }, + }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const bundleManifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + bundleManifest.exports = { '.': { import: './index.js' } } + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(bundleManifest)) + const nestedDir = join(bundleDir, 'node_modules', 'nested-esm') + mkdirSync(nestedDir, { recursive: true }) + writeFileSync(join(nestedDir, 'package.json'), JSON.stringify({ + name: 'nested-esm', + version: '0.0.0', + type: 'module', + exports: { import: './index.js' }, + })) + writeFileSync(join(nestedDir, 'index.js'), 'export const nested = "proxied"\n') + const home = tmp() + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + await healProfilesModuleFallback(anchor, home) + const fallback = join(home, 'profiles', 'node_modules') + await expect(import(join(fallback, 'bundle-a', 'entry-0.js'))).resolves.toMatchObject({ packageName: 'bundle-a' }) + await expect(import(join(fallback, 'nested-esm', 'entry-0.js'))).resolves.toMatchObject({ nested: 'proxied' }) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('uses the legacy index fallback when a package has no exports or main', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + delete manifest.main + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + const home = tmp() + await healProfilesModuleFallback(anchor, home) + await expect(import(join(home, 'profiles', 'node_modules', 'bundle-a', 'entry-0.js'))) + .resolves.toMatchObject({ packageName: 'bundle-a' }) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('fails loud on a missing legacy main entry or ESM export target', async () => { + for (const mode of ['legacy', 'exports']) { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + if (mode === 'legacy') delete manifest.main + else manifest.exports = { '.': { import: './missing.js' } } + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) + rmSync(join(bundleDir, 'index.js')) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + await expect(healProfilesModuleFallback(anchor, tmp())).rejects.toThrow( + mode === 'legacy' ? 'main entry is missing' : 'cannot resolve ESM export bundle-a', + ) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + } + }) + + it('requires a package version before writing a packaged proxy', async () => { const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record @@ -369,7 +461,7 @@ describe('healProfilesModuleFallback', () => { writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) try { - expect(() => { healProfilesModuleFallback(anchor, tmp()) }).toThrow( + await expect(healProfilesModuleFallback(anchor, tmp())).rejects.toThrow( 'installed package bundle-a must declare a non-empty version', ) } finally { @@ -377,23 +469,23 @@ describe('healProfilesModuleFallback', () => { } }) - it('replaces plain-node links and stale managed proxies in packaged mode', () => { + it('replaces plain-node links and stale managed proxies in packaged mode', async () => { const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) const home = tmp() - healProfilesModuleFallback(anchor, home) + await healProfilesModuleFallback(anchor, home) const proxy = join(home, 'profiles', 'node_modules', 'bundle-a') expect(lstatSync(proxy).isSymbolicLink()).toBe(true) Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) try { - healProfilesModuleFallback(anchor, home) + await healProfilesModuleFallback(anchor, home) expect(lstatSync(proxy).isDirectory()).toBe(true) const stale = JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8')) as { version: string } stale.version = 'stale' writeFileSync(join(proxy, 'package.json'), JSON.stringify(stale)) - healProfilesModuleFallback(anchor, home) + await healProfilesModuleFallback(anchor, home) expect(JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8'))).toMatchObject({ version: '0.0.0', }) @@ -402,7 +494,23 @@ describe('healProfilesModuleFallback', () => { } }) - it('rejects foreign packaged fallback directories with valid or invalid metadata', () => { + it('replaces a managed packaged proxy with a plain-node symlink', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const home = tmp() + const fallback = join(home, 'profiles', 'node_modules', 'bundle-a') + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + await healProfilesModuleFallback(anchor, home) + expect(lstatSync(fallback).isDirectory()).toBe(true) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + + await healProfilesModuleFallback(anchor, home) + expect(lstatSync(fallback).isSymbolicLink()).toBe(true) + }) + + it('rejects foreign packaged fallback directories with valid or invalid metadata', async () => { const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) try { @@ -411,7 +519,7 @@ describe('healProfilesModuleFallback', () => { const proxy = join(home, 'profiles', 'node_modules', 'bundle-a') mkdirSync(proxy, { recursive: true }) writeFileSync(join(proxy, 'package.json'), metadata) - expect(() => { healProfilesModuleFallback(anchor, home) }).toThrow( + await expect(healProfilesModuleFallback(anchor, home)).rejects.toThrow( 'exists and is not a dsh-managed module proxy', ) } diff --git a/packages/boot/app-boot/tsconfig.json b/packages/boot/app-boot/tsconfig.json index 6f0e03b8fb..c866abbc25 100644 --- a/packages/boot/app-boot/tsconfig.json +++ b/packages/boot/app-boot/tsconfig.json @@ -29,6 +29,9 @@ { "path": "../../core/system-prompt" }, + { + "path": "../../util/atomic-write" + }, { "path": "../../util/launch-environment" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9deab88d6b..c7c4e7d7f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1145,6 +1145,12 @@ importers: packages/boot/app-boot: dependencies: + '@deepseek-ai/dsh-atomic-write': + specifier: workspace:^ + version: link:../../util/atomic-write + import-meta-resolve: + specifier: ^4.2.0 + version: 4.2.0 js-yaml: specifier: ^4.2.0 version: 4.2.0 diff --git a/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index 7a8ef7c0fd..29b3a5a70b 100644 --- a/python/sdk/README.i18n.yaml +++ b/python/sdk/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 python/sdk/README.md -README.md: edf5ced63f181bb17895d81cc060577916554305 -README.zh.md: e03a19e5b8f0cef2e2f9d86f8d91b8808e268ff4 +README.md: ef2ba1764aa93fc6499941dd372a1de94f33d454 +README.zh.md: b5c36b6ff0fbce303649ae92efd7e1edac954e82 diff --git a/python/sdk/README.md b/python/sdk/README.md index edf5ced63f..ef2ba1764a 100644 --- a/python/sdk/README.md +++ b/python/sdk/README.md @@ -26,7 +26,7 @@ with DeepSeekHarness( print(result.final_response) ``` -`DeepSeekHarness` starts lazily and reuses its runtime until `close()` or context-manager exit. `cwd` is the agent workspace; `runtime_cwd` independently selects the subprocess working directory. Both become absolute before launch. `provider`, `model`, and optional positive `max_tokens` are sent during JSON-RPC initialization. `base_url` and `api_key` explicitly override `DEEPSEEK_BASE_URL` and `DEEPSEEK_API_KEY` in the child environment. +`DeepSeekHarness` starts lazily and reuses its runtime until `close()` or context-manager exit. The initial profile handshake has an independent 10-second default bound through `initialize_timeout_seconds`; ordinary turns remain unbounded unless `request_timeout_seconds` is set. A timeout names the selected profile and includes retained runtime diagnostics. `cwd` is the agent workspace; `runtime_cwd` independently selects the subprocess working directory. Both become absolute before launch. `provider`, `model`, and optional positive `max_tokens` are sent during JSON-RPC initialization. `base_url` and `api_key` explicitly override `DEEPSEEK_BASE_URL` and `DEEPSEEK_API_KEY` in the child environment. ## Customize plugins diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index e03a19e5b8..b5c36b6ff0 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -26,7 +26,7 @@ with DeepSeekHarness( print(result.final_response) ``` -`DeepSeekHarness` 延迟启动运行时,并在调用 `close()` 或退出上下文管理器前复用该进程。`cwd` 是 agent workspace;`runtime_cwd` 独立选择子进程工作目录。两者都会在启动前转成绝对路径。`provider`、`model` 和可选的正整数 `max_tokens` 通过 JSON-RPC 初始化发送。`base_url` 与 `api_key` 会显式覆盖子进程环境中的 `DEEPSEEK_BASE_URL` 与 `DEEPSEEK_API_KEY`。 +`DeepSeekHarness` 延迟启动运行时,并在调用 `close()` 或退出上下文管理器前复用该进程。首次 profile 握手通过 `initialize_timeout_seconds` 使用独立的 10 秒默认上限;普通轮次在未设置 `request_timeout_seconds` 时仍不设上限。超时诊断会指明所选 profile,并包含保留的运行时诊断。`cwd` 是 agent workspace;`runtime_cwd` 独立选择子进程工作目录。两者都会在启动前转成绝对路径。`provider`、`model` 和可选的正整数 `max_tokens` 通过 JSON-RPC 初始化发送。`base_url` 与 `api_key` 会显式覆盖子进程环境中的 `DEEPSEEK_BASE_URL` 与 `DEEPSEEK_API_KEY`。 ## 自定义插件 diff --git a/python/sdk/src/deepseek_harness/api.py b/python/sdk/src/deepseek_harness/api.py index 1b33524118..09286a1ad1 100644 --- a/python/sdk/src/deepseek_harness/api.py +++ b/python/sdk/src/deepseek_harness/api.py @@ -29,6 +29,7 @@ class DeepSeekHarnessConfig: patches: tuple[str, ...] = () dsh_home: str | None = None env: dict[str, str] = field(default_factory=dict) + initialize_timeout_seconds: float = 10.0 request_timeout_seconds: float | None = None shutdown_timeout_seconds: float | None = 1.0 base_url: str | None = None @@ -79,6 +80,7 @@ class DeepSeekHarness: dsh_home=self.config.dsh_home, cwd=runtime_cwd, env=env, + initialize_timeout_seconds=self.config.initialize_timeout_seconds, request_timeout_seconds=self.config.request_timeout_seconds, shutdown_timeout_seconds=self.config.shutdown_timeout_seconds, ), diff --git a/python/sdk/src/deepseek_harness/client.py b/python/sdk/src/deepseek_harness/client.py index 8969bc7901..5978d849ab 100644 --- a/python/sdk/src/deepseek_harness/client.py +++ b/python/sdk/src/deepseek_harness/client.py @@ -31,6 +31,7 @@ class HarnessConfig: dsh_home: str | None = None cwd: str | None = None env: dict[str, str] | None = None + initialize_timeout_seconds: float = 10.0 request_timeout_seconds: float | None = None shutdown_timeout_seconds: float | None = 1.0 _launch_args: tuple[str, ...] | None = None @@ -137,7 +138,15 @@ class HarnessClient: if max_tokens is not None: payload["maxTokens"] = max_tokens try: - return self.request("initialize", payload, response_model=InitializeResponse) + return self.request( + "initialize", + payload, + response_model=InitializeResponse, + timeout_seconds=self.config.initialize_timeout_seconds, + ) + except TimeoutError as error: + self.close() + raise TimeoutError(f"{error}\nselected dsh profile {self.config.profile!r}") from error except BaseException as error: self.close() diagnostics = self._runtime_diagnostics() diff --git a/python/sdk/tests/test_client.py b/python/sdk/tests/test_client.py index f1190407c9..1ad89ffd4b 100644 --- a/python/sdk/tests/test_client.py +++ b/python/sdk/tests/test_client.py @@ -731,7 +731,8 @@ time.sleep(60) with HarnessClient( HarnessConfig( _launch_args=(sys.executable, str(script)), - request_timeout_seconds=0.1, + profile="web", + initialize_timeout_seconds=0.1, ) ) as client: start = time.monotonic() @@ -740,6 +741,7 @@ time.sleep(60) except TimeoutError as exc: assert time.monotonic() - start < 2 assert "bridge is still starting" in str(exc) + assert "profile 'web'" in str(exc) else: raise AssertionError("initialize should time out") @@ -832,6 +834,8 @@ def test_public_signatures_omit_unsupported_wire_parameters() -> None: assert {"dsh_bin", "profile", "patches", "dsh_home"} <= set( HarnessConfig.__dataclass_fields__ ) + assert "initialize_timeout_seconds" in DeepSeekHarnessConfig.__dataclass_fields__ + assert "initialize_timeout_seconds" in HarnessConfig.__dataclass_fields__ for removed in ("cordis", "session_root", "runtime_bin", "bridge_bin", "launch_args_override"): assert removed not in DeepSeekHarnessConfig.__dataclass_fields__ assert removed not in HarnessConfig.__dataclass_fields__ diff --git a/scripts/build-exe-for-python-sdk-assets.spec.ts b/scripts/build-exe-for-python-sdk-assets.spec.ts new file mode 100644 index 0000000000..27167c5307 --- /dev/null +++ b/scripts/build-exe-for-python-sdk-assets.spec.ts @@ -0,0 +1,27 @@ +import { spawnSync } from 'node:child_process' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const root = resolve(import.meta.dirname, '..') +const script = resolve(root, 'scripts/build-exe-for-python-sdk.ts') + +describe('Python runtime executable assets', () => { + it('packages the dynamically resolved web frontend distribution', () => { + const result = spawnSync(process.execPath, [ + '--import', + 'tsx/esm', + script, + '--skip-build', + '--dry-run', + '--targets=node24-macos-arm64', + ], { + cwd: root, + encoding: 'utf8', + env: { ...process.env, npm_execpath: 'C:\\tools\\pnpm.cjs' }, + }) + + expect(result.status).toBe(0) + expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-web-frontend/dist/**/*') + expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*') + }) +}) diff --git a/scripts/build-exe-for-python-sdk.ts b/scripts/build-exe-for-python-sdk.ts index f7d696edfe..c8c30b4301 100644 --- a/scripts/build-exe-for-python-sdk.ts +++ b/scripts/build-exe-for-python-sdk.ts @@ -47,6 +47,7 @@ const ASSET_GLOBS = [ 'node_modules/**/*.mjs', 'node_modules/**/package.json', 'node_modules/**/*.json', + // Package-owned Markdown includes runtime skill instructions and badge content. 'node_modules/**/*.md', 'node_modules/**/*.dylib', 'node_modules/**/*.dll', @@ -56,6 +57,10 @@ const ASSET_GLOBS = [ 'node_modules/**/*.wasm', 'node_modules/**/*.yaml', 'node_modules/**/*.yml', + // web-app builds this path dynamically, so pkg cannot discover the static frontend. + 'node_modules/@deepseek-ai/dsh-web-frontend/dist/**/*', + // skill-badge resolves both Markdown and image resources through import.meta.url. + 'node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*', ] const PLATFORMS = ['linux', 'macos'] as const From d801f262d84b05c3af1f54ef8e9ce173cbd1dd2c Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:59:49 +0800 Subject: [PATCH 08/16] fix(python-sdk): resolve packaged proxies from real module entries The packaged dsh launcher must expose installation modules to profile-local plugins without writing symlinks into pkg's virtual filesystem. The first review fix selected ESM exports correctly in ordinary Node, but real carrier execution exposed package metadata and VFS behavior that a synthetic tree did not cover: executable and declaration packages have no import entry, legacy main fields rely on Node probing, and pkg's Windows VFS prevents filesystem package-scope resolution from seeing exports such as zod/mini and @google/genai/web. Resolve explicit exports directly from each installed manifest with the maintained resolve.exports package under Node import conditions. Publish only package-local candidate files that exist, reject escaping or malformed targets, preserve the package installation URL without realpath, and keep Node's legacy resolver only for exports-less packages. This avoids pkg filesystem package lookup entirely while retaining fail-loud behavior for broken runtime entries. Add regression coverage for import-only, nested, symlinked, zod-style, and genai-style condition maps; unavailable and types-only entries; invalid and escaping targets; executable/declaration packages; extensionless main; and legacy index fallback. profile.ts remains at 100% statements, branches, functions, and lines. Update the bilingual package and Agent Note contracts, replace the runtime dependency and generated notice, and regenerate the lockfile through pnpm. --- ...3-python-sdk-dsh-profile-runtime.i18n.yaml | 4 +- ...26-08-23-python-sdk-dsh-profile-runtime.md | 2 +- ...08-23-python-sdk-dsh-profile-runtime.zh.md | 2 +- THIRD_PARTY_NOTICES.md | 2 +- packages/boot/app-boot/README.i18n.yaml | 4 +- packages/boot/app-boot/README.md | 2 +- packages/boot/app-boot/README.zh.md | 2 +- packages/boot/app-boot/package.json | 4 +- packages/boot/app-boot/src/profile.ts | 69 ++++++--- packages/boot/app-boot/tests/profile.spec.ts | 145 ++++++++++++++++-- pnpm-lock.yaml | 12 +- 11 files changed, 206 insertions(+), 42 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml index 17e40be1db..158540a66f 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml @@ -2,5 +2,5 @@ # 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/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md -2026-08-23-python-sdk-dsh-profile-runtime.md: 14c58c85923aac1b54362c72f61f7f1e1c145124 -2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 9f584ca9bfcc7907caede70ef7e9781f2bc37aab +2026-08-23-python-sdk-dsh-profile-runtime.md: 74484f46b59c3395d7807ae6819026ad8793d033 +2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 0bc9b65115a18411b83bec4e791c2ddff41e76e1 diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md index 14c58c8592..74484f46b5 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md @@ -30,7 +30,7 @@ The runtime wheel installs a `dsh` console command. Ordinary profile and SDK exe The zero-code deployment manifest is `dsh-python-runtime-closure`. It packages `node_modules/@deepseek-ai/dsh/lib/bin.js` and profile, bundle, preset, native-addon, and shared-library assets into `deepseek-harness-sdk-runtime--`. The wheel distribution names, Python import modules, JSON-RPC messages, and wire-stable `serverInfo.name = deepseek-harness-sdk-runtime` remain unchanged. -Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit runtime exports under ESM import conditions and re-exports its virtual module URLs. One cross-process writer lock serializes fallback healing, preventing partial proxy visibility and allowing either carrier to replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module. +Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit ESM export map directly under Node import conditions, exposes targets that exist in the installation, and re-exports their virtual module URLs. Export rows without an ESM runtime target and executable-only or declaration-only packages produce no unusable proxy entry; malformed export maps fail startup. One cross-process writer lock serializes fallback healing, preventing partial proxy visibility and allowing either carrier to replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module. The published target set is Linux x64, Linux arm64, and macOS arm64. Installed-wheel black-box CI owns artifact provenance, default and patched profiles, external bundle installation, native tools, MCP, direct JSON-RPC, snapshots, and trusted real-provider turns on every target. diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md index 9f584ca9bf..0bc9b65115 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md @@ -30,7 +30,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml 零代码部署 manifest 是 `dsh-python-runtime-closure`。它把 `node_modules/@deepseek-ai/dsh/lib/bin.js` 以及 profile、bundle、preset、原生 addon 与共享库资源打包进 `deepseek-harness-sdk-runtime--`。Wheel distribution 名称、Python import 模块、JSON-RPC 消息和协议稳定的 `serverInfo.name = deepseek-harness-sdk-runtime` 保持不变。 -普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理按 ESM import 条件解析源包的显式运行时 exports,并重新导出其虚拟模块 URL。一把跨进程写入锁会串行执行后备修复,避免暴露未完整写入的代理,并允许任一载体替换另一载体留下的受管条目。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。 +普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理直接按 Node import 条件解析源包的显式 ESM exports map,公开安装中实际存在的目标,并重新导出其虚拟模块 URL。没有 ESM 运行时目标的 export 项以及仅含可执行入口或类型声明入口的包不会产生不可用的代理条目;格式错误的 exports map 会导致启动失败。一把跨进程写入锁会串行执行后备修复,避免暴露未完整写入的代理,并允许任一载体替换另一载体留下的受管条目。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。 已发布目标集合是 Linux x64、Linux arm64 与 macOS arm64。Installed-wheel 黑盒 CI 在每个目标上负责产物来源、默认及 patched profile、外部 bundle 安装、原生工具、MCP、直接 JSON-RPC、快照,以及可信真实提供方轮次。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 347f3b035e..2b2a3019c8 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -66,7 +66,6 @@ External packages that a workspace package resolves at runtime. The tier covers | [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT | | [`fflate`](https://github.com/101arrowz/fflate) | MIT | | [`immer`](https://github.com/immerjs/immer) | MIT | -| [`import-meta-resolve`](https://github.com/wooorm/import-meta-resolve) | MIT | | [`js-yaml`](https://github.com/nodeca/js-yaml) | MIT | | [`katex`](https://github.com/KaTeX/KaTeX) | MIT | | [`koffi`](https://github.com/Koromix/koffi) | MIT | @@ -89,6 +88,7 @@ External packages that a workspace package resolves at runtime. The tier covers | [`react`](https://github.com/facebook/react) | MIT | | [`react-dom`](https://github.com/facebook/react) | MIT | | [`readable-stream`](https://github.com/nodejs/readable-stream) | MIT | +| [`resolve.exports`](https://github.com/lukeed/resolve.exports) | MIT | | [`sharp`](https://github.com/lovell/sharp) | Apache-2.0 | | [`shiki`](https://github.com/shikijs/shiki) | MIT | | [`supports-color`](https://github.com/chalk/supports-color) | MIT | diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml index 6bd721e291..93f5f1ae4c 100644 --- a/packages/boot/app-boot/README.i18n.yaml +++ b/packages/boot/app-boot/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/boot/app-boot/README.md -README.md: f48abfa445e4ac604b5e1bc694f8bae395b566e7 -README.zh.md: e903c74fb12fee0fca1d201d217dd28be8b7e08d +README.md: 1fa00eefae367e2a5a44966d2f2debff9f95c074 +README.zh.md: ef57478b8a3de723e4fa8ce88f87d563eb5a81e5 diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md index f48abfa445..1fa00eefae 100644 --- a/packages/boot/app-boot/README.md +++ b/packages/boot/app-boot/README.md @@ -35,7 +35,7 @@ This package carries no loader hooks and no dev-mode surface. The [`dsh` app](.. ## Profiles -A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory under a cross-process writer lock. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable resolves exports with Node ESM import conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. The lock prevents concurrent launchers from observing partial proxies, and either carrier replaces the other carrier's managed entry. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged. +A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory under a cross-process writer lock. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable resolves available explicit exports directly from each installed manifest with Node ESM import conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Export targets absent from an installed package remain unavailable without blocking its other exports; malformed export maps fail startup. An executable-only or declaration-only package with no module entry produces no proxy. The lock prevents concurrent launchers from observing partial proxies, and either carrier replaces the other carrier's managed entry. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged. User-level machine-local preferences also live in the Harness home: diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md index e903c74fb1..ef57478b8a 100644 --- a/packages/boot/app-boot/README.zh.md +++ b/packages/boot/app-boot/README.zh.md @@ -35,7 +35,7 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面 ## Profiles -profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 在跨进程写入锁下维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则按 Node ESM import 条件解析 exports,并写入重新导出虚拟模块 URL 的真实代理包,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。该锁防止并发启动器观察到未完整写入的代理,而两种载体都会替换另一种载体留下的受管条目。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。 +profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 在跨进程写入锁下维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则直接从每个已安装 manifest 中按 Node ESM import 条件解析实际存在的显式 exports,并写入重新导出虚拟模块 URL 的真实代理包,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。安装包中不存在的 export 目标保持不可用,但不阻塞其他 exports;格式错误的 exports map 会导致启动失败。只有可执行入口或类型声明入口而没有模块入口的包不会生成代理。该锁防止并发启动器观察到未完整写入的代理,而两种载体都会替换另一种载体留下的受管条目。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。 用户级的机器本地偏好同样位于 harness home 中: diff --git a/packages/boot/app-boot/package.json b/packages/boot/app-boot/package.json index 96594a9117..d33cae8870 100644 --- a/packages/boot/app-boot/package.json +++ b/packages/boot/app-boot/package.json @@ -33,8 +33,8 @@ "license": "MIT", "dependencies": { "@deepseek-ai/dsh-atomic-write": "workspace:^", - "import-meta-resolve": "^4.2.0", - "js-yaml": "^4.2.0" + "js-yaml": "^4.2.0", + "resolve.exports": "^2.0.3" }, "peerDependencies": { "@deepseek-ai/cordis-plugin-group": "workspace:^", diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts index 3f2a3d11c0..dc7172ea5d 100644 --- a/packages/boot/app-boot/src/profile.ts +++ b/packages/boot/app-boot/src/profile.ts @@ -24,15 +24,15 @@ import { createRequire } from 'node:module' import { - existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync, + existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, } from 'node:fs' -import { basename, dirname, join } from 'node:path' -import { fileURLToPath, pathToFileURL } from 'node:url' +import { basename, dirname, join, relative, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' import { withFileLock } from '@deepseek-ai/dsh-atomic-write' import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include' import { resolveDshHome } from '@deepseek-ai/dsh-home-paths' -import { resolve as resolveImport } from 'import-meta-resolve' +import { resolve as resolvePackage, type Package as ResolvePackageManifest } from 'resolve.exports' import { loadOverlayPatches } from './index.ts' /** Directory under the Harness home holding every profile. */ @@ -276,27 +276,44 @@ function isPackagedExecutable(): boolean { return (process as NodeJS.Process & { pkg?: unknown }).pkg !== undefined } -/** Resolve one package export with the ESM conditions used by the generated proxy. */ -function packageEntryFromPackage(packageName: string, packageDir: string, specifier: string): string { +/** Resolve one available explicit package export under Node ESM import conditions. */ +function packageEntryFromPackage( + packageName: string, + packageDir: string, + declared: ResolvePackageManifest['exports'], + subpath: string, +): string | undefined { + let candidates: string[] | void try { - const resolved = resolveImport(specifier, pathToFileURL(join(packageDir, 'package.json')).href) - if (!resolved.startsWith('file:') || !existsSync(fileURLToPath(resolved))) { - throw new Error(`resolved to missing or non-file URL ${resolved}`) - } - return resolved + candidates = resolvePackage({ name: packageName, exports: declared }, subpath) } catch (error) { + if ((error as Error).message.startsWith('No known conditions for ')) return undefined + const specifier = subpath === '.' ? packageName : packageName + subpath.slice(1) throw new Error(`dsh: cannot resolve ESM export ${specifier} from installed package ${packageName}`, { cause: error }) } + for (const candidate of candidates ?? []) { + const target = candidate + const entry = resolve(packageDir, target) + const relativeEntry = relative(packageDir, entry) + if (!target.startsWith('./') || /^\.\.(?:[\\/]|$)/u.test(relativeEntry)) { + throw new Error(`dsh: installed package ${packageName} export ${subpath} resolves outside its package: ${target}`) + } + if (existsSync(entry) && statSync(entry).isFile()) return pathToFileURL(entry).href + } + return undefined } -/** Resolve every explicit runtime export that an out-of-tree plugin can import. */ +/** Resolve every explicit ESM runtime export that an out-of-tree plugin can import. */ function packageProxySource( packageName: string, packageDir: string, ): { version: string; targets: Record } { const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as { + bin?: unknown exports?: unknown main?: unknown + types?: unknown + typings?: unknown version?: unknown } if (typeof manifest.version !== 'string' || manifest.version.length === 0) { @@ -304,20 +321,34 @@ function packageProxySource( } const declared = manifest.exports if (declared === undefined) { - const entry = join(packageDir, typeof manifest.main === 'string' ? manifest.main : 'index.js') - if (!existsSync(entry)) { - throw new Error(`dsh: installed package ${packageName} main entry is missing at ${entry}`) + const main = typeof manifest.main === 'string' && manifest.main.length > 0 ? manifest.main : undefined + const entry = join(packageDir, main ?? 'index') + try { + const resolved = createRequire(join(packageDir, 'package.json')).resolve(entry) + return { version: manifest.version, targets: { '.': pathToFileURL(resolved).href } } + } catch (error) { + if (main === undefined + && (manifest.bin !== undefined || manifest.types !== undefined || manifest.typings !== undefined)) { + return { version: manifest.version, targets: {} } + } + throw new Error(`dsh: installed package ${packageName} main entry is missing at ${entry}`, { cause: error }) } - return { version: manifest.version, targets: { '.': pathToFileURL(entry).href } } } const subpaths = declared !== null && typeof declared === 'object' && !Array.isArray(declared) && Object.keys(declared).some(key => key.startsWith('.')) - ? Object.keys(declared).filter(key => key === '.' || (key.startsWith('./') && !key.includes('*') && key !== './package.json')) + ? Object.keys(declared).filter(key => key === '.' || ( + key.startsWith('./') && !key.includes('*') && !key.endsWith('/') && key !== './package.json' + )) : ['.'] const targets: Record = {} for (const subpath of subpaths) { - const specifier = subpath === '.' ? packageName : packageName + subpath.slice(1) - targets[subpath] = packageEntryFromPackage(packageName, packageDir, specifier) + const target = packageEntryFromPackage( + packageName, + packageDir, + declared as ResolvePackageManifest['exports'], + subpath, + ) + if (target !== undefined) targets[subpath] = target } return { version: manifest.version, targets } } diff --git a/packages/boot/app-boot/tests/profile.spec.ts b/packages/boot/app-boot/tests/profile.spec.ts index 6eb37bc65a..552bf79174 100644 --- a/packages/boot/app-boot/tests/profile.spec.ts +++ b/packages/boot/app-boot/tests/profile.spec.ts @@ -359,7 +359,12 @@ describe('healProfilesModuleFallback', () => { const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') const bundleManifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record - bundleManifest.exports = { '.': './index.js', './feature': './feature.js' } + bundleManifest.exports = { + '.': './index.js', + './feature': './feature.js', + './legacy/': './legacy/', + './types': { types: './feature.d.ts' }, + } writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(bundleManifest)) writeFileSync(join(bundleDir, 'feature.js'), 'export const feature = "proxied"\n') const home = tmp() @@ -416,6 +421,61 @@ describe('healProfilesModuleFallback', () => { } }) + it('resolves explicit condition targets without filesystem package lookup', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + manifest.exports = { + '.': { import: './index.js', require: './index.cjs' }, + './mini': { types: './mini/index.d.ts', import: './mini/index.js', require: './mini/index.cjs' }, + './web': { types: './dist/web/web.d.ts', import: './dist/web/index.mjs', default: './dist/web/index.mjs' }, + } + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) + mkdirSync(join(bundleDir, 'mini')) + writeFileSync(join(bundleDir, 'mini', 'index.js'), 'export const mini = true\n') + mkdirSync(join(bundleDir, 'dist', 'web'), { recursive: true }) + writeFileSync(join(bundleDir, 'dist', 'web', 'index.mjs'), 'export const web = true\n') + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + const home = tmp() + await healProfilesModuleFallback(anchor, home) + const proxy = join(home, 'profiles', 'node_modules', 'bundle-a') + await expect(import(join(proxy, 'entry-1.js'))).resolves.toMatchObject({ mini: true }) + await expect(import(join(proxy, 'entry-2.js'))).resolves.toMatchObject({ web: true }) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('preserves the installation path while resolving packaged exports', async () => { + const anchor = stageInstallation({}) + const appDir = join(anchor, '..') + const physical = tmp() + writeFileSync(join(physical, 'package.json'), JSON.stringify({ + name: 'linked-esm', + version: '0.0.0', + type: 'module', + exports: { import: './index.js' }, + })) + writeFileSync(join(physical, 'index.js'), 'export const linked = true\n') + symlinkSync(physical, join(appDir, 'node_modules', 'linked-esm'), 'junction') + const appManifest = JSON.parse(readFileSync(anchor, 'utf8')) as { dependencies: Record } + appManifest.dependencies['linked-esm'] = '0.0.0' + writeFileSync(anchor, JSON.stringify(appManifest)) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + const home = tmp() + await healProfilesModuleFallback(anchor, home) + const proxyManifest = JSON.parse(readFileSync( + join(home, 'profiles', 'node_modules', 'linked-esm', 'package.json'), + 'utf8', + )) as { dsh: { moduleFallback: { targets: Record } } } + expect(proxyManifest.dsh.moduleFallback.targets['.']).toContain('/app/node_modules/linked-esm/index.js') + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + it('uses the legacy index fallback when a package has no exports or main', async () => { const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') @@ -433,20 +493,87 @@ describe('healProfilesModuleFallback', () => { } }) - it('fails loud on a missing legacy main entry or ESM export target', async () => { - for (const mode of ['legacy', 'exports']) { + it('uses Node legacy resolution for an extensionless main entry', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + manifest.main = './index' + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + const home = tmp() + await healProfilesModuleFallback(anchor, home) + await expect(import(join(home, 'profiles', 'node_modules', 'bundle-a', 'entry-0.js'))) + .resolves.toMatchObject({ packageName: 'bundle-a' }) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('skips executable-only and declaration-only packages without import entries', async () => { + for (const marker of ['bin', 'types', 'typings']) { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const manifest = JSON.parse(readFileSync(anchor, 'utf8')) as Record + delete manifest.main + manifest[marker] = marker === 'bin' ? { dsh: './lib/bin.js' } : './index.d.ts' + if (marker === 'types') manifest.main = '' + writeFileSync(anchor, JSON.stringify(manifest)) + rmSync(join(anchor, '..', 'index.js')) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + const home = tmp() + await healProfilesModuleFallback(anchor, home) + const fallback = join(home, 'profiles', 'node_modules') + expect(existsSync(join(fallback, 'dsh-app'))).toBe(false) + expect(existsSync(join(fallback, 'bundle-a', 'entry-0.js'))).toBe(true) + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + } + }) + + it('fails loud on a missing legacy main entry', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') + const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record + delete manifest.main + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) + rmSync(join(bundleDir, 'index.js')) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + try { + await expect(healProfilesModuleFallback(anchor, tmp())).rejects.toThrow('main entry is missing') + } finally { + delete (process as NodeJS.Process & { pkg?: unknown }).pkg + } + }) + + it('omits unavailable ESM exports and rejects malformed export targets', async () => { + for (const mode of ['missing', 'directory', 'absent-map', 'invalid', 'escape', 'null', 'null-subpath']) { const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a') const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record - if (mode === 'legacy') delete manifest.main - else manifest.exports = { '.': { import: './missing.js' } } + const target = mode === 'missing' ? './missing.js' + : mode === 'directory' ? './mini' + : mode === 'escape' ? './../outside.js' + : '../outside.js' + manifest.exports = mode === 'absent-map' ? null + : mode === 'null-subpath' ? { './bad': null } + : { '.': mode === 'null' ? null : { import: target } } writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest)) - rmSync(join(bundleDir, 'index.js')) + if (mode === 'directory') mkdirSync(join(bundleDir, 'mini')) Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) try { - await expect(healProfilesModuleFallback(anchor, tmp())).rejects.toThrow( - mode === 'legacy' ? 'main entry is missing' : 'cannot resolve ESM export bundle-a', - ) + const home = tmp() + if (mode === 'missing' || mode === 'directory' || mode === 'absent-map') { + await healProfilesModuleFallback(anchor, home) + expect(existsSync(join(home, 'profiles', 'node_modules', 'bundle-a'))).toBe(false) + } else { + await expect(healProfilesModuleFallback(anchor, home)).rejects.toThrow( + mode === 'null' || mode === 'null-subpath' + ? 'cannot resolve ESM export bundle-a' + : 'resolves outside its package', + ) + } } finally { delete (process as NodeJS.Process & { pkg?: unknown }).pkg } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7c4e7d7f1..daec43dbff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1148,12 +1148,12 @@ importers: '@deepseek-ai/dsh-atomic-write': specifier: workspace:^ version: link:../../util/atomic-write - import-meta-resolve: - specifier: ^4.2.0 - version: 4.2.0 js-yaml: specifier: ^4.2.0 version: 4.2.0 + resolve.exports: + specifier: ^2.0.3 + version: 2.0.3 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -14965,6 +14965,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -20607,6 +20611,8 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve.exports@2.0.3: {} + retry@0.13.1: {} rfdc@1.4.1: {} From c2ad69344f288034f19d246d73ea808b37b80724 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:10:12 +0800 Subject: [PATCH 09/16] fix(python-sdk): make the minimal profile an explicit allowlist Give the SDK JSON-RPC server a per-root tool filter and let deployments mark the configured persona as the complete system prompt. The checked-in minimal overlay now names only bash and str_replace_editor, so later global tools and unrelated guidance from dsh-base cannot appear implicitly. Keep the shared SDK host services and packaged Web capability intact. Only workspace instructions, compaction, and the conflicting one-shot Bash row remain disabled. Unit coverage pins the configuration paths, and a real dsh profile smoke proves the assembled prompt and exact two-tool request. --- ...3-python-sdk-dsh-profile-runtime.i18n.yaml | 4 +- ...26-08-23-python-sdk-dsh-profile-runtime.md | 2 + ...08-23-python-sdk-dsh-profile-runtime.zh.md | 2 + docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 20 +++- docs/config-catalog.zh.md | 22 ++++- docs/user/guide/python-sdk.i18n.yaml | 4 +- docs/user/guide/python-sdk.md | 2 +- docs/user/guide/python-sdk.zh.md | 2 +- examples/python-sdk-agent/README.i18n.yaml | 4 +- examples/python-sdk-agent/README.md | 4 +- examples/python-sdk-agent/README.zh.md | 4 +- examples/python-sdk-agent/minimal.patch.yml | 49 +++------- .../tests/keyless-smoke.e2e.ts | 92 +++++++++++++++++++ packages/core/system-prompt/README.i18n.yaml | 4 +- packages/core/system-prompt/README.md | 1 + packages/core/system-prompt/README.zh.md | 1 + packages/core/system-prompt/src/index.ts | 4 + .../system-prompt/tests/system-prompt.spec.ts | 15 +++ .../agent-spine-demo/README.i18n.yaml | 4 +- packages/examples/agent-spine-demo/README.md | 4 +- .../examples/agent-spine-demo/README.zh.md | 4 +- .../examples/agent-spine-demo/src/index.ts | 13 ++- .../agent-spine-demo/tests/agent-core.spec.ts | 4 + packages/sdk/server/README.i18n.yaml | 4 +- packages/sdk/server/README.md | 4 +- packages/sdk/server/README.zh.md | 4 +- packages/sdk/server/src/index.ts | 13 +++ packages/sdk/server/src/server.ts | 7 ++ .../sdk/server/tests/plugin-apply.spec.ts | 54 ++++++++++- packages/sdk/server/tests/server.spec.ts | 41 +++++++++ scripts/smoke-python-runtime.py | 24 ++--- 32 files changed, 327 insertions(+), 93 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml index 158540a66f..2082ebf627 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml @@ -2,5 +2,5 @@ # 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/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md -2026-08-23-python-sdk-dsh-profile-runtime.md: 74484f46b59c3395d7807ae6819026ad8793d033 -2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 0bc9b65115a18411b83bec4e791c2ddff41e76e1 +2026-08-23-python-sdk-dsh-profile-runtime.md: 19c870b3b0e10b25480bacc85b9db29b01d2577d +2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 404798ff4ae1fcafbaa8403c7187297adf374a19 diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md index 74484f46b5..19c870b3b0 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md @@ -24,6 +24,8 @@ Every Python launch requires either explicit `dsh_home` or a non-empty `DSH_HOME Persistent SDK customization uses the same profile interfaces as direct CLI use. `dsh plugin --profile sdk ...` manages external dependencies and bundle order, `$DSH_HOME/profiles/sdk/cordis.patch.yml` owns persistent row changes, the home patch applies machine-local changes across profiles, and Python `patches` supplies invocation-specific overlays. A different profile is valid only when it retains an SDK server row. Missing profiles, bundles, server rows, and invalid patches fail without a complete-config fallback; a profile that remains alive without serving JSON-RPC fails the independently bounded initialization handshake with a diagnostic naming that profile. +The checked-in minimal overlay retains the shared SDK profile while configuring the server with a root-agent tool allow list. Later base-bundle tools remain unavailable unless the overlay names them. Its deployment persona is a complete system prompt, so unrelated guidance sections cannot describe hidden tools; dynamic runtime context, workspace instructions, compaction, and the conflicting one-shot Bash row are disabled separately. + The runtime wheel installs a `dsh` console command. Ordinary profile and SDK execution remains Node-free; external package management requires a caller-installed `pnpm`. ### Executable packaging diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md index 0bc9b65115..404798ff4a 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md @@ -24,6 +24,8 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml 持久 SDK 自定义使用与直接 CLI 相同的 profile 接口。`dsh plugin --profile sdk ...` 管理外部依赖与 bundle 顺序,`$DSH_HOME/profiles/sdk/cordis.patch.yml` 负责持久配置项变更,home patch 对所有 profile 应用机器本地变更,Python `patches` 则提供单次启动 overlay。另一个 profile 只有保留 SDK server 配置项时才有效。缺失 profile、bundle、server 配置项或非法 patch 都会直接失败,不存在完整配置回退;保持运行却不提供 JSON-RPC 服务的 profile 会在独立有界的初始化握手中失败,诊断会指明该 profile。 +检入的极简 overlay 会保留共享 SDK profile,并为 server 配置根 agent 工具 allow 列表。基础 bundle 后续新增的工具只有在 overlay 指名时才可用。其部署 persona 是完整系统提示词,因此无关引导段不会描述已隐藏的工具;动态运行时上下文、workspace 指令、compaction 与存在名称冲突的单次 Bash 配置项会分别停用。 + 运行时 wheel 安装 `dsh` 控制台命令。普通 profile 与 SDK 运行仍不需要 Node;外部包管理要求调用方自行安装 `pnpm`。 ### 可执行程序打包 diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 9dc39c909c..4998dcd505 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/config-catalog.md -config-catalog.md: acd32b7e76180f13956ec8a54ca16d0ef99bf203 -config-catalog.zh.md: eacca1575191cff6dde93cf80e5ac576e47f485c +config-catalog.md: cb156837fa0768dfa74d9b351cc3e362449ca248 +config-catalog.zh.md: 151a72190332a400b643111495d36e2d7f205d93 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index acd32b7e76..cb156837fa 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -166,9 +166,10 @@ Source: [`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/ag * Bundle config: each field forwarded verbatim to the child that owns it — * `agents` to the agent loop (an app that pre-creates no agents, like the ACP * bridge, simply omits it), `includeHarnessIdentity`, `includeRuntimeContext`, - * `persona`, and `toolOrder` to the system-prompt plugin (the fixed opener, - * dynamic-context policy, deployment persona, and explicit model-facing tool - * order), the `tools` object to the tool registry (its presentation `mode`), + * `persona`, `personaComplete`, and `toolOrder` to the system-prompt plugin + * (the fixed opener, dynamic-context policy, deployment persona completeness, + * and explicit model-facing tool order), the `tools` object to the tool + * registry (its presentation `mode`), * `dshHome` to bash environment and local skill discovery, `sessionTitle` to * the fallback title service, `skills` to the * skill registry/local provider/tool consumer, `workspaceContext` to the @@ -196,6 +197,8 @@ export interface Config { includeRuntimeContext?: SystemPromptConfig['includeRuntimeContext'] /** The deployment persona (see dsh-system-prompt's `Config`). */ persona?: SystemPromptConfig['persona'] + /** Whether the deployment persona is the complete system prompt. */ + personaComplete?: SystemPromptConfig['personaComplete'] /** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */ toolOrder?: SystemPromptConfig['toolOrder'] /** The tool registry's config — its presentation `mode` (see dsh-tools' `Config`). */ @@ -247,7 +250,7 @@ export interface GoalConfig { Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`JobsConfig`](#deepseek-aidsh-jobs-local) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillFileSystem`](../packages/skill/skill-filesystem/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/shell/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`toolJobs`](../packages/jobs/tool-jobs/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`workspaceContext`](../packages/context/agent-instructions/src/index.ts) -Source: [`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts) +Source: [`packages/examples/agent-spine-demo/src/index.ts:93`](../packages/examples/agent-spine-demo/src/index.ts) @@ -1689,6 +1692,13 @@ Requires: `agents` export interface JsonRpcConfig { /** Report max-token turn/subagent termination as a successful SDK result. */ maxTokensAsSuccess?: boolean + /** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */ + toolFilter?: { + /** Global tool names that remain visible. */ + allow?: string[] + /** Global tool names removed from visibility. */ + deny?: string[] + } /** Transport input override; production uses `process.stdin`. */ input?: Readable /** Transport output override; production uses `process.stdout`. */ @@ -2414,6 +2424,8 @@ export interface Config { * `deployment:persona` shadows it; `{{variable}}` references are strict. */ persona?: string + /** Treat the deployment persona as the complete system prompt (default false). */ + personaComplete?: boolean /** * Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once. * Invalid fields fail at load and unknown names fail at assembly; known names diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index eacca15751..151a721903 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -168,9 +168,10 @@ export type PresetTrust = 'system' | 'user' * Bundle config: each field forwarded verbatim to the child that owns it — * `agents` to the agent loop (an app that pre-creates no agents, like the ACP * bridge, simply omits it), `includeHarnessIdentity`, `includeRuntimeContext`, - * `persona`, and `toolOrder` to the system-prompt plugin (the fixed opener, - * dynamic-context policy, deployment persona, and explicit model-facing tool - * order), the `tools` object to the tool registry (its presentation `mode`), + * `persona`, `personaComplete`, and `toolOrder` to the system-prompt plugin + * (the fixed opener, dynamic-context policy, deployment persona completeness, + * and explicit model-facing tool order), the `tools` object to the tool + * registry (its presentation `mode`), * `dshHome` to bash environment and local skill discovery, `sessionTitle` to * the fallback title service, `skills` to the * skill registry/local provider/tool consumer, `workspaceContext` to the @@ -198,6 +199,8 @@ export interface Config { includeRuntimeContext?: SystemPromptConfig['includeRuntimeContext'] /** The deployment persona (see dsh-system-prompt's `Config`). */ persona?: SystemPromptConfig['persona'] + /** Whether the deployment persona is the complete system prompt. */ + personaComplete?: SystemPromptConfig['personaComplete'] /** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */ toolOrder?: SystemPromptConfig['toolOrder'] /** The tool registry's config — its presentation `mode` (see dsh-tools' `Config`). */ @@ -249,7 +252,7 @@ export interface GoalConfig { 依赖:[`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`JobsConfig`](#deepseek-aidsh-jobs-local) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillFileSystem`](../packages/skill/skill-filesystem/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/shell/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`toolJobs`](../packages/jobs/tool-jobs/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`workspaceContext`](../packages/context/agent-instructions/src/index.ts) -来源:[`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts) +来源:[`packages/examples/agent-spine-demo/src/index.ts:93`](../packages/examples/agent-spine-demo/src/index.ts) @@ -1691,6 +1694,13 @@ export interface Config { export interface JsonRpcConfig { /** Report max-token turn/subagent termination as a successful SDK result. */ maxTokensAsSuccess?: boolean + /** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */ + toolFilter?: { + /** Global tool names that remain visible. */ + allow?: string[] + /** Global tool names removed from visibility. */ + deny?: string[] + } /** Transport input override; production uses `process.stdin`. */ input?: Readable /** Transport output override; production uses `process.stdout`. */ @@ -1702,7 +1712,7 @@ export interface JsonRpcConfig { 依赖:`Readable`(`node:stream`)· `Writable`(`node:stream`) -来源:[`packages/sdk/server/src/index.ts:29`](../packages/sdk/server/src/index.ts) +来源:[`packages/sdk/server/src/index.ts:25`](../packages/sdk/server/src/index.ts) @@ -2416,6 +2426,8 @@ export interface Config { * `deployment:persona` shadows it; `{{variable}}` references are strict. */ persona?: string + /** Treat the deployment persona as the complete system prompt (default false). */ + personaComplete?: boolean /** * Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once. * Invalid fields fail at load and unknown names fail at assembly; known names diff --git a/docs/user/guide/python-sdk.i18n.yaml b/docs/user/guide/python-sdk.i18n.yaml index bbb6cff7ec..69e9720f11 100644 --- a/docs/user/guide/python-sdk.i18n.yaml +++ b/docs/user/guide/python-sdk.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/user/guide/python-sdk.md -python-sdk.md: 4fbc6b343714b232b90a7954b35bf6863716281b -python-sdk.zh.md: 51637ed986d298d46633891077bf2a9d27c35bdd +python-sdk.md: 98d7f8160dee0874338c1d914796dcfb72400a48 +python-sdk.zh.md: 8a0417b24be859b8d327a8650ce1db23adb4dba2 diff --git a/docs/user/guide/python-sdk.md b/docs/user/guide/python-sdk.md index 4fbc6b3437..98d7f8160d 100644 --- a/docs/user/guide/python-sdk.md +++ b/docs/user/guide/python-sdk.md @@ -101,7 +101,7 @@ Another `profile` is valid when it includes `@deepseek-ai/dsh-sdk-app` or anothe | Context compaction | Disabled | | Session persistence | Zstandard JSONL under `/sessions` | -The overlay removes runtime-context prompt messages and most default tools while retaining the SDK application's protocol and persistence. Persistent Bash and the editor can modify any path visible to the runtime, so use a disposable checkout or container. The PTY implementation makes this example POSIX-only. +The overlay allowlists persistent Bash and the editor for every SDK-created root agent, so later base-profile tools cannot appear implicitly. It suppresses unrelated prompt sections and runtime-context messages, disables local instruction discovery and compaction, and retains the SDK application's protocol, persistence, policy, settings, credentials, and providers. Persistent Bash and the editor can modify any path visible to the runtime, so use a disposable checkout or container. The PTY implementation makes this example POSIX-only. Use a fresh home when profiles, plugins, credentials, settings, and sessions must be isolated. Use a fresh session id for independent work; reuse a harness, home, and id only to continue the same durable conversation and session-owned resources. diff --git a/docs/user/guide/python-sdk.zh.md b/docs/user/guide/python-sdk.zh.md index 51637ed986..8a0417b24b 100644 --- a/docs/user/guide/python-sdk.zh.md +++ b/docs/user/guide/python-sdk.zh.md @@ -101,7 +101,7 @@ dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle | 上下文压缩 | 禁用 | | 会话持久化 | `/sessions` 下的 Zstandard JSONL | -该 overlay 会移除运行时上下文提示消息与大多数默认工具,但保留 SDK 应用的协议与持久化。持久 Bash 与 editor 可以修改运行时可见的任何路径,因此应使用一次性 checkout 或容器。由于采用 PTY 实现,本示例只支持 POSIX。 +该 overlay 会为每个由 SDK 创建的根 agent allowlist 持久 Bash 与 editor,因此基础 profile 以后新增的工具不会隐式出现。它会抑制无关提示词段与运行时上下文消息,停用本地指令发现与 compaction,并保留 SDK 应用的协议、持久化、策略、settings、credentials 与 provider。持久 Bash 与 editor 可以修改运行时可见的任何路径,因此应使用一次性 checkout 或容器。由于采用 PTY 实现,本示例只支持 POSIX。 需要隔离 profile、插件、凭据、设置与会话时,应使用新的 home。独立工作应使用新的 session id;只有继续同一段持久对话和会话资源时,才同时复用 harness、home 与 id。 diff --git a/examples/python-sdk-agent/README.i18n.yaml b/examples/python-sdk-agent/README.i18n.yaml index 1f9b897250..f0f1ef1980 100644 --- a/examples/python-sdk-agent/README.i18n.yaml +++ b/examples/python-sdk-agent/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 examples/python-sdk-agent/README.md -README.md: e46765b0419b0b614163fa507cadef0e5c5a53b9 -README.zh.md: 13c297bdbf01b612369ded5909ec8b7f71bab26c +README.md: b1718bfa3fb6ea4cb241bb4211b58a21bacc17a2 +README.zh.md: dbc4053093f66a60a18ab02bb727611a279fe03c diff --git a/examples/python-sdk-agent/README.md b/examples/python-sdk-agent/README.md index e46765b041..b1718bfa3f 100644 --- a/examples/python-sdk-agent/README.md +++ b/examples/python-sdk-agent/README.md @@ -19,12 +19,12 @@ python examples/python-sdk-agent/minimal.py \ Set `DEEPSEEK_BASE_URL` for a compatible proxy, `DSH_MODEL` for the default model, or `DSH_SYSTEM_PROMPT` for the deployment persona. `--model` and `--profile` override their script defaults. The selected home stores the generated profile and Zstandard session logs under `sessions/`; the script never reads `~/.dsh` implicitly. -[`minimal.patch.yml`](minimal.patch.yml) is an ordered overlay on the shipped SDK profile. It preserves the SDK application bundle but narrows model-visible behavior to: +[`minimal.patch.yml`](minimal.patch.yml) is an ordered overlay on the shipped SDK profile. Its root-agent tool allowlist exposes exactly: - owner-scoped persistent `bash` - `str_replace_editor` with `view`, `create`, `str_replace`, and `insert` -The patch omits Harness identity and runtime-context messages, local instruction discovery, skills, compaction, plan/goal/task/web/subagent/workflow tools, and the profile's one-shot Bash. It inserts the local PTY and persistent Bash providers and sets the editor output limit to 16,000 characters. +The allowlist excludes every other current or later global tool without requiring a disable entry for each base row. A complete deployment persona suppresses unrelated tool-guidance sections; runtime-context messages, local instruction discovery, compaction, and the conflicting one-shot Bash row are disabled separately. The overlay inserts the local PTY and persistent Bash providers and sets the editor output limit to 16,000 characters. Other SDK-profile services remain mounted, including persistence, policy, settings, credentials, and providers. This variant is intentionally POSIX-only. Its persistent PTY and editor can modify any path available to the runtime process, so use a disposable checkout or container. diff --git a/examples/python-sdk-agent/README.zh.md b/examples/python-sdk-agent/README.zh.md index 13c297bdbf..dbc4053093 100644 --- a/examples/python-sdk-agent/README.zh.md +++ b/examples/python-sdk-agent/README.zh.md @@ -19,12 +19,12 @@ python examples/python-sdk-agent/minimal.py \ 兼容代理使用 `DEEPSEEK_BASE_URL`,默认模型使用 `DSH_MODEL`,deployment persona 使用 `DSH_SYSTEM_PROMPT`。`--model` 与 `--profile` 会覆盖脚本默认值。所选 home 保存生成的 profile,并在 `sessions/` 下保存 Zstandard 会话日志;脚本绝不会隐式读取 `~/.dsh`。 -[`minimal.patch.yml`](minimal.patch.yml) 是随附 SDK profile 上的有序 overlay。它保留 SDK 应用 bundle,但将模型可见行为收窄为: +[`minimal.patch.yml`](minimal.patch.yml) 是随附 SDK profile 上的有序 overlay。其根 agent 工具 allow 列表只暴露: - agent 所有的持久 `bash` - 支持 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor` -该 patch 会省略 Harness 身份与运行时上下文消息、本地指令发现、skill、compaction,以及 plan/goal/task/web/subagent/workflow 工具和 profile 的单次 Bash。它插入本地 PTY 与持久 Bash provider,并把 editor 输出上限设为 16,000 字符。 +Allow 列表会排除当前及以后出现的其他所有全局工具,无需为每个基础配置项添加 disable。完整部署 persona 会抑制无关工具的引导段;运行时上下文消息、本地指令发现、compaction 与存在名称冲突的单次 Bash 配置项会分别停用。该 overlay 会插入本地 PTY 与持久 Bash provider,并将 editor 输出上限设为 16,000 字符。其他 SDK profile 服务仍保持挂载,包括持久化、策略、settings、credentials 与 provider。 此变体刻意只支持 POSIX。其持久 PTY 与 editor 可以修改运行时进程可访问的任何路径,因此只应在一次性 checkout 或容器中使用。 diff --git a/examples/python-sdk-agent/minimal.patch.yml b/examples/python-sdk-agent/minimal.patch.yml index be3f292cc0..2c3ac0f5bc 100644 --- a/examples/python-sdk-agent/minimal.patch.yml +++ b/examples/python-sdk-agent/minimal.patch.yml @@ -1,49 +1,28 @@ -# Minimal Python SDK overlay for `dsh --profile sdk`: keep only persistent -# Bash and the string-replacement editor, with no runtime-context prompt or -# compaction. The profile still owns JSON-RPC serving and persistence. +# Minimal Python SDK overlay for `dsh --profile sdk`. The SDK server applies +# the model-facing allowlist to every root session, so later base-bundle tools +# cannot appear implicitly. The profile still owns JSON-RPC and persistence. - id: system-prompt config: includeHarnessIdentity: false includeRuntimeContext: false persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.' + personaComplete: true +- id: sdk-jsonrpc-server + config: + maxTokensAsSuccess: !!js "process.env.DSH_MAX_TOKENS_AS_SUCCESS === undefined ? true : JSON.parse(process.env.DSH_MAX_TOKENS_AS_SUCCESS)" + toolFilter: + allow: + - bash + - str_replace_editor + +# Remove non-tool model inputs and turn rewriting, plus the one-shot Bash row +# that would conflict with the persistent Bash tool inserted below. - id: agent-instructions disabled: true -- id: skill-filesystem - disabled: true -- id: tool-skill - disabled: true - id: tool-bash disabled: true -- id: tool-jobs - disabled: true -- id: tool-fs - disabled: true -- id: tool-fs-search - disabled: true -- id: tool-subagent-control - disabled: true -- id: tool-subagent-list-agents - disabled: true -- id: tool-subagent - disabled: true -- id: tool-subagent-fork - disabled: true -- id: tool-subagent-report - disabled: true -- id: tool-workflow - disabled: true -- id: tool-todo - disabled: true -- id: tool-goal - disabled: true -- id: tool-ralph - disabled: true -- id: tool-web - disabled: true -- id: plan-mode - disabled: true - id: compaction-basic disabled: true - id: command-compact diff --git a/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts b/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts index c0aabb48b8..4b2519c7f8 100644 --- a/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts +++ b/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts @@ -10,6 +10,7 @@ import { describe, expect, it } from 'vitest' const binScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url)) const patchPath = fileURLToPath(new URL('./keyless.patch.yml', import.meta.url)) +const minimalPatchPath = fileURLToPath(new URL('../minimal.patch.yml', import.meta.url)) const repoRoot = fileURLToPath(new URL('../../..', import.meta.url)) const decompress = promisify(zstdDecompress) @@ -180,6 +181,97 @@ describe('Python SDK dsh profile keyless smoke', () => { } }, 40_000) + it('keeps the minimal overlay on an explicit model-facing allowlist', async () => { + const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-minimal-')) + const modelRequests: Record[] = [] + const modelServer = createServer((request, response) => { + let body = '' + request.setEncoding('utf8') + request.on('data', (chunk: string) => { body += chunk }) + request.on('end', () => { + modelRequests.push(JSON.parse(body) as Record) + response.writeHead(200, { 'content-type': 'text/event-stream' }) + response.write('data: {"choices":[{"delta":{"role":"assistant","content":null}}]}\n\n') + response.write('data: {"choices":[{"delta":{"content":"done"}}]}\n\n') + response.write('data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}\n\n') + response.end('data: [DONE]\n\n') + }) + }) + await new Promise(resolve => modelServer.listen(0, '127.0.0.1', resolve)) + const address = modelServer.address() + if (address === null || typeof address === 'string') throw new Error('model server did not bind a TCP port') + const child = execa(process.execPath, [ + '--import', + 'tsx/esm', + binScript, + '--profile', + 'sdk', + '--patch', + minimalPatchPath, + ], { + cwd: repoRoot, + env: { + DSH_HOME: join(root, '.dsh'), + DSH_PERMISSION_MODE: 'danger-full-access', + DSH_SYSTEM_PROMPT: 'Minimal allowlist prompt.', + DSH_TELEMETRY_DISABLED: '1', + DEEPSEEK_API_KEY: 'keyless-smoke-no-call', + DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`, + }, + timeout: 35_000, + killSignal: 'SIGKILL', + reject: false, + }) + const lines: string[] = [] + let stdoutBuffer = '' + let stderr = '' + child.stdout.on('data', (chunk: Buffer) => { + stdoutBuffer += chunk.toString('utf8') + const parts = stdoutBuffer.split('\n') + stdoutBuffer = parts.pop() ?? '' + lines.push(...parts) + }) + child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8') }) + + try { + child.stdin.write(`${JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { cwd: root, provider: 'deepseek-official', model: 'deepseek-v4-pro' }, + })}\n`) + await waitForLine(lines, value => value.id === 1, () => stderr) + child.stdin.write(`${JSON.stringify({ + jsonrpc: '2.0', + id: 2, + method: 'session/prompt', + params: { sessionId: 'minimal', contentBlocks: [{ type: 'text', text: 'inspect tools' }] }, + })}\n`) + await waitForLine(lines, (value) => { + const params = value.params as Record | undefined + const event = params?.event as Record | undefined + return params?.sessionId === 'minimal' && event?.type === 'turn/end' + }, () => stderr) + + const request = modelRequests[0] as { + messages?: Array<{ role?: string; content?: unknown }> + tools?: Array<{ function?: { name?: string } }> + } + expect(request.messages?.[0]).toMatchObject({ role: 'system', content: 'Minimal allowlist prompt.' }) + expect(request.tools?.map(tool => tool.function?.name).sort()).toEqual(['bash', 'str_replace_editor']) + + child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`) + await waitForLine(lines, value => value.id === 3, () => stderr) + const exit = await child + expect(exit.exitCode, `signal=${String(exit.signal)}; stderr=${stderr}`).toBe(0) + } finally { + child.kill('SIGKILL') + await child + await new Promise(resolve => modelServer.close(() => { resolve() })) + await rm(root, { recursive: true, force: true }) + } + }, 40_000) + it('rejects an invalid max-token success env value', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-runtime-invalid-')) try { diff --git a/packages/core/system-prompt/README.i18n.yaml b/packages/core/system-prompt/README.i18n.yaml index dd68b6a139..c34ff9547a 100644 --- a/packages/core/system-prompt/README.i18n.yaml +++ b/packages/core/system-prompt/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/core/system-prompt/README.md -README.md: d750a507e628e7609af542227e4528d4d4934ce8 -README.zh.md: ec5b32d742b96c8044a3707c35f15e30ba642b4f +README.md: a52aa3e4c2782993fed5a525cc827aba4e3eaeb0 +README.zh.md: cf0ba43aa2f2f47ea61ef13c74fbf182fbd9f2ee diff --git a/packages/core/system-prompt/README.md b/packages/core/system-prompt/README.md index d750a507e6..a52aa3e4c2 100644 --- a/packages/core/system-prompt/README.md +++ b/packages/core/system-prompt/README.md @@ -11,6 +11,7 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem | `includeHarnessIdentity` | `true` | Include the fixed `You are an AI agent powered by DeepSeek Harness.` order-−100 opener. Set false only when a compatibility deployment owns the complete system prompt. | | `includeRuntimeContext` | `true` | Include ordered dynamic contexts in assembly. When false, context providers are not evaluated and contexts added by `system-prompt/assemble` listeners are discarded after the waterfall; other services and their enforcement remain active. | | `persona` | `''` | The global deployment-persona default: the ONE config-authored prompt fragment, rendered as the order-0 `deployment:persona` section unless an agent-scoped contribution shadows it. A template — complete `{{…}}` groups are interpreted strictly against the registered variables (the shipped loop registers `{{model}}`/`{{cwd}}`), with no escape syntax for literal braces yet. Empty ⇒ the section is dropped at render. | +| `personaComplete` | `false` | Treat `persona` as the complete system prompt after assembly. Other sections remain registered but are omitted from model requests; tool schemas and variables remain available. | | `toolOrder` | — | Explicit model-facing tool order, as a list of `ToolSchema.name`s with one `''` rest entry (`TOOL_ORDER_REST`): listed tools take their listed position, unlisted tools land at the rest entry in lexicographic name order. Absent ⇒ plain lexicographic name order. Applied to the collected tools BEFORE the `system-prompt/assemble` waterfall — like the sections' `order` sort, it canonicalizes what the registry contributed (registration order is a plugin-load artifact), and a waterfall listener that mutates the list owns the determinism of what it emits. Misconfiguration fails loud: a list without exactly one rest entry, or with duplicates, throws at load; a listed name with no registered tool rejects every `assemble()`; a tool provider returning the reserved rest-entry name also rejects. Under the shipped loop the turn fails before any model request. Why a central list and not per-plugin weights: [Explicit model-facing tool order](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md). | ## Service: `SystemPrompt` (ctx key: `systemPrompt`) diff --git a/packages/core/system-prompt/README.zh.md b/packages/core/system-prompt/README.zh.md index ec5b32d742..cf0ba43aa2 100644 --- a/packages/core/system-prompt/README.zh.md +++ b/packages/core/system-prompt/README.zh.md @@ -11,6 +11,7 @@ | `includeHarnessIdentity` | `true` | 是否包含顺序为 −100 的固定开场白 `You are an AI agent powered by DeepSeek Harness.`。仅当兼容性部署拥有完整系统提示词时设为 false。 | | `includeRuntimeContext` | `true` | 是否在组装中包含有序动态上下文。设为 false 时不会求值上下文提供方,并会在 waterfall 后丢弃 `system-prompt/assemble` 监听器添加的上下文;其他服务及其强制机制仍然生效。 | | `persona` | `''` | 全局部署 persona 默认值:唯一由配置提供的提示词片段,渲染为顺序为 0 的 `deployment:persona` 段,除非 agent 作用域的贡献将其遮蔽。它是模板,完整的 `{{…}}` 组会严格按已注册变量解释(随附循环注册 `{{model}}`/`{{cwd}}`),目前没有表达字面量花括号的转义语法。为空 ⇒ 渲染时删除该段。 | +| `personaComplete` | `false` | 在组装后将 `persona` 作为完整系统提示词。其他段仍保持注册,但不会进入模型请求;工具 schema 与变量仍然可用。 | | `toolOrder` | 无 | 显式指定面向模型的工具顺序。该列表由 `ToolSchema.name` 组成,并且必须恰好包含一个 `''` 其余项标记(`TOOL_ORDER_REST`):已列工具按列表位置排列,未列工具则按名称字典序插入该标记所在的位置。缺席 ⇒ 直接按名称字典序排列。该顺序会在 `system-prompt/assemble` waterfall(瀑布式事件)之前应用于已收集的工具。与段的 `order` 排序一样,它会规范化注册表贡献的内容;注册顺序只是插件加载时序的产物。修改列表的 waterfall 监听器对其输出的确定性负责。配置错误会明确失败:列表没有恰好一个其余项或存在重复项,会在加载时抛出;已列名称没有对应已注册工具,会使每次 `assemble()` 被拒绝;工具提供方返回保留的其余项名称也会被拒绝。在随附循环下,轮次会在任何模型请求前失败。为何采用中心列表而非每插件权重,见[显式面向模型工具顺序](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.zh.md)。 | ## 服务:`SystemPrompt`(ctx 键:`systemPrompt`) diff --git a/packages/core/system-prompt/src/index.ts b/packages/core/system-prompt/src/index.ts index ffc052e0b9..ec36b32432 100644 --- a/packages/core/system-prompt/src/index.ts +++ b/packages/core/system-prompt/src/index.ts @@ -193,6 +193,8 @@ export interface Config { * `deployment:persona` shadows it; `{{variable}}` references are strict. */ persona?: string + /** Treat the deployment persona as the complete system prompt (default false). */ + personaComplete?: boolean /** * Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once. * Invalid fields fail at load and unknown names fail at assembly; known names @@ -340,6 +342,7 @@ export class SystemPrompt extends Service { includeHarnessIdentity: z.boolean().default(true), includeRuntimeContext: z.boolean().default(true), persona: z.string().default(''), + personaComplete: z.boolean().default(false), // Preserve omission because an explicit empty order lacks the rest marker. toolOrder: z.array(z.string()).default(undefined as unknown as string[]), }) @@ -366,6 +369,7 @@ export class SystemPrompt extends Service { order: PERSONA_ORDER, // The fallback narrows the optional input type; the schema already defaults it. text: config.persona ?? '', + complete: config.personaComplete ?? false, }) if (!(config.includeRuntimeContext ?? true)) this.suppressRuntimeContext() } diff --git a/packages/core/system-prompt/tests/system-prompt.spec.ts b/packages/core/system-prompt/tests/system-prompt.spec.ts index cf196892a7..c4018103d3 100644 --- a/packages/core/system-prompt/tests/system-prompt.spec.ts +++ b/packages/core/system-prompt/tests/system-prompt.spec.ts @@ -49,6 +49,21 @@ describe('SystemPrompt', () => { expect(renderPrompt(assembly)).toBe('You are a helpful software engineer assistant.') }) + it('can make the deployment persona the complete system prompt', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt, { + persona: 'You are a focused SDK agent.', + personaComplete: true, + }) + ctx.systemPrompt.section({ name: 'tool:future', order: 100, text: 'Future tool guidance.' }) + + const assembly = await ctx.systemPrompt.assemble() + expect(assembly.sections).toEqual([ + { name: 'deployment:persona', text: 'You are a focused SDK agent.' }, + ]) + expect(renderPrompt(assembly)).toBe('You are a focused SDK agent.') + }) + it('can suppress runtime context without evaluating providers or accepting waterfall additions', async () => { const ctx = new Context() await ctx.plugin(SystemPrompt, { includeRuntimeContext: false }) diff --git a/packages/examples/agent-spine-demo/README.i18n.yaml b/packages/examples/agent-spine-demo/README.i18n.yaml index 1144fb7047..dedce4969d 100644 --- a/packages/examples/agent-spine-demo/README.i18n.yaml +++ b/packages/examples/agent-spine-demo/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/examples/agent-spine-demo/README.md -README.md: 509e4e6d6b5885fb0c685e4ee34fe8078944415b -README.zh.md: 6b429d139a40daa516735b1192af8924e8c9f7cd +README.md: 28e1496a8c941f6524ec4b4dfbfad52d17df5d71 +README.zh.md: c2838bb4cf78d6ac863bac2eba4d2e4df335fa55 diff --git a/packages/examples/agent-spine-demo/README.md b/packages/examples/agent-spine-demo/README.md index 509e4e6d6b..28e1496a8c 100644 --- a/packages/examples/agent-spine-demo/README.md +++ b/packages/examples/agent-spine-demo/README.md @@ -55,11 +55,11 @@ This applies the [Service Definition / Service Provider / Consumer separation](. ```ts import type { Config } from '@deepseek-ai/dsh-agent-spine-demo' -// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, jobs?, toolJobs?, goals?, invariants? } +// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, personaComplete?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, jobs?, toolJobs?, goals?, invariants? } // workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults. ``` -The bundle forwards each field to the child that owns it. App packages supply any pre-created agents: headless and JSON-RPC compositions create `main`, while the ACP app creates agents on demand at `session/new`. `includeRuntimeContext: false` is forwarded to `dsh-system-prompt` and suppresses all dynamic context snapshots for fresh sessions without disabling their policy services. Prompt, tool, title, skill, agent-instructions, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages; `jobs.maxConcurrentJobsPerOwner` configures the local provider independently of the model-facing `toolJobs` controls. `pickSpineConfig()` copies only fields owned by this bundle, and conflicting `dshHome` values fail during composition. +The bundle forwards each field to the child that owns it. App packages supply any pre-created agents: headless and JSON-RPC compositions create `main`, while the ACP app creates agents on demand at `session/new`. `includeRuntimeContext: false` suppresses all dynamic context snapshots for fresh sessions without disabling their policy services; `personaComplete: true` makes the deployment persona the sole system-prompt section. Prompt, tool, title, skill, agent-instructions, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages; `jobs.maxConcurrentJobsPerOwner` configures the local provider independently of the model-facing `toolJobs` controls. `pickSpineConfig()` copies only fields owned by this bundle, and conflicting `dshHome` values fail during composition. For example, `{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` keeps the package-owned companions mounted but suppresses the blocked owner. Blocklist matches override allowlist matches; see [`dsh-invariants`](../../runtime-diagnostics/invariants/README.md) for regex and lifecycle rules. diff --git a/packages/examples/agent-spine-demo/README.zh.md b/packages/examples/agent-spine-demo/README.zh.md index 6b429d139a..c2838bb4cf 100644 --- a/packages/examples/agent-spine-demo/README.zh.md +++ b/packages/examples/agent-spine-demo/README.zh.md @@ -55,11 +55,11 @@ ```ts import type { Config } from '@deepseek-ai/dsh-agent-spine-demo' -// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, jobs?, toolJobs?, goals?, invariants? } +// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, personaComplete?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, jobs?, toolJobs?, goals?, invariants? } // workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults. ``` -组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent:无头和 JSON-RPC 组合会创建 `main`,ACP 应用则在 `session/new` 按需创建 agent。`includeRuntimeContext: false` 会转发给 `dsh-system-prompt`,为新建会话抑制所有动态上下文快照,但不禁用其策略服务。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值;`jobs.maxConcurrentJobsPerOwner` 配置本地 Service Provider,并与面向模型的 `toolJobs` 控制工具相互独立。`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。 +组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent:无头和 JSON-RPC 组合会创建 `main`,ACP 应用则在 `session/new` 按需创建 agent。`includeRuntimeContext: false` 会为新建会话抑制所有动态上下文快照,但不禁用其策略服务;`personaComplete: true` 会让部署 persona 成为唯一系统提示词段。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值;`jobs.maxConcurrentJobsPerOwner` 配置本地 Service Provider,并与面向模型的 `toolJobs` 控制工具相互独立。`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。 例如,`{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` 会让包拥有的配套插件保持挂载,但抑制被阻止的拥有者。Blocklist 匹配优先于 allowlist 匹配;正则表达式与生命周期规则见 [`dsh-invariants`](../../runtime-diagnostics/invariants/README.zh.md)。 diff --git a/packages/examples/agent-spine-demo/src/index.ts b/packages/examples/agent-spine-demo/src/index.ts index 87098c38da..a60ebd9e43 100644 --- a/packages/examples/agent-spine-demo/src/index.ts +++ b/packages/examples/agent-spine-demo/src/index.ts @@ -70,9 +70,10 @@ export interface GoalConfig { * Bundle config: each field forwarded verbatim to the child that owns it — * `agents` to the agent loop (an app that pre-creates no agents, like the ACP * bridge, simply omits it), `includeHarnessIdentity`, `includeRuntimeContext`, - * `persona`, and `toolOrder` to the system-prompt plugin (the fixed opener, - * dynamic-context policy, deployment persona, and explicit model-facing tool - * order), the `tools` object to the tool registry (its presentation `mode`), + * `persona`, `personaComplete`, and `toolOrder` to the system-prompt plugin + * (the fixed opener, dynamic-context policy, deployment persona completeness, + * and explicit model-facing tool order), the `tools` object to the tool + * registry (its presentation `mode`), * `dshHome` to bash environment and local skill discovery, `sessionTitle` to * the fallback title service, `skills` to the * skill registry/local provider/tool consumer, `workspaceContext` to the @@ -100,6 +101,8 @@ export interface Config { includeRuntimeContext?: SystemPromptConfig['includeRuntimeContext'] /** The deployment persona (see dsh-system-prompt's `Config`). */ persona?: SystemPromptConfig['persona'] + /** Whether the deployment persona is the complete system prompt. */ + personaComplete?: SystemPromptConfig['personaComplete'] /** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */ toolOrder?: SystemPromptConfig['toolOrder'] /** The tool registry's config — its presentation `mode` (see dsh-tools' `Config`). */ @@ -185,6 +188,7 @@ export function pickSpineConfig(config: Omit): Omit): Omit { includeHarnessIdentity: false, includeRuntimeContext: false, persona: 'You are a helpful software engineer assistant.', + personaComplete: true, workspaceContext: false, skills: { enabled: false }, toolBash: false, @@ -723,6 +724,7 @@ describe('dsh-agent-spine-demo bundle', () => { expect(ctx.tools.schemas()).toEqual([]) ctx.systemPrompt.context({ name: 'policy', order: 0, text: 'hidden policy' }) expect((await ctx.systemPrompt.assemble()).contexts).toEqual([]) + ctx.systemPrompt.section({ name: 'hidden', order: 100, text: 'hidden guidance' }) expect(renderPrompt(await ctx.systemPrompt.assemble())) .toBe('You are a helpful software engineer assistant.') @@ -736,6 +738,7 @@ describe('dsh-agent-spine-demo bundle', () => { includeHarnessIdentity: false, includeRuntimeContext: false, persona: 'You are merged.', + personaComplete: true, toolOrder: ['zulu'], tools: { mode: 'native' as const }, dshHome: '/tmp/dsh-home', @@ -754,6 +757,7 @@ describe('dsh-agent-spine-demo bundle', () => { includeHarnessIdentity: appConfig.includeHarnessIdentity, includeRuntimeContext: appConfig.includeRuntimeContext, persona: appConfig.persona, + personaComplete: appConfig.personaComplete, toolOrder: appConfig.toolOrder, tools: appConfig.tools, dshHome: appConfig.dshHome, diff --git a/packages/sdk/server/README.i18n.yaml b/packages/sdk/server/README.i18n.yaml index 5301b4b19e..3ffdb5d1e4 100644 --- a/packages/sdk/server/README.i18n.yaml +++ b/packages/sdk/server/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/sdk/server/README.md -README.md: 547836ddab99d5c5a5c39c013c38b4bb25ba7e7d -README.zh.md: 97a7e9897fbf338a79c2cb33a8bab809c1cd3899 +README.md: 2fac60b9313c66a8eb1653adb02405f2f5fe4b08 +README.zh.md: ed2c18f0a2ee96fdacdf0d998314d349f0b0264b diff --git a/packages/sdk/server/README.md b/packages/sdk/server/README.md index 547836ddab..2fac60b931 100644 --- a/packages/sdk/server/README.md +++ b/packages/sdk/server/README.md @@ -10,7 +10,7 @@ The `jsonrpc` plugin serves newline-delimited JSON-RPC over stdio so out-of-proc ## Config -`maxTokensAsSuccess` defaults to `false` and affects only the deployment-mapped status on `subagent.finished`; root-session prompts have no prompt-level status. `JsonRpcConfig.input`, `output`, and `exit` are runtime-only transport hooks; production uses process stdio and `process.exit`. +`maxTokensAsSuccess` defaults to `false` and affects only the deployment-mapped status on `subagent.finished`; root-session prompts have no prompt-level status. Optional `toolFilter.allow` and `toolFilter.deny` restrict each SDK-created root agent through `ctx.tools.restrict()`. An allow list excludes later global tool registrations that it does not name, so a fixed SDK deployment cannot silently gain model-facing tools when its base bundle expands. Unknown names and an empty filter fail when the first session is created. `JsonRpcConfig.input`, `output`, and `exit` are runtime-only transport hooks; production uses process stdio and `process.exit`. ## stdout is the protocol @@ -30,7 +30,7 @@ The plugin answers `shutdown`, flushes the response, disposes the root context s #### What the model sees -For each accepted `session/prompt`, the conversation model receives the caller-supplied `contentBlocks` verbatim as one user message in that SDK session. This package adds no system-prompt prose or tool schema; those come from the other plugins in the composition. +For each accepted `session/prompt`, the conversation model receives the caller-supplied `contentBlocks` verbatim as one user message in that SDK session. This package adds no system-prompt prose or tool schema; those come from the other plugins in the composition. A configured `toolFilter` projects that composition's global tool registry before the request is assembled and executed. #### Token effect diff --git a/packages/sdk/server/README.zh.md b/packages/sdk/server/README.zh.md index 97a7e9897f..ed2c18f0a2 100644 --- a/packages/sdk/server/README.zh.md +++ b/packages/sdk/server/README.zh.md @@ -10,7 +10,7 @@ ## 配置 -`maxTokensAsSuccess` 默认为 `false`,且只影响 `subagent.finished` 上由部署映射的状态;根会话提示词没有提示词级状态。`JsonRpcConfig.input`、`output` 和 `exit` 是仅供运行时使用的传输钩子;生产环境使用进程 stdio 和 `process.exit`。 +`maxTokensAsSuccess` 默认为 `false`,且只影响 `subagent.finished` 上由部署映射的状态;根会话提示词没有提示词级状态。可选的 `toolFilter.allow` 与 `toolFilter.deny` 通过 `ctx.tools.restrict()` 限制每个由 SDK 创建的根 agent。Allow 列表会排除之后出现但未指名的全局工具,因此固定的 SDK 部署不会在基础 bundle 扩展时静默获得面向模型的新工具。未知名称与空筛选器会在创建首个会话时明确失败。`JsonRpcConfig.input`、`output` 和 `exit` 是仅供运行时使用的传输钩子;生产环境使用进程 stdio 和 `process.exit`。 ## stdout 即协议 @@ -30,7 +30,7 @@ Stdout 只承载 JSON-RPC 帧。部署不得组合 stdout logger;诊断应写 #### 模型看到的内容 -对于每个已接受的 `session/prompt`,对话模型会将调用方提供的 `contentBlocks` 原样作为该 SDK 会话中的一条用户消息接收。此包不会添加系统提示词文本或工具 schema;这些内容来自组合中的其他插件。 +对于每个已接受的 `session/prompt`,对话模型会将调用方提供的 `contentBlocks` 原样作为该 SDK 会话中的一条用户消息接收。此包不会添加系统提示词文本或工具 schema;这些内容来自组合中的其他插件。配置的 `toolFilter` 会在请求组装与执行前投影该组合的全局工具注册表。 #### Token 影响 diff --git a/packages/sdk/server/src/index.ts b/packages/sdk/server/src/index.ts index 963b4fb3bd..17e9d3a89f 100644 --- a/packages/sdk/server/src/index.ts +++ b/packages/sdk/server/src/index.ts @@ -25,6 +25,13 @@ export const inject = ['agents'] export interface JsonRpcConfig { /** Report max-token turn/subagent termination as a successful SDK result. */ maxTokensAsSuccess?: boolean + /** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */ + toolFilter?: { + /** Global tool names that remain visible. */ + allow?: string[] + /** Global tool names removed from visibility. */ + deny?: string[] + } /** Transport input override; production uses `process.stdin`. */ input?: Readable /** Transport output override; production uses `process.stdout`. */ @@ -35,6 +42,11 @@ export interface JsonRpcConfig { export const Config: Schema = Schema.object({ maxTokensAsSuccess: Schema.boolean().default(false), + // Preserve omission; Schemastery's materialized empty object is not a valid restriction. + toolFilter: Schema.object({ + allow: Schema.array(Schema.string()).default(undefined as unknown as string[]), + deny: Schema.array(Schema.string()).default(undefined as unknown as string[]), + }).default(undefined as unknown as { allow: string[]; deny: string[] }), }) /** @@ -59,6 +71,7 @@ export function apply(ctx: Context, config: JsonRpcConfig): void { const transport = new JsonRpcLineTransport(input, output) const server = new HarnessSdkJsonRpcServer(ctx, transport, { maxTokensAsSuccess: resolvedConfig.maxTokensAsSuccess, + ...resolvedConfig.toolFilter === undefined ? {} : { toolFilter: resolvedConfig.toolFilter }, }) // Share one exit task so racing shutdown requests cannot dispose the root or diff --git a/packages/sdk/server/src/server.ts b/packages/sdk/server/src/server.ts index ccc3b6192d..a2ec97d9f4 100644 --- a/packages/sdk/server/src/server.ts +++ b/packages/sdk/server/src/server.ts @@ -13,6 +13,7 @@ import { carrierKeyOf, type Scoped } from '@deepseek-ai/dsh-scope' import { SessionId } from '@deepseek-ai/dsh-session' import type SubagentRuntime from '@deepseek-ai/dsh-subagent' import type { SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent' +import type { ToolRestriction } from '@deepseek-ai/dsh-tools' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import type { InitializeParams, @@ -38,6 +39,8 @@ function subagentParentOf(carrier: Scoped): Agent { export interface HarnessSdkJsonRpcServerOptions { /** Report max-token termination as an accepted result instead of an infrastructure error. */ maxTokensAsSuccess?: boolean + /** Restrict each SDK-created root agent to an explicit subset of global tools. */ + toolFilter?: ToolRestriction } function successStatus(reason: string, options: HarnessSdkJsonRpcServerOptions): 'ok' | 'error' { @@ -220,6 +223,7 @@ export class HarnessSdkJsonRpcServer { // rows in the host plane, so this agent reads them from the global layer. A // deployment that configures a roster has to join one here first // (@deepseek-ai/dsh-agent-presets README, "Composing a child agent"). + const toolFilter = this.options.toolFilter const handle = await this.ctx.agents.create({ sessionId: SessionId(sessionId), meta: { cwd: this.cwd }, @@ -228,6 +232,9 @@ export class HarnessSdkJsonRpcServer { model: this.model, ...this.maxTokens === undefined ? {} : { maxTokens: this.maxTokens }, }, + ...toolFilter === undefined + ? {} + : { setup: (agentCtx: Context) => { agentCtx.tools.restrict(toolFilter) } }, }) const rec: SessionRecord = { handle } this.sessions.set(sessionId, rec) diff --git a/packages/sdk/server/tests/plugin-apply.spec.ts b/packages/sdk/server/tests/plugin-apply.spec.ts index 28c89c19fe..41954007eb 100644 --- a/packages/sdk/server/tests/plugin-apply.spec.ts +++ b/packages/sdk/server/tests/plugin-apply.spec.ts @@ -11,6 +11,7 @@ import * as agentCore from '@deepseek-ai/dsh-agent-spine-demo' import { LlmAdapter } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' +import { defineTool } from '@deepseek-ai/dsh-tools' import * as jsonrpc from '../src/index.ts' /** @@ -71,6 +72,7 @@ async function mountPlugin( writeDelayMs?: number failFlush?: boolean beforeServer?: (ctx: Context) => Promise | void + toolFilter?: jsonrpc.JsonRpcConfig['toolFilter'] } = {}, ): Promise { const ctx = new Context() @@ -116,7 +118,12 @@ async function mountPlugin( const exit = (code: number): void => { events.push({ kind: 'exit', code }) } ctx.effect(() => () => { events.push({ kind: 'root-disposed' }) }, 'jsonrpc test root-disposal witness') - const fiber = await ctx.plugin(jsonrpc, { input, output, exit }) + const fiber = await ctx.plugin(jsonrpc, { + input, + output, + exit, + ...options.toolFilter === undefined ? {} : { toolFilter: options.toolFilter }, + }) const frames = (): Record[] => events.flatMap(event => event.kind === 'frame' ? [event.frame] : []) @@ -282,6 +289,51 @@ describe('dsh-sdk-jsonrpc-server plugin apply', () => { } }) + it('applies the configured root-agent tool filter through the Loader plugin', async () => { + const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-apply-tool-filter-')) + const llmServer = await mockCompletionServer() + vi.stubEnv('DEEPSEEK_API_KEY', 'test-key') + vi.stubEnv('DEEPSEEK_BASE_URL', llmServer.url) + const harness = await mountPlugin(storageDir, { + toolFilter: { allow: ['kept'] }, + beforeServer: (ctx) => { + for (const name of ['kept', 'excluded']) { + ctx.tools.register(defineTool({ + name, + description: name, + parameters: {}, + output: { + schema: { type: 'string' }, + render: (_args, value) => [{ type: 'text', text: value }], + }, + execute: async () => name, + })) + } + }, + }) + try { + harness.send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { cwd: storageDir, provider: 'deepseek-official', model: 'filtered-model' } }) + await harness.waitForFrame(frame => frame.id === 1, 'initialize response') + harness.send({ + jsonrpc: '2.0', + id: 2, + method: 'session/prompt', + params: { sessionId: 'filtered', contentBlocks: [{ type: 'text', text: 'inspect tools' }] }, + }) + await harness.waitForFrame( + frame => frame.method === 'session.status' + && (frame.params as { status?: string } | undefined)?.status === 'idle', + 'filtered session idle status', + ) + + const request = llmServer.requests[0] as { tools?: Array<{ function?: { name?: string } }> } + expect(request.tools?.map(entry => entry.function?.name)).toEqual(['kept']) + } finally { + await harness.dispose() + await rm(storageDir, { recursive: true, force: true }) + } + }) + it('answers shutdown before exiting 0 exactly once, even against a racing second shutdown', async () => { const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-apply-shutdown-')) const harness = await mountPlugin(storageDir, { writeDelayMs: 10 }) diff --git a/packages/sdk/server/tests/server.spec.ts b/packages/sdk/server/tests/server.spec.ts index 495f1d90f4..d9527e10d5 100644 --- a/packages/sdk/server/tests/server.spec.ts +++ b/packages/sdk/server/tests/server.spec.ts @@ -14,6 +14,7 @@ import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import SubagentRuntime, { type SubagentResult, type SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent' import type { JsonRpcTransportPeer } from '@deepseek-ai/dsh-sdk-protocol' +import { defineTool } from '@deepseek-ai/dsh-tools' import { HarnessSdkJsonRpcServer } from '../src/index.ts' class FakeTransport implements JsonRpcTransportPeer { @@ -171,6 +172,46 @@ describe('HarnessSdkJsonRpcServer', () => { } }) + it('allowlists each root session against current and later global tools', { timeout: 15_000 }, async () => { + const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-tool-filter-')) + const llmServer = await mockCompletionServer() + vi.stubEnv('DEEPSEEK_API_KEY', 'test-key') + vi.stubEnv('DEEPSEEK_BASE_URL', llmServer.url) + const ctx = await makeHarness(storageDir) + const tool = (name: string) => defineTool({ + name, + description: name, + parameters: {}, + output: { + schema: { type: 'string' as const }, + render: (_args, value) => [{ type: 'text' as const, text: value }], + }, + execute: async () => name, + }) + ctx.tools.register(tool('kept')) + ctx.tools.register(tool('excluded')) + const server = new HarnessSdkJsonRpcServer(ctx, new FakeTransport(), { + toolFilter: { allow: ['kept'] }, + }) + try { + await server.initialize({ cwd: storageDir, provider: 'deepseek-official', model: 'filtered-model' }) + await server.prompt({ sessionId: 'first', contentBlocks: [{ type: 'text', text: 'first' }] }) + await vi.waitFor(() => { expect(llmServer.requests).toHaveLength(1) }) + ctx.tools.register(tool('future')) + await server.prompt({ sessionId: 'second', contentBlocks: [{ type: 'text', text: 'second' }] }) + await vi.waitFor(() => { expect(llmServer.requests).toHaveLength(2) }) + + expect(llmServer.requests.map((request) => { + const tools = (request as { tools?: Array<{ function?: { name?: string } }> }).tools ?? [] + return tools.map(entry => entry.function?.name) + })).toEqual([['kept'], ['kept']]) + await server.shutdown() + } finally { + await ctx.fiber.dispose() + await rm(storageDir, { recursive: true, force: true }) + } + }) + it('queues overlapping prompts for one session without blocking other sessions', async () => { const mainFollowup = vi.fn() const mainAgent = ({ diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 06653c5741..3be8a9bcfb 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -985,23 +985,7 @@ def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) - sessions = dsh_home / "sessions" disabled = [ "agent-instructions", - "skill-filesystem", - "tool-skill", "tool-bash", - "tool-jobs", - "tool-fs", - "tool-fs-search", - "tool-subagent-control", - "tool-subagent-list-agents", - "tool-subagent", - "tool-subagent-fork", - "tool-subagent-report", - "tool-workflow", - "tool-todo", - "tool-goal", - "tool-ralph", - "tool-web", - "plan-mode", "compaction-basic", "command-compact", "tool-result-pruner", @@ -1013,6 +997,14 @@ def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) - "includeHarnessIdentity": False, "includeRuntimeContext": False, "persona": "You are a helpful software engineer assistant.", + "personaComplete": True, + }, + }, + { + "id": "sdk-jsonrpc-server", + "config": { + "maxTokensAsSuccess": True, + "toolFilter": {"allow": ["bash", "str_replace_editor"]}, }, }, *({"id": row_id, "disabled": True} for row_id in disabled), From ab4e65ba82a48e6bc298ed4f32788cbade3a23ad Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:11:05 +0800 Subject: [PATCH 10/16] perf(app-boot): avoid fallback locks for complete profiles Resolve the installation fallback generation before locking and return immediately when every required symlink or packaged proxy is complete. Parallel SDK rollouts sharing an initialized DSH_HOME therefore do not queue on profiles/node_modules.lock. Missing or stale entries still acquire the cross-process writer lock, recheck the generation, and repair under exclusive ownership. Tests hold the lock to prove the steady-state bypass and verify that a partial repair retains already-correct siblings. --- ...3-python-sdk-dsh-profile-runtime.i18n.yaml | 4 +- ...26-08-23-python-sdk-dsh-profile-runtime.md | 2 +- ...08-23-python-sdk-dsh-profile-runtime.zh.md | 2 +- packages/boot/app-boot/README.i18n.yaml | 4 +- packages/boot/app-boot/README.md | 2 +- packages/boot/app-boot/README.zh.md | 2 +- packages/boot/app-boot/src/profile.ts | 130 ++++++++++++------ packages/boot/app-boot/tests/profile.spec.ts | 41 +++++- 8 files changed, 133 insertions(+), 54 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml index 2082ebf627..8cc79728eb 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml @@ -2,5 +2,5 @@ # 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/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md -2026-08-23-python-sdk-dsh-profile-runtime.md: 19c870b3b0e10b25480bacc85b9db29b01d2577d -2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 404798ff4ae1fcafbaa8403c7187297adf374a19 +2026-08-23-python-sdk-dsh-profile-runtime.md: e3df2d01e3aef7e6eadaa011d51c9ab87456d35f +2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 3d50cac36d469172e91be450cea658f3a4830ccf diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md index 19c870b3b0..e3df2d01e3 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md @@ -32,7 +32,7 @@ The runtime wheel installs a `dsh` console command. Ordinary profile and SDK exe The zero-code deployment manifest is `dsh-python-runtime-closure`. It packages `node_modules/@deepseek-ai/dsh/lib/bin.js` and profile, bundle, preset, native-addon, and shared-library assets into `deepseek-harness-sdk-runtime--`. The wheel distribution names, Python import modules, JSON-RPC messages, and wire-stable `serverInfo.name = deepseek-harness-sdk-runtime` remain unchanged. -Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit ESM export map directly under Node import conditions, exposes targets that exist in the installation, and re-exports their virtual module URLs. Export rows without an ESM runtime target and executable-only or declaration-only packages produce no unusable proxy entry; malformed export maps fail startup. One cross-process writer lock serializes fallback healing, preventing partial proxy visibility and allowing either carrier to replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module. +Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit ESM export map directly under Node import conditions, exposes targets that exist in the installation, and re-exports their virtual module URLs. Export rows without an ESM runtime target and executable-only or declaration-only packages produce no unusable proxy entry; malformed export maps fail startup. A complete matching generation returns without acquiring the cross-process writer lock. A missing or stale entry acquires the lock, rechecks the generation, and repairs it without exposing partial proxies; either carrier can replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module. The published target set is Linux x64, Linux arm64, and macOS arm64. Installed-wheel black-box CI owns artifact provenance, default and patched profiles, external bundle installation, native tools, MCP, direct JSON-RPC, snapshots, and trusted real-provider turns on every target. diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md index 404798ff4a..3d50cac36d 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md @@ -32,7 +32,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml 零代码部署 manifest 是 `dsh-python-runtime-closure`。它把 `node_modules/@deepseek-ai/dsh/lib/bin.js` 以及 profile、bundle、preset、原生 addon 与共享库资源打包进 `deepseek-harness-sdk-runtime--`。Wheel distribution 名称、Python import 模块、JSON-RPC 消息和协议稳定的 `serverInfo.name = deepseek-harness-sdk-runtime` 保持不变。 -普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理直接按 Node import 条件解析源包的显式 ESM exports map,公开安装中实际存在的目标,并重新导出其虚拟模块 URL。没有 ESM 运行时目标的 export 项以及仅含可执行入口或类型声明入口的包不会产生不可用的代理条目;格式错误的 exports map 会导致启动失败。一把跨进程写入锁会串行执行后备修复,避免暴露未完整写入的代理,并允许任一载体替换另一载体留下的受管条目。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。 +普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理直接按 Node import 条件解析源包的显式 ESM exports map,公开安装中实际存在的目标,并重新导出其虚拟模块 URL。没有 ESM 运行时目标的 export 项以及仅含可执行入口或类型声明入口的包不会产生不可用的代理条目;格式错误的 exports map 会导致启动失败。完整且匹配的 generation 不会获取跨进程写入锁。缺失或过期的配置项会获取该锁、重新检查 generation,并在不暴露半成品代理的前提下修复;任一载体都可以替换另一载体留下的受管配置项。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。 已发布目标集合是 Linux x64、Linux arm64 与 macOS arm64。Installed-wheel 黑盒 CI 在每个目标上负责产物来源、默认及 patched profile、外部 bundle 安装、原生工具、MCP、直接 JSON-RPC、快照,以及可信真实提供方轮次。 diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml index 93f5f1ae4c..ae60e877c0 100644 --- a/packages/boot/app-boot/README.i18n.yaml +++ b/packages/boot/app-boot/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/boot/app-boot/README.md -README.md: 1fa00eefae367e2a5a44966d2f2debff9f95c074 -README.zh.md: ef57478b8a3de723e4fa8ce88f87d563eb5a81e5 +README.md: 0adcb0ac20516b1eea97792a11471a383a3ab429 +README.zh.md: 1da1725b1e0681f83f2c82b82cb9bbaac8ea90cb diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md index 1fa00eefae..0adcb0ac20 100644 --- a/packages/boot/app-boot/README.md +++ b/packages/boot/app-boot/README.md @@ -35,7 +35,7 @@ This package carries no loader hooks and no dev-mode surface. The [`dsh` app](.. ## Profiles -A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory under a cross-process writer lock. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable resolves available explicit exports directly from each installed manifest with Node ESM import conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Export targets absent from an installed package remain unavailable without blocking its other exports; malformed export maps fail startup. An executable-only or declaration-only package with no module entry produces no proxy. The lock prevents concurrent launchers from observing partial proxies, and either carrier replaces the other carrier's managed entry. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged. +A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable resolves available explicit exports directly from each installed manifest with Node ESM import conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Export targets absent from an installed package remain unavailable without blocking its other exports; malformed export maps fail startup. An executable-only or declaration-only package with no module entry produces no proxy. A complete matching generation returns without acquiring the writer lock. A missing or stale entry acquires the cross-process lock, rechecks the full generation, and repairs it without exposing partial proxies; either carrier replaces the other carrier's managed entry. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged. User-level machine-local preferences also live in the Harness home: diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md index ef57478b8a..1da1725b1e 100644 --- a/packages/boot/app-boot/README.zh.md +++ b/packages/boot/app-boot/README.zh.md @@ -35,7 +35,7 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面 ## Profiles -profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 在跨进程写入锁下维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则直接从每个已安装 manifest 中按 Node ESM import 条件解析实际存在的显式 exports,并写入重新导出虚拟模块 URL 的真实代理包,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。安装包中不存在的 export 目标保持不可用,但不阻塞其他 exports;格式错误的 exports map 会导致启动失败。只有可执行入口或类型声明入口而没有模块入口的包不会生成代理。该锁防止并发启动器观察到未完整写入的代理,而两种载体都会替换另一种载体留下的受管条目。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。 +profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则直接从每个已安装 manifest 中按 Node ESM import 条件解析实际存在的显式 exports,并写入重新导出虚拟模块 URL 的真实代理包,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。安装包中不存在的 export 目标保持不可用,但不阻塞其他 exports;格式错误的 exports map 会导致启动失败。只有可执行入口或类型声明入口而没有模块入口的包不会生成代理。完整且匹配的 generation 不会获取写入锁。缺失或过期的配置项会获取跨进程锁、重新检查完整 generation,并在不暴露半成品代理的前提下修复;两种载体都会替换另一种载体留下的受管条目。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。 用户级的机器本地偏好同样位于 harness home 中: diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts index dc7172ea5d..17c6cf65ab 100644 --- a/packages/boot/app-boot/src/profile.ts +++ b/packages/boot/app-boot/src/profile.ts @@ -407,44 +407,12 @@ function ensureModuleProxy( } } -/** - * Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one - * entry per package in the dsh app's resolvable dependency CLOSURE (BFS - * over `dependencies` from the app manifest), each resolved from its own - * installation location. Plain Node uses symlinks. A pkg executable resolves - * exports under ESM import conditions and writes small proxy packages because - * the host filesystem cannot follow a symlink into pkg's virtual `/snapshot` - * tree; the proxy re-exports the virtual URL, preserving the executable's - * single module instance. One cross-process writer lock prevents partial - * proxies and serializes carrier transitions. Node's - * parent-directory walk from any profile finds this - * directory after the profile's own `node_modules`, so every in-box plugin - * resolves without pnpm ever managing it — the exact "bundles come from the - * installation" contract. The closure (not just direct dependencies) is - * required for out-of-tree plugins: their peer dependencies name Service - * Definition packages (`dsh-compaction`, `dsh-invariants`, ...) that the app - * reaches only through its Service Provider packages. Both a symlink target - * and a proxy's virtual target resolve transitive imports from the original - * package directory, so each package needs one flat fallback entry. - * Idempotent: correct entries are kept and changed installation targets are - * rewritten; under plain Node, a stale dangling link stays until its name is - * reused because resolution cannot discover it. - * @param installAnchor - absolute path of the dsh app's package.json. - * @param home - the Harness home; defaults to {@link resolveDshHome}. - * @returns settlement after the locked fallback generation is complete. - */ -export async function healProfilesModuleFallback(installAnchor: string, home: string = resolveDshHome()): Promise { - const profilesDir = join(home, PROFILES_DIR) - const modulesDir = join(profilesDir, 'node_modules') - mkdirSync(modulesDir, { recursive: true }) - await withFileLock(modulesDir, () => { - healProfilesModuleFallbackLocked(installAnchor, modulesDir) - return Promise.resolve() - }) -} +type ModuleFallbackEntry = + | { kind: 'symlink'; packageName: string; packageDir: string } + | { kind: 'proxy'; packageName: string; version: string; targets: Record } -/** Heal one module-fallback generation while the cross-process writer lock is held. */ -function healProfilesModuleFallbackLocked(installAnchor: string, modulesDir: string): void { +/** Resolve the installation generation that every profile must find through the fallback directory. */ +function resolveModuleFallbackEntries(installAnchor: string): ModuleFallbackEntry[] { const appManifest = JSON.parse(readFileSync(installAnchor, 'utf8')) as ProfileManifest const links = new Map() /* v8 ignore next -- a real app manifest always declares its name */ @@ -468,16 +436,88 @@ function healProfilesModuleFallbackLocked(installAnchor: string, modulesDir: str queue.push({ anchor: manifestPath, manifest: JSON.parse(readFileSync(manifestPath, 'utf8')) as ProfileManifest }) } } - for (const [packageName, target] of links) { - const link = join(modulesDir, packageName) + if (!isPackagedExecutable()) { + return [...links].map(([packageName, packageDir]) => ({ kind: 'symlink', packageName, packageDir })) + } + return [...links].flatMap(([packageName, packageDir]) => { + const source = packageProxySource(packageName, packageDir) + return Object.keys(source.targets).length === 0 + ? [] + : [{ kind: 'proxy' as const, packageName, version: source.version, targets: source.targets }] + }) +} + +/** Return whether one existing fallback entry already matches its resolved installation generation. */ +function moduleFallbackEntryCurrent(modulesDir: string, entry: ModuleFallbackEntry): boolean { + const link = join(modulesDir, entry.packageName) + try { + const stat = lstatSync(link) + if (entry.kind === 'symlink') { + return stat.isSymbolicLink() && readlinkSync(link) === entry.packageDir + } + if (!stat.isDirectory()) return false + const existing = readModuleProxyRecord(link) + return existing?.version === entry.version + && JSON.stringify(existing.dsh?.moduleFallback?.targets) === JSON.stringify(entry.targets) + && Object.keys(entry.targets).every((_, index) => existsSync(join(link, `entry-${index}.js`))) + } catch { + return false + } +} + +/** Return whether every required fallback entry is already ready for this installation. */ +function moduleFallbackCurrent(modulesDir: string, entries: readonly ModuleFallbackEntry[]): boolean { + return entries.every(entry => moduleFallbackEntryCurrent(modulesDir, entry)) +} + +/** + * Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one + * entry per package in the dsh app's resolvable dependency CLOSURE (BFS + * over `dependencies` from the app manifest), each resolved from its own + * installation location. Plain Node uses symlinks. A pkg executable resolves + * exports under ESM import conditions and writes small proxy packages because + * the host filesystem cannot follow a symlink into pkg's virtual `/snapshot` + * tree; the proxy re-exports the virtual URL, preserving the executable's + * single module instance. A complete matching generation returns without a + * writer lock; actual repairs acquire and recheck one cross-process lock so + * partial proxies and carrier transitions remain serialized. Node's + * parent-directory walk from any profile finds this + * directory after the profile's own `node_modules`, so every in-box plugin + * resolves without pnpm ever managing it — the exact "bundles come from the + * installation" contract. The closure (not just direct dependencies) is + * required for out-of-tree plugins: their peer dependencies name Service + * Definition packages (`dsh-compaction`, `dsh-invariants`, ...) that the app + * reaches only through its Service Provider packages. Both a symlink target + * and a proxy's virtual target resolve transitive imports from the original + * package directory, so each package needs one flat fallback entry. + * Idempotent: correct entries are kept and changed installation targets are + * rewritten; under plain Node, a stale dangling link stays until its name is + * reused because resolution cannot discover it. + * @param installAnchor - absolute path of the dsh app's package.json. + * @param home - the Harness home; defaults to {@link resolveDshHome}. + * @returns settlement after current-state validation or a locked repair. + */ +export async function healProfilesModuleFallback(installAnchor: string, home: string = resolveDshHome()): Promise { + const profilesDir = join(home, PROFILES_DIR) + const modulesDir = join(profilesDir, 'node_modules') + mkdirSync(modulesDir, { recursive: true }) + const entries = resolveModuleFallbackEntries(installAnchor) + if (moduleFallbackCurrent(modulesDir, entries)) return + await withFileLock(modulesDir, () => { + if (!moduleFallbackCurrent(modulesDir, entries)) healProfilesModuleFallbackLocked(entries, modulesDir) + return Promise.resolve() + }) +} + +/** Heal one module-fallback generation while the cross-process writer lock is held. */ +function healProfilesModuleFallbackLocked(entries: readonly ModuleFallbackEntry[], modulesDir: string): void { + for (const entry of entries) { + const link = join(modulesDir, entry.packageName) mkdirSync(dirname(link), { recursive: true }) - if (isPackagedExecutable()) { - const source = packageProxySource(packageName, target) - if (Object.keys(source.targets).length > 0) { - ensureModuleProxy(link, packageName, source.version, source.targets) - } + if (entry.kind === 'proxy') { + ensureModuleProxy(link, entry.packageName, entry.version, entry.targets) } else { - ensureSymlink(link, target) + ensureSymlink(link, entry.packageDir) } } } diff --git a/packages/boot/app-boot/tests/profile.spec.ts b/packages/boot/app-boot/tests/profile.spec.ts index 552bf79174..2e3b4e6187 100644 --- a/packages/boot/app-boot/tests/profile.spec.ts +++ b/packages/boot/app-boot/tests/profile.spec.ts @@ -4,7 +4,7 @@ * empty-root composition, and the installation module-fallback healing. */ -import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { withFileLock } from '@deepseek-ai/dsh-atomic-write' @@ -321,6 +321,20 @@ describe('healProfilesModuleFallback', () => { expect(readlinkSync(join(fallback, 'dsh-app'))).toContain('app') }) + it('retains current links while repairing a missing sibling', async () => { + const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } }) + const home = tmp() + const fallback = join(home, 'profiles', 'node_modules') + await healProfilesModuleFallback(anchor, home) + const appTarget = readlinkSync(join(fallback, 'dsh-app')) + unlinkSync(join(fallback, 'bundle-a')) + + await healProfilesModuleFallback(anchor, home) + + expect(readlinkSync(join(fallback, 'dsh-app'))).toBe(appTarget) + expect(lstatSync(join(fallback, 'bundle-a')).isSymbolicLink()).toBe(true) + }) + it('serializes concurrent healers and retains the identical link', async () => { const anchor = stageInstallation({}) const home = tmp() @@ -332,6 +346,31 @@ describe('healProfilesModuleFallback', () => { expect(lstatSync(join(fallback, 'dsh-app')).isSymbolicLink()).toBe(true) }) + it('does not acquire the writer lock for a complete generation', async () => { + const anchor = stageInstallation({}) + const home = tmp() + const modules = join(home, 'profiles', 'node_modules') + await healProfilesModuleFallback(anchor, home) + let releaseLock: (() => void) | undefined + let reportLock: (() => void) | undefined + const lockHeld = new Promise((resolve) => { reportLock = resolve }) + const release = new Promise((resolve) => { releaseLock = resolve }) + const holder = withFileLock(modules, async () => { + reportLock?.() + await release + }) + await lockHeld + + const healer = healProfilesModuleFallback(anchor, home) + const outcome = await Promise.race([ + healer.then(() => 'complete' as const), + new Promise<'blocked'>(resolve => setTimeout(() => { resolve('blocked') }, 100)), + ]) + releaseLock?.() + await Promise.all([holder, healer]) + expect(outcome).toBe('complete') + }) + it('waits for the module-fallback writer lock before publishing entries', async () => { const anchor = stageInstallation({}) const home = tmp() From 8dc3b0380e96a5b1a7c9e2d8b7641a1af8b07e99 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:50:41 +0800 Subject: [PATCH 11/16] feat(bundle): ship the standalone sdk-minimal profile Add a startup-only sdk-minimal template whose sole bundle inserts the complete JSON-RPC agent tree over the empty profile root. The roster is an explicit composition allowlist: it contains one DeepSeek adapter, the minimal agent spine, persistent Bash, the string-replace editor, local execution, and JSONL persistence, while dsh-base and Web remain absent. Reuse the SDK app startup provider so the new profile retains help, stdin EOF, and bounded launcher shutdown semantics. Make that provider render its configured profile name, which keeps both sdk and sdk-minimal help truthful without duplicating process lifecycle code. Register the package in the CLI closure, TypeScript graph, lockfile, Knip policy, and bilingual bundle references. Exact manifest, row-roster, profile-template, config-dump, and HMR tests make later additions visible instead of relying on a blacklist. --- apps/cli/package.json | 1 + apps/cli/tests/built-bin.e2e.ts | 33 +++++++ apps/cli/tests/profile-hmr.spec.ts | 6 +- knip.json | 5 + packages/boot/app-boot/src/profile.ts | 4 + packages/boot/app-boot/tests/profile.spec.ts | 4 + packages/bundle/README.i18n.yaml | 4 +- packages/bundle/README.md | 1 + packages/bundle/README.zh.md | 1 + packages/bundle/sdk-app/README.i18n.yaml | 4 +- packages/bundle/sdk-app/README.md | 6 +- packages/bundle/sdk-app/README.zh.md | 6 +- packages/bundle/sdk-app/cordis.patch.yml | 2 + packages/bundle/sdk-app/package.json | 1 + packages/bundle/sdk-app/src/index.ts | 24 ++++- packages/bundle/sdk-app/tests/startup.spec.ts | 10 +- packages/bundle/sdk-app/tsconfig.json | 3 + packages/bundle/sdk-minimal/README.i18n.yaml | 6 ++ packages/bundle/sdk-minimal/README.md | 31 ++++++ packages/bundle/sdk-minimal/README.zh.md | 31 ++++++ packages/bundle/sdk-minimal/cordis.patch.yml | 97 +++++++++++++++++++ packages/bundle/sdk-minimal/package.json | 67 +++++++++++++ packages/bundle/sdk-minimal/src/index.ts | 9 ++ packages/bundle/sdk-minimal/src/invariant.ts | 26 +++++ .../sdk-minimal/tests/sdk-minimal.spec.ts | 59 +++++++++++ packages/bundle/sdk-minimal/tsconfig.json | 18 ++++ pnpm-lock.yaml | 64 ++++++++++++ tsconfig.host.json | 1 + 28 files changed, 510 insertions(+), 14 deletions(-) create mode 100644 packages/bundle/sdk-minimal/README.i18n.yaml create mode 100644 packages/bundle/sdk-minimal/README.md create mode 100644 packages/bundle/sdk-minimal/README.zh.md create mode 100644 packages/bundle/sdk-minimal/cordis.patch.yml create mode 100644 packages/bundle/sdk-minimal/package.json create mode 100644 packages/bundle/sdk-minimal/src/index.ts create mode 100644 packages/bundle/sdk-minimal/src/invariant.ts create mode 100644 packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts create mode 100644 packages/bundle/sdk-minimal/tsconfig.json diff --git a/apps/cli/package.json b/apps/cli/package.json index 49f7fc84d1..72e81a8208 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -58,6 +58,7 @@ "@deepseek-ai/dsh-session-projection": "workspace:^", "@deepseek-ai/dsh-session-reference": "workspace:^", "@deepseek-ai/dsh-sdk-app": "workspace:^", + "@deepseek-ai/dsh-sdk-minimal": "workspace:^", "@deepseek-ai/dsh-time-context": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", "@deepseek-ai/dsh-skill-filesystem": "workspace:^", diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts index 2fffff6700..397e960075 100644 --- a/apps/cli/tests/built-bin.e2e.ts +++ b/apps/cli/tests/built-bin.e2e.ts @@ -12,7 +12,9 @@ import { type SessionNotification, } from '@agentclientprotocol/sdk' import { startMockLlmServer } from '@deepseek-ai/dsh-llm-mock-server' +import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' import { execa } from 'execa' +import * as yaml from 'js-yaml' import { afterEach, beforeEach, describe, expect, it } from 'vitest' /** Published-entry acceptance for argument errors, profile lifecycle, and boot-free config dumps. */ @@ -924,6 +926,37 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', expect(stdout).not.toMatch(/name: '@deepseek-ai\/dsh-client-/) }, 30_000) + it('prints the exact standalone sdk-minimal tree without dsh-base', async () => { + const { stdout, code, stderr } = await runBuiltBin( + ['--profile', 'sdk-minimal', '--dump-default-config'], + { DSH_HOME: home }, + ) + expect(code).toBe(0) + expect(stderr).toBe('') + const rows = yaml.load(stdout, { schema: entryListSchema }) as Array<{ id?: string; name?: string }> + expect(rows.map(row => [row.id, row.name])).toEqual([ + ['sdk-app-startup', '@deepseek-ai/dsh-sdk-app'], + ['sdk-jsonrpc-server', '@deepseek-ai/dsh-sdk-jsonrpc-server'], + ['deepseek-llm-api-extensions', '@deepseek-ai/dsh-deepseek-llm-api-extensions'], + ['session-log-deepseek', '@deepseek-ai/dsh-session-log-deepseek'], + ['plugin-package-inventory-deepseek', '@deepseek-ai/dsh-plugin-package-inventory-deepseek'], + ['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek'], + ['sandbox', '@deepseek-ai/dsh-sandbox-local'], + ['sandbox-policy', '@deepseek-ai/dsh-sandbox-policy'], + ['subprocess', '@deepseek-ai/dsh-subprocess-local'], + ['pty', '@deepseek-ai/dsh-terminal'], + ['terminal-bash', '@deepseek-ai/dsh-terminal-bash'], + ['fs-local', '@deepseek-ai/dsh-fs-local'], + ['agent-spine', '@deepseek-ai/dsh-agent-spine-demo'], + ['persistent-bash', '@deepseek-ai/dsh-tool-bash-persistent'], + ['str-replace-editor', '@deepseek-ai/dsh-tool-str-replace-editor'], + ['sessions', '@deepseek-ai/dsh-session-persistence-jsonl'], + ]) + expect(stdout).toContain('# == @deepseek-ai/dsh-sdk-minimal') + expect(stdout).not.toContain('@deepseek-ai/dsh-base') + expect(stdout).not.toContain('@deepseek-ai/dsh-web-app') + }, 30_000) + it('composes the profile user layer and a --patch overlay in order', async () => { // Auto-init the web profile first, then write its user layer. const init = await runBuiltBin(['--profile', 'web', '--dump-default-config'], { DSH_HOME: home }) diff --git a/apps/cli/tests/profile-hmr.spec.ts b/apps/cli/tests/profile-hmr.spec.ts index 6fed867d92..306bfe1458 100644 --- a/apps/cli/tests/profile-hmr.spec.ts +++ b/apps/cli/tests/profile-hmr.spec.ts @@ -9,7 +9,7 @@ import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include' const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)) /** Load one shipped bundle patch through the same parser as profile boot. */ -function bundle(name: 'acp-app' | 'base' | 'headless' | 'sdk-app' | 'web-app'): PatchOptions[] { +function bundle(name: 'acp-app' | 'base' | 'headless' | 'sdk-app' | 'sdk-minimal' | 'web-app'): PatchOptions[] { return loadOverlayPatches('profile-hmr test', join(REPOSITORY_ROOT, 'packages', 'bundle', name, 'cordis.patch.yml')) } @@ -39,4 +39,8 @@ describe('profile module-HMR policy', () => { config: { root: ['.'] }, }) }) + + it('keeps the standalone sdk-minimal tree free of module HMR', () => { + expect(composeEntries([bundle('sdk-minimal')]).find(entry => entry.id === 'hmr')).toBeUndefined() + }) }) diff --git a/knip.json b/knip.json index 6e03ebf7da..e8dc6139ee 100644 --- a/knip.json +++ b/knip.json @@ -699,6 +699,11 @@ "@deepseek-ai/dsh-sdk-jsonrpc-server" ] }, + "packages/bundle/sdk-minimal": { + "ignoreDependencies": [ + "@deepseek-ai/.+" + ] + }, "packages/bundle/web-app": { "ignoreDependencies": [ "@deepseek-ai/.+" diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts index 17c6cf65ab..b99dca29c7 100644 --- a/packages/boot/app-boot/src/profile.ts +++ b/packages/boot/app-boot/src/profile.ts @@ -146,6 +146,10 @@ export const PROFILE_TEMPLATES: Record = { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-sdk-app'], patchReload: 'startup', }, + 'sdk-minimal': { + bundles: ['@deepseek-ai/dsh-sdk-minimal'], + patchReload: 'startup', + }, } /** Installation-owned bundle tuples normalized to the shipped template. */ diff --git a/packages/boot/app-boot/tests/profile.spec.ts b/packages/boot/app-boot/tests/profile.spec.ts index 2e3b4e6187..4e7a2c5ddf 100644 --- a/packages/boot/app-boot/tests/profile.spec.ts +++ b/packages/boot/app-boot/tests/profile.spec.ts @@ -173,6 +173,10 @@ describe('loadProfile', () => { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-sdk-app'], patchReload: 'startup', }) + expect(PROFILE_TEMPLATES['sdk-minimal']).toEqual({ + bundles: ['@deepseek-ai/dsh-sdk-minimal'], + patchReload: 'startup', + }) try { loadProfile('t', 'web', anchor, home) } catch { diff --git a/packages/bundle/README.i18n.yaml b/packages/bundle/README.i18n.yaml index 5f7fbd40f4..1ca618d528 100644 --- a/packages/bundle/README.i18n.yaml +++ b/packages/bundle/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/bundle/README.md -README.md: d6b24a276fa64bb2eb80c2aad1783795e351ebc4 -README.zh.md: 36acc510cfab7979d28052ab26687dce58175155 +README.md: 44cf46a217e078e4d71d838afb6525f2bc908bd2 +README.zh.md: f2987b72c526698e2df949e2edfe55e1375f2d8e diff --git a/packages/bundle/README.md b/packages/bundle/README.md index d6b24a276f..44cf46a217 100644 --- a/packages/bundle/README.md +++ b/packages/bundle/README.md @@ -13,5 +13,6 @@ The manifest declaration, not this directory, defines Bundle identity. Domain pa | [`web-app/`](web-app/README.md) | Browser surface: web patch layer + runtime glue plugin | mounts rows | | [`headless/`](headless/README.md) | Direct one-shot task mode over base, with no Host or Web layer | mounts `headless-runner` | | [`sdk-app/`](sdk-app/README.md) | SDK stdio JSON-RPC application over base | mounts the SDK server | +| [`sdk-minimal/`](sdk-minimal/README.md) | Standalone minimal SDK application without base or Web | — (complete patch tree) | In-box bundles resolve from the dsh installation; out-of-tree bundles install into a profile through `dsh plugin --profile add `. diff --git a/packages/bundle/README.zh.md b/packages/bundle/README.zh.md index 36acc510cf..f2987b72c5 100644 --- a/packages/bundle/README.zh.md +++ b/packages/bundle/README.zh.md @@ -13,5 +13,6 @@ Bundle 身份由 manifest 声明决定,而不是由本目录决定。领域包 | [`web-app/`](web-app/README.zh.md) | 浏览器表层:web patch 层 + 运行时粘合插件 | 挂载多条配置行 | | [`headless/`](headless/README.zh.md) | 直接运行在 base 之上的一次性任务模式,不含 Host 或 Web 层 | 挂载 `headless-runner` | | [`sdk-app/`](sdk-app/README.zh.md) | 运行在 base 之上的 SDK stdio JSON-RPC 应用 | 挂载 SDK server | +| [`sdk-minimal/`](sdk-minimal/README.zh.md) | 不含 base 或 Web 的独立极简 SDK 应用 | 无(完整 patch 树) | 内置组合包从 dsh 安装目录解析;树外(out-of-tree)组合包通过 `dsh plugin --profile add ` 安装进 profile。 diff --git a/packages/bundle/sdk-app/README.i18n.yaml b/packages/bundle/sdk-app/README.i18n.yaml index 8deaf213fa..43e0f90976 100644 --- a/packages/bundle/sdk-app/README.i18n.yaml +++ b/packages/bundle/sdk-app/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/bundle/sdk-app/README.md -README.md: 0356d6f4a99d7baef6ff7619d505392ff7f7f1d2 -README.zh.md: c70eb685954ebff42bca6c3d289ab58e46298d50 +README.md: c5022bd2096fae931fff48944bfc167280e62476 +README.zh.md: 687a19dc09679d11696a207ad17db3cf463ee901 diff --git a/packages/bundle/sdk-app/README.md b/packages/bundle/sdk-app/README.md index 0356d6f4a9..c5022bd209 100644 --- a/packages/bundle/sdk-app/README.md +++ b/packages/bundle/sdk-app/README.md @@ -2,10 +2,14 @@ English | [中文](README.zh.md) -The SDK stdio application as a `dsh` profile bundle over [`dsh-base`](../base/README.md). It inherits the base's disabled module-HMR policy; its patch sets the coding-agent persona, mounts an app-owned zero-option command provider, and starts [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.md) only after that provider accepts the invocation. `dsh --profile sdk --help` therefore writes help and exits without claiming stdin or stdout. +The SDK stdio application as a `dsh` profile bundle over [`dsh-base`](../base/README.md). It inherits the base's disabled module-HMR policy; its patch sets the coding-agent persona, mounts an app-owned zero-option command provider, and starts [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.md) only after that provider accepts the invocation. `dsh --profile sdk --help` therefore writes help and exits without claiming stdin or stdout. The standalone [`sdk-minimal`](../sdk-minimal/README.md) bundle reuses the same startup provider and supplies its own profile name. The startup provider binds stdin EOF to the launcher's bounded successful shutdown. SDK protocol `shutdown`, SIGINT, and SIGTERM retain their owning server or launcher paths; disposal drains the root profile tree and persistence. Stdout is reserved for newline-delimited JSON-RPC frames. The bundle disables model-generated session titles because the SDK exposes no title surface; deterministic fallback titles remain durable without an auxiliary model request. A deployment selects a different complete composition through profile bundles and patch files, not another app bin. +| Config | Default | Behavior | +|---|---|---| +| `profile` | `sdk` | Profile name rendered in command help; a bundle mounting this provider sets its own shipped profile name. | + `DSH_MAX_TOKENS_AS_SUCCESS` retains the SDK deployment mapping: unset or JSON `true` reports token-limited subagent completion as accepted, while JSON `false` reports it as an error. Provider/model and workspace cwd arrive through the SDK initialization request; the base profile owns adapters, tools, persistence, policy, settings, and credentials. ## Model Experience diff --git a/packages/bundle/sdk-app/README.zh.md b/packages/bundle/sdk-app/README.zh.md index c70eb68595..687a19dc09 100644 --- a/packages/bundle/sdk-app/README.zh.md +++ b/packages/bundle/sdk-app/README.zh.md @@ -2,10 +2,14 @@ [English](README.md) | 中文 -以 [`dsh-base`](../base/README.zh.md) 为基础的 SDK stdio 应用 `dsh` profile 组合包。它继承 base 默认禁用模块 HMR(热模块替换)的策略;其 patch 设置 coding agent(编程智能体)persona、挂载应用自有的零选项命令提供方,并且只在该提供方接受调用后启动 [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.zh.md)。因此,`dsh --profile sdk --help` 会写出 help 并退出,不会占用 stdin 或 stdout。 +以 [`dsh-base`](../base/README.zh.md) 为基础的 SDK stdio 应用 `dsh` profile 组合包。它继承 base 默认禁用模块 HMR(热模块替换)的策略;其 patch 设置 coding agent(编程智能体)persona、挂载应用自有的零选项命令提供方,并且只在该提供方接受调用后启动 [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.zh.md)。因此,`dsh --profile sdk --help` 会写出 help 并退出,不会占用 stdin 或 stdout。独立的 [`sdk-minimal`](../sdk-minimal/README.zh.md) 组合包复用同一个启动提供方,并提供自己的 profile 名称。 启动提供方把 stdin EOF 接到启动器的有界成功关闭流程。SDK 协议 `shutdown`、SIGINT 与 SIGTERM 继续使用各自所属的 server 或启动器路径;dispose(资源释放)会排空根 profile 配置树与持久化。stdout 专用于按换行分隔的 JSON-RPC 帧。SDK 不提供 title 表层,因此本组合包禁用模型生成的 session title;确定性的 fallback title 仍会持久化,但不发起辅助模型请求。部署通过 profile 组合包与 patch 文件选择另一套完整组合,而不是使用另一个应用 bin。 +| 配置 | 默认值 | 行为 | +|---|---|---| +| `profile` | `sdk` | 命令 help 中呈现的 profile 名称;挂载此提供方的组合包会设置自己的随附 profile 名称。 | + `DSH_MAX_TOKENS_AS_SUCCESS` 保留 SDK 部署映射:未设置或 JSON `true` 把 token 达限的 subagent 完成报告为已接受,JSON `false` 则报告为错误。模型提供方/模型与工作区 cwd 通过 SDK 初始化请求传入;base profile 拥有适配器、工具、持久化、策略、settings 与 credentials。 ## 模型体验 diff --git a/packages/bundle/sdk-app/cordis.patch.yml b/packages/bundle/sdk-app/cordis.patch.yml index aa1795168c..373e7aeb63 100644 --- a/packages/bundle/sdk-app/cordis.patch.yml +++ b/packages/bundle/sdk-app/cordis.patch.yml @@ -11,6 +11,8 @@ - insert: - id: sdk-app-startup name: '@deepseek-ai/dsh-sdk-app' + config: + profile: sdk - id: sdk-jsonrpc-server name: '@deepseek-ai/dsh-sdk-jsonrpc-server' diff --git a/packages/bundle/sdk-app/package.json b/packages/bundle/sdk-app/package.json index 87214882fb..6ab4167d8d 100644 --- a/packages/bundle/sdk-app/package.json +++ b/packages/bundle/sdk-app/package.json @@ -41,6 +41,7 @@ "dependencies": { "@deepseek-ai/dsh-cmdline": "workspace:^", "@deepseek-ai/dsh-sdk-jsonrpc-server": "workspace:^", + "@deepseek-ai/schemastery": "workspace:^", "commander": "^15.0.0" }, "peerDependencies": { diff --git a/packages/bundle/sdk-app/src/index.ts b/packages/bundle/sdk-app/src/index.ts index 9ade81a965..fec847af53 100644 --- a/packages/bundle/sdk-app/src/index.ts +++ b/packages/bundle/sdk-app/src/index.ts @@ -7,6 +7,7 @@ import { Command } from 'commander' import type { Context } from '@deepseek-ai/cordis' +import z from '@deepseek-ai/schemastery' import { exitOnStdinEnd, parseCmdline } from '@deepseek-ai/dsh-cmdline' /** Stable Cordis plugin name. */ @@ -18,18 +19,30 @@ export const inject = ['cmdlineArgs'] /** Service the JSON-RPC server row waits for before claiming stdio. */ export const SDK_APP_STARTUP_SERVICE = 'sdkAppStartup' +/** SDK stdio startup configuration. */ +export interface Config { + /** Profile name rendered in help and diagnostics (default `sdk`). */ + profile?: string +} + +/** Validate and default SDK stdio startup configuration. */ +export const Config: z = z.object({ + profile: z.string().default('sdk'), +}) + /** * Build this app's zero-option command and help. + * @param profile - selected profile name rendered in the command grammar. * @returns a fresh program for one invocation. */ -function sdkCommand(): Command { +function sdkCommand(profile: string): Command { return new Command() - .name('dsh --profile sdk') + .name(`dsh --profile ${profile}`) .description('Serve DeepSeek Harness SDK clients over stdio JSON-RPC.') .helpOption('-h, --help', 'show this help') .addHelpText('after', ` Example: - dsh --profile sdk serve one SDK runtime until its client disconnects + dsh --profile ${profile} serve one SDK runtime until its client disconnects `) } @@ -37,9 +50,10 @@ Example: * Accept an SDK profile invocation, publish readiness, and bind EOF to the * launcher's bounded shutdown. * @param ctx - plugin context carrying command-line and exit launcher values. + * @param config - selected profile identity for command help. */ -export function apply(ctx: Context): void { - const program = sdkCommand() +export function apply(ctx: Context, config: Config = {}): void { + const program = sdkCommand(config.profile ?? 'sdk') program.action(() => { exitOnStdinEnd(ctx, 'sdk-app.stdin') ctx.provide(SDK_APP_STARTUP_SERVICE, { accepted: true }) diff --git a/packages/bundle/sdk-app/tests/startup.spec.ts b/packages/bundle/sdk-app/tests/startup.spec.ts index 65f2b76c5d..5f0ec6524a 100644 --- a/packages/bundle/sdk-app/tests/startup.spec.ts +++ b/packages/bundle/sdk-app/tests/startup.spec.ts @@ -27,7 +27,7 @@ afterEach(() => { }) /** Run the provider with captured command output and exit requests. */ -function start(args: string[]): { ctx: Context; exits: number[]; out: () => string; stdin: TestStdin } { +function start(args: string[], profile = 'sdk'): { ctx: Context; exits: number[]; out: () => string; stdin: TestStdin } { const ctx = new Context() const exits: number[] = [] const stdin = new TestStdin() @@ -41,7 +41,7 @@ function start(args: string[]): { ctx: Context; exits: number[]; out: () => stri exit: code => void exits.push(code), ready: { onReady: (listener) => { listener(); return () => {} } }, }) - apply(ctx) + apply(ctx, { profile }) return { ctx, exits, out: () => out, stdin } } @@ -62,4 +62,10 @@ describe('SDK app startup', () => { stdin.end() expect(exits).toEqual([0]) }) + + it('renders the selected SDK profile name in help', () => { + const { out } = start(['--help'], 'sdk-minimal') + expect(out()).toContain('Usage: dsh --profile sdk-minimal') + expect(out()).toContain('dsh --profile sdk-minimal') + }) }) diff --git a/packages/bundle/sdk-app/tsconfig.json b/packages/bundle/sdk-app/tsconfig.json index 1d644141bd..0a98d3117c 100644 --- a/packages/bundle/sdk-app/tsconfig.json +++ b/packages/bundle/sdk-app/tsconfig.json @@ -11,6 +11,9 @@ { "path": "../../../vendor/cordis" }, + { + "path": "../../../vendor/schemastery" + }, { "path": "../../runtime-diagnostics/invariants" }, diff --git a/packages/bundle/sdk-minimal/README.i18n.yaml b/packages/bundle/sdk-minimal/README.i18n.yaml new file mode 100644 index 0000000000..51cf2a832d --- /dev/null +++ b/packages/bundle/sdk-minimal/README.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 packages/bundle/sdk-minimal/README.md +README.md: 5bc60991624ab52b3d55acac4851b6af2b198218 +README.zh.md: 7e11140bcb8a81cf3e22db88530bfcb364436f96 diff --git a/packages/bundle/sdk-minimal/README.md b/packages/bundle/sdk-minimal/README.md new file mode 100644 index 0000000000..5bc6099162 --- /dev/null +++ b/packages/bundle/sdk-minimal/README.md @@ -0,0 +1,31 @@ +# `@deepseek-ai/dsh-sdk-minimal` + +English | [中文](README.zh.md) + +Standalone minimal SDK application bundle for `dsh --profile sdk-minimal`. Its single insert is the complete Cordis tree: SDK stdio startup and JSON-RPC serving, one environment-configured DeepSeek adapter, the executor-less agent spine, local subprocess and unrestricted filesystem providers, a persistent Bash PTY, the string-replace editor, and uncompressed JSONL session persistence under `$DSH_HOME/sessions`. It deliberately does not include [`dsh-base`](../base/README.md), Web, settings, managed credentials, telemetry, compaction, workspace instructions, skills, jobs tools, subagents, or any other model-facing tool. + +The profile remains part of the ordinary launcher and layering model. The bundle supplies the complete default tree; the profile patch, home patch, and ordered `--patch` files can replace rows or insert external bundles above it. `dsh plugin --profile sdk-minimal` manages persistent dependencies. The shipped template uses startup-only patches so one stdio connection never observes replacement of its server or agent dependencies. + +`DEEPSEEK_API_KEY` supplies the adapter credential. `DSH_MODEL` selects the sole configured model, `DSH_CONTEXT_WINDOW` sets its context window, and `DSH_SYSTEM_PROMPT` replaces the default persona. The process working directory is the sandbox-policy workspace and local-filesystem root. The bundle sets `danger-full-access`; its persistent shell and editor can modify any path available to the process. + +## Model Experience + +### Minimal coding-agent composition + +#### What the model sees + +The system prompt is `DSH_SYSTEM_PROMPT` or `You are a helpful software engineer assistant.`. The only advertised tools are owner-scoped persistent `bash` and `str_replace_editor`; runtime context, workspace instructions, skills, jobs controls, compaction, and Harness identity are absent. + +#### Token effect + +One stable persona plus the two tool schemas. Tool results and ordinary conversation history grow with the session. + +#### KV Cache effect + +Stable for a fixed persona, platform, provider, model, and bundle patch stack. Profile changes take effect on the next process. + +## Known Limitations and Deferred Work + +- **The profile is POSIX-only** — this layer uses a Bash PTY; Windows support belongs to the platform runtime layer above it. +- **The composition intentionally omits shared product services** — select `dsh --profile sdk` when settings, managed credentials, policy presets, telemetry, Web tools, or the full default tool roster are required. +- **User patches can expand the tree and corrupt stdout** — profile customization is trusted application composition; a plugin that writes ordinary text to stdout can break JSON-RPC framing. diff --git a/packages/bundle/sdk-minimal/README.zh.md b/packages/bundle/sdk-minimal/README.zh.md new file mode 100644 index 0000000000..7e11140bcb --- /dev/null +++ b/packages/bundle/sdk-minimal/README.zh.md @@ -0,0 +1,31 @@ +# `@deepseek-ai/dsh-sdk-minimal` + +[English](README.md) | 中文 + +供 `dsh --profile sdk-minimal` 使用的独立极简 SDK 应用组合包。它的单个 insert 构成完整 Cordis 树:SDK stdio 启动与 JSON-RPC 对外服务、一个由环境配置的 DeepSeek 适配器、无执行器的 agent 主干、本地子进程与不受限文件系统提供方、持久 Bash PTY、字符串替换编辑器,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 会话持久化。它刻意不包含 [`dsh-base`](../base/README.zh.md)、Web、settings、托管凭据、遥测、压缩(compaction)、workspace 指令、skills、jobs 工具、subagent 或任何其他面向模型的工具。 + +该 profile 仍遵循普通 launcher 与分层模型。组合包提供完整默认树;profile patch、home patch 与有序 `--patch` 文件可以在其上替换配置项或插入外部组合包。`dsh plugin --profile sdk-minimal` 管理持久依赖。随附模板仅在启动时应用 patch,因此一个 stdio 连接不会观察到服务器或 agent 依赖在运行中被替换。 + +`DEEPSEEK_API_KEY` 提供适配器凭据。`DSH_MODEL` 选择唯一配置的模型,`DSH_CONTEXT_WINDOW` 设置其上下文窗口,`DSH_SYSTEM_PROMPT` 替换默认 persona。进程工作目录同时作为沙箱策略 workspace 与本地文件系统根目录。该组合包设置 `danger-full-access`;其持久 shell 与编辑器可以修改进程可访问的任何路径。 + +## 模型体验 + +### 极简 coding agent 组合 + +#### 模型看到的内容 + +系统提示词取 `DSH_SYSTEM_PROMPT`,未设置时使用 `You are a helpful software engineer assistant.`。对外公布的工具只有 agent 所有的持久 `bash` 与 `str_replace_editor`;运行时上下文、workspace 指令、skills、jobs 控制、compaction 与 Harness 身份均不存在。 + +#### Token 影响 + +一个稳定 persona 加两个工具 schema。工具结果与普通对话历史随会话增长。 + +#### KV Cache 影响 + +当 persona、平台、提供方、模型与组合包 patch 栈固定时保持稳定。Profile 变更在下一个进程生效。 + +## 已知限制与待办工作 + +- **该 profile 仅支持 POSIX** — 此层使用 Bash PTY;Windows 支持属于其上的平台运行时层。 +- **该组合刻意省略共享产品服务** — 需要 settings、托管凭据、权限策略预设、遥测、Web 工具或完整默认工具清单时,请选择 `dsh --profile sdk`。 +- **用户 patch 可以扩展配置树并破坏 stdout** — profile 自定义属于受信任的应用组合;向 stdout 写入普通文本的插件会破坏 JSON-RPC 分帧。 diff --git a/packages/bundle/sdk-minimal/cordis.patch.yml b/packages/bundle/sdk-minimal/cordis.patch.yml new file mode 100644 index 0000000000..e43f3e2b03 --- /dev/null +++ b/packages/bundle/sdk-minimal/cordis.patch.yml @@ -0,0 +1,97 @@ +# Standalone minimal SDK application. Unlike the ordinary SDK profile, this +# bundle does not layer over dsh-base: this insert is the complete Cordis tree. +# User profile, home, and invocation patches still apply above it. + +- insert: + - id: sdk-app-startup + name: '@deepseek-ai/dsh-sdk-app' + config: + profile: sdk-minimal + + - id: sdk-jsonrpc-server + name: '@deepseek-ai/dsh-sdk-jsonrpc-server' + inject: [sdkAppStartup, loader] + config: + maxTokensAsSuccess: false + + - id: deepseek-llm-api-extensions + name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' + + - id: session-log-deepseek + name: '@deepseek-ai/dsh-session-log-deepseek' + + - id: plugin-package-inventory-deepseek + name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek' + + - id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek' + config: + apiKeyEnv: DEEPSEEK_API_KEY + streamIdleTimeoutMs: 172800000 + models: + - id: !!js process.env.DSH_MODEL ?? 'deepseek-v4-flash' + contextWindow: !!js Number(process.env.DSH_CONTEXT_WINDOW ?? 1000000) + + - id: sandbox + name: '@deepseek-ai/dsh-sandbox-local' + + - id: sandbox-policy + name: '@deepseek-ai/dsh-sandbox-policy' + config: + mode: danger-full-access + workspaceRoot: !!js process.cwd() + + - id: subprocess + name: '@deepseek-ai/dsh-subprocess-local' + + - id: pty + name: '@deepseek-ai/dsh-terminal' + + - id: terminal-bash + name: '@deepseek-ai/dsh-terminal-bash' + config: + timeoutMs: 300000 + + # The editor uses the bare local filesystem; persistent Bash still consumes + # the shared danger-full-access sandbox policy above. + - id: fs-local + name: '@deepseek-ai/dsh-fs-local' + config: + cwd: !!js process.cwd() + + - id: agent-spine + name: '@deepseek-ai/dsh-agent-spine-demo' + config: + includeHarnessIdentity: false + includeRuntimeContext: false + persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.' + workspaceContext: false + skills: + enabled: false + toolBash: false + toolJobs: false + + - id: persistent-bash + name: '@deepseek-ai/dsh-tool-bash-persistent' + config: + timeoutMs: 300000 + description: |- + Run commands in a bash shell + * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped. + * You don't have access to the internet via this tool. + * You do have access to a mirror of common linux and python packages via apt and pip. + * State is persistent across command calls and discussions with the user. + * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'. + * Please avoid commands that may produce a very large amount of output. + * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background. + + - id: str-replace-editor + name: '@deepseek-ai/dsh-tool-str-replace-editor' + config: + maxOutputChars: 16000 + + - id: sessions + name: '@deepseek-ai/dsh-session-persistence-jsonl' + config: + root: !!js dshHomePath('sessions') + compression: none diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json new file mode 100644 index 0000000000..1b3d5da5d2 --- /dev/null +++ b/packages/bundle/sdk-minimal/package.json @@ -0,0 +1,67 @@ +{ + "name": "@deepseek-ai/dsh-sdk-minimal", + "description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, editor, and JSONL sessions", + "version": "0.1.1-rc.2", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/bundle/sdk-minimal" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, + "./cordis.patch.yml": "./cordis.patch.yml", + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/invariant.js", + "cordis.patch.yml", + "lib/types/**/*.d.ts" + ], + "license": "MIT", + "dsh": { + "bundle": { + "patch": "./cordis.patch.yml" + } + }, + "dependencies": { + "@deepseek-ai/dsh-agent-spine-demo": "workspace:^", + "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^", + "@deepseek-ai/dsh-fs-local": "workspace:^", + "@deepseek-ai/dsh-llm-deepseek": "workspace:^", + "@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:^", + "@deepseek-ai/dsh-sandbox-local": "workspace:^", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", + "@deepseek-ai/dsh-sdk-app": "workspace:^", + "@deepseek-ai/dsh-sdk-jsonrpc-server": "workspace:^", + "@deepseek-ai/dsh-session-log-deepseek": "workspace:^", + "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", + "@deepseek-ai/dsh-subprocess-local": "workspace:^", + "@deepseek-ai/dsh-terminal": "workspace:^", + "@deepseek-ai/dsh-terminal-bash": "workspace:^", + "@deepseek-ai/dsh-tool-bash-persistent": "workspace:^", + "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^" + }, + "peerDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + }, + "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + } +} diff --git a/packages/bundle/sdk-minimal/src/index.ts b/packages/bundle/sdk-minimal/src/index.ts new file mode 100644 index 0000000000..a5f162161e --- /dev/null +++ b/packages/bundle/sdk-minimal/src/index.ts @@ -0,0 +1,9 @@ +/** + * @deepseek-ai/dsh-sdk-minimal — the standalone minimal SDK profile bundle. + * The package's substance is `cordis.patch.yml`, declared by the + * `dsh.bundle.patch` manifest field and resolved by the profile composer; + * this module carries no runtime interface. + * @module @deepseek-ai/dsh-sdk-minimal + */ + +export {} diff --git a/packages/bundle/sdk-minimal/src/invariant.ts b/packages/bundle/sdk-minimal/src/invariant.ts new file mode 100644 index 0000000000..e2f480504a --- /dev/null +++ b/packages/bundle/sdk-minimal/src/invariant.ts @@ -0,0 +1,26 @@ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-sdk-minimal`. + * @module @deepseek-ai/dsh-sdk-minimal/invariant + */ + +import type { Context } from '@deepseek-ai/cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-sdk-minimal' + +/** Cordis companion plugin name. */ +export const name = 'sdk-minimal-bundle-invariant' +/** Service required before the companion can register. */ +export const inject = ['invariants'] + +// No runtime invariant: the package is a static patch-list carrier whose +// inserted rows own their runtime relationships and invariant companions. +const install: InvariantInstaller = () => {} + +/** + * Register this package's invariant companion. + * @param ctx - Cordis context carrying the invariant service. + * @returns the installed registration's disposer after setup succeeds. + */ +export const apply = (ctx: Context): Promise<() => void> => + Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) diff --git a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts new file mode 100644 index 0000000000..4e9fbbcc6a --- /dev/null +++ b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts @@ -0,0 +1,59 @@ +/** The standalone SDK-minimal bundle's complete declared Cordis tree. */ + +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import * as yaml from 'js-yaml' +import { describe, expect, it } from 'vitest' +import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' + +describe('dsh-sdk-minimal bundle', () => { + it('declares one standalone allowlisted tree with every row dependency', () => { + const root = fileURLToPath(new URL('..', import.meta.url)) + const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { + dependencies?: Record + dsh?: { bundle?: { patch?: string } } + } + expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml') + const patches = yaml.load( + readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'), + { schema: entryListSchema }, + ) as Array<{ insert?: Array<{ id?: string; inject?: string[]; name?: string; config?: Record }> }> + expect(patches).toHaveLength(1) + const rows = patches[0]?.insert ?? [] + expect(rows.map(row => [row.id, row.name])).toEqual([ + ['sdk-app-startup', '@deepseek-ai/dsh-sdk-app'], + ['sdk-jsonrpc-server', '@deepseek-ai/dsh-sdk-jsonrpc-server'], + ['deepseek-llm-api-extensions', '@deepseek-ai/dsh-deepseek-llm-api-extensions'], + ['session-log-deepseek', '@deepseek-ai/dsh-session-log-deepseek'], + ['plugin-package-inventory-deepseek', '@deepseek-ai/dsh-plugin-package-inventory-deepseek'], + ['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek'], + ['sandbox', '@deepseek-ai/dsh-sandbox-local'], + ['sandbox-policy', '@deepseek-ai/dsh-sandbox-policy'], + ['subprocess', '@deepseek-ai/dsh-subprocess-local'], + ['pty', '@deepseek-ai/dsh-terminal'], + ['terminal-bash', '@deepseek-ai/dsh-terminal-bash'], + ['fs-local', '@deepseek-ai/dsh-fs-local'], + ['agent-spine', '@deepseek-ai/dsh-agent-spine-demo'], + ['persistent-bash', '@deepseek-ai/dsh-tool-bash-persistent'], + ['str-replace-editor', '@deepseek-ai/dsh-tool-str-replace-editor'], + ['sessions', '@deepseek-ai/dsh-session-persistence-jsonl'], + ]) + expect(rows.find(row => row.id === 'sdk-app-startup')?.config).toEqual({ profile: 'sdk-minimal' }) + expect(rows.find(row => row.id === 'sdk-jsonrpc-server')).toMatchObject({ + inject: ['sdkAppStartup', 'loader'], + config: { maxTokensAsSuccess: false }, + }) + expect(rows.find(row => row.id === 'agent-spine')?.config).toMatchObject({ + includeHarnessIdentity: false, + includeRuntimeContext: false, + workspaceContext: false, + skills: { enabled: false }, + toolBash: false, + toolJobs: false, + }) + expect(Object.keys(manifest.dependencies ?? {}).sort()).toEqual( + [...new Set(rows.map(row => row.name).filter((name): name is string => name !== undefined))].sort(), + ) + }) +}) diff --git a/packages/bundle/sdk-minimal/tsconfig.json b/packages/bundle/sdk-minimal/tsconfig.json new file mode 100644 index 0000000000..8f58ed6e28 --- /dev/null +++ b/packages/bundle/sdk-minimal/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../runtime-diagnostics/invariants" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index daec43dbff..a6b3e12d45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,9 @@ importers: '@deepseek-ai/dsh-sdk-app': specifier: workspace:^ version: link:../../packages/bundle/sdk-app + '@deepseek-ai/dsh-sdk-minimal': + specifier: workspace:^ + version: link:../../packages/bundle/sdk-minimal '@deepseek-ai/dsh-session-projection': specifier: workspace:^ version: link:../../packages/session/session-projection @@ -1524,6 +1527,9 @@ importers: '@deepseek-ai/dsh-sdk-jsonrpc-server': specifier: workspace:^ version: link:../../sdk/server + '@deepseek-ai/schemastery': + specifier: link:../../../vendor/schemastery + version: link:../../../vendor/schemastery commander: specifier: ^15.0.0 version: 15.0.0 @@ -1538,6 +1544,64 @@ importers: specifier: workspace:^ version: link:../../runtime-diagnostics/invariants + packages/bundle/sdk-minimal: + dependencies: + '@deepseek-ai/dsh-agent-spine-demo': + specifier: workspace:^ + version: link:../../examples/agent-spine-demo + '@deepseek-ai/dsh-deepseek-llm-api-extensions': + specifier: workspace:^ + version: link:../../llm/deepseek-llm-api-extensions + '@deepseek-ai/dsh-fs-local': + specifier: workspace:^ + version: link:../../fs/fs-local + '@deepseek-ai/dsh-llm-deepseek': + specifier: workspace:^ + version: link:../../llm/llm-deepseek + '@deepseek-ai/dsh-plugin-package-inventory-deepseek': + specifier: workspace:^ + version: link:../../llm/plugin-package-inventory-deepseek + '@deepseek-ai/dsh-sandbox-local': + specifier: workspace:^ + version: link:../../sandbox/sandbox-local + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../../sandbox/sandbox-policy + '@deepseek-ai/dsh-sdk-app': + specifier: workspace:^ + version: link:../sdk-app + '@deepseek-ai/dsh-sdk-jsonrpc-server': + specifier: workspace:^ + version: link:../../sdk/server + '@deepseek-ai/dsh-session-log-deepseek': + specifier: workspace:^ + version: link:../../session/session-log-deepseek + '@deepseek-ai/dsh-session-persistence-jsonl': + specifier: workspace:^ + version: link:../../session/session-persistence-jsonl + '@deepseek-ai/dsh-subprocess-local': + specifier: workspace:^ + version: link:../../subprocess/subprocess-local + '@deepseek-ai/dsh-terminal': + specifier: workspace:^ + version: link:../../terminal/terminal + '@deepseek-ai/dsh-terminal-bash': + specifier: workspace:^ + version: link:../../terminal/terminal-bash + '@deepseek-ai/dsh-tool-bash-persistent': + specifier: workspace:^ + version: link:../../shell/tool-bash-persistent + '@deepseek-ai/dsh-tool-str-replace-editor': + specifier: workspace:^ + version: link:../../fs/tool-str-replace-editor + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:^ + version: link:../../../vendor/cordis + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../runtime-diagnostics/invariants + packages/bundle/web-app: dependencies: '@deepseek-ai/dsh-agent-presets': diff --git a/tsconfig.host.json b/tsconfig.host.json index 91fbfd03f7..0bcbe50df3 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -263,6 +263,7 @@ { "path": "./packages/bundle/base" }, { "path": "./packages/bundle/headless" }, { "path": "./packages/bundle/sdk-app" }, + { "path": "./packages/bundle/sdk-minimal" }, { "path": "./packages/bundle/web-app" }, { "path": "./packages/boot/app-boot" }, { "path": "./packages/boot/cmdline" }, From 79a8f667f71ef5e09fda54fed5079fab93177ebc Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:04:24 +0800 Subject: [PATCH 12/16] refactor(python): launch the minimal example through sdk-minimal Make minimal.py select the shipped sdk-minimal profile directly and pass its selected model into the profile-owned adapter catalog. The Python SDK still starts only the bundled dsh CLI with an explicit Harness home; it no longer supplies an invocation overlay for this mode. Drive both the source keyless process test and installed-wheel smoke through the same named profile. The keyless test pins the generated profile manifest and exact two-tool model request, while the packaged smoke keeps the persistent-shell, editor, session-log, and model-visible snapshot evidence. Delete minimal.patch.yml and the unused complete-config/replay fixtures. Their composition now has one owner in @deepseek-ai/dsh-sdk-minimal, so the example and tests cannot drift into separate launch trees. --- examples/python-sdk-agent/minimal.cordis.yml | 90 ------------------- examples/python-sdk-agent/minimal.patch.yml | 47 ---------- examples/python-sdk-agent/minimal.py | 7 +- .../minimal.snapshot.cordis.yml | 20 ----- .../tests/keyless-smoke.e2e.ts | 17 ++-- scripts/smoke-python-runtime.py | 78 ++-------------- 6 files changed, 18 insertions(+), 241 deletions(-) delete mode 100644 examples/python-sdk-agent/minimal.cordis.yml delete mode 100644 examples/python-sdk-agent/minimal.patch.yml delete mode 100644 examples/python-sdk-agent/minimal.snapshot.cordis.yml diff --git a/examples/python-sdk-agent/minimal.cordis.yml b/examples/python-sdk-agent/minimal.cordis.yml deleted file mode 100644 index f849b798a0..0000000000 --- a/examples/python-sdk-agent/minimal.cordis.yml +++ /dev/null @@ -1,90 +0,0 @@ -# Complete minimal-agent composition fixture for lower-level snapshot tests. -# Python users apply `minimal.patch.yml` to `dsh --profile sdk`. This fixture -# retains the same two-tool behavior without defining a supported launch path. - -- id: sdk-jsonrpc-server - name: '@deepseek-ai/dsh-sdk-jsonrpc-server' - config: - maxTokensAsSuccess: false - -- id: deepseek-llm-api-extensions - name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' - -- id: session-log-deepseek - name: '@deepseek-ai/dsh-session-log-deepseek' - -- id: plugin-package-inventory-deepseek - name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek' - -- id: llm-deepseek - name: '@deepseek-ai/dsh-llm-deepseek' - config: - apiKeyEnv: DEEPSEEK_API_KEY - streamIdleTimeoutMs: 172800000 - models: - - id: !!js process.env.DSH_MODEL ?? 'deepseek-v4-flash' - contextWindow: !!js Number(process.env.DSH_CONTEXT_WINDOW ?? 1000000) - -- id: sandbox - name: '@deepseek-ai/dsh-sandbox-local' - -- id: sandbox-policy - name: '@deepseek-ai/dsh-sandbox-policy' - config: - mode: danger-full-access - workspaceRoot: !!js process.env.DSH_CWD ?? process.cwd() - -- id: subprocess - name: '@deepseek-ai/dsh-subprocess-local' - -- id: pty - name: '@deepseek-ai/dsh-terminal' - -- id: terminal-bash - name: '@deepseek-ai/dsh-terminal-bash' - config: - timeoutMs: 300000 - -# The editor uses the bare local filesystem; persistent Bash still consumes the -# shared danger-full-access sandbox policy above. -- id: fs-local - name: '@deepseek-ai/dsh-fs-local' - config: - cwd: !!js process.env.DSH_CWD ?? process.cwd() - -- id: agent-spine - name: '@deepseek-ai/dsh-agent-spine-demo' - config: - includeHarnessIdentity: false - includeRuntimeContext: false - persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.' - workspaceContext: false - skills: - enabled: false - toolBash: false - toolJobs: false - -- id: persistent-bash - name: '@deepseek-ai/dsh-tool-bash-persistent' - config: - timeoutMs: 300000 - description: |- - Run commands in a bash shell - * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped. - * You don't have access to the internet via this tool. - * You do have access to a mirror of common linux and python packages via apt and pip. - * State is persistent across command calls and discussions with the user. - * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'. - * Please avoid commands that may produce a very large amount of output. - * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background. - -- id: str-replace-editor - name: '@deepseek-ai/dsh-tool-str-replace-editor' - config: - maxOutputChars: 16000 - -- id: sessions - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: !!js process.env.DSH_SESSION_ROOT ?? './.sessions' - compression: none diff --git a/examples/python-sdk-agent/minimal.patch.yml b/examples/python-sdk-agent/minimal.patch.yml deleted file mode 100644 index 2c3ac0f5bc..0000000000 --- a/examples/python-sdk-agent/minimal.patch.yml +++ /dev/null @@ -1,47 +0,0 @@ -# Minimal Python SDK overlay for `dsh --profile sdk`. The SDK server applies -# the model-facing allowlist to every root session, so later base-bundle tools -# cannot appear implicitly. The profile still owns JSON-RPC and persistence. - -- id: system-prompt - config: - includeHarnessIdentity: false - includeRuntimeContext: false - persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.' - personaComplete: true - -- id: sdk-jsonrpc-server - config: - maxTokensAsSuccess: !!js "process.env.DSH_MAX_TOKENS_AS_SUCCESS === undefined ? true : JSON.parse(process.env.DSH_MAX_TOKENS_AS_SUCCESS)" - toolFilter: - allow: - - bash - - str_replace_editor - -# Remove non-tool model inputs and turn rewriting, plus the one-shot Bash row -# that would conflict with the persistent Bash tool inserted below. -- id: agent-instructions - disabled: true -- id: tool-bash - disabled: true -- id: compaction-basic - disabled: true -- id: command-compact - disabled: true -- id: tool-result-pruner - disabled: true - -- id: tool-str-replace-editor - config: - maxOutputChars: 16000 - -- insert: - - id: pty - name: '@deepseek-ai/dsh-terminal' - - id: terminal-bash - name: '@deepseek-ai/dsh-terminal-bash' - config: - timeoutMs: 300000 - - id: persistent-bash - name: '@deepseek-ai/dsh-tool-bash-persistent' - config: - timeoutMs: 300000 diff --git a/examples/python-sdk-agent/minimal.py b/examples/python-sdk-agent/minimal.py index 255a0dd3c0..86c1dc8082 100644 --- a/examples/python-sdk-agent/minimal.py +++ b/examples/python-sdk-agent/minimal.py @@ -10,9 +10,6 @@ from pathlib import Path from deepseek_harness import DeepSeekHarness -PATCH = Path(__file__).with_name("minimal.patch.yml") - - def main() -> None: """Parse one task and print the agent's final response.""" parser = argparse.ArgumentParser() @@ -24,7 +21,7 @@ def main() -> None: type=Path, default=Path(configured_home) if configured_home.strip() else None, ) - parser.add_argument("--profile", default="sdk") + parser.add_argument("--profile", default="sdk-minimal") parser.add_argument("--session-id") parser.add_argument("--provider", default="deepseek-official") parser.add_argument("--model", default=os.environ.get("DSH_MODEL", "deepseek-v4-flash")) @@ -42,7 +39,7 @@ def main() -> None: cwd=str(workspace), dsh_home=str(dsh_home), profile=args.profile, - patches=(str(PATCH.resolve()),), + env={"DSH_MODEL": args.model}, ) as harness: result = harness.run(args.prompt, session_id=args.session_id) print(result.final_response) diff --git a/examples/python-sdk-agent/minimal.snapshot.cordis.yml b/examples/python-sdk-agent/minimal.snapshot.cordis.yml deleted file mode 100644 index 0f26fa6716..0000000000 --- a/examples/python-sdk-agent/minimal.snapshot.cordis.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Keyless replay keeps the complete minimal composition intact and replaces -# only its live DeepSeek adapter with the fixture-backed provider. The replay -# catalog claims the same route initialized by the SDK. -- id: base - name: '@deepseek-ai/cordis-plugin-include' - config: - path: ./minimal.cordis.yml - patches: - - id: llm-deepseek - name: '@deepseek-ai/dsh-llm-deepseek' - disabled: true - - insert: - - id: llm-replay - name: '@deepseek-ai/dsh-llm-replay' - config: - providers: - - id: deepseek-official - name: DeepSeek - models: - - id: deepseek-v4-flash diff --git a/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts b/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts index 4b2519c7f8..e0bd4ed6c3 100644 --- a/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts +++ b/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts @@ -10,7 +10,6 @@ import { describe, expect, it } from 'vitest' const binScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url)) const patchPath = fileURLToPath(new URL('./keyless.patch.yml', import.meta.url)) -const minimalPatchPath = fileURLToPath(new URL('../minimal.patch.yml', import.meta.url)) const repoRoot = fileURLToPath(new URL('../../..', import.meta.url)) const decompress = promisify(zstdDecompress) @@ -181,7 +180,7 @@ describe('Python SDK dsh profile keyless smoke', () => { } }, 40_000) - it('keeps the minimal overlay on an explicit model-facing allowlist', async () => { + it('boots the standalone minimal profile with its exact model-facing roster', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-minimal-')) const modelRequests: Record[] = [] const modelServer = createServer((request, response) => { @@ -205,16 +204,13 @@ describe('Python SDK dsh profile keyless smoke', () => { 'tsx/esm', binScript, '--profile', - 'sdk', - '--patch', - minimalPatchPath, + 'sdk-minimal', ], { cwd: repoRoot, env: { DSH_HOME: join(root, '.dsh'), - DSH_PERMISSION_MODE: 'danger-full-access', + DSH_MODEL: 'deepseek-v4-pro', DSH_SYSTEM_PROMPT: 'Minimal allowlist prompt.', - DSH_TELEMETRY_DISABLED: '1', DEEPSEEK_API_KEY: 'keyless-smoke-no-call', DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`, }, @@ -259,6 +255,13 @@ describe('Python SDK dsh profile keyless smoke', () => { } expect(request.messages?.[0]).toMatchObject({ role: 'system', content: 'Minimal allowlist prompt.' }) expect(request.tools?.map(tool => tool.function?.name).sort()).toEqual(['bash', 'str_replace_editor']) + const profile = JSON.parse( + await readFile(join(root, '.dsh', 'profiles', 'sdk-minimal', 'package.json'), 'utf8'), + ) as { dsh?: { profile?: { bundles?: string[]; patchReload?: string } } } + expect(profile.dsh?.profile).toEqual({ + bundles: ['@deepseek-ai/dsh-sdk-minimal'], + patchReload: 'startup', + }) child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`) await waitForLine(lines, value => value.id === 3, () => stderr) diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 3be8a9bcfb..ba5279000d 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -46,14 +46,6 @@ MINIMAL_BASH_COMMAND = ( "printf 'COUNT=%s CWD=%s\\n' \"$counter\" \"$PWD\"; " "if [ \"$counter\" -eq 1 ]; then cd /tmp; fi" ) -MINIMAL_BASH_DESCRIPTION = """Run commands in a bash shell -* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped. -* You don't have access to the internet via this tool. -* You do have access to a mirror of common linux and python packages via apt and pip. -* State is persistent across command calls and discussions with the user. -* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'. -* Please avoid commands that may produce a very large amount of output. -* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.""" LEGACY_CUSTOM_DISABLED_ROWS = ( "agent-instructions", "goal", @@ -121,10 +113,6 @@ RESTART_SNAPSHOT_DIRECTORY = ( Path(__file__).resolve().parent / "snapshots" / "python-sdk-single-exe" / "restart" ) RESTART_SNAPSHOT_FILENAMES = ("result.json", "requests.json", "session.1.jsonl", "session.2.jsonl") -# The agent loop's dynamic runtime-context snapshot is the one model-visible message this -# expected output cannot carry: the same composition emits it on macOS and not on Linux -# (deepseek-harness#2488), and the file must replay on both. Everything else is compared. -RUNTIME_CONTEXT_PREFIX = "Current runtime context" MCP_SERVER_SCRIPT = """\ import json import os @@ -972,7 +960,7 @@ def smoke_sdk_custom(base_url: str, executable: Path) -> None: def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) -> None: - """Exercise the checked-in minimal composition through the packaged executable.""" + """Exercise the shipped standalone minimal profile through the packaged executable.""" from deepseek_harness import DeepSeekHarness # One mock model serves every scenario of a run, so the snapshot takes this turn's slice. @@ -983,59 +971,15 @@ def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) - prompt = f"{MINIMAL_PROMPT}\n{MINIMAL_EDITOR_PATH_PREFIX}{editor_path}" dsh_home = root / "home" sessions = dsh_home / "sessions" - disabled = [ - "agent-instructions", - "tool-bash", - "compaction-basic", - "command-compact", - "tool-result-pruner", - ] - patch = write_profile_patch(root, "minimal.patch.yml", sessions, [ - { - "id": "system-prompt", - "config": { - "includeHarnessIdentity": False, - "includeRuntimeContext": False, - "persona": "You are a helpful software engineer assistant.", - "personaComplete": True, - }, - }, - { - "id": "sdk-jsonrpc-server", - "config": { - "maxTokensAsSuccess": True, - "toolFilter": {"allow": ["bash", "str_replace_editor"]}, - }, - }, - *({"id": row_id, "disabled": True} for row_id in disabled), - {"id": "tool-str-replace-editor", "config": {"maxOutputChars": 16000}}, - {"insert": [ - {"id": "pty", "name": "@deepseek-ai/dsh-terminal"}, - { - "id": "terminal-bash", - "name": "@deepseek-ai/dsh-terminal-bash", - "config": {"timeoutMs": 300000}, - }, - { - "id": "persistent-bash", - "name": "@deepseek-ai/dsh-tool-bash-persistent", - "config": { - "timeoutMs": 300000, - "description": MINIMAL_BASH_DESCRIPTION, - }, - }, - ]}, - ]) with DeepSeekHarness( provider="deepseek-official", model="smoke-model", cwd=str(root), dsh_bin=str(executable), dsh_home=str(dsh_home), - patches=(str(patch),), + profile="sdk-minimal", env={ - "DSH_PERMISSION_MODE": "danger-full-access", - "DSH_TELEMETRY_DISABLED": "1", + "DSH_MODEL": "smoke-model", }, api_key="sk-keyless-smoke", base_url=base_url, @@ -1550,9 +1494,9 @@ def build_minimal_snapshot_files( Every assembled system prompt, advertised tool schema, and system or user message is kept verbatim: they carry what the deployment actually shows the model, so a plugin that contributes an unintended system section or user message cannot pass unnoticed. - Assistant and tool payloads keep only their call identity, and the dynamic - runtime-context snapshot is dropped, because their text differs across the platforms - this expected output must replay on. + Assistant and tool payloads keep only their call identity because their text differs + across the platforms this expected output must replay on. The shipped profile omits + dynamic runtime context, so every message it emits is compared. """ snapshot = [] for body in requests: @@ -1564,21 +1508,11 @@ def build_minimal_snapshot_files( "messages": [ minimal_snapshot_message(message, cwd) for message in messages - if not is_runtime_context_message(message) ], }) return {"model-visible.json": json.dumps(snapshot, indent=2, ensure_ascii=False) + "\n"} -def is_runtime_context_message(message: object) -> bool: - """Identify the agent loop's dynamic runtime-context snapshot, current or cleared.""" - return ( - isinstance(message, dict) - and message.get("role") == "user" - and message_text(message.get("content")).startswith(RUNTIME_CONTEXT_PREFIX) - ) - - def minimal_snapshot_message(message: object, cwd: Path) -> dict[str, object]: """Reduce one model-visible message to its stable, behavior-carrying parts.""" if not isinstance(message, dict): From 7a11f5fde3ad63994a3180dad2c6372f0723562f Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:39:43 +0800 Subject: [PATCH 13/16] docs(python): define the standalone minimal profile Record sdk-minimal as the narrow repository-owned exception to base-first profile composition: callers still launch only dsh and cannot provide an arbitrary Cordis tree, while the shipped bundle may own a complete explicit roster. Cross-link the launcher, profile-bundle, Python-runtime, minimal-agent, snapshot, and telemetry decisions; the supersession audit keeps each older note active because its remaining rationale is independent. Update the CLI, architecture, Python tutorial/reference, example, runtime-wheel reference, and bundle documentation. The docs distinguish the full sdk profile from sdk-minimal, explain explicit-home/plugin/patch customization, state the minimal permission and persistence choices, and retain the separately packaged web profile and frontend assets for direct dsh use. Correct dsh-base descriptions to cover base-backed profiles, make SDK startup configuration visible in the generated config catalog, add sdk-minimal to the module graph, and regenerate the base-composition graph. English and Chinese pairs are re-recorded at the exact reviewed contents. --- ...026-08-05-profile-plugin-bundles.i18n.yaml | 4 +- .../2026-08-05-profile-plugin-bundles.md | 6 +- .../2026-08-05-profile-plugin-bundles.zh.md | 6 +- ...-single-dsh-application-launcher.i18n.yaml | 4 +- ...6-08-22-single-dsh-application-launcher.md | 17 +++--- ...8-22-single-dsh-application-launcher.zh.md | 19 ++++--- ...3-python-sdk-dsh-profile-runtime.i18n.yaml | 4 +- ...26-08-23-python-sdk-dsh-profile-runtime.md | 10 ++-- ...08-23-python-sdk-dsh-profile-runtime.zh.md | 10 ++-- ...4-standalone-sdk-minimal-profile.i18n.yaml | 6 ++ ...26-08-24-standalone-sdk-minimal-profile.md | 57 +++++++++++++++++++ ...08-24-standalone-sdk-minimal-profile.zh.md | 57 +++++++++++++++++++ ...nimal-preset-owns-rl-composition.i18n.yaml | 4 +- ...8-10-minimal-preset-owns-rl-composition.md | 4 +- ...0-minimal-preset-owns-rl-composition.zh.md | 6 +- ...7-31-web-telemetry-default-mount.i18n.yaml | 4 +- .../2026-07-31-web-telemetry-default-mount.md | 4 +- ...26-07-31-web-telemetry-default-mount.zh.md | 4 +- ...l-profiles-bare-two-tool-runtime.i18n.yaml | 4 +- ...-minimal-profiles-bare-two-tool-runtime.md | 10 ++-- ...nimal-profiles-bare-two-tool-runtime.zh.md | 10 ++-- ...n-minimal-model-visible-snapshot.i18n.yaml | 4 +- ...3-python-minimal-model-visible-snapshot.md | 12 ++-- ...ython-minimal-model-visible-snapshot.zh.md | 12 ++-- apps/cli/README.i18n.yaml | 4 +- apps/cli/README.md | 7 ++- apps/cli/README.zh.md | 7 ++- apps/cli/composition.md | 2 +- apps/cli/reference/README.i18n.yaml | 4 +- apps/cli/reference/README.md | 9 +-- apps/cli/reference/README.zh.md | 9 +-- apps/cli/src/profile-boot.ts | 4 +- docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 10 ++-- docs/architecture.zh.md | 10 ++-- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 18 +++++- docs/config-catalog.zh.md | 18 +++++- docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 3 + docs/module-graph.zh.md | 3 + docs/user/guide/python-sdk.i18n.yaml | 4 +- docs/user/guide/python-sdk.md | 30 +++++----- docs/user/guide/python-sdk.zh.md | 30 +++++----- examples/python-sdk-agent/README.i18n.yaml | 4 +- examples/python-sdk-agent/README.md | 14 +++-- examples/python-sdk-agent/README.zh.md | 14 +++-- packages/boot/app-boot/README.i18n.yaml | 4 +- packages/boot/app-boot/README.md | 2 +- packages/boot/app-boot/README.zh.md | 2 +- packages/bundle/README.i18n.yaml | 4 +- packages/bundle/README.md | 2 +- packages/bundle/README.zh.md | 2 +- packages/bundle/base/README.i18n.yaml | 4 +- packages/bundle/base/README.md | 2 +- packages/bundle/base/README.zh.md | 2 +- packages/bundle/base/cordis.patch.yml | 2 +- packages/bundle/base/package.json | 2 +- packages/bundle/sdk-minimal/README.i18n.yaml | 4 +- packages/bundle/sdk-minimal/README.md | 2 +- packages/bundle/sdk-minimal/README.zh.md | 2 +- packages/examples/README.i18n.yaml | 4 +- packages/examples/README.md | 2 +- packages/examples/README.zh.md | 2 +- python/README.i18n.yaml | 4 +- python/README.md | 2 +- python/README.zh.md | 2 +- python/development.i18n.yaml | 4 +- python/development.md | 2 +- python/development.zh.md | 2 +- python/sdk-runtime/README.i18n.yaml | 4 +- python/sdk-runtime/README.md | 2 +- python/sdk-runtime/README.zh.md | 2 +- python/sdk/README.i18n.yaml | 4 +- python/sdk/README.md | 4 +- python/sdk/README.zh.md | 4 +- scripts/gen-doc-graphs.ts | 2 +- 77 files changed, 372 insertions(+), 204 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md create mode 100644 .agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml index f43fae952f..a70ebb7db2 100644 --- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml @@ -2,5 +2,5 @@ # 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/architecture/2026-08-05-profile-plugin-bundles.md -2026-08-05-profile-plugin-bundles.md: c9f685eecddcd4ea8d8580becef3c32a9693a329 -2026-08-05-profile-plugin-bundles.zh.md: 3f2db5ea7653fde356f6ae8f21898f1fdada3926 +2026-08-05-profile-plugin-bundles.md: 493568691dac3a54185f11cbbf8162bf6b6355b1 +2026-08-05-profile-plugin-bundles.zh.md: adfa95b6f8d0fdd6fe3c0ebbc7a62d935ebb1987 diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md index c9f685eecd..493568691d 100644 --- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md +++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md @@ -12,7 +12,7 @@ The `dsh` launcher hardcoded its compositions: `base.cordis.yml` + `web.cordis.y Everything becomes a **profile**: a directory `$DSH_HOME/profiles/` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the two manifest kinds live under distinct `dsh.profile` / `dsh.bundle` keys so a package.json states which role it plays. The tree composes over an empty root by applying each bundle's patch in `dsh.profile.bundles` order, then the user layer and `--patch` overlays — one `applyEntryPatches` call shared by boot and `--dump-config`. App invocation values later moved from launcher-derived patches to startup services in the [app-owned command-line decision](2026-08-06-app-owned-command-line.md). -The default Profile templates use `@deepseek-ai/dsh-base` (shared core rows), `@deepseek-ai/dsh-web-app` (browser Host rows and Web runtime glue), and `@deepseek-ai/dsh-headless` (a direct one-shot runner over base, without web-app). Generic `dsh --profile ` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, while headless owns its task positional. Patch overlays use launcher-owned `--patch`. `dsh plugin --profile ` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract. +The default Profile templates use `@deepseek-ai/dsh-base` as the shared core for `web`, `headless`, `sdk`, and `acp`, with one mode bundle above it. The [standalone `sdk-minimal` profile](2026-08-24-standalone-sdk-minimal-profile.md) instead lists one bundle that owns its complete explicit tree. Generic `dsh --profile ` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, headless owns its task positional, and the protocol profiles accept no app options. Patch overlays use launcher-owned `--patch`. `dsh plugin --profile ` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract. Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory — so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them — while bare plugin names in patch rows resolve through the profile directory's Node parent-walk into the maintained flat fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch). @@ -27,7 +27,7 @@ Two supporting refactors: the webserver's built-in static dist serving became th ## Consequences -- New composition surfaces (a TUI, provider packs) ship as ordinary npm packages installable per profile; the repository no longer needs a row for every deployment shape. +- New composition surfaces (a TUI, provider packs) ship as ordinary npm packages installable per profile, without a repository row for every deployment shape. - `apps/cli` shrank to argv parsing, profile machinery consumption, and the pnpm forwarder; `AppCLIEntry` and the per-surface boot paths are gone. - The keyless web e2e scaffold boots the same bundle layers over the same empty-root shape as production, including the profiles module fallback, so composition drift between test and product fails loudly. -- Backends reject nothing old on disk (pre-release stance): `$DSH_HOME/config.yaml` is simply no longer read. +- Under the pre-release stance, backends carry no compatibility behavior for old on-disk configuration; `$DSH_HOME/config.yaml` is ignored. diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md index 3f2db5ea76..adfa95b6f8 100644 --- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md @@ -12,7 +12,7 @@ Status: implemented 一切都变成 **profile**:即目录 `$DSH_HOME/profiles/`,其中包含一个 `package.json`(pnpm 管理的树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和一份用户 `cordis.patch.yml`。**组合包**(bundle)是声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;两种 manifest 分别位于互不相同的 `dsh.profile` / `dsh.bundle` 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 `dsh.profile.bundles` 顺序应用每个组合包的 patch,然后是用户层与 `--patch` overlay——启动与 `--dump-config` 共享同一条 `applyEntryPatches` 路径。随后,[应用持有命令行的决策](2026-08-06-app-owned-command-line.zh.md)又把调用期取值从启动器派生的 patch 迁移到了启动服务。 -默认 Profile 模板使用的组合包是 `@deepseek-ai/dsh-base`(共享核心配置行)、`@deepseek-ai/dsh-web-app`(浏览器 Host 配置行与 Web 运行时粘合层)和 `@deepseek-ai/dsh-headless`(直接叠加在 base 上且不含 web-app 的一次性 runner)。通用的 `dsh --profile ` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 则持有任务位置参数。patch overlay 使用启动器持有的 `--patch`。`dsh plugin --profile ` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](2026-08-09-headless-direct-core-entry-point.zh.md)负责 headless 组合约定。 +默认 Profile 模板为 `web`、`headless`、`sdk` 与 `acp` 使用 `@deepseek-ai/dsh-base` 作为共享核心,并在其上叠加一个模式组合包。[独立 `sdk-minimal` profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)则只列出一个拥有完整显式配置树的组合包。通用的 `dsh --profile ` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 持有任务位置参数,协议 profile 不接受应用选项。patch overlay 使用启动器持有的 `--patch`。`dsh plugin --profile ` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](2026-08-09-headless-direct-core-entry-point.zh.md)负责 headless 组合约定。 解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 `$DSH_HOME/profiles/node_modules`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。 @@ -27,7 +27,7 @@ Status: implemented ## Consequences -- 新的组合表层(TUI、提供方扩展包)以普通 npm 包形式交付,可按 profile 安装;仓库不再需要为每种部署形态各留一行。 +- 新的组合表层(TUI、提供方扩展包)以普通 npm 包形式交付,可按 profile 安装,无需在仓库中为每种部署形态各留一行。 - `apps/cli` 收缩为 argv 解析、profile 机制的消费方和 pnpm 转发器;`AppCLIEntry` 与各表层专属的启动路径全部移除。 - 无密钥 web e2e 脚手架以与生产相同的空根形态启动相同的组合包层,包括 profiles 模块回退,因此测试与产品之间的组合漂移会响亮失败。 -- 后端不拒绝磁盘上的任何旧格式(发布前姿态):`$DSH_HOME/config.yaml` 只是不再被读取。 +- 按发布前姿态,后端不携带旧磁盘配置的兼容行为;`$DSH_HOME/config.yaml` 会被忽略。 diff --git a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml index de2009b1ce..61028c878a 100644 --- a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml @@ -2,5 +2,5 @@ # 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/architecture/2026-08-22-single-dsh-application-launcher.md -2026-08-22-single-dsh-application-launcher.md: 27ec766dc62a203a3054ec67828b8897ab5ad245 -2026-08-22-single-dsh-application-launcher.zh.md: 5fb7fb4234b093a6f69446e0d9b1d7fbb7f46cf3 +2026-08-22-single-dsh-application-launcher.md: 48a45cb2454b5532a78474203b6d88aef3dd0697 +2026-08-22-single-dsh-application-launcher.zh.md: dbf2fb3a5bdc16208b0435482d8d0851bd7c44d7 diff --git a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md index 27ec766dc6..48a45cb245 100644 --- a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md +++ b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md @@ -14,13 +14,13 @@ The Python SDK distributes a native executable and three platform wheels. Its pa ### Launch scope -Every supported Node application starts through the `dsh` CLI and one named profile. The shipped application commands are `dsh web`, `dsh --profile headless`, `dsh --profile sdk`, and `dsh --profile acp`; `dsh web` is the deliberate convenience alias for `--profile web`, not another application entry. +Every supported Node application starts through the `dsh` CLI and one named profile. The shipped application commands are `dsh web`, `dsh --profile headless`, `dsh --profile sdk`, `dsh --profile sdk-minimal`, and `dsh --profile acp`; `dsh web` is the deliberate convenience alias for `--profile web`, not another application entry. Vendor CLIs, build-only and test-only executables, direct in-process plugin mounting, and the private browser WebWorker preview are outside the application-launch inventory. A package app bin or root demo that launches a package entry is not an accepted extension point. ### Profile applications -`@deepseek-ai/dsh-sdk-app` and `@deepseek-ai/dsh-acp-app` compose the protocol applications over `@deepseek-ai/dsh-base`. The SDK bundle adds the JSON-RPC server plus app-owned help and stdio lifetime; the ACP bundle adds the automation-only ACP server plus the same application responsibilities. Both adopt the base model, tools, persistence, settings, credentials, policy, and environment behavior. +`@deepseek-ai/dsh-sdk-app` and `@deepseek-ai/dsh-acp-app` compose the full protocol applications over `@deepseek-ai/dsh-base`. The SDK bundle adds the JSON-RPC server plus app-owned help and stdio lifetime; the ACP bundle adds the automation-only ACP server plus the same application responsibilities. Both adopt the base model, tools, persistence, settings, credentials, policy, and environment behavior. The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) reuses SDK startup and JSON-RPC serving but deliberately owns a complete explicit tree without `dsh-base`. Profile manifests own patch reload: @@ -29,11 +29,12 @@ Profile manifests own patch reload: | `web` | `live` | | `headless` | `startup` | | `sdk` | `startup` | +| `sdk-minimal` | `startup` | | `acp` | `startup` | Custom profiles default to `live`. A startup profile still applies its bundle, profile, home-level, and invocation `--patch` layers, but it does not watch them after boot. `dsh-base` inserts the module-HMR row disabled; a profile with a tested source-module reload lifecycle must enable it explicitly. None of the shipped profiles enable server module HMR: `patchReload: live` uses the launcher's config-only watcher while the startup profiles install no watcher. SDK and ACP cannot safely replace their server, agents, persistence, or tool registry inside one owned stdio connection. -The shipped protocol profiles reserve stdout for protocol frames, expose help without starting transport, and route stdin EOF and signals through bounded root disposal. ACP remains automation-only. The SDK JSON-RPC methods, notification fields, and `initialize.serverInfo.name` remain stable. Model-visible tool and persistence defaults come from `dsh-base`, and runnable snapshots own those assembled application outputs. +The shipped protocol profiles reserve stdout for protocol frames, expose help without starting transport, and route stdin EOF and signals through bounded root disposal. ACP remains automation-only. The SDK JSON-RPC methods, notification fields, and `initialize.serverInfo.name` remain stable. Full-profile model-visible tool and persistence defaults come from `dsh-base`; `sdk-minimal` owns its explicit defaults. Runnable snapshots own the assembled application outputs. ### TypeScript SDK customization @@ -45,7 +46,7 @@ Direct SDK use follows normal Harness-home resolution: explicit `dshHome`, inher ### Python runtime -The Python runtime wheel packages the ordinary `@deepseek-ai/dsh` CLI from `node_modules/@deepseek-ai/dsh/lib/bin.js` through the private `dsh-python-runtime-closure` deploy manifest. The Python client selects `dsh --profile sdk`, ordered patch files, and an explicit Harness home; the installed `dsh` console command exposes the same profile grammar. The runnable Python example is `examples/python-sdk-agent`. +The Python runtime wheel packages the ordinary `@deepseek-ai/dsh` CLI from `node_modules/@deepseek-ai/dsh/lib/bin.js` through the private `dsh-python-runtime-closure` deploy manifest. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable Python example selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar and the separately packaged `web` application. The executable family is `deepseek-harness-sdk-runtime--`. The SDK wire, wheel and import distribution names, sidecar names, and wire identity `deepseek-harness-sdk-runtime` remain stable. The SDK package family is `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and `@deepseek-ai/dsh-sdk-jsonrpc-server`; `@deepseek-ai/dsh-acp` remains the ACP protocol plugin. There is no Python-specific Node application, checked-in complete config, compatibility package, forwarding executable, fallback parser, or SDK/ACP launcher alias. @@ -57,7 +58,7 @@ The executable family is `deepseek-harness-sdk-runtime--`. The S This decision supersedes the application-launch and package-name facts in [profile plugin bundles](2026-08-05-profile-plugin-bundles.md), [TypeScript SDK client and subagent backend](../feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md), [remove the SDK project toolchain](../simplification/2026-08-11-remove-sdk-project-toolchain.md), and [single-file Python SDK runtime distribution](2026-07-10-single-file-executable-sdk-runtime-distribution.md). Those notes retain independent authority for profile layering, client/wire semantics, deleted project tooling, and native packaging. -The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md) remains authoritative for ACP wire and interaction scope. The [repository naming contract](2026-08-11-repository-naming-contract-and-rename-ledger.md) remains authoritative for role-based package names. No active note is fully superseded or eligible for archival. +The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md) remains authoritative for ACP wire and interaction scope. The [repository naming contract](2026-08-11-repository-naming-contract-and-rename-ledger.md) remains authoritative for role-based package names. The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) partially supersedes this note's base-first rule and complete-tree alternative while retaining this note's launcher ownership. No active note is fully superseded or eligible for archival. ## Alternatives considered @@ -65,7 +66,7 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o **Keep forwarding compatibility bins.** Rejected: a forwarding executable remains another public launch name and compatibility promise. The pre-release repository can move callers directly to profiles. -**Put complete standalone Cordis trees behind profile wrappers.** Rejected: that centralizes argv without centralizing application composition. `dsh-base` plus thin app bundles gives shared policy one owner while retaining protocol-specific negative guarantees. +**Put caller-supplied complete Cordis trees behind profile wrappers.** Rejected: that centralizes argv without centralizing application composition. Full profiles use `dsh-base` plus thin app bundles so shared policy has one owner. A repository-owned, versioned standalone bundle is allowed only when an explicit roster is the product behavior, as [sdk-minimal](2026-08-24-standalone-sdk-minimal-profile.md) records. **Accept inline plugins or a complete `cordis.yml` in the TypeScript constructor.** Rejected: the SDK would become another package installer and application composer. Named profiles and patch files already provide persistent and per-launch customization through one resolution model. @@ -79,7 +80,7 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o ## Verification -- Source and built CLI acceptance cover `sdk` and `acp` help, transport startup, stdout purity, EOF, signals, and root disposal. +- Source and built CLI acceptance cover `sdk`, `sdk-minimal`, and `acp` help, transport startup, stdout purity, EOF, signals, and root disposal. - Bundle configuration tests pin module HMR disabled in `dsh-base` and absent from shipped mode overrides; the custom live-profile e2e pins config reload through the launcher's watch-only fallback. - Focused unit suites cover profile launch resolution, initialization bounds, SDK retries, server readiness, and nested isolated homes with 100% coverage on the changed runtime sources. - Keyless ACP and SDK snapshots boot real `dsh` profiles and pin protocol output plus persisted logs; the nested SDK composition boots a second real profile runtime. @@ -91,7 +92,7 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o - A user changes an SDK application's plugin composition through a named profile and ordered patches, using the same installation and resolution model as every other dsh application. - A custom profile receives live config watching without server module HMR and opts into source-module replacement only through an explicit row override. -- SDK and ACP share the complete base application and one set of policy and tools; snapshots present intentional assembled differences explicitly. +- The full SDK and ACP profiles share the complete base application and one set of policy and tools; `sdk-minimal` owns its explicit standalone roster, and snapshots present intentional assembled differences. - Adding `@deepseek-ai/dsh` increases the TypeScript client's install size in exchange for a deterministic same-version runtime. - Trusted user patches can add a plugin that writes to stdout and corrupt their own protocol stream; shipped profiles guarantee purity, not arbitrary third-party composition. - Python packages the ordinary `dsh` profile launcher while retaining a closed native runtime and no system-Node requirement for wheel users. diff --git a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md index 5fb7fb4234..dbf2fb3a5b 100644 --- a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md @@ -14,13 +14,13 @@ Python SDK 分发一个原生可执行文件和三个平台 wheel 包。其打 ### 启动范围 -所有受支持的 Node 应用都通过 `dsh` CLI 与一个具名 profile 启动。随附应用命令是 `dsh web`、`dsh --profile headless`、`dsh --profile sdk` 与 `dsh --profile acp`;`dsh web` 是刻意为 `--profile web` 保留的便捷别名,不是另一个应用入口。 +所有受支持的 Node 应用都通过 `dsh` CLI 与一个具名 profile 启动。随附应用命令是 `dsh web`、`dsh --profile headless`、`dsh --profile sdk`、`dsh --profile sdk-minimal` 与 `dsh --profile acp`;`dsh web` 是刻意为 `--profile web` 保留的便捷别名,不是另一个应用入口。 Vendor CLI、仅用于构建和测试的可执行文件、进程内直接挂载插件以及私有浏览器 WebWorker 预览都不属于应用启动清单。包应用 bin 或直接启动包入口的根 demo 都不是可接受的扩展点。 ### Profile 应用 -`@deepseek-ai/dsh-sdk-app` 与 `@deepseek-ai/dsh-acp-app` 在 `@deepseek-ai/dsh-base` 之上组合协议应用。SDK 组合包增加 JSON-RPC 服务器、应用自有帮助和 stdio 生命周期;ACP 组合包增加仅用于自动化的 ACP 服务器与相同的应用职责。两者都采用 base 层的模型、工具、持久化、settings、credentials、策略和环境行为。 +`@deepseek-ai/dsh-sdk-app` 与 `@deepseek-ai/dsh-acp-app` 在 `@deepseek-ai/dsh-base` 之上组合完整协议应用。SDK 组合包增加 JSON-RPC 服务器、应用自有帮助和 stdio 生命周期;ACP 组合包增加仅用于自动化的 ACP 服务器与相同的应用职责。两者都采用 base 层的模型、工具、持久化、settings、credentials、策略和环境行为。[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)复用 SDK 启动与 JSON-RPC 服务,但刻意拥有不含 `dsh-base` 的完整显式配置树。 Profile manifest 负责 patch 重载: @@ -29,11 +29,12 @@ Profile manifest 负责 patch 重载: | `web` | `live` | | `headless` | `startup` | | `sdk` | `startup` | +| `sdk-minimal` | `startup` | | `acp` | `startup` | 自定义 profile 默认为 `live`。`startup` profile 仍会应用组合包、profile、home 级与调用时 `--patch` 各层,但启动后不会监视这些文件。`dsh-base` 插入的模块 HMR(热模块替换)配置项默认禁用;具有经过验证的源码模块重载生命周期的 profile 必须显式启用它。随附 profile 均不启用服务器模块 HMR:`patchReload: live` 使用启动器的仅配置 watcher,`startup` profile 则不安装 watcher。SDK 与 ACP 无法在一个自有 stdio 连接内安全替换其服务器、agent、持久化或工具注册表。 -随附协议 profile 将 stdout 保留给协议帧,显示帮助时不启动 transport,并通过有界根节点 dispose(资源释放)处理 stdin EOF 与信号。ACP 继续仅用于自动化。SDK JSON-RPC 方法、通知字段与 `initialize.serverInfo.name` 保持稳定。模型可见工具与持久化默认值来自 `dsh-base`,可运行快照负责钉住这些已组装的应用输出。 +随附协议 profile 将 stdout 保留给协议帧,显示帮助时不启动 transport,并通过有界根节点 dispose(资源释放)处理 stdin EOF 与信号。ACP 继续仅用于自动化。SDK JSON-RPC 方法、通知字段与 `initialize.serverInfo.name` 保持稳定。完整 profile 的模型可见工具与持久化默认值来自 `dsh-base`;`sdk-minimal` 拥有自己的显式默认值。可运行快照负责固定已组装的应用输出。 ### TypeScript SDK 自定义 @@ -45,7 +46,7 @@ SDK 用户通过 profile 自定义插件。`dsh plugin --profile ...` 管 ### Python 运行时 -Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest,打包来自 `node_modules/@deepseek-ai/dsh/lib/bin.js` 的普通 `@deepseek-ai/dsh` CLI。Python 客户端选择 `dsh --profile sdk`、有序 patch 文件和显式 Harness home;安装的 `dsh` 控制台命令暴露相同 profile 语法。可运行的 Python 示例是 `examples/python-sdk-agent`。 +Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest,打包来自 `node_modules/@deepseek-ai/dsh/lib/bin.js` 的普通 `@deepseek-ai/dsh` CLI。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;可运行 Python 示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同 profile 语法与单独打包的 `web` 应用。 可执行文件族是 `deepseek-harness-sdk-runtime--`。SDK 协议格式、wheel 与 import 分发名称、伴随文件名称,以及协议 identity `deepseek-harness-sdk-runtime` 保持稳定。SDK 包族是 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol` 与 `@deepseek-ai/dsh-sdk-jsonrpc-server`;`@deepseek-ai/dsh-acp` 继续作为 ACP 协议插件。仓库不保留 Python 专用 Node 应用、检入的完整配置、兼容包、转发可执行文件、后备解析器或 SDK/ACP 启动别名。 @@ -57,7 +58,7 @@ Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest 本决策取代 [profile 插件组合包](2026-08-05-profile-plugin-bundles.zh.md)、[TypeScript SDK 客户端与 SDK subagent 后端](../feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md)、[移除 SDK 项目工具链](../simplification/2026-08-11-remove-sdk-project-toolchain.zh.md)和[单文件 Python SDK 运行时分发](2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)中的应用启动与包名事实。这些 Note 对 profile 分层、客户端/协议语义、已删除的项目工具链与原生打包仍分别具有独立权威。 -[ACP 仅自动化协议](../simplification/2026-07-23-acp-automation-only-protocol.zh.md)继续负责 ACP 协议格式与交互范围。[仓库命名约定](2026-08-11-repository-naming-contract-and-rename-ledger.zh.md)继续负责基于角色的包名。没有任何活跃 Note 被完全取代,也没有 Note 符合归档条件。 +[ACP 仅自动化协议](../simplification/2026-07-23-acp-automation-only-protocol.zh.md)继续负责 ACP 协议格式与交互范围。[仓库命名约定](2026-08-11-repository-naming-contract-and-rename-ledger.zh.md)继续负责基于角色的包名。[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)部分取代本 Note 的 base 优先规则与完整配置树替代方案,同时保留本 Note 对 launcher 所有权的决策。没有任何活跃 Note 被完全取代,也没有 Note 符合归档条件。 ## 考虑过的替代方案 @@ -65,7 +66,7 @@ Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest **保留转发兼容 bin。** 拒绝:转发可执行文件仍然形成另一个公开启动名称与兼容承诺。预发布仓库可以让调用方直接迁移到 profile。 -**把完整独立 Cordis 树放到 profile wrapper 后面。** 拒绝:这只集中 argv,没有集中应用组合。`dsh-base` 加轻量应用组合包让共享策略只有一个归属,同时保留协议专属的负面保证。 +**把调用方提供的完整 Cordis 树放到 profile wrapper 后面。** 拒绝:这只集中 argv,没有集中应用组合。完整 profile 使用 `dsh-base` 加轻量应用组合包,使共享策略只有一个归属。只有当显式清单本身属于产品行为时,才允许仓库自有且有版本的独立组合包,具体见 [sdk-minimal](2026-08-24-standalone-sdk-minimal-profile.zh.md)。 **在 TypeScript 构造函数中接受内联插件或完整 `cordis.yml`。** 拒绝:SDK 会因此成为另一个包安装器和应用组合器。具名 profile 与 patch 文件已通过统一解析模型提供持久与逐次启动自定义。 @@ -79,8 +80,8 @@ Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest ## 验证 -- 源码与构建后 CLI 验收覆盖 `sdk` 和 `acp` 的帮助、transport 启动、stdout 纯净性、EOF、信号与根节点 dispose。 -- 组合包配置测试钉住 `dsh-base` 默认禁用模块 HMR,随附模式覆盖层不再重复该策略;自定义 live profile 的 e2e 钉住启动器仅监视 fallback 提供的配置重载。 +- 源码与构建后 CLI 验收覆盖 `sdk`、`sdk-minimal` 和 `acp` 的帮助、transport 启动、stdout 纯净性、EOF、信号与根节点 dispose。 +- 组合包配置测试钉住 `dsh-base` 默认禁用模块 HMR,随附模式覆盖层不含该策略;自定义 live profile 的 e2e 钉住启动器仅监视 fallback 提供的配置重载。 - 聚焦单元套件覆盖 profile 启动解析、初始化时限、SDK 重试、服务器就绪和嵌套隔离 home,并对变更后的运行时源码实现 100% 覆盖率。 - 免密钥 ACP 与 SDK 快照启动真实 `dsh` profile,并钉住协议输出与持久化日志;嵌套 SDK 组合会启动第二个真实 profile 运行时。 - 真实 API 工作流把文件并行度限制为 4,因为一个 profile e2e 文件可能拥有多个完整 `dsh` 子进程树;工作流测试会钉住该资源上限。 @@ -91,7 +92,7 @@ Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest - 用户通过具名 profile 与有序 patch 更改 SDK 应用的插件组合,使用与其他所有 dsh 应用相同的安装与解析模型。 - 自定义 profile 可以在不启用服务器模块 HMR 的情况下获得实时配置监视,只有显式覆盖配置项才会启用源码模块替换。 -- SDK 与 ACP 共享完整 base 应用和同一份策略与工具;快照以显式差异呈现刻意采用的组装变化。 +- 完整 SDK 与 ACP profile 共享完整 base 应用和同一份策略与工具;`sdk-minimal` 拥有自己的显式独立清单,快照会呈现这些刻意采用的组装差异。 - 增加 `@deepseek-ai/dsh` 会扩大 TypeScript 客户端的安装体积,换来确定的同版本运行时。 - 受信任用户 patch 可以增加写入 stdout 的插件并破坏自己的协议流;随附 profile 保证纯净,不为任意第三方组合提供保证。 - Python 打包普通 `dsh` profile 启动器,同时保留封闭原生运行时,wheel 用户无需系统 Node。 diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml index 8cc79728eb..f418c62b83 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml @@ -2,5 +2,5 @@ # 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/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md -2026-08-23-python-sdk-dsh-profile-runtime.md: e3df2d01e3aef7e6eadaa011d51c9ab87456d35f -2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 3d50cac36d469172e91be450cea658f3a4830ccf +2026-08-23-python-sdk-dsh-profile-runtime.md: 3298224688e8f0cd4216f8ed68d9e4364014d2a9 +2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 400e70113e1112aa6e4979c64a5046199c07e8a0 diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md index e3df2d01e3..3298224688 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md @@ -14,7 +14,7 @@ A normal profile cannot be adopted only at the Python wrapper. The runtime execu ### One application launcher -The runtime executable packages `@deepseek-ai/dsh` and runs its ordinary command grammar. The Python client selects `--profile sdk` by default, forwards ordered absolute `--patch` paths, and may select another `dsh` executable or profile. The private `@deepseek-ai/dsh-sdk-python-runtime` application package and checked-in runtime `cordis.yml` do not exist. JSON-RPC serving remains the `@deepseek-ai/dsh-sdk-app` bundle and `@deepseek-ai/dsh-sdk-jsonrpc-server` plugin, not a Python-owned boot path. +The runtime executable packages `@deepseek-ai/dsh` and runs its ordinary command grammar. The Python client selects `--profile sdk` by default, forwards ordered absolute `--patch` paths, and may select another `dsh` executable or profile. The runnable minimal example selects the shipped `sdk-minimal` profile. The private `@deepseek-ai/dsh-sdk-python-runtime` application package and checked-in runtime `cordis.yml` do not exist. JSON-RPC serving remains the `@deepseek-ai/dsh-sdk-app` bundle and `@deepseek-ai/dsh-sdk-jsonrpc-server` plugin, not a Python-owned boot path. The public Python configuration is `dsh_bin`, `profile`, ordered `patches`, `dsh_home`, process cwd/environment, provider/model/token selection, a bounded initialization timeout, and optional turn/shutdown timeouts. It does not expose a complete Cordis tree or arbitrary launch argv. `RunResult` reports the protocol-owned run values and does not duplicate the profile's persistence path. @@ -22,9 +22,9 @@ Every Python launch requires either explicit `dsh_home` or a non-empty `DSH_HOME ### Plugin customization -Persistent SDK customization uses the same profile interfaces as direct CLI use. `dsh plugin --profile sdk ...` manages external dependencies and bundle order, `$DSH_HOME/profiles/sdk/cordis.patch.yml` owns persistent row changes, the home patch applies machine-local changes across profiles, and Python `patches` supplies invocation-specific overlays. A different profile is valid only when it retains an SDK server row. Missing profiles, bundles, server rows, and invalid patches fail without a complete-config fallback; a profile that remains alive without serving JSON-RPC fails the independently bounded initialization handshake with a diagnostic naming that profile. +Persistent SDK customization uses the same profile interfaces as direct CLI use. `dsh plugin --profile ...` manages external dependencies and bundle order, `$DSH_HOME/profiles//cordis.patch.yml` owns persistent row changes, the home patch applies machine-local changes across profiles, and Python `patches` supplies invocation-specific overlays. A selected profile is valid only when it retains an SDK server row. Missing profiles, bundles, server rows, and invalid patches fail without a complete-config fallback; a profile that remains alive without serving JSON-RPC fails the independently bounded initialization handshake with a diagnostic naming that profile. -The checked-in minimal overlay retains the shared SDK profile while configuring the server with a root-agent tool allow list. Later base-bundle tools remain unavailable unless the overlay names them. Its deployment persona is a complete system prompt, so unrelated guidance sections cannot describe hidden tools; dynamic runtime context, workspace instructions, compaction, and the conflicting one-shot Bash row are disabled separately. +The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) lists one repository-owned bundle that inserts its complete explicit tree without `dsh-base`. Its persistent Bash and string-replace editor are present by composition rather than a server filter; dynamic runtime context, workspace instructions, settings, managed credentials, telemetry, compaction, and every other base row are absent. The same runtime still packages the full `sdk` and `web` profiles as separate choices. The runtime wheel installs a `dsh` console command. Ordinary profile and SDK execution remains Node-free; external package management requires a caller-installed `pnpm`. @@ -38,7 +38,7 @@ The published target set is Linux x64, Linux arm64, and macOS arm64. Installed-w ## Existing decisions and supersession -This decision implements and supersedes the Python exception and deferred-migration sections of [the single dsh application launcher](2026-08-22-single-dsh-application-launcher.md). It supersedes the private application, external complete-config, artifact-name, and customization facts in [the single-file Python SDK runtime distribution](2026-07-10-single-file-executable-sdk-runtime-distribution.md), which remains authoritative for pkg/SEA, wheel construction, native target validation, and publication. No active note is fully superseded, so none is archived. +This decision implements and supersedes the Python exception and deferred-migration sections of [the single dsh application launcher](2026-08-22-single-dsh-application-launcher.md). It supersedes the private application, external complete-config, artifact-name, and customization facts in [the single-file Python SDK runtime distribution](2026-07-10-single-file-executable-sdk-runtime-distribution.md), which remains authoritative for pkg/SEA, wheel construction, native target validation, and publication. The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) supersedes only this note's minimal-overlay realization. No active note is fully superseded, so none is archived. ## Alternatives considered @@ -52,4 +52,4 @@ This decision implements and supersedes the Python exception and deferred-migrat ## Consequences -Python callers configure the same profile vocabulary as TypeScript and direct CLI users, and arbitrary external bundles can extend an SDK profile without replacing the application tree. Homes must now be selected explicitly, complete-config and `session_root` parameters are unavailable, and the executable includes shared-library assets plus profile-module proxies. The stronger installed-wheel CI makes those package, profile, native, and provider paths release requirements rather than source-only assumptions. +Python callers configure the same profile vocabulary as TypeScript and direct CLI users, and arbitrary external bundles can extend an SDK profile without introducing another launcher. Homes are selected explicitly, complete-config and `session_root` parameters are unavailable, and the executable includes shared-library assets plus profile-module proxies. The full `sdk`, standalone `sdk-minimal`, and `web` applications remain separate profiles inside the same packaged CLI. The installed-wheel CI makes those package, profile, native, and provider paths release requirements rather than source-only assumptions. diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md index 3d50cac36d..400e70113e 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md @@ -14,7 +14,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml ### 一个应用启动器 -运行时可执行程序打包 `@deepseek-ai/dsh` 并运行其普通命令语法。Python 客户端默认选择 `--profile sdk`,转发有序绝对 `--patch` 路径,也可以选择另一个 `dsh` 可执行程序或 profile。私有 `@deepseek-ai/dsh-sdk-python-runtime` 应用包和检入的运行时 `cordis.yml` 均不存在。JSON-RPC 服务仍由 `@deepseek-ai/dsh-sdk-app` bundle 与 `@deepseek-ai/dsh-sdk-jsonrpc-server` 插件提供,而不是 Python 自有启动路径。 +运行时可执行程序打包 `@deepseek-ai/dsh` 并运行其普通命令语法。Python 客户端默认选择 `--profile sdk`,转发有序绝对 `--patch` 路径,也可以选择另一个 `dsh` 可执行程序或 profile。可运行极简示例选择随附 `sdk-minimal` profile。私有 `@deepseek-ai/dsh-sdk-python-runtime` 应用包和检入的运行时 `cordis.yml` 均不存在。JSON-RPC 服务仍由 `@deepseek-ai/dsh-sdk-app` bundle 与 `@deepseek-ai/dsh-sdk-jsonrpc-server` 插件提供,而不是 Python 自有启动路径。 公开 Python 配置包括 `dsh_bin`、`profile`、有序 `patches`、`dsh_home`、进程 cwd/环境、provider/model/token 选择、有界初始化 timeout,以及可选的轮次/关闭 timeout。它不暴露完整 Cordis 树或任意启动 argv。`RunResult` 报告协议所有的运行值,不重复 profile 的持久化路径。 @@ -22,9 +22,9 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml ### 插件自定义 -持久 SDK 自定义使用与直接 CLI 相同的 profile 接口。`dsh plugin --profile sdk ...` 管理外部依赖与 bundle 顺序,`$DSH_HOME/profiles/sdk/cordis.patch.yml` 负责持久配置项变更,home patch 对所有 profile 应用机器本地变更,Python `patches` 则提供单次启动 overlay。另一个 profile 只有保留 SDK server 配置项时才有效。缺失 profile、bundle、server 配置项或非法 patch 都会直接失败,不存在完整配置回退;保持运行却不提供 JSON-RPC 服务的 profile 会在独立有界的初始化握手中失败,诊断会指明该 profile。 +持久 SDK 自定义使用与直接 CLI 相同的 profile 接口。`dsh plugin --profile ...` 管理外部依赖与 bundle 顺序,`$DSH_HOME/profiles//cordis.patch.yml` 负责持久配置项变更,home patch 对所有 profile 应用机器本地变更,Python `patches` 则提供单次启动 overlay。所选 profile 只有保留 SDK server 配置项时才有效。缺失 profile、bundle、server 配置项或非法 patch 都会直接失败,不存在完整配置回退;保持运行却不提供 JSON-RPC 服务的 profile 会在独立有界的初始化握手中失败,诊断会指明该 profile。 -检入的极简 overlay 会保留共享 SDK profile,并为 server 配置根 agent 工具 allow 列表。基础 bundle 后续新增的工具只有在 overlay 指名时才可用。其部署 persona 是完整系统提示词,因此无关引导段不会描述已隐藏的工具;动态运行时上下文、workspace 指令、compaction 与存在名称冲突的单次 Bash 配置项会分别停用。 +[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)只列出一个仓库自有组合包,该组合包会插入不含 `dsh-base` 的完整显式配置树。持久 Bash 与字符串替换 editor 通过组合存在,而不是通过 server 筛选;动态运行时上下文、workspace 指令、settings、托管凭据、遥测、compaction 与其他所有 base 配置项均不存在。同一运行时仍会把完整 `sdk` 与 `web` profile 作为独立选择打包。 运行时 wheel 安装 `dsh` 控制台命令。普通 profile 与 SDK 运行仍不需要 Node;外部包管理要求调用方自行安装 `pnpm`。 @@ -38,7 +38,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml ## 既有决策与取代关系 -本决策实现并取代[单一 dsh 应用启动器](2026-08-22-single-dsh-application-launcher.zh.md)中的 Python 例外与延后迁移章节。它取代[单文件 Python SDK 运行时分发](2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)中的私有应用、外部完整配置、产物名称与自定义事实;后者继续负责 pkg/SEA、wheel 构建、原生目标验证与发布。没有任何 active note 被完全取代,因此无需归档。 +本决策实现并取代[单一 dsh 应用启动器](2026-08-22-single-dsh-application-launcher.zh.md)中的 Python 例外与延后迁移章节。它取代[单文件 Python SDK 运行时分发](2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)中的私有应用、外部完整配置、产物名称与自定义事实;后者继续负责 pkg/SEA、wheel 构建、原生目标验证与发布。[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)只取代本 Note 中的极简 overlay 实现。没有任何 active note 被完全取代,因此无需归档。 ## 考虑过的替代方案 @@ -52,4 +52,4 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml ## 结果 -Python 调用方使用与 TypeScript 和直接 CLI 用户相同的 profile 词汇,任意外部 bundle 可以扩展 SDK profile,而无需替换应用树。调用方现在必须显式选择 home,完整配置与 `session_root` 参数不可用,可执行程序则包含共享库资源和 profile 模块代理。更强的 installed-wheel CI 将包、profile、原生与提供方路径变成发布要求,而不是仅在源码中成立的假设。 +Python 调用方使用与 TypeScript 和直接 CLI 用户相同的 profile 词汇,任意外部 bundle 可以扩展 SDK profile,而无需引入另一个 launcher。调用方必须显式选择 home,完整配置与 `session_root` 参数不可用,可执行程序则包含共享库资源和 profile 模块代理。完整 `sdk`、独立 `sdk-minimal` 与 `web` 应用作为同一打包 CLI 内的不同 profile 保持分离。Installed-wheel CI 将包、profile、原生与提供方路径变成发布要求,而不是仅在源码中成立的假设。 diff --git a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml new file mode 100644 index 0000000000..6fb8fc0362 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.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/architecture/2026-08-24-standalone-sdk-minimal-profile.md +2026-08-24-standalone-sdk-minimal-profile.md: aa822d40a3b15bb55c36e8bcc585c4aaeb03c13d +2026-08-24-standalone-sdk-minimal-profile.zh.md: cea6ad6f575882b07d9494532e94cfe95bd61b43 diff --git a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md new file mode 100644 index 0000000000..aa822d40a3 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md @@ -0,0 +1,57 @@ +# Agent Note: Standalone sdk-minimal profile under dsh + +Status: implemented + +English | [中文](2026-08-24-standalone-sdk-minimal-profile.zh.md) + +## Problem + +A minimal SDK agent needs an explicit plugin roster. Expressing it as an overlay on the full `sdk` profile leaves every `dsh-base` service mounted and makes exclusion depend on filters and disable entries spread across unrelated plugins. A later base row can change runtime behavior even when the model-facing tools remain filtered. + +A complete caller-supplied Cordis tree gives an exact roster but bypasses profile initialization, bundle resolution, persistent plugin management, home and invocation patch layers, and the `dsh`-owned process lifecycle. The minimal mode needs composition-level exclusion without creating another launcher or Python-owned application. + +## Decision + +### Launch and ownership + +`dsh --profile sdk-minimal` is a shipped startup-only profile. Its manifest lists only `@deepseek-ai/dsh-sdk-minimal`; it does not list `@deepseek-ai/dsh-base`. The bundle inserts the complete Cordis tree over the launcher's empty profile root, while the profile patch, home patch, and ordered invocation patches retain their ordinary precedence above it. + +The `dsh` CLI remains the only application launcher. The Python example selects `sdk-minimal` through the public `profile` field and an explicit Harness home. Python exposes no complete-config or arbitrary-argv path. The full Python and TypeScript SDK defaults remain `sdk`. + +The bundle reuses `@deepseek-ai/dsh-sdk-app` for command help, stdin EOF, and bounded shutdown. The startup provider accepts a profile-name config so both SDK profiles render their actual command without duplicating process lifecycle code. + +### Explicit composition + +The bundle owns one DeepSeek adapter, SDK JSON-RPC serving, the executor-less agent spine, local subprocess and unrestricted filesystem providers, persistent Bash, the string-replace editor, and uncompressed JSONL sessions under `$DSH_HOME/sessions`. The persona comes from `DSH_SYSTEM_PROMPT`, the adapter route from `DSH_MODEL` and `DSH_CONTEXT_WINDOW`, and the credential from `DEEPSEEK_API_KEY`. + +Harness identity, runtime context, workspace instructions, skills, model-facing job controls, compaction, settings, managed credentials, telemetry, Web tools, subagents, and every other base row are absent rather than hidden. The profile pins `danger-full-access`, `maxTokensAsSuccess: false`, and startup-only patch loading. This layer is POSIX-only because its persistent terminal uses Bash. + +### Customization and Web + +`dsh plugin --profile sdk-minimal add ` installs persistent dependencies and bundle layers. The profile's `cordis.patch.yml`, the home patch, and Python `patches` provide persistent, machine-local, and invocation-specific row changes. Customization can expand or replace the explicit tree, but it still passes through the same launcher and profile resolution. + +The Python runtime continues to package `dsh-web-app` and the frontend assets. `dsh web` starts that separate browser application from the installed wheel; a Python SDK client cannot select `web` because it contains no JSON-RPC server row. + +## Existing decisions and supersession + +This decision partially supersedes the base-first and standalone-tree rejection in [one dsh launcher for application profiles](2026-08-22-single-dsh-application-launcher.md). Repository-owned, versioned standalone profile bundles are allowed when an explicit roster is the product behavior; caller-supplied complete trees and alternate executables remain rejected. + +It also supersedes the minimal-overlay realization in [Python SDK runtime through the dsh profile launcher](2026-08-23-python-sdk-dsh-profile-runtime.md) and the base-first default-profile statement in [profile plugin bundles](2026-08-05-profile-plugin-bundles.md). Those notes retain independent authority for launcher ownership, Python packaging and home requirements, general profile layering, and plugin management. No active note is fully superseded or eligible for archival. + +## Verification + +The bundle test pins the exact row and dependency roster. Profile-template and config-dump tests pin the one-bundle manifest, startup-only lifecycle, absence of `dsh-base`, and absence of module HMR. The keyless Python example test boots the real `dsh --profile sdk-minimal` process and asserts the generated manifest, complete system prompt, and two advertised tools. The installed-wheel minimal scenario exercises persistent shell state, editor effects, JSONL persistence, and the committed model-visible snapshot through the packaged executable. + +## Alternatives considered + +**Keep the minimal mode as an overlay on `sdk`.** Rejected because filtering model-visible tools does not remove base services, prompt contributors, persistence choices, or later runtime behavior. It also makes the minimal application depend on controls in shared SDK server and system-prompt interfaces. + +**Restore a Python `cordis` argument or environment-selected complete config.** Rejected because it recreates a Python-owned application composition and bypasses profile plugin management and launcher lifecycle. + +**Create a second minimal SDK startup plugin.** Rejected because profile-aware help is the only variation; the SDK startup provider can own that config while keeping EOF and shutdown behavior local. + +**Remove Web packages from the Python runtime closure.** Rejected because the wheel distributes the ordinary `dsh` application and Python deployments may also need `dsh web`; profile selection, not packaging divergence, separates those applications. + +## Consequences + +The minimal model and runtime roster changes only when its owning bundle changes or a trusted higher patch expands it. The price is deliberate duplication of a small complete application tree and omission of shared settings, credentials, policy controls, telemetry, and Web capabilities from that profile. Users choose the full `sdk` profile when they need those services, while both choices keep one launcher, one profile vocabulary, and one packaged runtime. diff --git a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md new file mode 100644 index 0000000000..cea6ad6f57 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md @@ -0,0 +1,57 @@ +# Agent Note: dsh 下的独立 sdk-minimal profile + +Status: implemented + +[English](2026-08-24-standalone-sdk-minimal-profile.md) | 中文 + +## 问题 + +极简 SDK agent 需要显式插件清单。若把它表达为完整 `sdk` profile 上的 overlay,所有 `dsh-base` 服务仍保持挂载,排除逻辑则依赖分散在无关插件中的筛选器与 disable 配置项。以后新增的 base 配置项即使没有进入面向模型的工具清单,也可能改变运行时行为。 + +由调用方提供完整 Cordis 配置树可以得到确切清单,但会绕过 profile 初始化、组合包解析、持久插件管理、home 与调用 patch 层,以及由 `dsh` 拥有的进程生命周期。极简模式需要在组合层排除功能,同时不能创建另一个 launcher 或 Python 自有应用。 + +## 决策 + +### 启动与所有权 + +`dsh --profile sdk-minimal` 是随附的仅启动时 profile。其 manifest 只列出 `@deepseek-ai/dsh-sdk-minimal`,不列出 `@deepseek-ai/dsh-base`。该组合包在 launcher 的空 profile 根之上插入完整 Cordis 配置树,而 profile patch、home patch 与有序调用 patch 仍在其上保持普通优先级。 + +`dsh` CLI 仍是唯一应用 launcher。Python 示例通过公开 `profile` 字段与显式 Harness home 选择 `sdk-minimal`。Python 不暴露完整配置或任意 argv 路径。完整 Python 与 TypeScript SDK 的默认值仍是 `sdk`。 + +该组合包复用 `@deepseek-ai/dsh-sdk-app` 提供命令 help、stdin EOF 与有界关闭。启动提供方接受 profile 名称配置,因此两个 SDK profile 都能呈现自己的实际命令,且无需复制进程生命周期代码。 + +### 显式组合 + +该组合包拥有一个 DeepSeek 适配器、SDK JSON-RPC 服务、无执行器的 agent 主干、本地子进程与不受限文件系统提供方、持久 Bash、字符串替换 editor,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 会话。Persona 来自 `DSH_SYSTEM_PROMPT`,适配器路由来自 `DSH_MODEL` 与 `DSH_CONTEXT_WINDOW`,凭据来自 `DEEPSEEK_API_KEY`。 + +Harness 身份、运行时上下文、workspace 指令、skills、面向模型的 job 控制、compaction、settings、托管凭据、遥测、Web 工具、subagent 与其他所有 base 配置项均不存在,而不是被隐藏。该 profile 固定使用 `danger-full-access`、`maxTokensAsSuccess: false` 与仅启动时 patch 加载。由于持久终端使用 Bash,此层只支持 POSIX。 + +### 自定义与 Web + +`dsh plugin --profile sdk-minimal add ` 安装持久依赖与组合包层。Profile 自身的 `cordis.patch.yml`、home patch 与 Python `patches` 分别提供持久、机器本地和逐次调用的配置项变更。自定义可以扩展或替换显式配置树,但仍经过同一个 launcher 与 profile 解析。 + +Python 运行时继续打包 `dsh-web-app` 与前端产物。`dsh web` 会从已安装 wheel 启动这个独立浏览器应用;Python SDK client 不能选择 `web`,因为其中没有 JSON-RPC server 配置项。 + +## 既有决策与取代关系 + +本决策部分取代[应用 profile 使用同一个 dsh launcher](2026-08-22-single-dsh-application-launcher.zh.md)中的 base 优先规则与独立配置树否决。显式清单属于产品行为时,可以使用仓库自有且有版本的独立 profile 组合包;由调用方提供的完整配置树与替代可执行程序仍被否决。 + +本决策也取代 [Python SDK 运行时通过 dsh profile launcher 启动](2026-08-23-python-sdk-dsh-profile-runtime.zh.md)中的极简 overlay 实现,以及 [profile 插件组合包](2026-08-05-profile-plugin-bundles.zh.md)中默认 profile 均以 base 开头的表述。这些 Agent Note 对 launcher 所有权、Python 打包与 home 要求、普通 profile 分层及插件管理仍保持独立权威。没有活跃 Agent Note 被完全取代或符合归档条件。 + +## 验证 + +组合包测试固定确切配置项与依赖清单。Profile 模板与配置 dump 测试固定单组合包 manifest、仅启动时生命周期、`dsh-base` 缺席与模块 HMR 缺席。Keyless Python 示例测试启动真实 `dsh --profile sdk-minimal` 进程,并断言生成的 manifest、完整系统提示词与两个对外公布的工具。Installed-wheel 极简场景通过打包可执行程序验证持久 shell 状态、editor 文件效果、JSONL 持久化与已提交的模型可见快照。 + +## 考虑过的替代方案 + +**继续把极简模式作为 `sdk` 上的 overlay。** 否决:筛选面向模型的工具不会移除 base 服务、提示词贡献方、持久化选择或后续运行时行为,还会让极简应用依赖共享 SDK server 与系统提示词接口中的控制项。 + +**恢复 Python `cordis` 参数或由环境选择的完整配置。** 否决:这会重新创建 Python 自有应用组合,并绕过 profile 插件管理与 launcher 生命周期。 + +**创建第二个极简 SDK 启动插件。** 否决:唯一变化是 profile 感知的 help;SDK 启动提供方可以拥有该配置,同时把 EOF 与关闭行为保持在一处。 + +**从 Python 运行时闭包移除 Web 包。** 否决:wheel 分发普通 `dsh` 应用,而且 Python 部署也可能需要 `dsh web`;这些应用由 profile 选择隔离,而不是由打包差异隔离。 + +## 后果 + +极简模型与运行时清单只有在所属组合包变化,或受信任的上层 patch 扩展它时才会变化。代价是刻意重复一棵较小的完整应用树,并在该 profile 中省略共享 settings、凭据、策略控制、遥测与 Web 功能。需要这些服务的用户选择完整 `sdk` profile;两种选择仍共用一个 launcher、一套 profile 词汇与一个打包运行时。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml index bad485523f..49ebbb6567 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml @@ -2,5 +2,5 @@ # 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/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md -2026-08-10-minimal-preset-owns-rl-composition.md: 47a1bbe9f8f4875e2437b3ff955663c4b3de7dce -2026-08-10-minimal-preset-owns-rl-composition.zh.md: 0fab1e1f23a314bec80b5fd355abcb2881c1b1a5 +2026-08-10-minimal-preset-owns-rl-composition.md: 2e9a3e56252f8e91008a5559ad738a7ca678446b +2026-08-10-minimal-preset-owns-rl-composition.zh.md: 31df6ebfbc15f35208bc73b391725b2039fe7819 diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md index 47a1bbe9f8..2e9a3e5625 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md +++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md @@ -22,7 +22,7 @@ The process-wide `core-web.cordis.yml` patch is absent. Browser UI, workspace at System-prompt and persona package tests prove final complete-section and runtime-context suppression, including waterfall mutation and duplicate rejection. The shipped-preset composition test asserts the exact prompt, Bash description, absolute editor schema, and two-tool catalog under the default native presentation. The keyless Web replay sends a real request through a `minimal` agent while global identity, Web-orientation text, dynamic policy contexts, and a test section are registered, asserts that no runtime-context snapshot exists, the entry-local filesystem is bare, and compaction is absent, then executes two persistent Bash calls to prove environment and cwd state survive and executes the editor through an absolute path. -The standalone [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/minimal.cordis.yml) is the complete two-tool composition for the bundled JSON-RPC runtime. The [bare two-tool runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) owns its launch-specific environment configuration, bare filesystem, and absence of compaction. Its keyless SDK replay asserts the assembled system prompt and two-tool catalog, executes persistent Bash across calls, and exercises the editor; the Python SDK tutorial provides the runnable entry point. +The standalone [`sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) is the complete two-tool composition for `dsh --profile sdk-minimal`. The [bare two-tool runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) owns its launch-specific environment configuration, bare filesystem, and absence of compaction; the [standalone-profile decision](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) owns its launcher and bundle placement. Its keyless SDK process test asserts the assembled system prompt and two-tool catalog, and the installed-wheel scenario executes persistent Bash across calls and exercises the editor; the Python SDK tutorial provides the runnable entry point. ## Alternatives considered @@ -36,4 +36,4 @@ The standalone [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/mini ## Consequences -The Web RL prompt is fixed rather than environment-overridable; the standalone JSON-RPC prompt is deployment-selected. The Web preset and standalone JSON-RPC example state the same two-tool contract for their respective launch paths. The model sees only persistent `bash` and `str_replace_editor`; shell state is per agent and disappears with that agent. The Web preset pays for its own PTY and bare filesystem service instances, while other presets pay nothing for them. The local persistent-shell backend requires the supported POSIX terminal substrate, so this preset does not support Windows agents. +The Web RL prompt is fixed rather than environment-overridable; the standalone JSON-RPC prompt is deployment-selected. The Web preset and `sdk-minimal` profile state the same two-tool behavior for their respective launch paths. The model sees only persistent `bash` and `str_replace_editor`; shell state is per agent and disappears with that agent. The Web preset pays for its own PTY and bare filesystem service instances, while other presets pay nothing for them. The local persistent-shell backend requires the supported POSIX terminal substrate, so this preset does not support Windows agents. diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md index 0fab1e1f23..31df6ebfbc 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md @@ -16,13 +16,13 @@ Status: implemented preset persona 恰好是 `You are a helpful software engineer assistant.`,它设置 `complete: true`,并为其 agent 作用域抑制 runtime context。complete `PromptSection` 参与常规组装,因此工具、变量和协作式监听器仍会解析;`system-prompt/assemble` waterfall(瀑布式事件)结束后,提示词注册表会将该段落的独立副本恢复为唯一的系统提示词段落,并丢弃每个动态上下文贡献。存在多个有效 complete 段时,组装会被拒绝。这些最终注册表约束可防止 harness 身份、Web 定位、工具引导、组装监听器、沙箱策略、批准策略、委派或其他动态上下文提供方添加模型输入。 -进程级 `core-web.cordis.yml` patch 不再存在。浏览器 UI、workspace 附加、持久化、子进程、沙箱、权限、模型路由及其他跨会话服务仍由宿主持有。选择 `minimal` 会改变一个 agent 面向模型的组合,并且仅为该 agent 遮蔽宿主文件系统提供方,不会改变 Web 进程中的其他会话。 +进程级 `core-web.cordis.yml` patch 缺席。浏览器 UI、workspace 附加、持久化、子进程、沙箱、权限、模型路由及其他跨会话服务仍由宿主持有。选择 `minimal` 会改变一个 agent 面向模型的组合,并且仅为该 agent 遮蔽宿主文件系统提供方,不会改变 Web 进程中的其他会话。 ## 验证 系统提示词与 persona 包测试证明了 complete 段最终约束与 runtime-context 抑制,包括 waterfall 修改与重复项拒绝。交付 preset 组合测试在默认原生呈现下断言精确的提示词、Bash 描述、要求绝对路径的编辑器 schema 和双工具目录。无密钥 Web 回放通过 `minimal` agent 发送一个真实请求,同时注册全局身份、Web 定位文本、动态策略上下文和一个测试段落;它断言不存在 runtime-context 快照、entry 本地文件系统是裸后端且压缩不存在,随后执行两次持久 Bash 调用,证明环境与 cwd 状态能够保留,并通过绝对路径执行编辑器。 -独立的 [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/minimal.cordis.yml) 是内置 JSON-RPC 运行时的完整双工具组合。[裸双工具运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)说明其启动方式专属的环境配置、裸文件系统和无压缩选择。其无密钥 SDK 回放会断言组装后的系统提示词与双工具目录,跨调用执行持久 Bash,并使用编辑器;Python SDK 教程提供可运行的入口。 +独立的 [`sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)是 `dsh --profile sdk-minimal` 的完整双工具组合。[裸双工具运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)说明其启动方式专属的环境配置、裸文件系统和无 compaction 选择;[独立 profile 决策](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)负责其 launcher 与组合包位置。其无密钥 SDK 进程测试会断言组装后的系统提示词与双工具目录,installed-wheel 场景会跨调用执行持久 Bash 并使用编辑器;Python SDK 教程提供可运行入口。 ## 考虑过的替代方案 @@ -36,4 +36,4 @@ preset persona 恰好是 `You are a helpful software engineer assistant.`,它 ## 后果 -Web RL 提示词固定不变,不能通过环境覆盖;独立 JSON-RPC 提示词由部署选择。Web preset 与独立 JSON-RPC 示例分别在各自的启动路径声明相同的双工具约定。模型只看到持久 `bash` 与 `str_replace_editor`;shell 状态按 agent 隔离,并随该 agent 一并消失。Web preset 为自身的 PTY 与裸文件系统服务实例承担开销,其他 preset 无需承担。持久 shell 的本地后端需要受支持的 POSIX 终端基础环境,因此该 preset 不支持 Windows agent。 +Web RL 提示词固定不变,不能通过环境覆盖;独立 JSON-RPC 提示词由部署选择。Web preset 与 `sdk-minimal` profile 分别为各自启动路径声明相同的双工具行为。模型只看到持久 `bash` 与 `str_replace_editor`;shell 状态按 agent 隔离,并随该 agent 一并消失。Web preset 为自身的 PTY 与裸文件系统服务实例承担开销,其他 preset 无需承担。持久 shell 的本地后端需要受支持的 POSIX 终端基础环境,因此该 preset 不支持 Windows agent。 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml index 5d04c6b846..2d37aaba9f 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml @@ -2,5 +2,5 @@ # 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/feature/2026-07-31-web-telemetry-default-mount.md -2026-07-31-web-telemetry-default-mount.md: 9399cbced62e326a46058a5f0c21c949e8737eff -2026-07-31-web-telemetry-default-mount.zh.md: 133c5293fce676567a9ef8d2a47196678f4dc0e4 +2026-07-31-web-telemetry-default-mount.md: a492356eccba9f272ee216777eb518750c7b6b62 +2026-07-31-web-telemetry-default-mount.zh.md: 3d852229c069ae32f328c8dae38cfdf1744c7293 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md index 9399cbced6..a492356ecc 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md @@ -10,7 +10,7 @@ The telemetry seam and OTel backend ([revival Note](2026-07-23-session-telemetry ## Decision -The shared dsh base bundle (`packages/bundle/base/cordis.patch.yml`) mounts the `session-telemetry-otel` row with a baked-in production endpoint, so every profile has one consistent telemetry capability. The [default-off decision](2026-08-10-telemetry-default-off.md) keeps that row in `DISABLED` mode unless a deployment explicitly selects `FULL` or `FEEDBACK_ONLY`; the endpoint alone does not authorize reporting. Web and headless use the [bounded, escalating process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) on SIGINT/SIGTERM, giving an enabled backend's three-second shutdown deadline time to drain before the five-second launcher bound. +The shared dsh base bundle (`packages/bundle/base/cordis.patch.yml`) mounts the `session-telemetry-otel` row with a baked-in production endpoint, so every base-backed profile has one consistent telemetry capability. The standalone [`sdk-minimal` profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) deliberately omits that row. The [default-off decision](2026-08-10-telemetry-default-off.md) keeps the mounted row in `DISABLED` mode unless a deployment explicitly selects `FULL` or `FEEDBACK_ONLY`; the endpoint alone does not authorize reporting. Web and headless use the [bounded, escalating process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) on SIGINT/SIGTERM, giving an enabled backend's three-second shutdown deadline time to drain before the five-second launcher bound. | Ruling | Value | Rationale | |---|---|---| @@ -27,7 +27,7 @@ The base bundle test pins the shipped `DISABLED` mode expression, the backend su ## Alternatives considered -**No default mount; deployments add the row themselves.** Rejected because the mounted `DISABLED` mode retains a local feedback warning and gives all profiles one patch target without authorizing any upload. +**No default mount; deployments add the row themselves.** Rejected because the mounted `DISABLED` mode retains a local feedback warning and gives every base-backed profile one patch target without authorizing any upload. **A config field instead of an env patch for the switch.** Infeasible: cordis rows have no config-level disable semantic, and `exporter.url` validation fails loud at plugin construction, so the switch must take effect before the Loader — AppCLIEntry's patch layer is the only seat. diff --git a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md index 133c5293fc..3d852229c0 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md @@ -10,7 +10,7 @@ Status: implemented ## 决策 -共享 dsh 基础组合包(`packages/bundle/base/cordis.patch.yml`)挂载带有内置生产 endpoint 的 `session-telemetry-otel` 配置行,使每个 profile 都具有一致的遥测能力。[默认关闭决策](2026-08-10-telemetry-default-off.zh.md)让该配置行保持 `DISABLED` 模式,除非部署方显式选择 `FULL` 或 `FEEDBACK_ONLY`;仅配置 endpoint 不构成上报授权。Web 与 headless 在 SIGINT/SIGTERM 时使用[有界、可升级的进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md),在启动器 5 秒上限到期前,先给已启用的后端 3 秒关闭截止时间完成排空。 +共享 dsh 基础组合包(`packages/bundle/base/cordis.patch.yml`)挂载带有内置生产 endpoint 的 `session-telemetry-otel` 配置行,使每个基于 base 的 profile 都具有一致的遥测能力。独立的 [`sdk-minimal` profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)刻意省略该配置项。[默认关闭决策](2026-08-10-telemetry-default-off.zh.md)让已挂载配置项保持 `DISABLED` 模式,除非部署方显式选择 `FULL` 或 `FEEDBACK_ONLY`;仅配置 endpoint 不构成上报授权。Web 与 headless 在 SIGINT/SIGTERM 时使用[有界、可升级的进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md),在启动器 5 秒上限到期前,先给已启用的后端 3 秒关闭截止时间完成排空。 | 决策项 | 取值 | 理由 | |---|---|---| @@ -28,7 +28,7 @@ Status: implemented ## 考虑过的替代方案 -**默认不挂载,部署方自行添加配置行。** 不采用:挂载的 `DISABLED` 模式会保留本地反馈警告,并为所有 profile 提供同一个 patch 目标,同时不授权任何上传。 +**默认不挂载,部署方自行添加配置行。** 不采用:挂载的 `DISABLED` 模式会保留本地反馈警告,并为每个基于 base 的 profile 提供同一个 patch 目标,同时不授权任何上传。 **开关做成 config 字段而非 env patch。** 不可行:cordis 行没有 config 层的 disable 语义,且 `exporter.url` 校验在插件构造期 fail-loud,开关必须在 Loader 之前生效——AppCLIEntry patch 层是唯一落点。 diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml index c548bbbcad..58474befe4 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml @@ -2,5 +2,5 @@ # 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/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md -2026-08-11-minimal-profiles-bare-two-tool-runtime.md: 7d068aebb6642602aac0a039c8635acf555ccfe8 -2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: c32a9a2e09ff0acda592062f780427c636fd65dd +2026-08-11-minimal-profiles-bare-two-tool-runtime.md: dbbe0e253d23a7663bdd40ac302e779d3090e8e0 +2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: 831b475f277beeef7e7542fb23c410311b45151b diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md index 7d068aebb6..dbbe0e253d 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md @@ -12,17 +12,17 @@ The two launch paths also have different configuration owners. Web mounts a per- ## Decision -Both shipped minimal profiles expose exactly persistent `bash` and `str_replace_editor`, mount no context-compaction provider, suppress every `dsh-system-prompt` runtime-context contribution for fresh sessions, and run the editor against `@deepseek-ai/dsh-fs-local`. The Web preset isolates `ctx.fs` inside the agent entry and mounts `fs-local` beside the editor, so other Web agents retain the host filesystem provider. Its persona remains the fixed complete prompt owned by the earlier [minimal-preset composition decision](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md) and applies runtime-context suppression only to that agent scope. The standalone spine forwards the same setting to its process-owned system-prompt service. Sandbox and approval services remain mounted and enforce their policies; only their model-facing dynamic context is absent. +Both shipped minimal profiles expose exactly persistent `bash` and `str_replace_editor`, mount no context-compaction provider, suppress every `dsh-system-prompt` runtime-context contribution for fresh sessions, and run the editor against `@deepseek-ai/dsh-fs-local`. The Web preset isolates `ctx.fs` inside the agent entry and mounts `fs-local` beside the editor, so other Web agents retain the host filesystem provider. Its persona remains the fixed complete prompt owned by the earlier [minimal-preset composition decision](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md) and applies runtime-context suppression only to that agent scope. The standalone spine forwards the same setting to its process-owned system-prompt service. The Web host retains its sandbox and approval services; the standalone profile mounts a danger-full-access sandbox policy and no approval service. Neither contributes model-facing policy context. -The standalone [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/minimal.cordis.yml) remains a complete JSON-RPC process composition. It mounts `dsh-sdk-jsonrpc-server`, the local PTY and subprocess services required by persistent Bash, `fs-local`, the two tool consumers, and uncompressed JSONL persistence. It does not mount `token-meter`, `compaction-basic`, `fs-sandbox`, or `fs-observation-policy`. Persistent Bash still consumes the deployment's danger-full-access sandbox policy; the editor is not confined by that policy. +The standalone [`@deepseek-ai/dsh-sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) remains a complete JSON-RPC process composition behind `dsh --profile sdk-minimal`. It mounts SDK startup and JSON-RPC serving, the local PTY and subprocess services required by persistent Bash, `fs-local`, the two tool consumers, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not mount `token-meter`, `compaction-basic`, `fs-sandbox`, or `fs-observation-policy`. Persistent Bash still consumes the profile's danger-full-access sandbox policy; the editor is not confined by that policy. The [standalone-profile decision](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) owns this bundle placement and its separation from `dsh-base`. -`DSH_SYSTEM_PROMPT` selects the standalone persona. `DSH_MODEL` names the DeepSeek provider catalog entry, and `DSH_CONTEXT_WINDOW` supplies that entry's capacity. Because the SDK client owns the JSON-RPC `initialize` request, [`minimal.py`](../../../../examples/python-sdk-agent/minimal.py) also uses `DSH_MODEL` as its default `model` argument; an explicit `--model` remains authoritative. Endpoint and credential variables stay owned by the DeepSeek adapter's existing environment-resolution path. +`DSH_SYSTEM_PROMPT` selects the standalone persona. `DSH_MODEL` names the DeepSeek provider catalog entry, and `DSH_CONTEXT_WINDOW` supplies that entry's capacity. Because the SDK client owns the JSON-RPC `initialize` request, [`minimal.py`](../../../../examples/python-sdk-agent/minimal.py) uses `DSH_MODEL` as its default `model` argument and passes an explicit `--model` back to the child environment so the catalog and request remain aligned. Endpoint and credential variables stay owned by the DeepSeek adapter's existing environment-resolution path. ## Verification The Web replay boots the complete Web host, creates the agent through the preset service, and asserts that the scoped filesystem is bare, no scoped compaction service exists, no system-prompt-owned runtime-context message was appended, and the assembled request contains exactly the fixed prompt and two tools. It then executes persistent Bash and the editor against the real scoped services. -The SDK replay boots the real JSON-RPC agent process through the SDK client, injects an environment-selected prompt, asserts the assembled prompt, exact two-tool catalog, and absence of every system-prompt-owned runtime-context message, and executes both tools. Python SDK bundled-runtime coverage initializes the standalone configuration through each available packaged carrier with environment-selected model, model capacity, and prompt values. Cordis validation checks that both configurations resolve their declared plugins and configuration fields. +The SDK keyless process test boots real `dsh --profile sdk-minimal`, injects an environment-selected prompt, and asserts the generated one-bundle manifest, assembled prompt, exact two-tool catalog, and absence of every system-prompt-owned runtime-context message. Python SDK bundled-runtime coverage initializes the standalone profile through each available packaged carrier with environment-selected model, model capacity, and prompt values, then executes both tools. Cordis validation checks that both configurations resolve their declared plugins and configuration fields. ## Alternatives considered @@ -36,4 +36,4 @@ The SDK replay boots the real JSON-RPC agent process through the SDK client, inj ## Consequences -Minimal sessions never summarize or replace earlier history and never add a runtime-context snapshot; callers must keep turns within the selected model's context capacity and must not rely on model-visible narration of standing sandbox or approval policy. The editor can address any absolute path visible to the runtime process, independently of the persistent shell's sandbox policy. The two launch paths share their model-facing tool, no-context, and no-compaction guarantees while retaining different prompt and model configuration appropriate to their owners. The Python SDK path continues to communicate only through the bundled stdio JSON-RPC runtime. +Minimal sessions never summarize or replace earlier history and never add a runtime-context snapshot; callers must keep turns within the selected model's context capacity and must not rely on model-visible narration of standing sandbox or approval policy. The editor can address any absolute path visible to the runtime process, independently of the persistent shell's sandbox policy. The two launch paths share their model-facing tool, no-context, and no-compaction guarantees while retaining different prompt and model configuration appropriate to their owners. The Python SDK path communicates only through the bundled `dsh` stdio JSON-RPC profile. diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md index c32a9a2e09..831b475f27 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md @@ -12,17 +12,17 @@ Web `minimal` preset 与独立 JSON-RPC minimal 组合对外提供持久 `bash` ## 决策 -两种随附 minimal profile 都只对外提供持久 `bash` 与 `str_replace_editor`,不挂载上下文压缩提供方,为新建会话抑制每个 `dsh-system-prompt` runtime-context 贡献,并让编辑器使用 `@deepseek-ai/dsh-fs-local`。Web preset 在 agent entry 内隔离 `ctx.fs`,将 `fs-local` 与编辑器一起挂载,因此其他 Web agent 仍使用宿主文件系统提供方。其 persona 继续采用较早的 [minimal preset 组合决策](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)所拥有的固定 complete 提示词,并仅为该 agent 作用域实施 runtime-context 抑制。独立 spine 将同一设置转发给其进程拥有的 system-prompt 服务。沙箱与批准服务仍保持挂载并强制其策略;只有它们面向模型的动态上下文缺席。 +两种随附 minimal profile 都只对外提供持久 `bash` 与 `str_replace_editor`,不挂载上下文压缩提供方,为新建会话抑制每个 `dsh-system-prompt` runtime-context 贡献,并让编辑器使用 `@deepseek-ai/dsh-fs-local`。Web preset 在 agent entry 内隔离 `ctx.fs`,将 `fs-local` 与编辑器一起挂载,因此其他 Web agent 仍使用宿主文件系统提供方。其 persona 继续采用较早的 [minimal preset 组合决策](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)所拥有的固定 complete 提示词,并仅为该 agent 作用域实施 runtime-context 抑制。独立 spine 将同一设置转发给其进程拥有的 system-prompt 服务。Web 宿主保留沙箱与批准服务;独立 profile 挂载 danger-full-access 沙箱策略,不挂载批准服务。两者都不贡献面向模型的策略上下文。 -独立的 [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/minimal.cordis.yml) 仍是完整的 JSON-RPC 进程组合。它挂载 `dsh-sdk-jsonrpc-server`、持久 Bash 所需的本地 PTY 和子进程服务、`fs-local`、两个工具消费方,以及未压缩的 JSONL 持久化。它不挂载 `token-meter`、`compaction-basic`、`fs-sandbox` 或 `fs-observation-policy`。持久 Bash 仍消费部署的 danger-full-access 沙箱策略;编辑器不受该策略限制。 +独立的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)仍是 `dsh --profile sdk-minimal` 后面的完整 JSON-RPC 进程组合。它挂载 SDK 启动与 JSON-RPC 服务、持久 Bash 所需的本地 PTY 和子进程服务、`fs-local`、两个工具消费方,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不挂载 `token-meter`、`compaction-basic`、`fs-sandbox` 或 `fs-observation-policy`。持久 Bash 仍消费该 profile 的 danger-full-access 沙箱策略;编辑器不受该策略限制。[独立 profile 决策](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)负责该组合包的位置及其与 `dsh-base` 的分离。 -`DSH_SYSTEM_PROMPT` 选择独立组合的 persona。`DSH_MODEL` 命名 DeepSeek 提供方目录项,`DSH_CONTEXT_WINDOW` 提供该目录项的容量。由于 SDK 客户端拥有 JSON-RPC `initialize` 请求,[`minimal.py`](../../../../examples/python-sdk-agent/minimal.py)也使用 `DSH_MODEL` 作为 `model` 参数的默认值;显式 `--model` 仍具有最高优先级。端点与凭据变量继续由 DeepSeek 适配器现有的环境解析路径持有。 +`DSH_SYSTEM_PROMPT` 选择独立组合的 persona。`DSH_MODEL` 命名 DeepSeek 提供方目录项,`DSH_CONTEXT_WINDOW` 提供该目录项的容量。由于 SDK 客户端拥有 JSON-RPC `initialize` 请求,[`minimal.py`](../../../../examples/python-sdk-agent/minimal.py)会使用 `DSH_MODEL` 作为 `model` 参数的默认值,并把显式 `--model` 传回子进程环境,使目录与请求保持一致。端点与凭据变量继续由 DeepSeek 适配器现有的环境解析路径持有。 ## 验证 Web 回放会启动完整 Web 宿主,通过 preset 服务创建 agent,并断言作用域文件系统为裸后端、不存在作用域压缩服务、没有追加 system-prompt 拥有的 runtime-context 消息,而且组装请求只包含固定提示词与两个工具。随后,它通过真实作用域服务执行持久 Bash 和编辑器。 -SDK 回放通过 SDK 客户端启动真实 JSON-RPC agent 进程,注入由环境选择的提示词,断言组装提示词与精确双工具目录,另外断言不存在任何 system-prompt 拥有的 runtime-context 消息,并执行两个工具。Python SDK 内置运行时覆盖会通过每种可用的打包载体,使用环境选择的模型、模型容量和提示词值初始化独立配置。Cordis 校验会检查两份配置能否解析声明的插件和配置字段。 +SDK keyless 进程测试启动真实 `dsh --profile sdk-minimal`,注入由环境选择的提示词,并断言生成的单组合包 manifest、组装提示词、精确双工具目录,以及不存在任何 system-prompt 拥有的 runtime-context 消息。Python SDK 内置运行时覆盖会通过每种可用的打包载体,使用环境选择的模型、模型容量和提示词值初始化独立 profile,然后执行两个工具。Cordis 校验会检查两份配置能否解析声明的插件和配置字段。 ## 考虑过的替代方案 @@ -36,4 +36,4 @@ SDK 回放通过 SDK 客户端启动真实 JSON-RPC agent 进程,注入由环 ## 后果 -Minimal 会话不会摘要或替换较早历史,也不会添加 runtime-context 快照;调用方必须让会话轮次保持在所选模型的上下文容量内,且不得依赖模型可见的常驻沙箱或批准策略说明。编辑器可以访问运行时进程可见的任何绝对路径,且不受持久 shell 沙箱策略影响。两条启动路径共享面向模型的工具、无上下文与无压缩保证,同时保留适合各自所有者的不同提示词和模型配置。Python SDK 路径继续仅通过内置 stdio JSON-RPC 运行时通信。 +Minimal 会话不会摘要或替换较早历史,也不会添加 runtime-context 快照;调用方必须让会话轮次保持在所选模型的上下文容量内,且不得依赖模型可见的常驻沙箱或批准策略说明。编辑器可以访问运行时进程可见的任何绝对路径,且不受持久 shell 沙箱策略影响。两条启动路径共享面向模型的工具、无上下文与无压缩保证,同时保留适合各自所有者的不同提示词和模型配置。Python SDK 路径只通过内置 `dsh` stdio JSON-RPC profile 通信。 diff --git a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.i18n.yaml b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.i18n.yaml index d2eb7495d9..534ca917cc 100644 --- a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.i18n.yaml @@ -2,5 +2,5 @@ # 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/testing/2026-08-13-python-minimal-model-visible-snapshot.md -2026-08-13-python-minimal-model-visible-snapshot.md: 37c76be93fb4f18fa99ceec7c15d20e572f2dfb3 -2026-08-13-python-minimal-model-visible-snapshot.zh.md: 5c2b0dd5fcd6ec5ea3a68eb884e19b0917bbe0be +2026-08-13-python-minimal-model-visible-snapshot.md: 1cef57c440ddd2209628016ab550174b448587bc +2026-08-13-python-minimal-model-visible-snapshot.zh.md: fbbb3bd6f4c3edadb61a02eebbae3dfaa97cb8e7 diff --git a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md index 37c76be93f..1cef57c440 100644 --- a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md +++ b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.md @@ -6,15 +6,13 @@ English | [中文](2026-08-13-python-minimal-model-visible-snapshot.zh.md) ## Problem -The Python lane never compared what the minimal composition actually shows the model. Dynamic runtime context reaches history as a user message, so the mock model's assertion that system-role messages equal the deployment persona could not see it, and the advanced executable snapshot replaces each request header's assembled system prompt with a token and each tool schema with its name. The sandbox-policy runtime-context message therefore rode along in the checked-in [minimal composition](../../../../examples/python-sdk-agent/minimal.cordis.yml) while `python-runtime` stayed green, and any plugin that adds a system section, a tool, or another context message could do the same. +The Python lane needs an exact record of what the standalone minimal profile shows the model. Functional tool assertions prove execution but do not reveal an added system section, tool description, or user-role context message, while the advanced executable snapshot replaces each request header's assembled system prompt with a token and each tool schema with its name. ## Decision -The `sdk-minimal` scenario in [the packaged-runtime smoke](../../../../scripts/smoke-python-runtime.py) records `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`: for every model request of the turn, the advertised tool schemas verbatim and the message list. System and user messages keep their full text with the scenario's temporary directory tokenized; assistant and tool messages keep only call identity, because their PTY and filesystem text differs across the platforms the expected output replays on. +The `sdk-minimal` scenario in [the packaged-runtime smoke](../../../../scripts/smoke-python-runtime.py) boots the shipped profile and records `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`: for every model request of the turn, the advertised tool schemas verbatim and the message list. System and user messages keep their full text with the scenario's temporary directory tokenized; assistant and tool messages keep only call identity, because their PTY and filesystem text differs across replay platforms. The profile omits dynamic runtime context, so every message it emits is compared. -One model-visible message is excluded: the agent loop's dynamic runtime-context snapshot. The same composition emits it on macOS and not on Linux, which the required lane runs, so no single expected output can carry it. That difference is a defect in its own right ([#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488)) — this expected output covers every other model-visible message rather than waiting for it. - -The mock model no longer asserts the minimal scenario's tools and system prompts — the snapshot owns that surface and reports a complete diff instead of the first mismatch. Snapshot comparison takes its directory and file set as arguments, so the `minimal` and `advanced` expected outputs use one implementation, and `--update-snapshots` accepts `sdk-minimal`. +The snapshot, rather than inline mock-model assertions, owns the minimal scenario's tools and system prompts and reports their complete diff. Snapshot comparison takes its directory and file set as arguments, so the `minimal` and `advanced` expected outputs use one implementation, and `--update-snapshots` accepts `sdk-minimal`. ## Alternatives considered @@ -22,12 +20,12 @@ The mock model no longer asserts the minimal scenario's tools and system prompts **Extend the mock model's inline assertions.** Every new model-visible contribution would need another hand-written expectation, and a failure names one mismatch rather than the whole surface. Tool descriptions would also be duplicated from the composition into the script. -**Rely on the TypeScript SDK snapshot.** Its `persistent-tools` scenario pins the same composition's system prompt, tool schemas, and runtime context, but through replayed model responses and a source or `lib` runtime, in a different required job. It cannot show what the deployed executable's closure assembles for a Python caller. +**Rely on the TypeScript SDK snapshot.** Its `persistent-tools` scenario pins a similar two-tool composition through replayed model responses and a source or `lib` runtime, in a different required job. It cannot show what the deployed executable's shipped profile assembles for a Python caller. ## Consequences A change to the minimal composition's model-visible surface — a system section, a tool, a tool description, or an added user message — now fails `python-runtime` with the exact diff, and landing it means rerunning `--scenario sdk-minimal --update-snapshots` and reviewing that diff. The minimal composition's tool descriptions become reviewed expected output. -Assistant and tool message text is no longer compared, and the runtime-context snapshot is not compared at all. The scenario's own assertions continue to own persistent-shell state, editor output, and the final response; [#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488) owns the excluded message until its platform difference is resolved. +Assistant and tool message text is not compared. The scenario's own assertions continue to own persistent-shell state, editor output, and the final response; the snapshot owns every model-visible message the profile emits. [AGENTS.md](../../../../AGENTS.md) and [the testing policy](../../../../docs/testing.md) now name both SDKs as independent projections of the agent loop, session lifecycle, and `SessionEventMap`, so a change to any of those carries updating both expected outputs rather than only the one a contributor happens to run. diff --git a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.zh.md b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.zh.md index 5c2b0dd5fc..fbbb3bd6f4 100644 --- a/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.zh.md +++ b/.agents/notes/implemented/testing/2026-08-13-python-minimal-model-visible-snapshot.zh.md @@ -6,15 +6,13 @@ Status: implemented ## 问题 -Python 通道从未比对极简组合实际展示给模型的内容。动态运行时上下文以 user 消息进入历史,因此 mock 模型"system 角色消息等于部署 persona"的断言看不见它;而进阶可执行文件快照会把每个请求头中已组装的系统提示词换成占位符、把每个工具 schema 换成其名称。于是 sandbox-policy 的运行时上下文消息一直搭车留在签入的[极简组合](../../../../examples/python-sdk-agent/minimal.cordis.yml)里,而 `python-runtime` 始终是绿的;任何新增系统分段、工具或其他上下文消息的插件都能照此蒙混过关。 +Python 通道需要精确记录独立极简 profile 实际展示给模型的内容。功能性工具断言可以证明执行,但无法发现新增系统分段、工具描述或 user 角色上下文消息;而进阶可执行文件快照会把每个请求头中已组装的系统提示词换成占位符,并把每个工具 schema 换成其名称。 ## 决策 -[打包运行时冒烟测试](../../../../scripts/smoke-python-runtime.py)的 `sdk-minimal` 场景会录制 `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`:对该回合的每个模型请求,逐字记录对外公布的工具 schema 与消息列表。system 与 user 消息保留全文,仅将场景的临时目录替换为占位符;assistant 与 tool 消息只保留调用标识,因为它们的 PTY 与文件系统文本在期望输出需要重放的各平台上并不相同。 +[打包运行时冒烟测试](../../../../scripts/smoke-python-runtime.py)的 `sdk-minimal` 场景会启动随附 profile,并录制 `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`:对该回合的每个模型请求,逐字记录对外公布的工具 schema 与消息列表。system 与 user 消息保留全文,仅将场景的临时目录替换为占位符;assistant 与 tool 消息只保留调用标识,因为它们的 PTY 与文件系统文本在各回放平台上并不相同。该 profile 省略动态运行时上下文,因此它发出的每条消息都会参与比对。 -有一条模型可见消息被排除在外:agent loop 的动态运行时上下文快照。同一组合在 macOS 上会发出它,在必需车道所用的 Linux 上不会,因此任何单一期望输出都无法承载它。该差异本身就是缺陷([#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488))——这份期望输出覆盖其余全部模型可见消息,而不是等它先被修复。 - -mock 模型不再断言极简场景的工具与系统提示词——该面由快照拥有,并给出完整差异而非首个不匹配项。快照比对以目录与文件集合为参数,因此 `minimal` 与 `advanced` 两份期望输出共用一套实现,且 `--update-snapshots` 接受 `sdk-minimal`。 +极简场景的工具与系统提示词由快照拥有,而不是由 mock 模型内联断言;快照会给出其完整差异。快照比对以目录与文件集合为参数,因此 `minimal` 与 `advanced` 两份期望输出共用一套实现,且 `--update-snapshots` 接受 `sdk-minimal`。 ## 曾考虑的替代方案 @@ -22,12 +20,12 @@ mock 模型不再断言极简场景的工具与系统提示词——该面由快 **扩展 mock 模型中的内联断言。** 每新增一项模型可见贡献都要再手写一条期望,且失败只会指出一处不匹配而非整个面。工具描述还会从组合复制进脚本,形成重复。 -**依赖 TypeScript SDK 快照。** 其 `persistent-tools` 场景固定了同一组合的系统提示词、工具 schema 与运行时上下文,但走的是重放的模型响应与 source 或 `lib` 运行时,且位于另一个必需任务中。它无法体现已部署可执行文件的闭包为 Python 调用方组装出什么。 +**依赖 TypeScript SDK 快照。** 其 `persistent-tools` 场景通过重放模型响应与 source 或 `lib` 运行时固定一套相似的双工具组合,且位于另一个必需任务中。它无法体现已部署可执行文件的随附 profile 为 Python 调用方组装出什么。 ## 后果 极简组合模型可见面的改动——系统分段、工具、工具描述或新增的 user 消息——现在会让 `python-runtime` 带着精确差异失败;要让它落地,就必须重新运行 `--scenario sdk-minimal --update-snapshots` 并审阅该差异。极简组合的工具描述由此成为经过审阅的期望输出。 -assistant 与 tool 消息文本不再参与比对,运行时上下文快照则完全不参与比对。持久 shell 状态、编辑器输出与最终响应仍由该场景自身的断言拥有;被排除的那条消息由 [#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488) 负责,直到其平台差异得到解决。 +assistant 与 tool 消息文本不参与比对。持久 shell 状态、编辑器输出与最终响应仍由该场景自身的断言拥有;快照负责该 profile 发出的每条模型可见消息。 [AGENTS.md](../../../../AGENTS.md) 与[测试政策](../../../../docs/testing.zh.md)现已点明两个 SDK 都是 agent loop、会话生命周期与 `SessionEventMap` 的独立投影,因此改动其中任何一项都要连带更新两侧的期望输出,而不只是贡献者恰好会运行的那一侧。 diff --git a/apps/cli/README.i18n.yaml b/apps/cli/README.i18n.yaml index c23418172b..f73476f371 100644 --- a/apps/cli/README.i18n.yaml +++ b/apps/cli/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 apps/cli/README.md -README.md: b64861ce68b4c6de8edc3142fb95d83f1d395d15 -README.zh.md: a42a1b0539703dc03adf0ac53e8b5819940429a2 +README.md: ff0efb03d8a5d6da747f1b5bc0d05361a6c8a567 +README.zh.md: 8e9c122afa87525c99e8045f220b96b4511c04fe diff --git a/apps/cli/README.md b/apps/cli/README.md index b64861ce68..ff0efb03d8 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The `dsh` command is the sole supported Node application launcher: profiles are ordered stacks of plugin-bundle patch layers under the user's own overrides. SDK and ACP are profiles, not separate public bins. The Python runtime wheel packages this same command and launches its `sdk` profile. [`src/args.ts`](src/args.ts) owns the command grammar, and [`src/bin.ts`](src/bin.ts) loads only the selected runner. Invalid commands, options from another mode, configuration errors, and boot failures exit nonzero. +The `dsh` command is the sole supported Node application launcher: profiles are ordered stacks of plugin-bundle patch layers under the user's own overrides. SDK and ACP are profiles, not separate public bins. The Python runtime wheel packages this same command; the SDK defaults to `sdk`, and the minimal example selects `sdk-minimal`. [`src/args.ts`](src/args.ts) owns the command grammar, and [`src/bin.ts`](src/bin.ts) loads only the selected runner. Invalid commands, options from another mode, configuration errors, and boot failures exit nonzero. ## Entry modes @@ -12,10 +12,11 @@ The `dsh` command is the sole supported Node application launcher: profiles are | `dsh --profile acp` | Serve automation clients over ACP stdio until disconnect. | | `dsh --profile headless "job"` | Run one fresh persisted session, print the final answer, and exit. | | `dsh --profile sdk` | Serve SDK clients over JSON-RPC stdio until shutdown or disconnect. | +| `dsh --profile sdk-minimal` | Serve SDK clients with the standalone minimal agent tree. | | `dsh web` | Alias of `--profile web`. | | `dsh plugin --profile ` | Manage a profile's plugins by forwarding to pnpm in the profile directory. | -The invoking directory is the default workspace root. The `web`, `headless`, `sdk`, and `acp` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`. +The invoking directory is the default workspace root. The `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`. ## App arguments @@ -38,7 +39,7 @@ The tree composes over an empty root: - then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml` - then `--patch` overlays -Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-acp-app`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. +Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-sdk-minimal`, `@deepseek-ai/dsh-acp-app`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it. diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md index a42a1b0539..8e9c122afa 100644 --- a/apps/cli/README.zh.md +++ b/apps/cli/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -`dsh` 是唯一受支持的 Node 应用启动器;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。SDK 与 ACP 都是 profile,而不是独立的公开 bin。Python 运行时 wheel 会打包同一个命令,并启动其 `sdk` profile。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。 +`dsh` 是唯一受支持的 Node 应用启动器;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。SDK 与 ACP 都是 profile,而不是独立的公开 bin。Python 运行时 wheel 会打包同一个命令;SDK 默认使用 `sdk`,极简示例选择 `sdk-minimal`。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。 ## 入口模式 @@ -12,10 +12,11 @@ | `dsh --profile acp` | 通过 ACP stdio 为自动化 client 提供服务,直至断开连接。 | | `dsh --profile headless "job"` | 运行一个全新的持久化会话,打印最终答案并退出。 | | `dsh --profile sdk` | 通过 JSON-RPC stdio 为 SDK client 提供服务,直至关闭或断开连接。 | +| `dsh --profile sdk-minimal` | 以独立极简 agent 配置树为 SDK client 提供服务。 | | `dsh web` | `--profile web` 的别名。 | | `dsh plugin --profile ` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 | -运行命令时所在的目录将作为默认 workspace 根目录。`web`、`headless`、`sdk` 和 `acp` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。 +运行命令时所在的目录将作为默认 workspace 根目录。`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。 ## 应用参数 @@ -40,7 +41,7 @@ profile 目录包含一个 `package.json`,其中记录树外插件依赖,以 - profile 自身的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml` - `--patch` 指定的覆盖层 -`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-acp-app`),再从 profile 自身的 `node_modules` 解析;pnpm 会将树外插件安装到该目录。 +`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-sdk-minimal`、`@deepseek-ai/dsh-acp-app`),再从 profile 自身的 `node_modules` 解析;pnpm 会将树外插件安装到该目录。 使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。 diff --git a/apps/cli/composition.md b/apps/cli/composition.md index 9e119a6100..9ec9a9a22d 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -3,7 +3,7 @@ # DSH Base Composition -The dsh-base bundle patch every profile applies first; mode bundles (dsh-web-app, dsh-headless) and the user's profile layer patch over it. +The dsh-base bundle patch shared by the web, headless, sdk, and acp profiles; their mode bundles and user layers patch over it, while sdk-minimal owns a separate standalone tree. ```mermaid flowchart LR diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index f432cc953b..96e7b4c7b0 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 apps/cli/reference/README.md -README.md: 2692731ecdfb4a3f798ca0506954289f2c9bdca8 -README.zh.md: 0a2c0a14151f7b774ed00aab808c7313716c131a +README.md: 33de399dc4b2b8e67ed24ed046fcc0da2ff0a7ac +README.zh.md: 0f20245f318e31159780d29cca955fc85a5b5481 diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index 2692731ecd..33de399dc4 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -8,9 +8,9 @@ This reference defines the profile, web-alias, plugin-management, and config-dum `dsh --profile ` boots the profile at `$DSH_HOME/profiles/`. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's `dsh.profile.bundles` list, the profile's own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml` (machine-local preferences shared by every profile, so it outranks the per-profile layer), and each `--patch ` overlay in argv order. Later layers win per row; a patch replaces the targeted row's complete `config` value rather than deep-merging keys, and may insert new rows. `dsh.profile.patchReload` selects `live` patch-file watching or `startup` one-time loading; omission defaults a custom profile to `live`. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit. -Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-acp-app`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules`. Plain Node installations place one healed symlink there per dependency-closure package. A pkg executable instead places a real ESM proxy that mirrors explicit exports and re-exports the virtual package URL, because operating-system symlinks cannot enter pkg's `/snapshot` filesystem. +Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-sdk-minimal`, `@deepseek-ai/dsh-acp-app`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules`. Plain Node installations place one healed symlink there per dependency-closure package. A pkg executable instead places a real ESM proxy that mirrors explicit exports and re-exports the virtual package URL, because operating-system symlinks cannot enter pkg's `/snapshot` filesystem. -The `web`, `headless`, `sdk`, and `acp` profiles auto-initialize from shipped templates on first use (`web`: base + web-app with live patches; `headless`: base + headless with startup-only patches; `sdk`: base + sdk-app with startup-only patches; `acp`: base + acp-app with startup-only patches). Any other missing profile fails loud with a hint to run `dsh plugin --profile add `. +The `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` profiles auto-initialize from shipped templates on first use (`web`: base + web-app with live patches; `headless`: base + headless with startup-only patches; `sdk`: base + sdk-app with startup-only patches; `sdk-minimal`: its standalone bundle with startup-only patches; `acp`: base + acp-app with startup-only patches). Any other missing profile fails loud with a hint to run `dsh plugin --profile add `. ### App arguments @@ -27,6 +27,7 @@ The shipped apps own these command lines: | `web` | `--host`, `--port`, repeatable `--trusted-host`, `--no-open` | | `headless` | the task text, as the positional argument | | `sdk` | no options; stdio carries the JSON-RPC protocol | +| `sdk-minimal` | no options; stdio carries the same JSON-RPC protocol | | `acp` | no options; stdio carries Agent Client Protocol | A one-shot task (`dsh --profile headless "run the tests"`) creates one fresh persisted Agent through the core registry, submits the task, waits for quiescence, and flushes the Session before deriving the last non-empty assistant text and final `turn/end` reason from its durable interval. It prints the text on stdout and exits 0 for `completed`, else 1. An invocation with no task is a usage error from that app. The shipped headless profile mounts no ApiProxy, Host, HTTP server, Web runtime, or browser client; a successful run writes nothing to stderr and opens no listening port. @@ -80,9 +81,9 @@ The production Web runner needs built package and frontend artifacts (`pnpm run Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain — `SIGTERM` is a supervisor's ordinary stop request and exits 0 on every surface, `SIGINT` reports 130; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed. -All modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. A `patchReload: live` profile watches valid edits of both `cordis.patch.yml` layers (profile and home) and reapplies them transactionally; a `startup` profile applies them once. A one-shot surface exits through its bounded shutdown, which disposes any live watchers. +The base-backed modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. The standalone `sdk-minimal` profile uses the invoking directory as its local filesystem and sandbox-policy root but intentionally omits instruction discovery and SQLite. A `patchReload: live` profile watches valid edits of both `cordis.patch.yml` layers (profile and home) and reapplies them transactionally; a `startup` profile applies them once. A one-shot surface exits through its bounded shutdown, which disposes any live watchers. -New sessions default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads and network access are not confined, while process visibility depends on the selected sandbox backend — bwrap runs commands in a private PID namespace that hides host processes, and Landlock and Seatbelt leave host process visibility unchanged. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. +New sessions in base-backed profiles default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads and network access are not confined, while process visibility depends on the selected sandbox backend — bwrap runs commands in a private PID namespace that hides host processes, and Landlock and Seatbelt leave host process visibility unchanged. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. The standalone `sdk-minimal` tree instead pins `danger-full-access` and mounts no approval or permission-settings service. `DSH_TOOLS_MODE` selects `native`, `code`, or `both` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and composes only persistent `bash` plus `str_replace_editor`. Select 极简模式 when creating a Web session; every other prompt section and model-facing plugin remains absent from that agent while the shared browser, workspace, persistence, sandbox, and permission host stays in place. diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index 0a2c0a1415..0f20245f31 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -8,9 +8,9 @@ `dsh --profile ` 启动位于 `$DSH_HOME/profiles/` 的 profile。生效配置树以空根节点为起点,依次叠加 profile manifest(元数据清单)的 `dsh.profile.bundles` 列表中指定的各组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(这是各 profile 共享的机器本地偏好,因此优先于逐 profile 配置层),以及按 argv 顺序指定的各个 `--patch ` 覆盖层。对同一配置行,后应用的层优先。patch 会替换目标行的整个 `config` 值,而不是深度合并其中的键;patch 也可以插入新行。`dsh.profile.patchReload` 可选择 `live` patch 文件监视或 `startup` 单次加载;自定义 profile 省略该值时默认使用 `live`。配置解析、schema 校验、模块解析或插件启动失败时,系统会报告错误并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。 -组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。普通 Node 安装会为依赖闭包中的每个包放置并修复一个符号链接。pkg 可执行程序则放置真实 ESM 代理,镜像显式 exports 并重新导出虚拟包 URL,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统。 +组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-sdk-minimal`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。普通 Node 安装会为依赖闭包中的每个包放置并修复一个符号链接。pkg 可执行程序则放置真实 ESM 代理,镜像显式 exports 并重新导出虚拟包 URL,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统。 -`web`、`headless`、`sdk` 和 `acp` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app,实时应用 patch;`headless`:base + headless,只在启动时应用 patch;`sdk`:base + sdk-app,只在启动时应用 patch;`acp`:base + acp-app,只在启动时应用 patch)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile add `。 +`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app,实时应用 patch;`headless`:base + headless,只在启动时应用 patch;`sdk`:base + sdk-app,只在启动时应用 patch;`sdk-minimal`:独立组合包,只在启动时应用 patch;`acp`:base + acp-app,只在启动时应用 patch)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile add `。 ### 应用参数 @@ -27,6 +27,7 @@ | `web` | `--host`、`--port`、可重复的 `--trusted-host`、`--no-open` | | `headless` | 任务文本,作为位置参数 | | `sdk` | 无选项;stdio 携带 JSON-RPC 协议 | +| `sdk-minimal` | 无选项;stdio 携带相同的 JSON-RPC 协议 | | `acp` | 无选项;stdio 携带 Agent Client Protocol | 一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent(智能体),提交任务、等待完全停稳并对会话执行 flush,再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 stdout 打印文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载 ApiProxy、Host、HTTP 服务器、Web 运行时或浏览器客户端;成功运行不会向 stderr 写入任何内容,也不会打开监听端口。 @@ -80,9 +81,9 @@ dsh web --help 进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT` 或 `SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。 -所有模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose(资源释放)所有实时监视器。 +基于 base 的模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。独立的 `sdk-minimal` profile 把运行命令时所在的目录作为本地文件系统与沙箱策略根目录,但刻意省略指令发现与 SQLite。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose(资源释放)所有实时监视器。 -新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。 +基于 base 的 profile 中,新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。独立的 `sdk-minimal` 配置树则固定为 `danger-full-access`,且不挂载 approval 或权限 settings 服务。 `DSH_TOOLS_MODE` 为进程选择 `native`、`code` 或 `both`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并且仅组合持久 `bash` 和 `str_replace_editor`。创建 Web 会话时请选择极简模式;该 agent 不包含任何其他提示词段落或面向模型的插件,而共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。 diff --git a/apps/cli/src/profile-boot.ts b/apps/cli/src/profile-boot.ts index 1c77b1ff8a..30058a380f 100644 --- a/apps/cli/src/profile-boot.ts +++ b/apps/cli/src/profile-boot.ts @@ -145,8 +145,8 @@ function allPatches(composed: ComposedProfile): PatchOptions[] { /** * Load `name` and compose its effective patch stack: bundle layers in - * `dsh.profile.bundles` order (the base bundle gates the shell stacks by - * platform on its own rows), the profile's user layer, the home-level user + * `dsh.profile.bundles` order (a base-backed profile gets the base bundle's + * platform-gated shell rows), the profile's user layer, the home-level user * layer (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply * to every profile, so it outranks the per-profile layer), `--patch` overlays, * then the telemetry switch. diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index fe84ec6adf..82e57fd2f7 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/architecture.md -architecture.md: 362a3758fabdb67ea883bcf84ea5df4cf4e77b0f -architecture.zh.md: 421c38ad5b90db7b7ac1b5fe6bb10ccc0793feb5 +architecture.md: add615c252948adab8db7dec7059f94b8f45e52c +architecture.zh.md: 48baf14d6a29e5ef77e6e47f4d9fa9adc4e9e748 diff --git a/docs/architecture.md b/docs/architecture.md index 362a3758fa..add615c252 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -16,17 +16,17 @@ There is no privileged core to patch: you extend dsh by mounting a plugin beside A running `dsh` is a plugin tree composed at boot from ordered layers. -A **profile** is a named composition stored in the Harness home. It lists the bundles it stacks, holds any out-of-tree plugins it installs, and keeps the user's own `cordis.patch.yml`. `web`, `headless`, `sdk`, and `acp` ship as templates. +A **profile** is a named composition stored in the Harness home. It lists the bundles it stacks, holds any out-of-tree plugins it installs, and keeps the user's own `cordis.patch.yml`. `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` ship as templates. A **bundle** is a distribution format for Cordis config rows and the code they mount, so whatever it inserts stays patchable by the layers above it. Each declares itself in its own `package.json` under a `dsh` field: `dsh.profile` lists a profile's bundles, and `dsh.bundle` points at a bundle's patch file. -[`dsh-base`](../packages/bundle/base/README.md) is the first layer of every profile: model adapters, tools, persistence, sandbox and approval policy, settings, credentials, telemetry. [`dsh-web-app`](../packages/bundle/web-app/README.md) adds the browser application, [`dsh-headless`](../packages/bundle/headless/README.md) adds a one-shot runner with no server, [`dsh-sdk-app`](../packages/bundle/sdk-app/README.md) adds the SDK JSON-RPC server, and [`dsh-acp-app`](../packages/bundle/acp-app/README.md) adds the automation-only ACP server. +[`dsh-base`](../packages/bundle/base/README.md) is the shared first layer of the `web`, `headless`, `sdk`, and `acp` profiles: model adapters, tools, persistence, sandbox and approval policy, settings, credentials, telemetry. [`dsh-web-app`](../packages/bundle/web-app/README.md) adds the browser application, [`dsh-headless`](../packages/bundle/headless/README.md) adds a one-shot runner with no server, [`dsh-sdk-app`](../packages/bundle/sdk-app/README.md) adds the SDK JSON-RPC server, and [`dsh-acp-app`](../packages/bundle/acp-app/README.md) adds the automation-only ACP server. [`dsh-sdk-minimal`](../packages/bundle/sdk-minimal/README.md) is the deliberate exception: one bundle owns its complete explicit SDK tree and does not apply `dsh-base`. Layers apply to an empty entry list in this order: each bundle in the profile's listed order, then the profile's `cordis.patch.yml`, then the home-level one, then any `--patch` overlay. A patch targets a row by id and replaces its whole config, or inserts new rows. -Custom profiles default to live patch reload. The shipped `web` profile is live; `headless`, `sdk`, and `acp` apply all layers once at startup because replacing a one-shot or stdio application's dependencies after it owns work would invalidate that lifecycle. +Custom profiles default to live patch reload. The shipped `web` profile is live; `headless`, `sdk`, `sdk-minimal`, and `acp` apply all layers once at startup because replacing a one-shot or stdio application's dependencies after it owns work would invalidate that lifecycle. To see the tree your machine actually boots: @@ -40,11 +40,11 @@ Composition mechanics are in [app-boot](../packages/boot/app-boot/README.md#prof ## Application launch -Every supported Node application starts at the `dsh` CLI with a named profile. The shipped applications are `dsh web` (the deliberate alias for `--profile web`), `dsh --profile headless`, `dsh --profile sdk`, and `dsh --profile acp`. The TypeScript SDK resolves its same-version `dsh` dependency and selects `sdk`; custom plugin composition remains a profile plus ordered patch files, not another executable or inline application tree. +Every supported Node application starts at the `dsh` CLI with a named profile. The shipped applications are `dsh web` (the deliberate alias for `--profile web`), `dsh --profile headless`, `dsh --profile sdk`, `dsh --profile sdk-minimal`, and `dsh --profile acp`. The TypeScript SDK resolves its same-version `dsh` dependency and selects `sdk`; custom plugin composition remains a profile plus ordered patch files, not another executable or inline application tree. `sdk-minimal` is a repository-owned standalone bundle behind the same launcher, not a caller-supplied Cordis tree. Vendored CLIs, build-only and test-only executables, direct in-process plugin mounting, and the private browser WebWorker preview are not Harness application launchers. [`verify-application-entrypoints`](../scripts/verify-application-entrypoints.ts) keeps every package bin, executable source, and root demo in an explicit class and rejects a Node application path that bypasses `dsh`. -The Python SDK follows the same application architecture. Its runtime wheel packages the normal `dsh` CLI as `deepseek-harness-sdk-runtime--`, and the client launches `dsh --profile sdk` with an explicit Harness home. Python exposes profile selection and ordered patch files rather than a complete Cordis tree; persistent external plugins are installed through `dsh plugin`. The removed private direct-config carrier has no compatibility bin or fallback parser. +The Python SDK follows the same application architecture. Its runtime wheel packages the normal `dsh` CLI as `deepseek-harness-sdk-runtime--`, and the client launches `dsh --profile sdk` with an explicit Harness home by default. The minimal example selects the shipped `sdk-minimal` profile. Python exposes profile selection and ordered patch files rather than a complete Cordis tree; persistent external plugins are installed through `dsh plugin`. The removed private direct-config carrier has no compatibility bin or fallback parser. ## Core packages diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 421c38ad5b..48baf14d6a 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -16,17 +16,17 @@ 运行中的 `dsh` 是一棵插件树,由启动时按序叠加的各层组合而成。 -**profile** 是存放在 Harness home 中的具名组装。它列出自己叠放的组合包,存放自己安装的树外插件,并保存用户自己的 `cordis.patch.yml`。`web`、`headless`、`sdk` 和 `acp` 作为模板随发行版交付。 +**profile** 是存放在 Harness home 中的具名组装。它列出自己叠放的组合包,存放自己安装的树外插件,并保存用户自己的 `cordis.patch.yml`。`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` 作为模板随发行版交付。 **组合包**是 Cordis 配置项及其挂载代码的分发格式,因此它插入的内容始终可被其上各层 patch。 两者都在各自的 `package.json` 中通过 `dsh` 字段声明自己:`dsh.profile` 列出一个 profile 的组合包,`dsh.bundle` 指向一个组合包的 patch 文件。 -[`dsh-base`](../packages/bundle/base/README.zh.md) 是每个 profile 的第一层:模型适配器、工具、持久化、沙箱与审批策略、设置、凭据、遥测。[`dsh-web-app`](../packages/bundle/web-app/README.zh.md) 增加浏览器应用,[`dsh-headless`](../packages/bundle/headless/README.zh.md) 增加不带服务器的一次性运行器,[`dsh-sdk-app`](../packages/bundle/sdk-app/README.zh.md) 增加 SDK JSON-RPC 服务器,[`dsh-acp-app`](../packages/bundle/acp-app/README.zh.md) 增加仅用于自动化的 ACP 服务器。 +[`dsh-base`](../packages/bundle/base/README.zh.md) 是 `web`、`headless`、`sdk` 与 `acp` profile 的共享第一层:模型适配器、工具、持久化、沙箱与审批策略、设置、凭据、遥测。[`dsh-web-app`](../packages/bundle/web-app/README.zh.md) 增加浏览器应用,[`dsh-headless`](../packages/bundle/headless/README.zh.md) 增加不带服务器的一次性运行器,[`dsh-sdk-app`](../packages/bundle/sdk-app/README.zh.md) 增加 SDK JSON-RPC 服务器,[`dsh-acp-app`](../packages/bundle/acp-app/README.zh.md) 增加仅用于自动化的 ACP 服务器。[`dsh-sdk-minimal`](../packages/bundle/sdk-minimal/README.zh.md) 是刻意保留的例外:一个组合包拥有完整的显式 SDK 配置树,不应用 `dsh-base`。 各层按此顺序应用在空条目列表之上:先按 profile 列出的顺序应用每个组合包,然后是 profile 的 `cordis.patch.yml`,然后是 home 级的那份,最后是任意 `--patch` overlay。一条 patch 按 id 定位某个条目并替换其整个 config,或插入新条目。 -自定义 profile 默认实时重载 patch。随附的 `web` profile 使用实时重载;`headless`、`sdk` 和 `acp` 则只在启动时应用一次所有配置层,因为一次性应用或 stdio 应用拥有工作之后,替换其依赖会破坏该生命周期。 +自定义 profile 默认实时重载 patch。随附的 `web` profile 使用实时重载;`headless`、`sdk`、`sdk-minimal` 和 `acp` 则只在启动时应用一次所有配置层,因为一次性应用或 stdio 应用拥有工作之后,替换其依赖会破坏该生命周期。 要查看你的机器实际启动的配置树: @@ -40,11 +40,11 @@ dsh --profile web --dump-config ## 应用启动 -所有受支持的 Node 应用都从 `dsh` CLI 与具名 profile 启动。随附应用是 `dsh web`(刻意为 `--profile web` 保留的别名)、`dsh --profile headless`、`dsh --profile sdk` 与 `dsh --profile acp`。TypeScript SDK 会解析其同版本 `dsh` 依赖并选择 `sdk`;自定义插件组合继续由 profile 与有序 patch 文件表达,而不是另一个可执行文件或内联应用树。 +所有受支持的 Node 应用都从 `dsh` CLI 与具名 profile 启动。随附应用是 `dsh web`(刻意为 `--profile web` 保留的别名)、`dsh --profile headless`、`dsh --profile sdk`、`dsh --profile sdk-minimal` 与 `dsh --profile acp`。TypeScript SDK 会解析其同版本 `dsh` 依赖并选择 `sdk`;自定义插件组合继续由 profile 与有序 patch 文件表达,而不是另一个可执行文件或内联应用树。`sdk-minimal` 是位于同一 launcher 后的仓库自有独立组合包,而不是由调用方提供的 Cordis 配置树。 Vendored CLI、仅用于构建和测试的可执行文件、进程内直接挂载插件以及私有浏览器 WebWorker 预览都不属于 Harness 应用启动器。[`verify-application-entrypoints`](../scripts/verify-application-entrypoints.ts)将每个包 bin、可执行源码与根 demo 归入显式类别,并拒绝任何绕过 `dsh` 的 Node 应用路径。 -Python SDK 遵循相同的应用架构。其运行时 wheel 把普通 `dsh` CLI 打包为 `deepseek-harness-sdk-runtime--`,客户端以显式 Harness home 启动 `dsh --profile sdk`。Python 暴露 profile 选择与有序 patch 文件,而不是完整 Cordis 树;持久外部插件通过 `dsh plugin` 安装。已删除的私有直读配置载体没有兼容 bin 或回退 parser。 +Python SDK 遵循相同的应用架构。其运行时 wheel 把普通 `dsh` CLI 打包为 `deepseek-harness-sdk-runtime--`,客户端默认以显式 Harness home 启动 `dsh --profile sdk`。极简示例选择随附的 `sdk-minimal` profile。Python 暴露 profile 选择与有序 patch 文件,而不是完整 Cordis 树;持久外部插件通过 `dsh plugin` 安装。已删除的私有直读配置载体没有兼容 bin 或回退 parser。 ## 核心包 diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 4998dcd505..eabedcfe3c 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/config-catalog.md -config-catalog.md: cb156837fa0768dfa74d9b351cc3e362449ca248 -config-catalog.zh.md: 151a72190332a400b643111495d36e2d7f205d93 +config-catalog.md: 7f7e5d3c58953ea50c43eed0d903f0d5469e7849 +config-catalog.zh.md: f9ab7a8537e29cb0e74e05e74b4a7890d146c28b diff --git a/docs/config-catalog.md b/docs/config-catalog.md index cb156837fa..7f7e5d3c58 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1681,6 +1681,22 @@ Depends on: [`SandboxMode`](subsystems/sandbox.md) Source: [`packages/sandbox/sandbox-policy/src/index.ts:67`](../packages/sandbox/sandbox-policy/src/index.ts) + + +## `@deepseek-ai/dsh-sdk-app` + +Requires: `cmdlineArgs` + +```ts config-catalog +/** SDK stdio startup configuration. */ +export interface Config { + /** Profile name rendered in help and diagnostics (default `sdk`). */ + profile?: string +} +``` + +Source: [`packages/bundle/sdk-app/src/index.ts:23`](../packages/bundle/sdk-app/src/index.ts) + ## `@deepseek-ai/dsh-sdk-jsonrpc-server` @@ -3330,7 +3346,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-llm` ([`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts)) - `@deepseek-ai/dsh-lsp` ([`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts)) - `@deepseek-ai/dsh-schedule` — requires `agents` · `sessions` · `tools` · `sessionPersistence` ([`packages/schedule/schedule/src/index.ts`](../packages/schedule/schedule/src/index.ts)) -- `@deepseek-ai/dsh-sdk-app` — requires `cmdlineArgs` ([`packages/bundle/sdk-app/src/index.ts`](../packages/bundle/sdk-app/src/index.ts)) - `@deepseek-ai/dsh-session` ([`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts)) - `@deepseek-ai/dsh-session-checkpoint-policy` — requires `llm` · `sessionPersistence` · `sessions` · `tools` ([`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts)) - `@deepseek-ai/dsh-session-log-export` — requires `commands` ([`packages/session-query/session-log-export/src/index.ts`](../packages/session-query/session-log-export/src/index.ts)) @@ -3400,6 +3415,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. - `@deepseek-ai/dsh-sandbox-windows-acl` ([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts)) - `@deepseek-ai/dsh-scope` ([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts)) - `@deepseek-ai/dsh-sdk-client` ([`packages/sdk/client/src/index.ts`](../packages/sdk/client/src/index.ts)) +- `@deepseek-ai/dsh-sdk-minimal` ([`packages/bundle/sdk-minimal/src/index.ts`](../packages/bundle/sdk-minimal/src/index.ts)) - `@deepseek-ai/dsh-sdk-protocol` ([`packages/sdk/protocol/src/index.ts`](../packages/sdk/protocol/src/index.ts)) - `@deepseek-ai/dsh-session-telemetry` ([`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts)) - `@deepseek-ai/dsh-session-title-llm` ([`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 151a721903..f9ab7a8537 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -1683,6 +1683,22 @@ export interface Config { 来源:[`packages/sandbox/sandbox-policy/src/index.ts:67`](../packages/sandbox/sandbox-policy/src/index.ts) + + +## `@deepseek-ai/dsh-sdk-app` + +需要:`cmdlineArgs` + +```ts config-catalog +/** SDK stdio startup configuration. */ +export interface Config { + /** Profile name rendered in help and diagnostics (default `sdk`). */ + profile?: string +} +``` + +来源:[`packages/bundle/sdk-app/src/index.ts:23`](../packages/bundle/sdk-app/src/index.ts) + ## `@deepseek-ai/dsh-sdk-jsonrpc-server` @@ -3332,7 +3348,6 @@ export interface Config { - `@deepseek-ai/dsh-llm`([`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts)) - `@deepseek-ai/dsh-lsp`([`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts)) - `@deepseek-ai/dsh-schedule` — 需要 `agents` · `sessions` · `tools` · `sessionPersistence`([`packages/schedule/schedule/src/index.ts`](../packages/schedule/schedule/src/index.ts)) -- `@deepseek-ai/dsh-sdk-app` — 需要 `cmdlineArgs`([`packages/bundle/sdk-app/src/index.ts`](../packages/bundle/sdk-app/src/index.ts)) - `@deepseek-ai/dsh-session`([`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts)) - `@deepseek-ai/dsh-session-checkpoint-policy` — 需要 `llm` · `sessionPersistence` · `sessions` · `tools`([`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts)) - `@deepseek-ai/dsh-session-log-export` — 需要 `commands`([`packages/session-query/session-log-export/src/index.ts`](../packages/session-query/session-log-export/src/index.ts)) @@ -3401,6 +3416,7 @@ export interface Config { - `@deepseek-ai/dsh-sandbox-windows-acl`([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts)) - `@deepseek-ai/dsh-scope`([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts)) - `@deepseek-ai/dsh-sdk-client`([`packages/sdk/client/src/index.ts`](../packages/sdk/client/src/index.ts)) +- `@deepseek-ai/dsh-sdk-minimal`([`packages/bundle/sdk-minimal/src/index.ts`](../packages/bundle/sdk-minimal/src/index.ts)) - `@deepseek-ai/dsh-sdk-protocol`([`packages/sdk/protocol/src/index.ts`](../packages/sdk/protocol/src/index.ts)) - `@deepseek-ai/dsh-session-telemetry`([`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts)) - `@deepseek-ai/dsh-session-title-llm`([`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts)) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index e5f37e4a8c..a7edea20b2 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/module-graph.md -module-graph.md: d401df81c59de27a0171750d92031c40939c6564 -module-graph.zh.md: 8186217711a2ca320017dc0473227f7a9cf59b1a +module-graph.md: a2497ed002c6eec55ec35e1d7e9953feb2ed0ee5 +module-graph.zh.md: 230d67f4c07f9715d4b5157c1539edd09f9221c3 diff --git a/docs/module-graph.md b/docs/module-graph.md index d401df81c5..a2497ed002 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -123,6 +123,7 @@ flowchart TD pkg_base["base"] pkg_headless["headless"] pkg_sdk_app["sdk-app"] + pkg_sdk_minimal["sdk-minimal"] pkg_web_app["web-app"] end subgraph group_client["packages/client"] @@ -366,6 +367,7 @@ flowchart TD pkg_acp_app --> pkg_invariants pkg_base --> pkg_invariants pkg_sdk_app --> pkg_invariants + pkg_sdk_minimal --> pkg_invariants pkg_client_store --> pkg_invariants pkg_client_ui_primitives --> pkg_invariants pkg_client_ui_renderer --> pkg_invariants @@ -1656,6 +1658,7 @@ flowchart TD | [`acp-app`](../packages/bundle/acp-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sdk-app`](../packages/bundle/sdk-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`sdk-minimal`](../packages/bundle/sdk-minimal) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-store`](../packages/client/store) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-primitives`](../packages/client/ui-primitives) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 8186217711..230d67f4c0 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -125,6 +125,7 @@ flowchart TD pkg_base["base"] pkg_headless["headless"] pkg_sdk_app["sdk-app"] + pkg_sdk_minimal["sdk-minimal"] pkg_web_app["web-app"] end subgraph group_client["packages/client"] @@ -368,6 +369,7 @@ flowchart TD pkg_acp_app --> pkg_invariants pkg_base --> pkg_invariants pkg_sdk_app --> pkg_invariants + pkg_sdk_minimal --> pkg_invariants pkg_client_store --> pkg_invariants pkg_client_ui_primitives --> pkg_invariants pkg_client_ui_renderer --> pkg_invariants @@ -1658,6 +1660,7 @@ flowchart TD | [`acp-app`](../packages/bundle/acp-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sdk-app`](../packages/bundle/sdk-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`sdk-minimal`](../packages/bundle/sdk-minimal) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-store`](../packages/client/store) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-primitives`](../packages/client/ui-primitives) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) | diff --git a/docs/user/guide/python-sdk.i18n.yaml b/docs/user/guide/python-sdk.i18n.yaml index 69e9720f11..88d467f519 100644 --- a/docs/user/guide/python-sdk.i18n.yaml +++ b/docs/user/guide/python-sdk.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/user/guide/python-sdk.md -python-sdk.md: 98d7f8160dee0874338c1d914796dcfb72400a48 -python-sdk.zh.md: 8a0417b24be859b8d327a8650ce1db23adb4dba2 +python-sdk.md: 2cf6acbd6718be560ec4365b356c031b17fb070e +python-sdk.zh.md: 2c53886ba3409e6fc21ec57fc79b81bf6907349c diff --git a/docs/user/guide/python-sdk.md b/docs/user/guide/python-sdk.md index 98d7f8160d..2cf6acbd67 100644 --- a/docs/user/guide/python-sdk.md +++ b/docs/user/guide/python-sdk.md @@ -2,7 +2,7 @@ English | [中文](python-sdk.zh.md) -This tutorial installs the published Python SDK, runs the checked-in minimal profile overlay, and shows how to customize the same `dsh` profile from your own program. +This tutorial installs the published Python SDK, runs the shipped standalone minimal profile, and shows how to customize the same `dsh` profile from your own program. ## Prerequisites @@ -43,7 +43,7 @@ python examples/python-sdk-agent/minimal.py \ "Inspect the repository and fix the failing tests." ``` -The script prints the final assistant response. The selected home receives the generated `sdk` profile, settings, credentials if you add them, installed plugins, and Zstandard session logs under `sessions/`. The example and SDK never silently read `~/.dsh`. +The script prints the final assistant response. The selected home receives the generated `sdk-minimal` profile, installed plugins, and uncompressed JSONL session logs under `sessions/`. The example and SDK never silently read `~/.dsh`. ## Use the SDK in your program @@ -54,16 +54,14 @@ from deepseek_harness import DeepSeekHarness workspace = Path("/absolute/path/to/disposable-workspace").resolve() dsh_home = Path("/absolute/path/to/example-dsh-home").resolve() -patch = Path("examples/python-sdk-agent/minimal.patch.yml").resolve() - with DeepSeekHarness( provider="deepseek-official", model="deepseek-v4-flash", max_tokens=49_152, cwd=str(workspace), dsh_home=str(dsh_home), - profile="sdk", - patches=(str(patch),), + profile="sdk-minimal", + env={"DSH_MODEL": "deepseek-v4-flash"}, ) as harness: result = harness.run( "Inspect the repository and fix the failing tests.", @@ -73,7 +71,7 @@ with DeepSeekHarness( print(result.final_response) ``` -The SDK starts the bundled `dsh --profile sdk` process lazily and reuses it until context-manager exit. The profile, its persistent patch, the home patch, and the ordered `patches` tuple form the application configuration. There is no separate Python runtime bin or complete-config option. +The SDK starts the bundled `dsh --profile sdk-minimal` process lazily and reuses it until context-manager exit. The profile, its persistent patch, the home patch, and any ordered `patches` tuple form the application configuration. There is no separate Python runtime bin or complete-config option. ## Install or define plugins @@ -81,15 +79,15 @@ Use `dsh plugin` for dependencies and bundle layers that should persist in this ```sh export DSH_HOME=/absolute/path/to/example-dsh-home -dsh --profile sdk --dump-default-config >/dev/null -dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle +dsh --profile sdk-minimal --dump-default-config >/dev/null +dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle ``` -The first command initializes the shipped SDK profile. The second forwards package management to `pnpm`, then records any installed package that exports a `dsh.bundle` layer. Install `pnpm` only for this management command; launching the installed SDK does not need it. Edit `$DSH_HOME/profiles/sdk/cordis.patch.yml` for persistent row changes, or pass patch files from Python for per-launch changes. +The first command initializes the shipped standalone profile. The second forwards package management to `pnpm`, then records any installed package that exports a `dsh.bundle` layer. Install `pnpm` only for this management command; launching the installed SDK does not need it. Edit `$DSH_HOME/profiles/sdk-minimal/cordis.patch.yml` for persistent row changes, or pass patch files from Python for per-launch changes. Another `profile` is valid when it includes `@deepseek-ai/dsh-sdk-app` or another JSON-RPC server row. Missing server rows, unresolved plugins, and invalid patches fail during startup instead of falling back to another composition. -## Understand the minimal overlay +## Understand the minimal profile | Property | Value | |---|---| @@ -98,11 +96,13 @@ Another `profile` is valid when it includes `@deepseek-ai/dsh-sdk-app` or anothe | Model-facing tools | Persistent `bash` and `str_replace_editor` only | | Bash timeout | 300 seconds | | Editor output limit | 16,000 characters | -| Context compaction | Disabled | -| Session persistence | Zstandard JSONL under `/sessions` | +| Runtime context and compaction | Absent | +| Session persistence | Uncompressed JSONL under `/sessions` | -The overlay allowlists persistent Bash and the editor for every SDK-created root agent, so later base-profile tools cannot appear implicitly. It suppresses unrelated prompt sections and runtime-context messages, disables local instruction discovery and compaction, and retains the SDK application's protocol, persistence, policy, settings, credentials, and providers. Persistent Bash and the editor can modify any path visible to the runtime, so use a disposable checkout or container. The PTY implementation makes this example POSIX-only. +The profile's sole bundle inserts the complete tree over an empty root and does not include `dsh-base`; later base-profile tools therefore cannot appear implicitly. It contains the SDK protocol, one environment-configured DeepSeek adapter, local execution, and persistence, while settings, managed credentials, telemetry, Web tools, subagents, local instruction discovery, and compaction are absent. It pins `danger-full-access`, so persistent Bash and the editor can modify any path visible to the runtime; use a disposable checkout or container. The PTY implementation makes this example POSIX-only. + +The installed wheel still packages the full `web` profile and frontend assets. Run `dsh web` against an explicit `DSH_HOME` when a Python SDK deployment also needs the browser application; `web` is a separate CLI application and cannot serve a Python SDK client. Use a fresh home when profiles, plugins, credentials, settings, and sessions must be isolated. Use a fresh session id for independent work; reuse a harness, home, and id only to continue the same durable conversation and session-owned resources. -The [example reference](../../../examples/python-sdk-agent/README.md) owns the checked-in overlay. The [Python SDK reference](../../../python/sdk/README.md) covers lifecycle, results, notifications, and low-level behavior; the [dsh CLI reference](../../../apps/cli/reference/README.md) covers profile layering. +The [bundle reference](../../../packages/bundle/sdk-minimal/README.md) owns the exact tree, and the [example reference](../../../examples/python-sdk-agent/README.md) owns the runnable program. The [Python SDK reference](../../../python/sdk/README.md) covers lifecycle, results, notifications, and low-level behavior; the [dsh CLI reference](../../../apps/cli/reference/README.md) covers profile layering. diff --git a/docs/user/guide/python-sdk.zh.md b/docs/user/guide/python-sdk.zh.md index 8a0417b24b..2c53886ba3 100644 --- a/docs/user/guide/python-sdk.zh.md +++ b/docs/user/guide/python-sdk.zh.md @@ -2,7 +2,7 @@ [English](python-sdk.md) | 中文 -本教程安装已发布的 Python SDK,运行检入的极简 profile overlay,并说明如何从自己的程序自定义同一个 `dsh` profile。 +本教程安装已发布的 Python SDK,运行随附的独立极简 profile,并说明如何从自己的程序自定义同一个 `dsh` profile。 ## 前置条件 @@ -43,7 +43,7 @@ python examples/python-sdk-agent/minimal.py \ "Inspect the repository and fix the failing tests." ``` -脚本会打印最终 assistant 响应。所选 home 会保存生成的 `sdk` profile、设置、调用方添加的凭据、已安装插件,以及 `sessions/` 下的 Zstandard 会话日志。示例与 SDK 绝不会静默读取 `~/.dsh`。 +脚本会打印最终 assistant 响应。所选 home 会保存生成的 `sdk-minimal` profile、已安装插件,以及 `sessions/` 下的未压缩 JSONL 会话日志。示例与 SDK 绝不会静默读取 `~/.dsh`。 ## 在程序中使用 SDK @@ -54,16 +54,14 @@ from deepseek_harness import DeepSeekHarness workspace = Path("/absolute/path/to/disposable-workspace").resolve() dsh_home = Path("/absolute/path/to/example-dsh-home").resolve() -patch = Path("examples/python-sdk-agent/minimal.patch.yml").resolve() - with DeepSeekHarness( provider="deepseek-official", model="deepseek-v4-flash", max_tokens=49_152, cwd=str(workspace), dsh_home=str(dsh_home), - profile="sdk", - patches=(str(patch),), + profile="sdk-minimal", + env={"DSH_MODEL": "deepseek-v4-flash"}, ) as harness: result = harness.run( "Inspect the repository and fix the failing tests.", @@ -73,7 +71,7 @@ with DeepSeekHarness( print(result.final_response) ``` -SDK 会延迟启动内置的 `dsh --profile sdk` 进程,并复用到上下文管理器退出。Profile、其持久 patch、home patch 与有序 `patches` tuple 共同组成应用配置。不存在独立 Python 运行时 bin 或完整配置选项。 +SDK 会延迟启动内置的 `dsh --profile sdk-minimal` 进程,并复用到上下文管理器退出。Profile、其持久 patch、home patch 与任何有序 `patches` tuple 共同组成应用配置。不存在独立 Python 运行时 bin 或完整配置选项。 ## 安装或定义插件 @@ -81,15 +79,15 @@ SDK 会延迟启动内置的 `dsh --profile sdk` 进程,并复用到上下文 ```sh export DSH_HOME=/absolute/path/to/example-dsh-home -dsh --profile sdk --dump-default-config >/dev/null -dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle +dsh --profile sdk-minimal --dump-default-config >/dev/null +dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle ``` -第一个命令初始化随附的 SDK profile。第二个命令把包管理转发给 `pnpm`,然后记录所有导出 `dsh.bundle` 层的已安装包。只有执行此管理命令时才需要安装 `pnpm`;启动已安装 SDK 不需要它。持久配置项变更应编辑 `$DSH_HOME/profiles/sdk/cordis.patch.yml`;单次启动变更则从 Python 传入 patch 文件。 +第一个命令初始化随附的独立 profile。第二个命令把包管理转发给 `pnpm`,然后记录所有导出 `dsh.bundle` 层的已安装包。只有执行此管理命令时才需要安装 `pnpm`;启动已安装 SDK 不需要它。持久配置项变更应编辑 `$DSH_HOME/profiles/sdk-minimal/cordis.patch.yml`;单次启动变更则从 Python 传入 patch 文件。 另一个 `profile` 只有包含 `@deepseek-ai/dsh-sdk-app` 或另一个 JSON-RPC server 配置项时才有效。缺失 server 配置项、无法解析的插件和非法 patch 会在启动时失败,不会回退到其他组合。 -## 理解极简 overlay +## 理解极简 profile | 属性 | 值 | |---|---| @@ -98,11 +96,13 @@ dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle | 面向模型的工具 | 仅持久 `bash` 与 `str_replace_editor` | | Bash 超时 | 300 秒 | | Editor 输出上限 | 16,000 字符 | -| 上下文压缩 | 禁用 | -| 会话持久化 | `/sessions` 下的 Zstandard JSONL | +| 运行时上下文与 compaction | 不存在 | +| 会话持久化 | `/sessions` 下的未压缩 JSONL | -该 overlay 会为每个由 SDK 创建的根 agent allowlist 持久 Bash 与 editor,因此基础 profile 以后新增的工具不会隐式出现。它会抑制无关提示词段与运行时上下文消息,停用本地指令发现与 compaction,并保留 SDK 应用的协议、持久化、策略、settings、credentials 与 provider。持久 Bash 与 editor 可以修改运行时可见的任何路径,因此应使用一次性 checkout 或容器。由于采用 PTY 实现,本示例只支持 POSIX。 +该 profile 的唯一组合包会在空根之上插入完整配置树,且不包含 `dsh-base`,因此基础 profile 以后新增的工具不会隐式出现。它包含 SDK 协议、一个由环境配置的 DeepSeek 适配器、本地执行与持久化;settings、托管凭据、遥测、Web 工具、subagent、本地指令发现和 compaction 均不存在。它固定使用 `danger-full-access`,因此持久 Bash 与 editor 可以修改运行时可见的任何路径;应使用一次性 checkout 或容器。由于采用 PTY 实现,本示例只支持 POSIX。 + +已安装 wheel 仍会打包完整 `web` profile 与前端产物。如果 Python SDK 部署还需要浏览器应用,请针对显式 `DSH_HOME` 运行 `dsh web`;`web` 是独立 CLI 应用,不能为 Python SDK client 提供服务。 需要隔离 profile、插件、凭据、设置与会话时,应使用新的 home。独立工作应使用新的 session id;只有继续同一段持久对话和会话资源时,才同时复用 harness、home 与 id。 -[示例参考](../../../examples/python-sdk-agent/README.zh.md)定义检入 overlay。[Python SDK 参考](../../../python/sdk/README.zh.md)介绍生命周期、结果、通知与底层行为;[dsh CLI 参考](../../../apps/cli/reference/README.zh.md)介绍 profile 分层。 +[组合包参考](../../../packages/bundle/sdk-minimal/README.zh.md)定义确切配置树,[示例参考](../../../examples/python-sdk-agent/README.zh.md)定义可运行程序。[Python SDK 参考](../../../python/sdk/README.zh.md)介绍生命周期、结果、通知与底层行为;[dsh CLI 参考](../../../apps/cli/reference/README.zh.md)介绍 profile 分层。 diff --git a/examples/python-sdk-agent/README.i18n.yaml b/examples/python-sdk-agent/README.i18n.yaml index f0f1ef1980..0c3a5fe203 100644 --- a/examples/python-sdk-agent/README.i18n.yaml +++ b/examples/python-sdk-agent/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 examples/python-sdk-agent/README.md -README.md: b1718bfa3fb6ea4cb241bb4211b58a21bacc17a2 -README.zh.md: dbc4053093f66a60a18ab02bb727611a279fe03c +README.md: 5bb958c3e9202642926a0186d055615a895d9586 +README.zh.md: 97bfe9e1d19a7cd37ed41fccc64340dc054ecec7 diff --git a/examples/python-sdk-agent/README.md b/examples/python-sdk-agent/README.md index b1718bfa3f..5bb958c3e9 100644 --- a/examples/python-sdk-agent/README.md +++ b/examples/python-sdk-agent/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Runnable Python SDK example over the sole application launcher, `dsh --profile sdk`. The Python client owns JSON-RPC stdio; the profile owns the agent composition, persistence, permissions, and plugins. +Runnable Python SDK example over the sole application launcher, `dsh --profile sdk-minimal`. The Python client owns JSON-RPC stdio; the profile owns the agent composition, persistence, execution policy, and plugins. ## Run the minimal agent @@ -17,14 +17,14 @@ python examples/python-sdk-agent/minimal.py \ "Inspect the repository and fix the failing tests." ``` -Set `DEEPSEEK_BASE_URL` for a compatible proxy, `DSH_MODEL` for the default model, or `DSH_SYSTEM_PROMPT` for the deployment persona. `--model` and `--profile` override their script defaults. The selected home stores the generated profile and Zstandard session logs under `sessions/`; the script never reads `~/.dsh` implicitly. +Set `DEEPSEEK_BASE_URL` for a compatible proxy, `DSH_MODEL` for the default model, or `DSH_SYSTEM_PROMPT` for the deployment persona. `--model` overrides the model and passes the same value to the profile-owned adapter catalog; `--profile` can select another SDK-serving profile. The selected home stores the generated `sdk-minimal` profile and uncompressed JSONL session logs under `sessions/`; the script never reads `~/.dsh` implicitly. -[`minimal.patch.yml`](minimal.patch.yml) is an ordered overlay on the shipped SDK profile. Its root-agent tool allowlist exposes exactly: +The shipped [`@deepseek-ai/dsh-sdk-minimal` bundle](../../packages/bundle/sdk-minimal/README.md) is the complete explicit Cordis tree for this mode. It exposes exactly: - owner-scoped persistent `bash` - `str_replace_editor` with `view`, `create`, `str_replace`, and `insert` -The allowlist excludes every other current or later global tool without requiring a disable entry for each base row. A complete deployment persona suppresses unrelated tool-guidance sections; runtime-context messages, local instruction discovery, compaction, and the conflicting one-shot Bash row are disabled separately. The overlay inserts the local PTY and persistent Bash providers and sets the editor output limit to 16,000 characters. Other SDK-profile services remain mounted, including persistence, policy, settings, credentials, and providers. +The bundle does not include `dsh-base`, so every additional row is an explicit profile change. Runtime context, local instruction discovery, compaction, settings, managed credentials, telemetry, Web tools, subagents, and the full default tool roster are absent. The tree retains SDK startup and JSON-RPC serving, one environment-configured DeepSeek adapter, local execution, and JSONL persistence. This variant is intentionally POSIX-only. Its persistent PTY and editor can modify any path available to the runtime process, so use a disposable checkout or container. @@ -34,9 +34,11 @@ Use the runtime wheel's `dsh` command against the same explicit home for persist ```sh export DSH_HOME=/absolute/path/to/example-dsh-home -dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle +dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle ``` -The Python call can also pass additional absolute patch paths in `patches=(...)`; later files win. A selected profile must retain `@deepseek-ai/dsh-sdk-app` or another JSON-RPC server row. Complete standalone Cordis files in this directory remain test fixtures for lower-level composition coverage; they are not Python SDK launch interfaces. +Use `sdk-minimal` in that command to extend this example, or `sdk` to extend the full base-backed SDK profile. The Python call can also pass additional absolute patch paths in `patches=(...)`; later files win. A selected profile must retain `@deepseek-ai/dsh-sdk-app` or another JSON-RPC server row. The example accepts no complete Cordis file or arbitrary process argv. + +The same runtime wheel packages the `web` profile and its frontend assets for direct CLI use: `dsh web` starts that separate application. A Python SDK client cannot select `web` because it has no JSON-RPC server row. See the [Python SDK tutorial](../../docs/user/guide/python-sdk.md) and [SDK reference](../../python/sdk/README.md). diff --git a/examples/python-sdk-agent/README.zh.md b/examples/python-sdk-agent/README.zh.md index dbc4053093..97bfe9e1d1 100644 --- a/examples/python-sdk-agent/README.zh.md +++ b/examples/python-sdk-agent/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -基于唯一应用启动器 `dsh --profile sdk` 的可运行 Python SDK 示例。Python 客户端负责 JSON-RPC stdio;profile 负责 agent 组合、持久化、权限与插件。 +基于唯一应用启动器 `dsh --profile sdk-minimal` 的可运行 Python SDK 示例。Python 客户端负责 JSON-RPC stdio;profile 负责 agent 组合、持久化、执行策略与插件。 ## 运行极简 agent @@ -17,14 +17,14 @@ python examples/python-sdk-agent/minimal.py \ "Inspect the repository and fix the failing tests." ``` -兼容代理使用 `DEEPSEEK_BASE_URL`,默认模型使用 `DSH_MODEL`,deployment persona 使用 `DSH_SYSTEM_PROMPT`。`--model` 与 `--profile` 会覆盖脚本默认值。所选 home 保存生成的 profile,并在 `sessions/` 下保存 Zstandard 会话日志;脚本绝不会隐式读取 `~/.dsh`。 +兼容代理使用 `DEEPSEEK_BASE_URL`,默认模型使用 `DSH_MODEL`,deployment persona 使用 `DSH_SYSTEM_PROMPT`。`--model` 会覆盖模型,并将同一值传给 profile 自有的适配器目录;`--profile` 可以选择另一个提供 SDK 服务的 profile。所选 home 保存生成的 `sdk-minimal` profile,并在 `sessions/` 下保存未压缩 JSONL 会话日志;脚本绝不会隐式读取 `~/.dsh`。 -[`minimal.patch.yml`](minimal.patch.yml) 是随附 SDK profile 上的有序 overlay。其根 agent 工具 allow 列表只暴露: +随附的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../packages/bundle/sdk-minimal/README.zh.md)是该模式完整且显式的 Cordis 配置树。它只暴露: - agent 所有的持久 `bash` - 支持 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor` -Allow 列表会排除当前及以后出现的其他所有全局工具,无需为每个基础配置项添加 disable。完整部署 persona 会抑制无关工具的引导段;运行时上下文消息、本地指令发现、compaction 与存在名称冲突的单次 Bash 配置项会分别停用。该 overlay 会插入本地 PTY 与持久 Bash provider,并将 editor 输出上限设为 16,000 字符。其他 SDK profile 服务仍保持挂载,包括持久化、策略、settings、credentials 与 provider。 +该组合包不包含 `dsh-base`,因此每一个新增配置项都是显式 profile 变更。运行时上下文、本地指令发现、compaction、settings、托管凭据、遥测、Web 工具、subagent 与完整默认工具清单均不存在。配置树保留 SDK 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、本地执行和 JSONL 持久化。 此变体刻意只支持 POSIX。其持久 PTY 与 editor 可以修改运行时进程可访问的任何路径,因此只应在一次性 checkout 或容器中使用。 @@ -34,9 +34,11 @@ Allow 列表会排除当前及以后出现的其他所有全局工具,无需 ```sh export DSH_HOME=/absolute/path/to/example-dsh-home -dsh plugin --profile sdk add file:/absolute/path/to/my-plugin-bundle +dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle ``` -Python 调用也可以在 `patches=(...)` 中传入更多绝对 patch 路径;后面的文件优先。所选 profile 必须保留 `@deepseek-ai/dsh-sdk-app` 或另一个 JSON-RPC server 配置项。本目录中的完整独立 Cordis 文件仍作为底层组合测试 fixture;它们不是 Python SDK 启动接口。 +在该命令中使用 `sdk-minimal` 可扩展本示例,使用 `sdk` 则扩展基于完整 base 的 SDK profile。Python 调用也可以在 `patches=(...)` 中传入更多绝对 patch 路径;后面的文件优先。所选 profile 必须保留 `@deepseek-ai/dsh-sdk-app` 或另一个 JSON-RPC server 配置项。该示例不接受完整 Cordis 文件或任意进程 argv。 + +同一个运行时 wheel 还为直接 CLI 使用打包 `web` profile 及其前端产物:`dsh web` 会启动这个独立应用。Python SDK client 不能选择 `web`,因为其中没有 JSON-RPC server 配置项。 另见 [Python SDK 教程](../../docs/user/guide/python-sdk.zh.md)与 [SDK 参考](../../python/sdk/README.zh.md)。 diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml index ae60e877c0..7f7c3badcf 100644 --- a/packages/boot/app-boot/README.i18n.yaml +++ b/packages/boot/app-boot/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/boot/app-boot/README.md -README.md: 0adcb0ac20516b1eea97792a11471a383a3ab429 -README.zh.md: 1da1725b1e0681f83f2c82b82cb9bbaac8ea90cb +README.md: a7c6272fd96fdf24b4f21bb8b60c087af4a4dc96 +README.zh.md: 09a63765414ba76d78a7c88f3777f63eecd835f7 diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md index 0adcb0ac20..a7c6272fd9 100644 --- a/packages/boot/app-boot/README.md +++ b/packages/boot/app-boot/README.md @@ -35,7 +35,7 @@ This package carries no loader hooks and no dev-mode surface. The [`dsh` app](.. ## Profiles -A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable resolves available explicit exports directly from each installed manifest with Node ESM import conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Export targets absent from an installed package remain unavailable without blocking its other exports; malformed export maps fail startup. An executable-only or declaration-only package with no module entry produces no proxy. A complete matching generation returns without acquiring the writer lock. A missing or stale entry acquires the cross-process lock, rechecks the full generation, and repairs it without exposing partial proxies; either carrier replaces the other carrier's managed entry. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged. +A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable resolves available explicit exports directly from each installed manifest with Node ESM import conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Export targets absent from an installed package remain unavailable without blocking its other exports; malformed export maps fail startup. An executable-only or declaration-only package with no module entry produces no proxy. A complete matching generation returns without acquiring the writer lock. A missing or stale entry acquires the cross-process lock, rechecks the full generation, and repairs it without exposing partial proxies; either carrier replaces the other carrier's managed entry. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`sdk-minimal`/`acp` with startup-only patches; `sdk-minimal` lists only its standalone bundle, while the other templates retain their base-plus-mode stacks. Other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged. User-level machine-local preferences also live in the Harness home: diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md index 1da1725b1e..09a6376541 100644 --- a/packages/boot/app-boot/README.zh.md +++ b/packages/boot/app-boot/README.zh.md @@ -35,7 +35,7 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面 ## Profiles -profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则直接从每个已安装 manifest 中按 Node ESM import 条件解析实际存在的显式 exports,并写入重新导出虚拟模块 URL 的真实代理包,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。安装包中不存在的 export 目标保持不可用,但不阻塞其他 exports;格式错误的 exports map 会导致启动失败。只有可执行入口或类型声明入口而没有模块入口的包不会生成代理。完整且匹配的 generation 不会获取写入锁。缺失或过期的配置项会获取跨进程锁、重新检查完整 generation,并在不暴露半成品代理的前提下修复;两种载体都会替换另一种载体留下的受管条目。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。 +profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则直接从每个已安装 manifest 中按 Node ESM import 条件解析实际存在的显式 exports,并写入重新导出虚拟模块 URL 的真实代理包,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。安装包中不存在的 export 目标保持不可用,但不阻塞其他 exports;格式错误的 exports map 会导致启动失败。只有可执行入口或类型声明入口而没有模块入口的包不会生成代理。完整且匹配的 generation 不会获取写入锁。缺失或过期的配置项会获取跨进程锁、重新检查完整 generation,并在不暴露半成品代理的前提下修复;两种载体都会替换另一种载体留下的受管条目。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`sdk-minimal`/`acp`;`sdk-minimal` 只列出自己的独立组合包,其他模板保留 base 加模式层的组合。其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。 用户级的机器本地偏好同样位于 harness home 中: diff --git a/packages/bundle/README.i18n.yaml b/packages/bundle/README.i18n.yaml index 1ca618d528..6ad818ee64 100644 --- a/packages/bundle/README.i18n.yaml +++ b/packages/bundle/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/bundle/README.md -README.md: 44cf46a217e078e4d71d838afb6525f2bc908bd2 -README.zh.md: f2987b72c526698e2df949e2edfe55e1375f2d8e +README.md: c0ea5be0c6f1b2457166b24a1718f6ab3aa0ffe6 +README.zh.md: 6c3520668d8f83cfbdc75652fac81f29d9bb810d diff --git a/packages/bundle/README.md b/packages/bundle/README.md index 44cf46a217..c0ea5be0c6 100644 --- a/packages/bundle/README.md +++ b/packages/bundle/README.md @@ -8,7 +8,7 @@ The manifest declaration, not this directory, defines Bundle identity. Domain pa | Package | Role | ctx key | |---|---|---| -| [`base/`](base/README.md) | The shared dsh core every profile applies first | — (patch only) | +| [`base/`](base/README.md) | The shared dsh core applied first by base-backed profiles | — (patch only) | | [`acp-app/`](acp-app/README.md) | Automation-only ACP stdio application over base | mounts the ACP bridge | | [`web-app/`](web-app/README.md) | Browser surface: web patch layer + runtime glue plugin | mounts rows | | [`headless/`](headless/README.md) | Direct one-shot task mode over base, with no Host or Web layer | mounts `headless-runner` | diff --git a/packages/bundle/README.zh.md b/packages/bundle/README.zh.md index f2987b72c5..6c3520668d 100644 --- a/packages/bundle/README.zh.md +++ b/packages/bundle/README.zh.md @@ -8,7 +8,7 @@ Bundle 身份由 manifest 声明决定,而不是由本目录决定。领域包 | 包 | 职责 | ctx key | |---|---|---| -| [`base/`](base/README.zh.md) | 每个 profile 最先应用的共享 dsh 核心 | —(仅 patch) | +| [`base/`](base/README.zh.md) | 基于 base 的 profile 最先应用的共享 dsh 核心 | —(仅 patch) | | [`acp-app/`](acp-app/README.zh.md) | 运行在 base 之上的 automation-only ACP stdio 应用 | 挂载 ACP bridge | | [`web-app/`](web-app/README.zh.md) | 浏览器表层:web patch 层 + 运行时粘合插件 | 挂载多条配置行 | | [`headless/`](headless/README.zh.md) | 直接运行在 base 之上的一次性任务模式,不含 Host 或 Web 层 | 挂载 `headless-runner` | diff --git a/packages/bundle/base/README.i18n.yaml b/packages/bundle/base/README.i18n.yaml index bfcd5c6a66..0ab5140ece 100644 --- a/packages/bundle/base/README.i18n.yaml +++ b/packages/bundle/base/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/bundle/base/README.md -README.md: 74f1288b46dd20643a494acb1829dbe38c367622 -README.zh.md: dda46a89f3c2b161d7358109e4317a976eaa65c8 +README.md: 9fb1264f39aee3f3961ff4fd3c07f35b0b7cefdf +README.zh.md: 844254acc1f349a9debf38494e8d97f5f7e2d00b diff --git a/packages/bundle/base/README.md b/packages/bundle/base/README.md index 74f1288b46..9fb1264f39 100644 --- a/packages/bundle/base/README.md +++ b/packages/bundle/base/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, telemetry, and the core spawn/fork subagent providers — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. The optional Codex and Claude Code providers stay outside this package and its production dependency closure; a Profile installs either [product provider Bundle](../../subagent/README.md) only when needed. The default `@deepseek-ai/dsh` production closure therefore includes neither product provider, the Claude Agent SDK, nor the Codex wrapper and platform payloads. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code. +The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, telemetry, and the core spawn/fork subagent providers — over the empty profile root, as the first layer of each base-backed profile's `dsh.profile.bundles` list. The standalone [`sdk-minimal`](../sdk-minimal/README.md) profile deliberately does not include this bundle. The optional Codex and Claude Code providers stay outside this package and its production dependency closure; a Profile installs either [product provider Bundle](../../subagent/README.md) only when needed. The default `@deepseek-ai/dsh` production closure therefore includes neither product provider, the Claude Agent SDK, nor the Codex wrapper and platform payloads. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code. The base module-HMR row is disabled. A profile with a tested source-module reload lifecycle enables that row explicitly; `patchReload: live` config watching is independent and uses the launcher's watch-only fallback while module HMR remains disabled. diff --git a/packages/bundle/base/README.zh.md b/packages/bundle/base/README.zh.md index dda46a89f3..844254acc1 100644 --- a/packages/bundle/base/README.zh.md +++ b/packages/bundle/base/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.zh.md) 选择、工具、持久化、策略、settings/credentials、遥测与核心 spawn/fork subagent provider——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。可选的 Codex 与 Claude Code provider 不属于本包及其生产依赖闭包;Profile 仅在需要时安装任一[产品 provider Bundle](../../subagent/README.zh.md)。因此,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一产品 provider、Claude Agent SDK,也不包含 Codex wrapper 及其平台载荷。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.zh.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析 patch,绝不通过代码。 +以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.zh.md) 选择、工具、持久化、策略、settings/credentials、遥测与核心 spawn/fork subagent provider——作为每个基于 base 的 profile 的 `dsh.profile.bundles` 列表中的第一层。独立的 [`sdk-minimal`](../sdk-minimal/README.zh.md) profile 刻意不包含本组合包。可选的 Codex 与 Claude Code provider 不属于本包及其生产依赖闭包;Profile 仅在需要时安装任一[产品 provider Bundle](../../subagent/README.zh.md)。因此,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一产品 provider、Claude Agent SDK,也不包含 Codex wrapper 及其平台载荷。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.zh.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析 patch,绝不通过代码。 base 的模块 HMR 配置项默认禁用。具有经过验证的源码模块重载生命周期的 profile 必须显式启用该配置项;`patchReload: live` 配置监视与之独立,在模块 HMR 保持禁用时使用启动器的仅监视 fallback。 diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index e7e963e59f..65da35f554 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -1,4 +1,4 @@ -# The dsh-base bundle patch: the shared core of every dsh profile, applied as +# The dsh-base bundle patch: the shared core of each base-backed profile, applied as # ONE insert over the empty profile root. Later bundle patches and the user's # profile cordis.patch.yml address these rows by id, with the last write # winning per row. diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index 2d0977a727..80f33257db 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-base", - "description": "The shared dsh core as a profile bundle: every profile's first patch layer, inserting the base plugin rows over the empty profile root", + "description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root", "version": "0.1.1-rc.2", "publishConfig": { "access": "public" diff --git a/packages/bundle/sdk-minimal/README.i18n.yaml b/packages/bundle/sdk-minimal/README.i18n.yaml index 51cf2a832d..715a58381e 100644 --- a/packages/bundle/sdk-minimal/README.i18n.yaml +++ b/packages/bundle/sdk-minimal/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/bundle/sdk-minimal/README.md -README.md: 5bc60991624ab52b3d55acac4851b6af2b198218 -README.zh.md: 7e11140bcb8a81cf3e22db88530bfcb364436f96 +README.md: 7f37d0cb4996a9c48e4bcb934565ff2865f5e7e3 +README.zh.md: 3e2c5b5f66314e8dc2da9622bbd1937d85d98490 diff --git a/packages/bundle/sdk-minimal/README.md b/packages/bundle/sdk-minimal/README.md index 5bc6099162..7f37d0cb49 100644 --- a/packages/bundle/sdk-minimal/README.md +++ b/packages/bundle/sdk-minimal/README.md @@ -26,6 +26,6 @@ Stable for a fixed persona, platform, provider, model, and bundle patch stack. P ## Known Limitations and Deferred Work -- **The profile is POSIX-only** — this layer uses a Bash PTY; Windows support belongs to the platform runtime layer above it. +- **The profile is POSIX-only** — this composition uses a Bash PTY; a Windows profile must select a PowerShell terminal and tool instead. - **The composition intentionally omits shared product services** — select `dsh --profile sdk` when settings, managed credentials, policy presets, telemetry, Web tools, or the full default tool roster are required. - **User patches can expand the tree and corrupt stdout** — profile customization is trusted application composition; a plugin that writes ordinary text to stdout can break JSON-RPC framing. diff --git a/packages/bundle/sdk-minimal/README.zh.md b/packages/bundle/sdk-minimal/README.zh.md index 7e11140bcb..3e2c5b5f66 100644 --- a/packages/bundle/sdk-minimal/README.zh.md +++ b/packages/bundle/sdk-minimal/README.zh.md @@ -26,6 +26,6 @@ ## 已知限制与待办工作 -- **该 profile 仅支持 POSIX** — 此层使用 Bash PTY;Windows 支持属于其上的平台运行时层。 +- **该 profile 仅支持 POSIX** — 此组合使用 Bash PTY;Windows profile 必须改为选择 PowerShell 终端与工具。 - **该组合刻意省略共享产品服务** — 需要 settings、托管凭据、权限策略预设、遥测、Web 工具或完整默认工具清单时,请选择 `dsh --profile sdk`。 - **用户 patch 可以扩展配置树并破坏 stdout** — profile 自定义属于受信任的应用组合;向 stdout 写入普通文本的插件会破坏 JSON-RPC 分帧。 diff --git a/packages/examples/README.i18n.yaml b/packages/examples/README.i18n.yaml index c3e1d67e92..c0c43e838d 100644 --- a/packages/examples/README.i18n.yaml +++ b/packages/examples/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/examples/README.md -README.md: 1193ae404bb7f1a69636f45ff36eee82ec648b2c -README.zh.md: 935bbd1329082f857a692df913a89e7007054b63 +README.md: 2d17e60ed990c8cb77ebfd5823ba2f67e63e8837 +README.zh.md: deea149a11dc70a329bb27c6d851d62d46c6d16e diff --git a/packages/examples/README.md b/packages/examples/README.md index 1193ae404b..2d17e60ed9 100644 --- a/packages/examples/README.md +++ b/packages/examples/README.md @@ -8,7 +8,7 @@ Pre-composed plugin bundles a thin leaf `cordis.yml` loads instead of assembling |---|---|---| | [`agent-spine-demo/`](agent-spine-demo/README.md) | `@deepseek-ai/dsh-agent-spine-demo` | Reusable agent-spine bundle | -`agent-spine-demo` is the shared bundle. Product SDK, ACP, and one-shot execution belong to `dsh --profile sdk`, `dsh --profile acp`, and `dsh --profile headless`; no package in this directory provides an application entry. +`agent-spine-demo` is the shared bundle. Product SDK, ACP, and one-shot execution belong to `dsh --profile sdk` / `dsh --profile sdk-minimal`, `dsh --profile acp`, and `dsh --profile headless`; no package in this directory provides an application entry. These packages are not product API. Product seams and entry points remain in their owning groups; demo bundles select concrete compositions. diff --git a/packages/examples/README.zh.md b/packages/examples/README.zh.md index 935bbd1329..deea149a11 100644 --- a/packages/examples/README.zh.md +++ b/packages/examples/README.zh.md @@ -8,7 +8,7 @@ |---|---|---| | [`agent-spine-demo/`](agent-spine-demo/README.zh.md) | `@deepseek-ai/dsh-agent-spine-demo` | 可复用的 agent-spine(智能体主干)组合包 | -`agent-spine-demo` 是共享组合包。产品 SDK、ACP 与一次性执行分别由 `dsh --profile sdk`、`dsh --profile acp` 和 `dsh --profile headless` 提供;本目录没有任何包提供应用入口。 +`agent-spine-demo` 是共享组合包。产品 SDK、ACP 与一次性执行分别由 `dsh --profile sdk`/`dsh --profile sdk-minimal`、`dsh --profile acp` 和 `dsh --profile headless` 提供;本目录没有任何包提供应用入口。 这些包不是产品 API。产品 seam 与产品入口仍位于各自的归属组;演示组合包选择具体组合。 diff --git a/python/README.i18n.yaml b/python/README.i18n.yaml index fee9373a2d..1076f068de 100644 --- a/python/README.i18n.yaml +++ b/python/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 python/README.md -README.md: 7cb822e475acaa36a2a1637b0b4830e7dcabcb7a -README.zh.md: e571b936dad5d14db834fcabed363b5b7c199807 +README.md: d82195fa8a3f39bea129030b2eaf7d65536041e0 +README.zh.md: cb0fb28fff7d2442c948205b07c457243fb85022 diff --git a/python/README.md b/python/README.md index 7cb822e475..d82195fa8a 100644 --- a/python/README.md +++ b/python/README.md @@ -13,7 +13,7 @@ Python packages for driving DeepSeek Harness as a subprocess. The client SDK com ## Behavior -The SDK starts the matching bundled `dsh --profile sdk` runtime unless the caller selects another `dsh` executable or profile. Every launch requires an explicitly selected Harness home; Python never silently reads `~/.dsh`. The [SDK reference](sdk/README.md) and [runtime carrier reference](sdk-runtime/README.md) own runtime selection, profiles, patches, and external plugin management. +The SDK starts the matching bundled `dsh --profile sdk` runtime unless the caller selects another `dsh` executable or profile. The runnable minimal example selects the shipped standalone `sdk-minimal` profile; the same runtime also packages `dsh web` and its frontend assets for separate CLI use. Every launch requires an explicitly selected Harness home; Python never silently reads `~/.dsh`. The [SDK reference](sdk/README.md) and [runtime carrier reference](sdk-runtime/README.md) own runtime selection, profiles, patches, and external plugin management. ## Contributor workflows diff --git a/python/README.zh.md b/python/README.zh.md index e571b936da..cb0fb28fff 100644 --- a/python/README.zh.md +++ b/python/README.zh.md @@ -13,7 +13,7 @@ ## 行为 -除非调用方选择另一个 `dsh` 可执行程序或 profile,否则 SDK 会启动匹配的内置 `dsh --profile sdk` 运行时。每次启动都要求显式选择 Harness home;Python 绝不会静默读取 `~/.dsh`。[SDK 参考](sdk/README.zh.md)和[运行时载体参考](sdk-runtime/README.zh.md)定义运行时选择、profile、patch 与外部插件管理约定。 +除非调用方选择另一个 `dsh` 可执行程序或 profile,否则 SDK 会启动匹配的内置 `dsh --profile sdk` 运行时。可运行极简示例选择随附的独立 `sdk-minimal` profile;同一运行时还会为独立 CLI 使用打包 `dsh web` 及其前端产物。每次启动都要求显式选择 Harness home;Python 绝不会静默读取 `~/.dsh`。[SDK 参考](sdk/README.zh.md)和[运行时载体参考](sdk-runtime/README.zh.md)定义运行时选择、profile、patch 与外部插件管理约定。 ## 贡献者工作流 diff --git a/python/development.i18n.yaml b/python/development.i18n.yaml index bff1468133..a372e966ac 100644 --- a/python/development.i18n.yaml +++ b/python/development.i18n.yaml @@ -2,5 +2,5 @@ # 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 python/development.md -development.md: 145f958a9df28e76456fb16a68b36fe318afa323 -development.zh.md: 557d6ba1f766dca61ab7bc73bf71ced2eb79afbe +development.md: 61094a277d2b91063a0d368ec30f444eeb132128 +development.zh.md: a73de6e091050cebb0b26037a7cca3adc814d961 diff --git a/python/development.md b/python/development.md index 145f958a9d..61094a277d 100644 --- a/python/development.md +++ b/python/development.md @@ -34,7 +34,7 @@ uv run --project python/sdk python scripts/smoke-python-runtime.py \ --scenario sdk-minimal --exe dist-exe/deepseek-harness-sdk-runtime-macos-arm64 ``` -Three scenarios compare committed expected output under `scripts/snapshots/python-sdk-single-exe/`. `minimal/model-visible.json` pins the checked-in minimal composition's assembled system prompts, advertised tool schemas, and model-visible messages, so a plugin that contributes an unintended system section or user message fails the job; it drops the dynamic runtime-context snapshot, which the same composition emits on macOS and not on Linux ([#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488)). `advanced/` pins one complex process's SDK result and parent/child session logs. `restart/` launches two complete SDK runtime processes against one persistence root and snapshots their isolated model histories, high-level results, and separate durable logs. Rerun the owning scenario with `--update-snapshots` and review that diff before committing it. +Three scenarios compare committed expected output under `scripts/snapshots/python-sdk-single-exe/`. `minimal/model-visible.json` pins the shipped `sdk-minimal` profile's assembled system prompts, advertised tool schemas, and model-visible messages, so a plugin that contributes an unintended system section or user message fails the job. `advanced/` pins one complex process's SDK result and parent/child session logs. `restart/` launches two complete SDK runtime processes against one persistence root and snapshots their isolated model histories, high-level results, and separate durable logs. Rerun the owning scenario with `--update-snapshots` and review that diff before committing it. Trusted pull requests also run `--scenario sdk-live --installed-wheel` on every native target. That scenario performs two tool-using turns against `https://api.deepseek.com`, verifies the created file externally, and fails when the repository secret is absent instead of self-skipping. Fork and Dependabot pull requests run the complete keyless installed-wheel path but receive no key. diff --git a/python/development.zh.md b/python/development.zh.md index 557d6ba1f7..a73de6e091 100644 --- a/python/development.zh.md +++ b/python/development.zh.md @@ -34,7 +34,7 @@ uv run --project python/sdk python scripts/smoke-python-runtime.py \ --scenario sdk-minimal --exe dist-exe/deepseek-harness-sdk-runtime-macos-arm64 ``` -其中三个场景会比对 `scripts/snapshots/python-sdk-single-exe/` 下已提交的期望输出。`minimal/model-visible.json` 固定了签入的极简组合所组装的系统提示词、对外公布的工具 schema 以及模型可见消息,因此插件一旦贡献出计划外的系统分段或 user 消息,该任务即失败;它会丢弃动态运行时上下文快照——同一组合在 macOS 上会发出它,在 Linux 上不会([#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488))。`advanced/` 固定一个复杂进程的 SDK 结果及父/子会话日志。`restart/` 针对同一持久化根目录启动两个完整 SDK 运行时进程,并固定其彼此隔离的模型历史、高层结果与独立持久日志。重新运行对应场景时加上 `--update-snapshots`,并在提交前审阅该差异。 +其中三个场景会比对 `scripts/snapshots/python-sdk-single-exe/` 下已提交的期望输出。`minimal/model-visible.json` 固定随附 `sdk-minimal` profile 所组装的系统提示词、对外公布的工具 schema 与模型可见消息,因此插件一旦贡献出计划外的系统分段或 user 消息,该任务即失败。`advanced/` 固定一个复杂进程的 SDK 结果及父/子会话日志。`restart/` 针对同一持久化根目录启动两个完整 SDK 运行时进程,并固定其彼此隔离的模型历史、高层结果与独立持久日志。重新运行对应场景时加上 `--update-snapshots`,并在提交前审阅该差异。 可信拉取请求还会在每个原生目标上运行 `--scenario sdk-live --installed-wheel`。该场景面向 `https://api.deepseek.com` 执行两个使用工具的轮次,从外部验证已创建文件,并在仓库密钥缺失时失败而不是自行 skip。Fork 与 Dependabot 拉取请求会运行完整的 keyless 安装后 wheel 路径,但不会获得密钥。 diff --git a/python/sdk-runtime/README.i18n.yaml b/python/sdk-runtime/README.i18n.yaml index ddcef99f9e..51965002ce 100644 --- a/python/sdk-runtime/README.i18n.yaml +++ b/python/sdk-runtime/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 python/sdk-runtime/README.md -README.md: 52103b538be0ebc79572d8477e407f324e419ce6 -README.zh.md: e4fe7673a510102504454d5a6113ec7ec6ffd086 +README.md: 1552f2a120938ecab6d05dd244a745bec65bb00a +README.zh.md: 9524617ee1a950080476a91db5ec6e14727518ce diff --git a/python/sdk-runtime/README.md b/python/sdk-runtime/README.md index 52103b538b..1552f2a120 100644 --- a/python/sdk-runtime/README.md +++ b/python/sdk-runtime/README.md @@ -12,7 +12,7 @@ Production executables are named `deepseek-harness-sdk-runtime-- Repository builds also materialize a dev-only `runtime/node/` carrier. It runs `node runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. It is never selected automatically and is excluded from wheels and sdists. -Both carriers execute the same `dsh` grammar and profiles. The private `dsh-python-runtime-closure` manifest defines the packaged dependency closure; there is no Python-specific Node application or checked-in default `cordis.yml`. +Both carriers execute the same `dsh` grammar and shipped profiles, including the standalone `sdk-minimal` tree and the full `web` profile with its frontend assets. The private `dsh-python-runtime-closure` manifest defines the packaged dependency closure; there is no Python-specific Node application or checked-in default `cordis.yml`. ## Python module API diff --git a/python/sdk-runtime/README.zh.md b/python/sdk-runtime/README.zh.md index e4fe7673a5..9524617ee1 100644 --- a/python/sdk-runtime/README.zh.md +++ b/python/sdk-runtime/README.zh.md @@ -12,7 +12,7 @@ Wheel 会安装 `dsh` 控制台命令和 `deepseek_harness_runtime` Python 模 仓库构建还会物化仅限开发的 `runtime/node/` 载体。它在系统 Node 22.19 或更高版本上运行 `node runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。系统不会自动选择它,而且 wheel 与 sdist 均不包含它。 -两种载体执行相同的 `dsh` 语法和 profile。私有 `dsh-python-runtime-closure` manifest 定义打包依赖闭包;不存在 Python 专用 Node 应用或检入的默认 `cordis.yml`。 +两种载体执行相同的 `dsh` 语法与随附 profile,包括独立的 `sdk-minimal` 配置树,以及包含前端产物的完整 `web` profile。私有 `dsh-python-runtime-closure` manifest 定义打包依赖闭包;不存在 Python 专用 Node 应用或检入的默认 `cordis.yml`。 ## Python 模块 API diff --git a/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index 29b3a5a70b..1e98c9d0a3 100644 --- a/python/sdk/README.i18n.yaml +++ b/python/sdk/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 python/sdk/README.md -README.md: ef2ba1764aa93fc6499941dd372a1de94f33d454 -README.zh.md: b5c36b6ff0fbce303649ae92efd7e1edac954e82 +README.md: 4138ff5b8fb476da2d82f710f72369d3855066a2 +README.zh.md: a51777a427deb65b461213aed28bb9df179ed1b1 diff --git a/python/sdk/README.md b/python/sdk/README.md index ef2ba1764a..4138ff5b8f 100644 --- a/python/sdk/README.md +++ b/python/sdk/README.md @@ -53,12 +53,14 @@ with DeepSeekHarness( `profile` may select another existing profile, but that composition must retain `@deepseek-ai/dsh-sdk-app` or another `@deepseek-ai/dsh-sdk-jsonrpc-server` row. Misconfiguration fails during CLI boot or SDK initialization; there is no complete-config fallback. `dsh_bin` may select another `dsh` executable while preserving the same profile grammar. Arbitrary argv replacement remains an internal fake-runtime test adapter, not public API. +The shipped `sdk-minimal` profile is a standalone explicit tree rather than an overlay on `dsh-base`. Select it with `profile="sdk-minimal"` and set the same model in `env={"DSH_MODEL": model}` so its one adapter route matches SDK initialization. It provides persistent Bash, the string-replace editor, local execution, and JSONL sessions; settings, managed credentials, telemetry, Web tools, and the full default tool roster remain available through the separate full `sdk` and `web` profiles. + ## Results and notifications `Session.run()` owns an activity interval from its prompt's durable inbox receipt through the next whole-agent idle and returns `RunResult(session_id, final_response, finish_reason, events, notifications)`. `final_response` is the last committed root-session assistant text in the interval. `finish_reason` is the `kind` of the last root-session `turn/end`, such as `completed`, `max-tokens`, or `error`, and is `None` when no turn ended. A `turn/end` without a string `data.reason.kind` violates the protocol and raises `SdkProtocolError`. `HarnessClient` retains discovered subagent ancestry for the runtime process lifetime. During `Session.run()`, `RunResult.notifications` and `on_notification` receive the root session and known descendants in wire order. `RunResult.events` contains root-session events only, so descendant output cannot replace the root response. The low-level `session_prompt()` returns the queued message id immediately; callers that bypass `Session.run()` own the later activity boundary. -The selected home stores profiles, plugins, credentials, settings, and sessions. Use a fresh home when those resources must be isolated, and a fresh session id for independent work. Reusing both a harness and session id continues the durable conversation and session-owned resources. +The selected home stores profiles, plugins, and every profile-owned durable resource. The full `sdk` profile uses its credentials, settings, and session stores; `sdk-minimal` uses only its JSONL session store. Use a fresh home when those resources must be isolated, and a fresh session id for independent work. Reusing both a harness and session id continues the durable conversation and session-owned resources. See the [Python tutorial](../../docs/user/guide/python-sdk.md), [`python-sdk-agent` example](../../examples/python-sdk-agent/README.md), and [runtime wheel reference](../sdk-runtime/README.md). diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index b5c36b6ff0..a51777a427 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -53,12 +53,14 @@ with DeepSeekHarness( `profile` 可以选择另一个已存在的 profile,但该组合必须保留 `@deepseek-ai/dsh-sdk-app` 或另一个 `@deepseek-ai/dsh-sdk-jsonrpc-server` 配置项。配置错误会在 CLI 启动或 SDK 初始化时失败;不存在完整配置回退。`dsh_bin` 可以选择另一个 `dsh` 可执行程序,同时保持相同的 profile 语法。任意 argv 替换仅是内部 fake-runtime 测试适配器,不属于公开 API。 +随附的 `sdk-minimal` profile 是独立显式配置树,而不是 `dsh-base` 上的 overlay。使用 `profile="sdk-minimal"` 选择它,并在 `env={"DSH_MODEL": model}` 中设置相同模型,使其唯一适配器路由与 SDK 初始化一致。它提供持久 Bash、字符串替换 editor、本地执行与 JSONL 会话;settings、托管凭据、遥测、Web 工具与完整默认工具清单仍由独立的完整 `sdk` 与 `web` profile 提供。 + ## 结果与通知 `Session.run()` 的活动区间从提示词被持久 inbox 接收时开始,到整个 agent 下一次进入 idle 时结束,并返回 `RunResult(session_id, final_response, finish_reason, events, notifications)`。`final_response` 是该区间内根会话最后提交的 assistant 文本。`finish_reason` 是最后一个根会话 `turn/end` 的 `kind`,例如 `completed`、`max-tokens` 或 `error`;没有轮次结束时为 `None`。缺少字符串 `data.reason.kind` 的 `turn/end` 违反协议,并会抛出 `SdkProtocolError`。 `HarnessClient` 会在运行时进程的整个生命周期内保留已发现的子 agent 祖先关系。在 `Session.run()` 期间,`RunResult.notifications` 与 `on_notification` 按协议顺序接收根会话和已知后代的通知。`RunResult.events` 只包含根会话事件,因此后代输出不会替换根响应。底层 `session_prompt()` 会立即返回已排队消息的 id;绕过 `Session.run()` 的调用方自行负责后续活动边界。 -所选 home 保存 profile、插件、凭据、设置和会话。需要隔离这些资源时应使用新的 home;独立工作应使用新的 session id。同时复用 harness 与 session id 会延续持久对话和会话资源。 +所选 home 保存 profile、插件与每个 profile 自有的持久资源。完整 `sdk` profile 使用其中的凭据、设置与会话存储;`sdk-minimal` 只使用自己的 JSONL 会话存储。需要隔离这些资源时应使用新的 home;独立工作应使用新的 session id。同时复用 harness 与 session id 会延续持久对话和会话资源。 另见 [Python 教程](../../docs/user/guide/python-sdk.zh.md)、[`python-sdk-agent` 示例](../../examples/python-sdk-agent/README.zh.md)和[运行时 wheel 参考](../sdk-runtime/README.zh.md)。 diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 79407ece40..c5452d8d4a 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -763,7 +763,7 @@ const APP_EXAMPLES = [ title: 'DSH Base Composition', label: 'packages/bundle/base/cordis.patch.yml', config: 'packages/bundle/base/cordis.patch.yml', - summary: 'The dsh-base bundle patch every profile applies first; mode bundles (dsh-web-app, dsh-headless) and the user\'s profile layer patch over it.', + summary: 'The dsh-base bundle patch shared by the web, headless, sdk, and acp profiles; their mode bundles and user layers patch over it, while sdk-minimal owns a separate standalone tree.', }, { id: 'headless', From e2920f0109bb6c5f83f72569faa7c05c0a615d2d Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:02:51 +0800 Subject: [PATCH 14/16] fix(sdk-minimal): make the SDK model argument authoritative Stop narrowing the standalone DeepSeek adapter to a DSH_MODEL-derived one-entry catalog. The direct adapter already accepts model ids outside its advisory catalog, so retain only the DSH_CONTEXT_WINDOW fallback and let the JSON-RPC initialize model be the single runtime selection. Remove model mirroring from minimal.py and the packaged smoke. The keyless process now initializes deepseek-v4-pro without DSH_MODEL, while the packaged scenario continues to use its unlisted smoke-model; together they prove both cataloged and arbitrary SDK model arguments reach the adapter directly. Update the bundle, tutorial, SDK/example references, and owning Agent Notes to keep DSH_MODEL only as minimal.py's optional default input, never as a second value callers must synchronize. --- .../2026-08-24-standalone-sdk-minimal-profile.i18n.yaml | 4 ++-- .../2026-08-24-standalone-sdk-minimal-profile.md | 2 +- .../2026-08-24-standalone-sdk-minimal-profile.zh.md | 2 +- ...26-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml | 4 ++-- .../2026-08-11-minimal-profiles-bare-two-tool-runtime.md | 4 ++-- .../2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md | 4 ++-- docs/user/guide/python-sdk.i18n.yaml | 4 ++-- docs/user/guide/python-sdk.md | 1 - docs/user/guide/python-sdk.zh.md | 1 - examples/python-sdk-agent/README.i18n.yaml | 4 ++-- examples/python-sdk-agent/README.md | 2 +- examples/python-sdk-agent/README.zh.md | 2 +- examples/python-sdk-agent/minimal.py | 1 - examples/python-sdk-agent/tests/keyless-smoke.e2e.ts | 1 - packages/bundle/sdk-minimal/README.i18n.yaml | 4 ++-- packages/bundle/sdk-minimal/README.md | 2 +- packages/bundle/sdk-minimal/README.zh.md | 2 +- packages/bundle/sdk-minimal/cordis.patch.yml | 4 +--- packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts | 5 +++++ python/sdk/README.i18n.yaml | 4 ++-- python/sdk/README.md | 2 +- python/sdk/README.zh.md | 2 +- scripts/smoke-python-runtime.py | 3 --- 23 files changed, 30 insertions(+), 34 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml index 6fb8fc0362..4a06faa161 100644 --- a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.i18n.yaml @@ -2,5 +2,5 @@ # 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/architecture/2026-08-24-standalone-sdk-minimal-profile.md -2026-08-24-standalone-sdk-minimal-profile.md: aa822d40a3b15bb55c36e8bcc585c4aaeb03c13d -2026-08-24-standalone-sdk-minimal-profile.zh.md: cea6ad6f575882b07d9494532e94cfe95bd61b43 +2026-08-24-standalone-sdk-minimal-profile.md: bc1a177dc4a232201004e6869caa452a7d55deb9 +2026-08-24-standalone-sdk-minimal-profile.zh.md: ae6fdb95079f748f26758a30c968c27548e9a87f diff --git a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md index aa822d40a3..bc1a177dc4 100644 --- a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md +++ b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md @@ -22,7 +22,7 @@ The bundle reuses `@deepseek-ai/dsh-sdk-app` for command help, stdin EOF, and bo ### Explicit composition -The bundle owns one DeepSeek adapter, SDK JSON-RPC serving, the executor-less agent spine, local subprocess and unrestricted filesystem providers, persistent Bash, the string-replace editor, and uncompressed JSONL sessions under `$DSH_HOME/sessions`. The persona comes from `DSH_SYSTEM_PROMPT`, the adapter route from `DSH_MODEL` and `DSH_CONTEXT_WINDOW`, and the credential from `DEEPSEEK_API_KEY`. +The bundle owns one DeepSeek adapter, SDK JSON-RPC serving, the executor-less agent spine, local subprocess and unrestricted filesystem providers, persistent Bash, the string-replace editor, and uncompressed JSONL sessions under `$DSH_HOME/sessions`. The SDK initialization request owns the model id; `DSH_CONTEXT_WINDOW` supplies fallback capacity for models outside the adapter's advisory catalog. The persona comes from `DSH_SYSTEM_PROMPT`, and the credential from `DEEPSEEK_API_KEY`. Harness identity, runtime context, workspace instructions, skills, model-facing job controls, compaction, settings, managed credentials, telemetry, Web tools, subagents, and every other base row are absent rather than hidden. The profile pins `danger-full-access`, `maxTokensAsSuccess: false`, and startup-only patch loading. This layer is POSIX-only because its persistent terminal uses Bash. diff --git a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md index cea6ad6f57..ae6fdb9507 100644 --- a/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md @@ -22,7 +22,7 @@ Status: implemented ### 显式组合 -该组合包拥有一个 DeepSeek 适配器、SDK JSON-RPC 服务、无执行器的 agent 主干、本地子进程与不受限文件系统提供方、持久 Bash、字符串替换 editor,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 会话。Persona 来自 `DSH_SYSTEM_PROMPT`,适配器路由来自 `DSH_MODEL` 与 `DSH_CONTEXT_WINDOW`,凭据来自 `DEEPSEEK_API_KEY`。 +该组合包拥有一个 DeepSeek 适配器、SDK JSON-RPC 服务、无执行器的 agent 主干、本地子进程与不受限文件系统提供方、持久 Bash、字符串替换 editor,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 会话。SDK 初始化请求拥有模型 id;`DSH_CONTEXT_WINDOW` 为不在适配器建议目录中的模型提供后备容量。Persona 来自 `DSH_SYSTEM_PROMPT`,凭据来自 `DEEPSEEK_API_KEY`。 Harness 身份、运行时上下文、workspace 指令、skills、面向模型的 job 控制、compaction、settings、托管凭据、遥测、Web 工具、subagent 与其他所有 base 配置项均不存在,而不是被隐藏。该 profile 固定使用 `danger-full-access`、`maxTokensAsSuccess: false` 与仅启动时 patch 加载。由于持久终端使用 Bash,此层只支持 POSIX。 diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml index 58474befe4..4a576e9339 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml @@ -2,5 +2,5 @@ # 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/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md -2026-08-11-minimal-profiles-bare-two-tool-runtime.md: dbbe0e253d23a7663bdd40ac302e779d3090e8e0 -2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: 831b475f277beeef7e7542fb23c410311b45151b +2026-08-11-minimal-profiles-bare-two-tool-runtime.md: 6ea86832b632c631b7e02d6c486f3858fd2632a4 +2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: 73f6f16a51c14a7c98915a84878b39596d12f245 diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md index dbbe0e253d..6ea86832b6 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md @@ -16,7 +16,7 @@ Both shipped minimal profiles expose exactly persistent `bash` and `str_replace_ The standalone [`@deepseek-ai/dsh-sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) remains a complete JSON-RPC process composition behind `dsh --profile sdk-minimal`. It mounts SDK startup and JSON-RPC serving, the local PTY and subprocess services required by persistent Bash, `fs-local`, the two tool consumers, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not mount `token-meter`, `compaction-basic`, `fs-sandbox`, or `fs-observation-policy`. Persistent Bash still consumes the profile's danger-full-access sandbox policy; the editor is not confined by that policy. The [standalone-profile decision](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) owns this bundle placement and its separation from `dsh-base`. -`DSH_SYSTEM_PROMPT` selects the standalone persona. `DSH_MODEL` names the DeepSeek provider catalog entry, and `DSH_CONTEXT_WINDOW` supplies that entry's capacity. Because the SDK client owns the JSON-RPC `initialize` request, [`minimal.py`](../../../../examples/python-sdk-agent/minimal.py) uses `DSH_MODEL` as its default `model` argument and passes an explicit `--model` back to the child environment so the catalog and request remain aligned. Endpoint and credential variables stay owned by the DeepSeek adapter's existing environment-resolution path. +`DSH_SYSTEM_PROMPT` selects the standalone persona, and `DSH_CONTEXT_WINDOW` supplies fallback capacity for a model without exact catalog metadata. The SDK client's JSON-RPC `initialize` request is the sole runtime model selection. [`minimal.py`](../../../../examples/python-sdk-agent/minimal.py) may read `DSH_MODEL` only as the command's default `model` argument; an explicit `--model` needs no matching child environment value. Endpoint and credential variables stay owned by the DeepSeek adapter's existing environment-resolution path. ## Verification @@ -32,7 +32,7 @@ The SDK keyless process test boots real `dsh --profile sdk-minimal`, injects an **Use one Cordis leaf for Web and Python SDK startup.** Rejected because a Web preset contributes agent-scoped services to an existing multi-session host, while the Python SDK must launch a complete process containing the JSON-RPC server and its process-wide dependencies. -**Read `DSH_MODEL` only inside Cordis.** Rejected because Cordis configures the provider catalog but does not own the SDK client's JSON-RPC `initialize` request. The launcher must pass the same model to the client request for the environment value to select the routed model. +**Mirror the requested model into `DSH_MODEL`.** Rejected because the direct adapter accepts model ids outside its advisory catalog and resolves fallback context metadata for them. Mirroring creates two inputs for one selection; the SDK initialization request is authoritative, while `DSH_MODEL` remains only a convenience default in `minimal.py`. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md index 831b475f27..73f6f16a51 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md @@ -16,7 +16,7 @@ Web `minimal` preset 与独立 JSON-RPC minimal 组合对外提供持久 `bash` 独立的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)仍是 `dsh --profile sdk-minimal` 后面的完整 JSON-RPC 进程组合。它挂载 SDK 启动与 JSON-RPC 服务、持久 Bash 所需的本地 PTY 和子进程服务、`fs-local`、两个工具消费方,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不挂载 `token-meter`、`compaction-basic`、`fs-sandbox` 或 `fs-observation-policy`。持久 Bash 仍消费该 profile 的 danger-full-access 沙箱策略;编辑器不受该策略限制。[独立 profile 决策](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)负责该组合包的位置及其与 `dsh-base` 的分离。 -`DSH_SYSTEM_PROMPT` 选择独立组合的 persona。`DSH_MODEL` 命名 DeepSeek 提供方目录项,`DSH_CONTEXT_WINDOW` 提供该目录项的容量。由于 SDK 客户端拥有 JSON-RPC `initialize` 请求,[`minimal.py`](../../../../examples/python-sdk-agent/minimal.py)会使用 `DSH_MODEL` 作为 `model` 参数的默认值,并把显式 `--model` 传回子进程环境,使目录与请求保持一致。端点与凭据变量继续由 DeepSeek 适配器现有的环境解析路径持有。 +`DSH_SYSTEM_PROMPT` 选择独立组合的 persona,`DSH_CONTEXT_WINDOW` 为没有确切目录元数据的模型提供后备容量。SDK 客户端的 JSON-RPC `initialize` 请求是唯一运行时模型选择。[`minimal.py`](../../../../examples/python-sdk-agent/minimal.py)可以只把 `DSH_MODEL` 读作命令的默认 `model` 参数;显式 `--model` 不需要匹配的子进程环境值。端点与凭据变量继续由 DeepSeek 适配器现有的环境解析路径持有。 ## 验证 @@ -32,7 +32,7 @@ SDK keyless 进程测试启动真实 `dsh --profile sdk-minimal`,注入由环 **为 Web 与 Python SDK 启动使用同一个 Cordis leaf。** 不予采用,因为 Web preset 向现有多会话宿主贡献 agent 作用域服务,而 Python SDK 必须启动包含 JSON-RPC 服务器及其进程级依赖的完整进程。 -**只在 Cordis 内读取 `DSH_MODEL`。** 不予采用,因为 Cordis 配置提供方目录,但不拥有 SDK 客户端的 JSON-RPC `initialize` 请求。launcher 必须向客户端请求传递同一个模型,环境值才能选择路由模型。 +**把请求模型镜像到 `DSH_MODEL`。** 不予采用,因为直接适配器接受不在建议目录中的模型 id,并为它们解析后备上下文元数据。镜像会为同一项选择制造两个输入;SDK 初始化请求具有权威,`DSH_MODEL` 只保留为 `minimal.py` 的便捷默认值。 ## 后果 diff --git a/docs/user/guide/python-sdk.i18n.yaml b/docs/user/guide/python-sdk.i18n.yaml index 88d467f519..4ddf1a6015 100644 --- a/docs/user/guide/python-sdk.i18n.yaml +++ b/docs/user/guide/python-sdk.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/user/guide/python-sdk.md -python-sdk.md: 2cf6acbd6718be560ec4365b356c031b17fb070e -python-sdk.zh.md: 2c53886ba3409e6fc21ec57fc79b81bf6907349c +python-sdk.md: 24f5594a20eab6870d9725e0f1acfce5dff62f74 +python-sdk.zh.md: 47b420ab20df04d28e5498807dc73a425c7a9666 diff --git a/docs/user/guide/python-sdk.md b/docs/user/guide/python-sdk.md index 2cf6acbd67..24f5594a20 100644 --- a/docs/user/guide/python-sdk.md +++ b/docs/user/guide/python-sdk.md @@ -61,7 +61,6 @@ with DeepSeekHarness( cwd=str(workspace), dsh_home=str(dsh_home), profile="sdk-minimal", - env={"DSH_MODEL": "deepseek-v4-flash"}, ) as harness: result = harness.run( "Inspect the repository and fix the failing tests.", diff --git a/docs/user/guide/python-sdk.zh.md b/docs/user/guide/python-sdk.zh.md index 2c53886ba3..47b420ab20 100644 --- a/docs/user/guide/python-sdk.zh.md +++ b/docs/user/guide/python-sdk.zh.md @@ -61,7 +61,6 @@ with DeepSeekHarness( cwd=str(workspace), dsh_home=str(dsh_home), profile="sdk-minimal", - env={"DSH_MODEL": "deepseek-v4-flash"}, ) as harness: result = harness.run( "Inspect the repository and fix the failing tests.", diff --git a/examples/python-sdk-agent/README.i18n.yaml b/examples/python-sdk-agent/README.i18n.yaml index 0c3a5fe203..e57c422380 100644 --- a/examples/python-sdk-agent/README.i18n.yaml +++ b/examples/python-sdk-agent/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 examples/python-sdk-agent/README.md -README.md: 5bb958c3e9202642926a0186d055615a895d9586 -README.zh.md: 97bfe9e1d19a7cd37ed41fccc64340dc054ecec7 +README.md: 46a8dc2384d96db39841c4c1e4cdc88d82ff55eb +README.zh.md: 5e6e2f89f27398dd7404894f15391a6a693d689b diff --git a/examples/python-sdk-agent/README.md b/examples/python-sdk-agent/README.md index 5bb958c3e9..46a8dc2384 100644 --- a/examples/python-sdk-agent/README.md +++ b/examples/python-sdk-agent/README.md @@ -17,7 +17,7 @@ python examples/python-sdk-agent/minimal.py \ "Inspect the repository and fix the failing tests." ``` -Set `DEEPSEEK_BASE_URL` for a compatible proxy, `DSH_MODEL` for the default model, or `DSH_SYSTEM_PROMPT` for the deployment persona. `--model` overrides the model and passes the same value to the profile-owned adapter catalog; `--profile` can select another SDK-serving profile. The selected home stores the generated `sdk-minimal` profile and uncompressed JSONL session logs under `sessions/`; the script never reads `~/.dsh` implicitly. +Set `DEEPSEEK_BASE_URL` for a compatible proxy, `DSH_MODEL` for the script's default model, or `DSH_SYSTEM_PROMPT` for the deployment persona. `--model` is the single runtime model selection; no matching environment variable is required. `--profile` can select another SDK-serving profile. The selected home stores the generated `sdk-minimal` profile and uncompressed JSONL session logs under `sessions/`; the script never reads `~/.dsh` implicitly. The shipped [`@deepseek-ai/dsh-sdk-minimal` bundle](../../packages/bundle/sdk-minimal/README.md) is the complete explicit Cordis tree for this mode. It exposes exactly: diff --git a/examples/python-sdk-agent/README.zh.md b/examples/python-sdk-agent/README.zh.md index 97bfe9e1d1..5e6e2f89f2 100644 --- a/examples/python-sdk-agent/README.zh.md +++ b/examples/python-sdk-agent/README.zh.md @@ -17,7 +17,7 @@ python examples/python-sdk-agent/minimal.py \ "Inspect the repository and fix the failing tests." ``` -兼容代理使用 `DEEPSEEK_BASE_URL`,默认模型使用 `DSH_MODEL`,deployment persona 使用 `DSH_SYSTEM_PROMPT`。`--model` 会覆盖模型,并将同一值传给 profile 自有的适配器目录;`--profile` 可以选择另一个提供 SDK 服务的 profile。所选 home 保存生成的 `sdk-minimal` profile,并在 `sessions/` 下保存未压缩 JSONL 会话日志;脚本绝不会隐式读取 `~/.dsh`。 +兼容代理使用 `DEEPSEEK_BASE_URL`,脚本默认模型使用 `DSH_MODEL`,deployment persona 使用 `DSH_SYSTEM_PROMPT`。`--model` 是唯一运行时模型选择,不要求匹配的环境变量;`--profile` 可以选择另一个提供 SDK 服务的 profile。所选 home 保存生成的 `sdk-minimal` profile,并在 `sessions/` 下保存未压缩 JSONL 会话日志;脚本绝不会隐式读取 `~/.dsh`。 随附的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../packages/bundle/sdk-minimal/README.zh.md)是该模式完整且显式的 Cordis 配置树。它只暴露: diff --git a/examples/python-sdk-agent/minimal.py b/examples/python-sdk-agent/minimal.py index 86c1dc8082..28d2fd8c54 100644 --- a/examples/python-sdk-agent/minimal.py +++ b/examples/python-sdk-agent/minimal.py @@ -39,7 +39,6 @@ def main() -> None: cwd=str(workspace), dsh_home=str(dsh_home), profile=args.profile, - env={"DSH_MODEL": args.model}, ) as harness: result = harness.run(args.prompt, session_id=args.session_id) print(result.final_response) diff --git a/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts b/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts index e0bd4ed6c3..04eff29319 100644 --- a/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts +++ b/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts @@ -209,7 +209,6 @@ describe('Python SDK dsh profile keyless smoke', () => { cwd: repoRoot, env: { DSH_HOME: join(root, '.dsh'), - DSH_MODEL: 'deepseek-v4-pro', DSH_SYSTEM_PROMPT: 'Minimal allowlist prompt.', DEEPSEEK_API_KEY: 'keyless-smoke-no-call', DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`, diff --git a/packages/bundle/sdk-minimal/README.i18n.yaml b/packages/bundle/sdk-minimal/README.i18n.yaml index 715a58381e..c573869472 100644 --- a/packages/bundle/sdk-minimal/README.i18n.yaml +++ b/packages/bundle/sdk-minimal/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/bundle/sdk-minimal/README.md -README.md: 7f37d0cb4996a9c48e4bcb934565ff2865f5e7e3 -README.zh.md: 3e2c5b5f66314e8dc2da9622bbd1937d85d98490 +README.md: b33ccb429ab0291d46f0271329b957f0ac7011fa +README.zh.md: 9e4ab381f5595630cbf139b1a1e8e30cef147f39 diff --git a/packages/bundle/sdk-minimal/README.md b/packages/bundle/sdk-minimal/README.md index 7f37d0cb49..b33ccb429a 100644 --- a/packages/bundle/sdk-minimal/README.md +++ b/packages/bundle/sdk-minimal/README.md @@ -6,7 +6,7 @@ Standalone minimal SDK application bundle for `dsh --profile sdk-minimal`. Its s The profile remains part of the ordinary launcher and layering model. The bundle supplies the complete default tree; the profile patch, home patch, and ordered `--patch` files can replace rows or insert external bundles above it. `dsh plugin --profile sdk-minimal` manages persistent dependencies. The shipped template uses startup-only patches so one stdio connection never observes replacement of its server or agent dependencies. -`DEEPSEEK_API_KEY` supplies the adapter credential. `DSH_MODEL` selects the sole configured model, `DSH_CONTEXT_WINDOW` sets its context window, and `DSH_SYSTEM_PROMPT` replaces the default persona. The process working directory is the sandbox-policy workspace and local-filesystem root. The bundle sets `danger-full-access`; its persistent shell and editor can modify any path available to the process. +`DEEPSEEK_API_KEY` supplies the adapter credential. The SDK initialization request is the sole model selection; the adapter accepts that model id even when it is absent from its advisory catalog. `DSH_CONTEXT_WINDOW` sets the fallback capacity for such models, and `DSH_SYSTEM_PROMPT` replaces the default persona. The process working directory is the sandbox-policy workspace and local-filesystem root. The bundle sets `danger-full-access`; its persistent shell and editor can modify any path available to the process. ## Model Experience diff --git a/packages/bundle/sdk-minimal/README.zh.md b/packages/bundle/sdk-minimal/README.zh.md index 3e2c5b5f66..9e4ab381f5 100644 --- a/packages/bundle/sdk-minimal/README.zh.md +++ b/packages/bundle/sdk-minimal/README.zh.md @@ -6,7 +6,7 @@ 该 profile 仍遵循普通 launcher 与分层模型。组合包提供完整默认树;profile patch、home patch 与有序 `--patch` 文件可以在其上替换配置项或插入外部组合包。`dsh plugin --profile sdk-minimal` 管理持久依赖。随附模板仅在启动时应用 patch,因此一个 stdio 连接不会观察到服务器或 agent 依赖在运行中被替换。 -`DEEPSEEK_API_KEY` 提供适配器凭据。`DSH_MODEL` 选择唯一配置的模型,`DSH_CONTEXT_WINDOW` 设置其上下文窗口,`DSH_SYSTEM_PROMPT` 替换默认 persona。进程工作目录同时作为沙箱策略 workspace 与本地文件系统根目录。该组合包设置 `danger-full-access`;其持久 shell 与编辑器可以修改进程可访问的任何路径。 +`DEEPSEEK_API_KEY` 提供适配器凭据。SDK 初始化请求是唯一模型选择;即使该模型 id 不在适配器的建议目录中,适配器也会接受它。`DSH_CONTEXT_WINDOW` 为这类模型设置后备容量,`DSH_SYSTEM_PROMPT` 替换默认 persona。进程工作目录同时作为沙箱策略 workspace 与本地文件系统根目录。该组合包设置 `danger-full-access`;其持久 shell 与编辑器可以修改进程可访问的任何路径。 ## 模型体验 diff --git a/packages/bundle/sdk-minimal/cordis.patch.yml b/packages/bundle/sdk-minimal/cordis.patch.yml index e43f3e2b03..24d707c7e9 100644 --- a/packages/bundle/sdk-minimal/cordis.patch.yml +++ b/packages/bundle/sdk-minimal/cordis.patch.yml @@ -27,10 +27,8 @@ name: '@deepseek-ai/dsh-llm-deepseek' config: apiKeyEnv: DEEPSEEK_API_KEY + defaultContextWindow: !!js Number(process.env.DSH_CONTEXT_WINDOW ?? 1000000) streamIdleTimeoutMs: 172800000 - models: - - id: !!js process.env.DSH_MODEL ?? 'deepseek-v4-flash' - contextWindow: !!js Number(process.env.DSH_CONTEXT_WINDOW ?? 1000000) - id: sandbox name: '@deepseek-ai/dsh-sandbox-local' diff --git a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts index 4e9fbbcc6a..f8c23e92ac 100644 --- a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts +++ b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts @@ -44,6 +44,11 @@ describe('dsh-sdk-minimal bundle', () => { inject: ['sdkAppStartup', 'loader'], config: { maxTokensAsSuccess: false }, }) + expect(rows.find(row => row.id === 'llm-deepseek')?.config).toEqual({ + apiKeyEnv: 'DEEPSEEK_API_KEY', + defaultContextWindow: { __jsExpr: 'Number(process.env.DSH_CONTEXT_WINDOW ?? 1000000)' }, + streamIdleTimeoutMs: 172800000, + }) expect(rows.find(row => row.id === 'agent-spine')?.config).toMatchObject({ includeHarnessIdentity: false, includeRuntimeContext: false, diff --git a/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index 1e98c9d0a3..5295ed3c11 100644 --- a/python/sdk/README.i18n.yaml +++ b/python/sdk/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 python/sdk/README.md -README.md: 4138ff5b8fb476da2d82f710f72369d3855066a2 -README.zh.md: a51777a427deb65b461213aed28bb9df179ed1b1 +README.md: cf9bb3e3ccac4908e9212d8f7247545b5a6b5d8e +README.zh.md: 9acb8f26129144a77a834f94854cdd3f1a200086 diff --git a/python/sdk/README.md b/python/sdk/README.md index 4138ff5b8f..cf9bb3e3cc 100644 --- a/python/sdk/README.md +++ b/python/sdk/README.md @@ -53,7 +53,7 @@ with DeepSeekHarness( `profile` may select another existing profile, but that composition must retain `@deepseek-ai/dsh-sdk-app` or another `@deepseek-ai/dsh-sdk-jsonrpc-server` row. Misconfiguration fails during CLI boot or SDK initialization; there is no complete-config fallback. `dsh_bin` may select another `dsh` executable while preserving the same profile grammar. Arbitrary argv replacement remains an internal fake-runtime test adapter, not public API. -The shipped `sdk-minimal` profile is a standalone explicit tree rather than an overlay on `dsh-base`. Select it with `profile="sdk-minimal"` and set the same model in `env={"DSH_MODEL": model}` so its one adapter route matches SDK initialization. It provides persistent Bash, the string-replace editor, local execution, and JSONL sessions; settings, managed credentials, telemetry, Web tools, and the full default tool roster remain available through the separate full `sdk` and `web` profiles. +The shipped `sdk-minimal` profile is a standalone explicit tree rather than an overlay on `dsh-base`. Select it with `profile="sdk-minimal"`; the ordinary `model` argument is the sole runtime model selection, including for model ids outside the adapter's advisory catalog. It provides persistent Bash, the string-replace editor, local execution, and JSONL sessions; settings, managed credentials, telemetry, Web tools, and the full default tool roster remain available through the separate full `sdk` and `web` profiles. ## Results and notifications diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index a51777a427..9acb8f2612 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -53,7 +53,7 @@ with DeepSeekHarness( `profile` 可以选择另一个已存在的 profile,但该组合必须保留 `@deepseek-ai/dsh-sdk-app` 或另一个 `@deepseek-ai/dsh-sdk-jsonrpc-server` 配置项。配置错误会在 CLI 启动或 SDK 初始化时失败;不存在完整配置回退。`dsh_bin` 可以选择另一个 `dsh` 可执行程序,同时保持相同的 profile 语法。任意 argv 替换仅是内部 fake-runtime 测试适配器,不属于公开 API。 -随附的 `sdk-minimal` profile 是独立显式配置树,而不是 `dsh-base` 上的 overlay。使用 `profile="sdk-minimal"` 选择它,并在 `env={"DSH_MODEL": model}` 中设置相同模型,使其唯一适配器路由与 SDK 初始化一致。它提供持久 Bash、字符串替换 editor、本地执行与 JSONL 会话;settings、托管凭据、遥测、Web 工具与完整默认工具清单仍由独立的完整 `sdk` 与 `web` profile 提供。 +随附的 `sdk-minimal` profile 是独立显式配置树,而不是 `dsh-base` 上的 overlay。使用 `profile="sdk-minimal"` 选择它;普通 `model` 参数是唯一运行时模型选择,也适用于不在适配器建议目录中的模型 id。它提供持久 Bash、字符串替换 editor、本地执行与 JSONL 会话;settings、托管凭据、遥测、Web 工具与完整默认工具清单仍由独立的完整 `sdk` 与 `web` profile 提供。 ## 结果与通知 diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index ba5279000d..4a5a3eb0be 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -978,9 +978,6 @@ def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) - dsh_bin=str(executable), dsh_home=str(dsh_home), profile="sdk-minimal", - env={ - "DSH_MODEL": "smoke-model", - }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, From 104fe9b9e786d695b6e27b4ad5063912d4fa4786 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:16:04 +0800 Subject: [PATCH 15/16] test(sdk-app): cover default profile configuration The startup test helper always constructed an explicit sdk profile, so coverage never exercised apply's supported default-config path even though runtime behavior depended on it.\n\nPass Config objects through the helper and default them to an empty config. The existing startup and help tests now prove sdk defaulting, while the explicit sdk-minimal case remains covered. --- packages/bundle/sdk-app/tests/startup.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bundle/sdk-app/tests/startup.spec.ts b/packages/bundle/sdk-app/tests/startup.spec.ts index 5f0ec6524a..ec128a1c4a 100644 --- a/packages/bundle/sdk-app/tests/startup.spec.ts +++ b/packages/bundle/sdk-app/tests/startup.spec.ts @@ -4,7 +4,7 @@ import { EventEmitter } from 'node:events' import { Context } from '@deepseek-ai/cordis' import { afterEach, describe, expect, it } from 'vitest' import { internals, provideCmdline } from '@deepseek-ai/dsh-cmdline' -import { apply, SDK_APP_STARTUP_SERVICE } from '../src/index.ts' +import { apply, type Config, SDK_APP_STARTUP_SERVICE } from '../src/index.ts' /** Controllable stdin for one startup invocation. */ class TestStdin extends EventEmitter { @@ -27,7 +27,7 @@ afterEach(() => { }) /** Run the provider with captured command output and exit requests. */ -function start(args: string[], profile = 'sdk'): { ctx: Context; exits: number[]; out: () => string; stdin: TestStdin } { +function start(args: string[], config: Config = {}): { ctx: Context; exits: number[]; out: () => string; stdin: TestStdin } { const ctx = new Context() const exits: number[] = [] const stdin = new TestStdin() @@ -41,7 +41,7 @@ function start(args: string[], profile = 'sdk'): { ctx: Context; exits: number[] exit: code => void exits.push(code), ready: { onReady: (listener) => { listener(); return () => {} } }, }) - apply(ctx, { profile }) + apply(ctx, config) return { ctx, exits, out: () => out, stdin } } @@ -64,7 +64,7 @@ describe('SDK app startup', () => { }) it('renders the selected SDK profile name in help', () => { - const { out } = start(['--help'], 'sdk-minimal') + const { out } = start(['--help'], { profile: 'sdk-minimal' }) expect(out()).toContain('Usage: dsh --profile sdk-minimal') expect(out()).toContain('dsh --profile sdk-minimal') }) From 4719bef9329b67da11d78a7ef94428dca8b09a66 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:27:52 +0800 Subject: [PATCH 16/16] test(python): exercise the shipped SDK profile directly The keyless max-token smoke carried a disable overlay that reproduced the removed private carrier's reduced tool roster. That legacy roster is now owned by the standalone sdk-minimal profile, so mutating the full sdk profile hides the application the SDK actually ships.\n\nLaunch sdk without a patch for both the successful and invalid-config paths. The separate sdk-minimal process test continues to pin its exact platform-selected two-tool request. --- .../tests/keyless-smoke.e2e.ts | 15 -------- .../python-sdk-agent/tests/keyless.patch.yml | 38 ------------------- 2 files changed, 53 deletions(-) delete mode 100644 examples/python-sdk-agent/tests/keyless.patch.yml diff --git a/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts b/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts index 04eff29319..625fa2f16f 100644 --- a/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts +++ b/examples/python-sdk-agent/tests/keyless-smoke.e2e.ts @@ -9,7 +9,6 @@ import { execa } from 'execa' import { describe, expect, it } from 'vitest' const binScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url)) -const patchPath = fileURLToPath(new URL('./keyless.patch.yml', import.meta.url)) const repoRoot = fileURLToPath(new URL('../../..', import.meta.url)) const decompress = promisify(zstdDecompress) @@ -77,8 +76,6 @@ describe('Python SDK dsh profile keyless smoke', () => { binScript, '--profile', 'sdk', - '--patch', - patchPath, ], { cwd: repoRoot, env: { @@ -147,17 +144,7 @@ describe('Python SDK dsh profile keyless smoke', () => { }, }, }) - const tools = modelRequests[0]?.tools as { function?: { name?: string } }[] expect(modelRequests[0]?.max_tokens).toBe(1234) - expect(tools.map(tool => tool.function?.name).sort()).toEqual([ - 'bash', - 'edit', - 'read', - 'read_image', - 'subagent', - 'todo_write', - 'write', - ]) child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`) const shutdown = await waitForLine(lines, value => value.id === 3, () => stderr) @@ -283,8 +270,6 @@ describe('Python SDK dsh profile keyless smoke', () => { binScript, '--profile', 'sdk', - '--patch', - patchPath, ], { cwd: repoRoot, env: { diff --git a/examples/python-sdk-agent/tests/keyless.patch.yml b/examples/python-sdk-agent/tests/keyless.patch.yml deleted file mode 100644 index 71b5ec6ebd..0000000000 --- a/examples/python-sdk-agent/tests/keyless.patch.yml +++ /dev/null @@ -1,38 +0,0 @@ -# Preserve the focused SDK test roster over the shipped sdk profile. - -- id: agent-instructions - disabled: true -- id: tool-jobs - disabled: true -- id: tool-fs-search - disabled: true -- id: skill-filesystem - disabled: true -- id: tool-skill - disabled: true -- id: tool-str-replace-editor - disabled: true -- id: tool-subagent-control - disabled: true -- id: tool-subagent-list-agents - disabled: true -- id: tool-subagent-fork - disabled: true -- id: tool-subagent-report - disabled: true -- id: tool-workflow - disabled: true -- id: tool-goal - disabled: true -- id: plan-mode - disabled: true -- id: tool-ralph - disabled: true -- id: tool-web - disabled: true - -- id: tool-subagent - config: - provider: spawn - toolName: subagent - backgroundMode: one-shot