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 5b400d15a2..aaa8fdbedc 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 @@ -18,14 +18,14 @@ en: 5fdde26e9e8c6108 zh: 629559d733c1f0c5 /agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/plugin-resolution-the-vfs-holds-a-real-package-tree-the-closure-manifest-is-the-deploy-root: - en: c35ea4b858448b49 - zh: 1f8ff0a9f82ca881 + en: 985e0eb63f3d1fd6 + zh: 337cc44fadc6227e /agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/build-pipeline-and-artifacts: - en: d3bbd24ead02af4f - zh: 64623d88bee4314b + en: 921085cb9d894688 + zh: 12d99fe153d36738 /agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/python-sdk-distribution-two-carriers-exe-for-production-node-for-development: - en: 9bd60549d4a52ec0 - zh: e2343b7210212ee0 + en: 02e6b36f9f92c83a + zh: edd8b2db7a787483 /agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/naming-lineage: en: 31a5aecb8bf828bb zh: 2dffb6fd2e0f8194 @@ -33,8 +33,8 @@ en: 0426f598f3f44e64 zh: fc66728e17f3bcfb /agent-note-single-file-executable-sdk-runtime-distribution-single-exe/testing: - en: b3c9820f4dc7c6c8 - zh: 3b5768ef1770bfcb + en: 8e7f46bbb595afa9 + zh: 4641894ed285d717 /agent-note-single-file-executable-sdk-runtime-distribution-single-exe/alternatives-considered: en: 50d965ca81a401ba zh: 0c6296245b97b9ac 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 27d37ed541..60228735f5 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 @@ -44,19 +44,19 @@ The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-ru 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. The optional resource service provides MCP Resources; MCP Prompts remain unsupported. The executable and installed-wheel smokes start a temporary stdio server, discover its tool, and complete one model-requested call. -The [Office kit](2026-09-14-independent-libreoffice-kit.md) and its installed dependency tree live in `-office/node_modules`, preserving complete package contents and helper permissions. Top-level `pkg.ignore` excludes kit packages from the SEA archive, including assets selected by dependency `files` fields; small dependencies shared with other consumers remain available in the VFS. The private Python bootstrap uses Node `registerHooks` to resolve the kit entry and its package manifest from the real directory. Native executables and URL Workers therefore retain their ordinary paths without modifying the kit or the public CLI. Missing required packages and missing installed package manifests fail the copy; absent optionals preserve the kit's engine selection. +The [Office kit](2026-09-14-independent-libreoffice-kit.md) and its installed dependency tree live in the explicitly downloaded sidecar’s `node_modules`, preserving complete package contents and helper permissions. Top-level `pkg.ignore` excludes kit packages from the SEA archive, including assets selected by dependency `files` fields; small dependencies shared with other consumers remain available in the VFS. The private Python bootstrap uses Node `registerHooks` to resolve the kit entry and its package manifest from the real directory. Native executables and URL Workers therefore retain their ordinary paths without modifying the kit or the public CLI. The build locks the selected engine and dependency locations to exact npm tarballs; downloads verify their integrity before publication. ### 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-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → 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 → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering 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 with its target-specific Office directory into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. 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`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted supplies package directories that staging makes symlink-free; staging removes nested workspace copies only when pnpm records the same dependency identity at the payload root, preserving singleton services; disabling workspace hoisting excludes unrelated workspace source packages; 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. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation. +[`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.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → 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 → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering 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 with its lightweight resource manifest and Office skill assets into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. 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`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted supplies package directories that staging makes symlink-free; staging removes nested workspace copies only when pnpm records the same dependency identity at the payload root, preserving singleton services; disabling workspace hoisting excludes unrelated workspace source packages; 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. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation. CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) runs [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) on Linux/Windows x64 for pull requests and Linux ARM64 plus both macOS architectures for master pushes. The [public publication workflow](../../archived/process/2026-08-11-python-publication-workflow.md) calls it for all five targets; `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / macos-x64 (`macos-15-intel`) / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, verifies their package and executable origin, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests and master pushes additionally run a real DeepSeek two-turn tool smoke on their selected targets; 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 checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five 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 five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows target and the explicit exclusion of Windows arm64. ### Python SDK distribution: two carriers, exe for production, node for development -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, `-office/` directory, 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`. +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, lightweight download metadata and Office skills, 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/runtime-bootstrap.mjs` 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, its Office directory, and its architecture-matched ripgrep sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, `py3-none-macosx_14_0_arm64`, `py3-none-macosx_14_0_x86_64`, or `py3-none-win_amd64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms. Both macOS tags deliberately declare a conservative 14.0 installation floor: the packaged Node 24 executables declare macOS 13.5, and the x64 PTY helper declares 10.7, but release validation proves the complete payload only against the 14.0 wheel claim rather than promising each observed component minimum as a supported host. The two macOS wheels remain architecture-specific; no universal2 wheel is published. +[`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, lightweight resource metadata and skills, and its architecture-matched ripgrep sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, `py3-none-macosx_14_0_arm64`, `py3-none-macosx_14_0_x86_64`, or `py3-none-win_amd64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms. Both macOS tags deliberately declare a conservative 14.0 installation floor: the packaged Node 24 executables declare macOS 13.5, and the x64 PTY helper declares 10.7, but release validation proves the complete payload only against the 14.0 wheel claim rather than promising each observed component minimum as a supported host. The two macOS wheels remain architecture-specific; no universal2 wheel is published. 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. @@ -74,7 +74,7 @@ The verification surface has three tiers. Mechanism tier: the measured conclusio Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disposes immediately, so a short-lived pipe aborts an in-flight turn — pipe-driven runs must keep stdin open until the turn ends. -The installed-wheel `sdk-office` scenario copies the target's complete payload to a private temporary directory, requires exactly one target backend (the declared native target, or WASM when no native target is declared), and converts DOCX once through the shipped SDK profile with that engine. It checks that the kit module resolves inside the relocated directory, that conversion reports the expected backend, and that the output contains PDF bytes. Wheel validation checks the declared engine assets and native helper execute bit; CI uploads the completed wheel, so nested helper permissions stay inside the ZIP archive. +The installed-wheel `sdk-office` scenario relocates the lightweight wheel payload and explicitly downloads the complete npm Office sidecar with its independent Node. It requires exactly one target backend, verifies that the SDK loads the kit from the downloaded sidecar, and converts DOCX through the skill CLI with an empty PATH. Python Office operations use the same CLI. Explicit downloads reuse the existing build preparation implementation; SDK startup performs no downloads. The [runtime reference](../../../../python/sdk-runtime/README.md) owns cache and activation behavior. ## Alternatives considered 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 4cc6e20459..272c0e80c9 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 @@ -44,19 +44,19 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真 部署根目录显式包含 `@deepseek-ai/dsh-mcp-client`,将其作为自定义配置可用的插件,即使随附 preset 均未挂载该插件。外部配置因此可以连接由用户提供的 stdio 与 Streamable HTTP MCP server 并注册其工具;分发物不包含这些 server。可选资源服务提供 MCP Resources;MCP Prompts 仍不受支持。可执行程序与已安装 wheel 包的冒烟测试会启动临时 stdio server,发现其工具,并完成一次由模型请求的调用。 -[Office kit](2026-09-14-independent-libreoffice-kit.zh.md) 及其已安装依赖树位于 `-office/node_modules`,保留完整包内容与辅助程序权限。顶层 `pkg.ignore` 从 SEA 归档排除 kit 包,包括依赖 `files` 字段选中的资源;与其他使用方共享的小型依赖仍可在 VFS 中使用。Python 私有 bootstrap 使用 Node `registerHooks` 从真实目录解析 kit 入口及其包清单。因此原生可执行程序与 URL Worker 保留普通文件路径,无需修改 kit 或公开 CLI。必需包缺失或已安装包缺少清单会使复制失败;未安装的可选包保留 kit 自身的引擎选择规则。 +[Office kit](2026-09-14-independent-libreoffice-kit.zh.md) 及其已安装依赖树位于显式下载的 sidecar 的 `node_modules`,保留完整包内容与辅助程序权限。顶层 `pkg.ignore` 从 SEA 归档排除 kit 包,包括依赖 `files` 字段选中的资源;与其他使用方共享的小型依赖仍可在 VFS 中使用。Python 私有 bootstrap 使用 Node `registerHooks` 从真实目录解析 kit 入口及其包清单。因此原生可执行程序与 URL Worker 保留普通文件路径,无需修改 kit 或公开 CLI。构建将所选引擎和依赖位置锁定到确切的 npm 归档;下载会在发布缓存前校验完整性。 ### 构建流水线与产物 -[`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.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime--` 写入 `dist-exe/`,并将它与对应目标的 Office 目录一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。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` 提供包目录,再由暂存步骤消除符号链接;只有 pnpm 在载荷根目录记录了相同依赖标识时,暂存步骤才删除对应的嵌套工作区副本,以保留服务单例;关闭工作区提升可排除无关的工作区源码包;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`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.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime--` 写入 `dist-exe/`,并将它与轻量资源清单及 Office skill 资源一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。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` 提供包目录,再由暂存步骤消除符号链接;只有 pnpm 在载荷根目录记录了相同依赖标识时,暂存步骤才删除对应的嵌套工作区副本,以保留服务单例;关闭工作区提升可排除无关的工作区源码包;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`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 包验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)在拉取请求上运行 Linux/Windows x64,在 master 推送上运行 Linux ARM64 与两种 macOS 架构。[公开发布工作流](../../archived/process/2026-08-11-python-publication-workflow.md)调用它构建全部五个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64、macos-x64(`macos-15-intel`)与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求与 master 推送还会在各自选定的目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则检查 runtime、ripgrep 与 PTY helper 的架构,并验证三个载荷的部署目标都符合 wheel 包标签。完整构建五个目标时保留 6 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 5 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v` 标签流水线,构建一个 SDK wheel 包和 5 个原生运行时 wheel 包,再由单个串行任务校验并将这 6 个文件发布到项目的 PyPI 注册表。[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 目标及对 Windows arm64 的明确排除。 ### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发 -Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件、`-office/` 目录和可选的 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`。 +Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件、轻量下载元数据与 Office skills 和可选的 macOS helper,以及供仓库开发使用的构建注入 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 默认选择可执行文件;显式设置 `DSH_RUNTIME_MODE=node` 会在系统 Node 22.19 或更高版本上运行 `runtime/node/runtime-bootstrap.mjs`。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、其 Office 目录及架构匹配的 ripgrep 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`、`py3-none-macosx_14_0_arm64`、`py3-none-macosx_14_0_x86_64` 或 `py3-none-win_amd64`;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。两个 macOS 标签都会特意声明保守的 14.0 安装下限:打包后的 Node 24 可执行文件声明 macOS 13.5,x64 PTY helper 声明 10.7,但发布验证只按照 14.0 wheel 声明证明完整载荷,不会把各组件实测的最低版本承诺为受支持宿主。两个 macOS wheel 包仍按架构分别发布,不发布 universal2 wheel 包。 +[`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、轻量资源元数据与 skills 及架构匹配的 ripgrep 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`、`py3-none-macosx_14_0_arm64`、`py3-none-macosx_14_0_x86_64` 或 `py3-none-win_amd64`;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。两个 macOS 标签都会特意声明保守的 14.0 安装下限:打包后的 Node 24 可执行文件声明 macOS 13.5,x64 PTY helper 声明 10.7,但发布验证只按照 14.0 wheel 声明证明完整载荷,不会把各组件实测的最低版本承诺为受支持宿主。两个 macOS wheel 包仍按架构分别发布,不发布 universal2 wheel 包。 Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和显式 Harness home 启动打包后的 `dsh` 命令。Profile 负责 JSON-RPC 服务和应用组合;缺失 home、profile、bundle、patch 或 server 配置项都会失败,不存在外部完整配置回退。 @@ -74,7 +74,7 @@ Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和 手工驱动注意:`bin` 将 stdin EOF 视为「客户端已离开」并立即 dispose,生命周期较短的管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。 -安装后 wheel 的 `sdk-office` 场景会将目标的完整载荷复制到私有临时目录,要求仅存在一个目标后端(使用已声明的原生目标,未声明原生目标时使用 WASM),并通过随附 SDK profile 使用该引擎转换一次 DOCX。它检查 kit 模块确实解析到迁移后的目录、转换报告预期后端,且输出包含 PDF 字节。wheel 校验会检查声明的引擎资源与原生辅助程序执行位;CI 上传已完成的 wheel,因此嵌套辅助程序权限保留在 ZIP 归档中。 +安装后 wheel 的 `sdk-office` 场景迁移轻量 wheel 载荷,并显式下载完整 npm Office sidecar 及其独立 Node。它要求仅有一个目标后端,验证 SDK 从下载的 sidecar 加载 kit,并通过 skill CLI 在空 PATH 下转换 DOCX。Python Office 操作使用同一 CLI。显式下载复用现有构建准备实现;SDK 启动不执行下载。[运行时参考](../../../../python/sdk-runtime/README.zh.md)拥有缓存和启用行为的说明。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.i18n.yaml index 3afca5571b..a5863d41c7 100644 --- a/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.i18n.yaml @@ -15,8 +15,8 @@ en: a1750de2d48551af zh: ffea0b75c292f186 /agent-note-client-shell-layering-and-dynamic-package-boundaries/decision/shared-module-requests: - en: cedddcca8a7bae6d - zh: a568a1130229f48c + en: 96661da17622132a + zh: dab3424093130c2f /agent-note-client-shell-layering-and-dynamic-package-boundaries/decision/parser-preloading-and-react-handoff: en: 4b503618b271fc28 zh: e2148f44efa769d2 diff --git a/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md b/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md index ad2bb26b84..68c9ef1235 100644 --- a/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md +++ b/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md @@ -32,7 +32,7 @@ The `staticLinked` preset leaves every bare specifier as an external import in ` ### Shared module requests -Dynamic browser bundles implicitly externalize the common baseline: `PLATFORM_MODULES` names shell-seeded React, Cordis, and static UI identities, while `PRELOADED_CLIENT_EXTERNALS` is reserved for a dynamic identity that must arrive before shell boot and is currently empty. A package uses `dsh.client.external` only for an exact non-baseline value request. Type-only imports are erased and create no request; permitted third-party implementation libraries remain private bundle contents. +Dynamic browser bundles implicitly externalize the common baseline: `PLATFORM_MODULES` names shell-seeded React, Cordis, selected third-party libraries, and static UI identities, while `PRELOADED_CLIENT_EXTERNALS` is reserved for a dynamic identity that must arrive before shell boot and is currently empty. A package uses `dsh.client.external` only for an exact non-baseline value request. Type-only imports are erased and create no request; non-baseline third-party implementation libraries remain private bundle contents. Shared third-party libraries use the shell's declared versions without per-plugin version negotiation; promotion into the baseline requires a measured reduction across the shell and shipped plugin bundles. A request has exactly two suppliers: diff --git a/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md b/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md index 70563962b3..79017823a7 100644 --- a/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md @@ -32,7 +32,7 @@ Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundl ### 共享模块请求 -动态浏览器 bundle 会隐式 external 统一基座:`PLATFORM_MODULES` 命名由外壳播种的 React、Cordis 和静态 UI 身份,`PRELOADED_CLIENT_EXTERNALS` 则为必须先于 shell 启动到达的动态身份预留,当前为空。包只在精确请求基座外实体时使用 `dsh.client.external`。纯类型 import 会被擦除,不产生请求;允许的第三方实现库保留为 bundle 私有内容。 +动态浏览器 bundle 会隐式 external 统一基座:`PLATFORM_MODULES` 命名由外壳播种的 React、Cordis、选定的第三方库和静态 UI 身份,`PRELOADED_CLIENT_EXTERNALS` 则为必须先于 shell 启动到达的动态身份预留,当前为空。包只在精确请求基座外实体时使用 `dsh.client.external`。纯类型 import 会被擦除,不产生请求;基座外的第三方实现库保留为 bundle 私有内容。共享第三方库使用外壳声明的版本,不为单个插件协商版本;只有测量确认外壳与随包插件 bundle 的总体积减少,才将库纳入基座。 请求只有两种提供方: 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 5e193c9063..ba50248d0c 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 @@ -21,8 +21,8 @@ en: 8ee88035cf64eeb3 zh: 6184e50fc4d47348 /agent-note-one-dsh-launcher-for-application-profiles/decision/python-runtime: - en: 5962abadfdece709 - zh: 5db20e96deed49d1 + en: 4b4cf5071b740c85 + zh: 595e5fe8127f9067 /agent-note-one-dsh-launcher-for-application-profiles/decision/enforcement: en: 9ce8d86b65064f38 zh: 27b8ad426cb319a2 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 7d4bb1a9c2..27a8315fe6 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 @@ -46,7 +46,7 @@ Direct SDK use follows normal Harness-home resolution: explicit `dshHome`, inher ### Python runtime -The Python runtime wheel stages [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the `dsh-python-runtime-closure` entry. Its ordinary branch calls the public CLI export; a provider-private selector dispatches to the internal subprocess runner before CLI parsing and is not an application entry point. The [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private dispatch. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable example under `python/sdk/examples` selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar, including the `web` profile. +The Python runtime wheel stages [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the `dsh-python-runtime-closure` entry. Its ordinary branch calls the public CLI export; a provider-private selector dispatches to the internal subprocess runner before CLI parsing and is not an application entry point. A private `DSH_RUNTIME_DOWNLOAD` selector prepares optional resources before application launch and is removed before spawning preparation subprocesses; ordinary CLI arguments do not select downloads. The [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private dispatch. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable example under `python/sdk/examples` selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar, including the `web` profile. 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. [docs/architecture.md](../../../../docs/architecture.md) owns this launch, and the [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows carrier. 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 eb7d5ff8eb..f9a50ebf5a 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 @@ -46,7 +46,7 @@ SDK 用户通过 profile 自定义插件。`dsh plugin --profile ...` 管 ### Python 运行时 -Python 运行时 wheel 将 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) 暂存为 `dsh-python-runtime-closure` 入口。其普通分支调用公开 CLI export;提供方私有选择会在 CLI 解析前分派到内部子进程 runner,而不是应用入口。[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责该私有分派。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;`python/sdk/examples` 下的可运行示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同的 profile 语法,包括 `web` profile。 +Python 运行时 wheel 将 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) 暂存为 `dsh-python-runtime-closure` 入口。其普通分支调用公开 CLI export;提供方私有选择会在 CLI 解析前分派到内部子进程 runner,而不是应用入口。私有 `DSH_RUNTIME_DOWNLOAD` 选择器在应用启动前准备可选资源,并在启动资源准备子进程前移除;普通 CLI 参数不选择下载。[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责该私有分派。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;`python/sdk/examples` 下的可运行示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同的 profile 语法,包括 `web` profile。 可执行文件族是 `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 启动别名。[docs/architecture.md](../../../../docs/architecture.zh.md)负责该启动方式,[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 载体。 diff --git a/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.i18n.yaml index 72e167ce59..f12b7ff463 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.i18n.yaml @@ -12,8 +12,8 @@ en: e01df5ba880c2972 zh: e62074a6a9ba5685 /agent-note-bundle-the-desktop-runtime-and-retain-external-plugins/package-ownership: - en: 37cae6b28fd359ef - zh: 63118e361fa8f84c + en: f683605bf4655db8 + zh: de09d610ee1d4193 /agent-note-bundle-the-desktop-runtime-and-retain-external-plugins/transactions-and-upgrades: en: 53d48f71fcc1c52e zh: 7d61923484dbec1c diff --git a/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.md b/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.md index f8f6599fe6..ccb70a68ab 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.md +++ b/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.md @@ -26,7 +26,7 @@ The resource descriptor records the exact release, Node version, platform, archi The [Desktop file policy](../../../../apps/desktop/scripts/runtime-file-policy.ts) applies after production npm installation and before native signing or descriptor generation. npm publication lists serve library consumers and can include declarations, maps, tests, and native build inputs; they do not identify the files needed by the Desktop process. The Desktop copy omits declarations and recognized source maps because Host execution uses JavaScript and generated Typert artifacts. The Host inherits the user environment. Published npm packages and external plugin directories retain their own files. Source debugger navigation is a development-package capability. -Package-specific exclusions remove Domino tests, fs-ext compilation outputs, Koffi's Windows import library, and non-target node-pty prebuilds and debug symbols. The policy retains native executable dependencies, node-pty's ConPTY source distribution, licenses, and unrecognized assets; broad `src`, `test`, `.ts`, or `.map` exclusions could remove executable code or runtime data. Copy tests preserve sentinel assets and seal the filtered inventory; the Electron [payload smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs) verifies PTY output, native file seeking, FFI, image conversion, and HTML parsing. Runtime preparation still verifies every retained byte and boots the complete Host with an external plugin. +Package-specific exclusions remove Domino tests, fs-ext compilation outputs, Koffi's Windows import library, and non-target node-pty prebuilds and debug symbols. The policy retains native executable dependencies, licenses, and unrecognized assets; node-pty's ConPTY source distribution is retained only for Windows targets; broad `src`, `test`, `.ts`, or `.map` exclusions could remove executable code or runtime data. Copy tests preserve sentinel assets and seal the filtered inventory; the Electron [payload smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs) verifies PTY output, native file seeking, FFI, image conversion, and HTML parsing. Runtime preparation still verifies every retained byte and boots the complete Host with an external plugin. The shared profile runner supplies missing installation and selected-bundle dependencies through the runtime resolution. pnpm-installed packages take precedence. Runtime resolution delegates to the selected package paths, so Host and plugin imports reaching the same export share its module instance. Distinct ESM and CommonJS conditional exports remain distinct entry points; runtime resolution does not merge a package’s dual implementations. diff --git a/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md b/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md index 6d14494858..c75bc1c411 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md @@ -26,7 +26,7 @@ Desktop 初始化时安装核心依赖图,会重复发布构建器已经完成 [桌面文件规则](../../../../apps/desktop/scripts/runtime-file-policy.ts)在生产 npm 依赖安装之后、原生签名或描述文件生成之前执行。npm 发布列表服务于库的使用者,可以包含声明、map、测试和原生构建输入,不能直接表示桌面进程需要哪些文件。桌面副本排除声明和已识别的 source map,因为 Host 执行 JavaScript 和生成的 Typert 产物。Host 继承用户环境。已发布的 npm 包和外部插件目录保留各自的文件。源码调试导航由开发包提供。 -包专用排除项包括 Domino 测试、fs-ext 编译产物、Koffi 的 Windows 导入库,以及非目标平台的 node-pty 预构建文件和调试符号。规则保留原生可执行依赖、node-pty 的 ConPTY 源分发内容、许可证和未知资源;宽泛排除 `src`、`test`、`.ts` 或 `.map` 可能移除可执行代码或运行时数据。复制测试保留哨兵资源并封存过滤后的清单;Electron 的[产物 smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs)验证 PTY 输出、原生文件定位、FFI、图像转换和 HTML 解析。运行时准备仍会验证每个保留字节,并携带外部插件启动完整 Host。 +包专用排除项包括 Domino 测试、fs-ext 编译产物、Koffi 的 Windows 导入库,以及非目标平台的 node-pty 预构建文件和调试符号。规则保留原生可执行依赖、许可证和未知资源;node-pty 的 ConPTY 源分发内容仅在 Windows 目标中保留;宽泛排除 `src`、`test`、`.ts` 或 `.map` 可能移除可执行代码或运行时数据。复制测试保留哨兵资源并封存过滤后的清单;Electron 的[产物 smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs)验证 PTY 输出、原生文件定位、FFI、图像转换和 HTML 解析。运行时准备仍会验证每个保留字节,并携带外部插件启动完整 Host。 共享 profile runner 通过 runtime resolution 补全安装包与选中 bundle 缺失的依赖。pnpm 安装的包优先。运行时解析委托给所选包路径,因此 Host 与插件导入同一导出时共享其模块实例。不同的 ESM 与 CommonJS 条件导出仍是不同入口;运行时解析不会合并包的双重实现。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.i18n.yaml new file mode 100644 index 0000000000..1fe1553059 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.i18n.yaml @@ -0,0 +1,22 @@ +# Bilingual-pair consistency record for 2026-09-15-authenticated-browser-host-preferences.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.md +/agent-note-share-host-preferences-with-every-authenticated-browser: + en: f4b8b4500f8dca9c + zh: bf27ded54fba703b +/agent-note-share-host-preferences-with-every-authenticated-browser/problem: + en: 22bf57b616581253 + zh: d1108a4042646e21 +/agent-note-share-host-preferences-with-every-authenticated-browser/decision: + en: 2d54784b8b3558cf + zh: 31190a9b284867b7 +/agent-note-share-host-preferences-with-every-authenticated-browser/alternatives-considered: + en: 299105745b084fc0 + zh: 8c2d82de44ac2aad +/agent-note-share-host-preferences-with-every-authenticated-browser/consequences: + en: 520028b8c4031009 + zh: 8c130624261538cc +/agent-note-share-host-preferences-with-every-authenticated-browser/testing: + en: e433f139fe72e69f + zh: 279cc8b1cd3d5377 diff --git a/.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.md b/.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.md new file mode 100644 index 0000000000..120903b88d --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.md @@ -0,0 +1,29 @@ +# Agent Note: Share Host preferences with every authenticated browser + +Status: implemented + +English | [中文](2026-09-15-authenticated-browser-host-preferences.zh.md) + +## Problem + +[Persist Web user preferences through Host settings](../../archived/bug-fix/2026-08-06-host-backed-web-preferences.md) chose the preference store by page locality, so a non-loopback page kept Appearance, Language, busy-Enter, and the welcome acknowledgement in a process-local copy. A trusted remote host, and a deployment published behind the configured public URL, therefore lost every choice on reload even though Connection authenticates the complete settings API. The second mode also split the shared runtime: theme, locale, the settings shell, the Models page, and the welcome notice each rendered a durable row that could never persist for a remote page. + +## Decision + +Locality selects nothing. One `settings.describe` mirror, one `ctx.configForms.get(entryId)` per namespace derived from it, and the `settings.mutate` write path serve every authenticated browser. Browser token authentication of the Connection API is the identity boundary; the fixed `remote.$host` locality facts are request-routing evidence that no store selector consults. The mirror's state space is `idle`, `loading`, and `ready`, a form snapshot carries no mode, and a form reports `unavailable` only for a namespace the Host did not register. + +The settings shell keeps one locality-dependent UI element: it builds its document controller from `ctx.remote.$host.isLoopback`, so only a loopback page renders **Open configuration file** and issues the `hasDocument` read behind it. The `settings/openSettingsDocument` Remote itself stays browser-authenticated like every other Host method; the omission is a presentation choice for pages that cannot see the Host's native editor, not a persistence rule. + +## Alternatives considered + +**Keep the memory-branch locality gate.** Rejected: browser token authentication already covers every Host method for remote pages, so the gate protected nothing, and locality is request-routing evidence rather than identity. It also made every Host-backed row inert on trusted remote and public deployments. + +## Consequences + +Every authenticated browser reaching one Host shares one preference document. Two browsers on the same deployment observe each other's Appearance, Language, busy-Enter, and welcome-acknowledgement choices, and a value written remotely is the value a loopback browser loads next. Preference access is governed by API authentication and `settings.describe.writable`. + +A remote page omits the **Open configuration file** action, the one locality-dependent UI element that remains. Concurrent writers on one document resolve through the namespace revision each form sends as `expectedRevision`. + +## Testing + +Focused unit coverage pins the contract: the ui-settings mirror and configuration-form suites construct both objects with no persistence selector, the theme apply spec persists from a non-loopback bench and asserts the Host write, the ui-settings-general apply spec keeps the shared describe read and the Host write path off-loopback while asserting the native document action stays unregistered, and the welcome store retains its acknowledgement across a settings-plugin recreation. The keyless Web settings scenario `apps/web/tests/remote-settings.e2e.ts` acknowledges the welcome notice, configures a model provider, and selects the dark Appearance from a trusted remote page, reloads after each, and re-asserts the welcome, model, and Appearance state from the Host settings document. diff --git a/.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.zh.md b/.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.zh.md new file mode 100644 index 0000000000..10aa7af847 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.zh.md @@ -0,0 +1,29 @@ +# Agent Note: 每个已认证浏览器共用 Host 偏好 + +Status: implemented + +[English](2026-09-15-authenticated-browser-host-preferences.md) | 中文 + +## 问题 + +[通过 Host settings 持久化 Web 用户偏好](../../archived/bug-fix/2026-08-06-host-backed-web-preferences.md)按页面本地性选择偏好存储,因此非回环页面把 Appearance、Language、繁忙态 Enter 与欢迎提示的确认留在一份进程内副本中。可信远程主机,以及配置了公共 URL 的部署,因而在重新加载时丢失全部选择,尽管 Connection 认证完整的 settings API。这套第二种模式也割裂了共享运行时:主题、语言、设置外壳、Models 页面与欢迎提示各自都在渲染一行对远程页面永远无法持久化的界面。 + +## 决策 + +本地性不选择任何东西。一个 `settings.describe` 镜像、由它派生的逐 namespace `ctx.configForms.get(entryId)`,以及 `settings.mutate` 写入路径服务每个已认证浏览器。Connection API 的浏览器令牌认证是身份边界;固定的 `remote.$host` 本地性事实只是请求路由的依据,没有任何存储选择器会读取它。镜像的状态空间只有 `idle`、`loading` 与 `ready`,表单快照不携带 mode,表单也只有在 Host 未注册该 namespace 时才报告 `unavailable`。 + +settings 外壳保留唯一一个依赖本地性的 UI 元素:它依据 `ctx.remote.$host.isLoopback` 构造文档控制器,因此只有回环页面会渲染**打开配置文件**并发出其背后的 `hasDocument` 读取。`settings/openSettingsDocument` Remote 本身与其他 Host 方法一样保持浏览器认证;省略该操作是面向看不到 Host 原生编辑器的页面的展示选择,而非持久化规则。 + +## 曾考虑的替代方案 + +**保留内存分支的本地性门禁。** 已否决:浏览器令牌认证已覆盖远程页面的每个 Host 方法,该门禁保护不了任何东西,而本地性只是请求路由的依据而非身份。它还会让受信远程与公共部署上每一行依赖 Host 的界面失效。 + +## 后果 + +每个访问同一 Host 的已认证浏览器共用一份偏好文档。同一部署上的两个浏览器会看到彼此选择的 Appearance、Language、繁忙态 Enter 与欢迎提示确认,远程写入的值就是回环浏览器下一次加载的值。偏好可访问性由 API 认证与 `settings.describe.writable` 决定。 + +远程页面省略**打开配置文件**操作,这是仍依赖本地性的唯一 UI 元素。同一文档上的并发写入由各表单作为 `expectedRevision` 发送的 namespace revision 裁决。 + +## 测试 + +聚焦的单元测试固定当前契约:ui-settings 的镜像与配置表单测试在构造这两个对象时不传持久化选择器,主题 apply 测试从非回环 bench 持久化并断言 Host 写入,ui-settings-general 的 apply 测试在非回环下保留共享 describe 读取与 Host 写入路径、同时断言原生文档操作保持未注册,欢迎 store 则在 settings 插件重建后保留其确认。无密钥 Web settings 场景 `apps/web/tests/remote-settings.e2e.ts` 从可信远程页面确认欢迎通知、配置一个模型提供商并选择深色 Appearance,每步之后重新加载,并从 Host settings 文档重新确认欢迎提示、模型与 Appearance 状态。 diff --git a/.agents/notes/implemented/feature/2026-08-28-auto-review.i18n.yaml b/.agents/notes/implemented/feature/2026-08-28-auto-review.i18n.yaml index f985dfc4a3..787ac9d839 100644 --- a/.agents/notes/implemented/feature/2026-08-28-auto-review.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-28-auto-review.i18n.yaml @@ -21,8 +21,8 @@ en: 237018634b5f9dcf zh: cbc49d9c1b794065 /agent-note-experimental-auto-review-before-each-tool-call/decision/process-catalog-and-children: - en: 6ebe6f90241fd322 - zh: eafdebc5277e83a1 + en: 0042c645360c8361 + zh: 5dba39466d376559 /agent-note-experimental-auto-review-before-each-tool-call/alternatives-considered: en: fe97128ed05b9e43 zh: 4aaa344933fa89b4 diff --git a/.agents/notes/implemented/feature/2026-08-28-auto-review.md b/.agents/notes/implemented/feature/2026-08-28-auto-review.md index c19e780d01..ff54f9d927 100644 --- a/.agents/notes/implemented/feature/2026-08-28-auto-review.md +++ b/.agents/notes/implemented/feature/2026-08-28-auto-review.md @@ -58,7 +58,7 @@ A persisted Auto Session cannot publish when the complete integration is absent The permission owner publishes one complete process catalog through generated `permissionPresets` Remote methods; the BFF explicitly mounts it and forwards a payload-free invalidation event. One browser directory subscribes before reading and serves both pickers. Epoch and connection-generation checks publish only the winning complete result. A winning failure or connection reset clears the previous snapshot; only a later existing read, notification, or reset retries. Disposed or stale settlements cannot publish. Each catalog invalidation dismisses the slash picker and its pending confirmation through the command owner while preserving the draft, so a picker waiting on its own read keeps its failure and retry state; reopening loads the current catalog. Session projection carries current selection only, so catalog installation or removal writes no Session event or sequence. -Auto appears with a superscript `EXP` badge. Both visible current-session pickers require the experimental confirmation; explicit `/permission auto` is already consent. The composer uses the generic Menu's existing portal placement to stay within the viewport while keeping its 218–360px bounds. The slash popup keeps `min(220px, 100%)` with `max-width: 100%`, including when a narrow composer collapses its trigger. +Auto appears with an `EXP` badge in picker options; the selected composer control omits it. Both visible current-session pickers require the experimental confirmation; explicit `/permission auto` is already consent. The composer uses the generic Menu's existing portal placement to stay within the viewport while keeping its 218–360px bounds. The slash popup keeps `min(220px, 100%)` with `max-width: 100%`, including when a narrow composer collapses its trigger. The [delegation-time policy capture](../../../../packages/subagent/subagent/README.md) records Auto or Full access before the first await and appends that existing preset event after fork seeding and sandbox/approval overrides. One-shot and continuable creation share the rule; cold resume reads only the child log. Later parent switches do not alter that child, while a later child switch can win. Read Only and Workspace Write retain sandbox inheritance plus `approval: never` and may therefore remain `custom`. Auto children classify each call independently using the existing lineage and messages, without parent call metadata, delegation records, receipts, Header/descriptor additions, or a Session-format change. Out-of-process children retain their own permission systems after the parent delegation is allowed. diff --git a/.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md b/.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md index bd99839132..a77aec0259 100644 --- a/.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md +++ b/.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md @@ -58,7 +58,7 @@ Reviewer 可以输出 reasoning blocks,随后恰好一个 JSON text block 和 Permission owner 通过生成的 `permissionPresets` Remote 方法发布一份完整进程目录;BFF 显式挂载它,并转发无 payload 的失效事件。一个浏览器目录在读取前订阅,为两个选择器提供数据。Epoch 与 connection-generation 检查只发布胜出的完整结果。胜出读取失败或 connection reset 会清空旧快照;只有后续既有读取、通知或 reset 才重试。已 dispose 或陈旧的结算不能发布。每次目录失效都通过命令 owner 关闭 slash 选择器与待确认对话框,同时保留草稿,因此等待自身读取的选择器仍保留失败与重试状态;重新打开时加载当前目录。Session 投影只携带当前选择,因此目录安装或移除不写 Session 事件或序号。 -Auto 带右上标 `EXP`。两个可见当前会话选择器都要求实验确认;显式 `/permission auto` 已构成同意。Composer 使用通用 Menu 既有 portal 定位保持在视口内,同时保留 218–360px 边界。Slash popup 保留 `min(220px, 100%)` 与 `max-width: 100%`,窄 composer 将 trigger 折叠时也一样。 +Auto 在选择器选项中带 `EXP` 标记;选中后的 composer 控件不显示该标记。两个可见当前会话选择器都要求实验确认;显式 `/permission auto` 已构成同意。Composer 使用通用 Menu 既有 portal 定位保持在视口内,同时保留 218–360px 边界。Slash popup 保留 `min(220px, 100%)` 与 `max-width: 100%`,窄 composer 将 trigger 折叠时也一样。 [委派时权限捕获](../../../../packages/subagent/subagent/README.zh.md)在首次 await 前记录 Auto 或 Full access,并在 fork seed 与 sandbox/approval override 之后追加该既有 preset event。单次与可继续创建共用此规则;cold resume 只读取 child 日志。后续 parent 切换不改变该 child,后续 child 自己的切换仍可胜出。Read Only 与 Workspace Write 保留 sandbox 继承加 `approval: never`,因此可能保持 `custom`。Auto child 使用既有 lineage 与消息独立分类每次调用,不增加父 call metadata、委派记录、receipt、Header/descriptor 字段或 Session format。进程外 child 在父委派获准后保留自己的权限系统。 diff --git a/.agents/skills/dsh-official-plugin-artwork/SKILL.md b/.agents/skills/dsh-official-plugin-artwork/SKILL.md new file mode 100644 index 0000000000..2336c7dbc5 --- /dev/null +++ b/.agents/skills/dsh-official-plugin-artwork/SKILL.md @@ -0,0 +1,37 @@ +--- +name: dsh-official-plugin-artwork +description: Draw and review official DeepSeek Harness plugin icons, including experimental plugins. Use for package icon.svg assets and PluginArtwork glyphs; covers semantic references, proportions, lettering, transparency, package metadata, and designer review. Not for toolbar or action icons. +--- + +# Official plugin artwork + +Draw recognizable artwork that belongs beside the existing official icons. Official and official experimental plugins share this visual language; the UI label communicates experimental status. This guidance does not replace designer approval. + +## Establish the subject + +- Inspect the actual feature, its output, and its owning manifest before drawing. A badge-producing skill may need a tag, not a medal. Reuse designer-supplied assets; do not invent a brand mark or change product names, identifiers, or unrelated icons without approval. +- Read the [existing artwork](../../../packages/client/ui-primitives/src/plugin-artwork.tsx) and the nearest relevant package SVGs. Borrow one coherent treatment, not every reference's effects. Action icons remain with [dsh-client-ui-ux](../dsh-client-ui-ux/SKILL.md#reuse-before-adding). +- Check every affected entry, list, detail, and component row. Bundle artwork and a component's name and icon can have separate metadata owners; changing one does not update the other. + +## Draw and refine + +1. **Start simple.** Use one dominant subject and only the detail needed to explain it. Related capabilities can share a metaphor; distinguish them through proportions, silhouette, or palette rather than extra panels, symbols, or badges. +2. **Judge optical size.** Use `width="36" height="36" viewBox="0 0 36 36"`. Center the artwork with deliberate margins; 18–22 units is a useful starting extent, not a requirement for every silhouette or wide text tag. The [page renderer](../../../packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx) owns display sizes and outer tile framing. Do not bake that tile into the SVG or enlarge a toolbar glyph to fill it. +3. **Keep geometry consistent.** Balance the main subject and secondary element. Align repeated marks and corners; keep tails and handles intentional. Refine one requested dimension at a time without changing approved parts. +4. **Use restrained color by default.** Balance saturation and brightness with neighboring icons; prefer existing palettes such as [Auto Authorization Review](../../../packages/experimental/auto-review/icon.svg) and [Codex subagent](../../../packages/subagent/subagent-codex/icon.svg). Reserve unusually vivid, bright, or neon colors for approved brand requirements, essential contrast, or explicit designer direction. Tune saturation separately from hue and lightness; do not make artwork gray or muddy merely to reduce saturation. Keep flat fills or short gradients and limited layering; avoid glow, drop shadows, glossy highlights, and emoji. Page CSS still uses semantic theme tokens. +5. **Align lettering by visible ink.** Use consistent letterforms, cap height, baseline, weight, and spacing. Compare rendered stroke edges, not just path coordinates: stroke caps and joins can make equal coordinates appear unequal. Export lettering as paths without a font dependency, and keep it readable at actual size. +6. **Make empty areas genuinely transparent.** A hollow lens must reveal the background, including where it overlaps another shape. Remove the underlying fill with a local mask or cutout; do not substitute a white or dark disk. Check both themes. + +## Export at the owner + +- Use the manifest's `icon` path and publish its resources. Update intentional bundle/component copies together; verify localized display names and descriptions when those are in scope. Do not change the feature's produced assets merely to change its plugin icon. +- Package images render through ``: export self-contained SVG geometry with local, uniquely named gradient, mask, and clip references. No scripts, event handlers, animation, external images/styles, fonts, or `foreignObject`. Inline React artwork needs per-instance IDs; separate image documents do not share IDs. +- Keep the asset change local. Do not add runtime dependencies, primitive exports, or renderer special cases just to display it. + +## Review and verify + +1. Show a few numbered alternatives before polishing uncertain geometry. Preserve their labels through review and apply the chosen version; an existing file or generated draft does not establish designer approval. Keep comparison artifacts in a gitignored directory, not the product package. +2. Compare old and new artwork beside unchanged references in light and dark themes, at actual row/card sizes as well as enlarged. Check balance, lettering, empty space, clipping, and image decoding. +3. Run `pnpm run verify-package-meta` for [package metadata and SVG artwork](../../../scripts/verify-package-meta.ts), `pnpm run constraints` for the canonical package payload, and `pnpm run verify-official-bundle-catalog`. Regenerate a stale catalog with `pnpm run gen-official-bundle-catalog`, never by hand. Structural checks do not replace visual review. +4. Build affected artifacts and inspect the real page through `dsh web`. Batch approved refinements before restarting a metadata-caching Host, then verify the entry and nested component rows in both themes. +5. Follow the repository's UI testing policy and [record-browser-gif](../record-browser-gif/SKILL.md) for a GUI PR. Obtain designer approval before landing new glyphs. Record reusable drawing principles here, not the history of rejected drafts. diff --git a/AGENTS.md b/AGENTS.md index c29ff51cff..b3dbc0482c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -150,6 +150,7 @@ Real-API tests/demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and - **Prefer symmetry for parallel values**; unexplained asymmetry usually signals a missed extraction. - **Tests describe behavior, not correctness.** Change obsolete behavior with its tests; explain why in the PR. - **Create Agent Notes only for durable decision rationale;** mechanical/local edits are exempt, including local UI changes ([scope](.agents/notes/README.md#when-to-write-one)). Archived notes are frozen: never edit or treat them as current authority ([archive policy](.agents/notes/README.md#archiving-and-deletion)). +- **Official plugin artwork:** use [dsh-official-plugin-artwork](.agents/skills/dsh-official-plugin-artwork/SKILL.md) for official and official experimental plugin icons; obtain designer approval before landing new glyphs. - **Client UI copy is locale-owned.** Route product text through typed dictionaries and `t` or localized primitive props; `verify-client-ui-i18n` rejects hardcoded copy ([decision](.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md)). - **Testing policy** — [docs/testing.md](docs/testing.md). Every non-trivial model- or product-user-visible change updates a keyless recorded-session snapshot; [snapshot ownership](snapshots/AGENTS.md) reserves the top-level tree for session-driven cases and keeps other expected output owner-local. Fixtures replay on macOS/Linux; fix fixtures, not normalizers. - **Design each tool's UI presentation up front.** Host presenters stay pure; Web cards derive from raw events and persisted result metadata ([cookbook](docs/cookbook/adding-a-tool.md)). diff --git a/apps/cli/package.json b/apps/cli/package.json index e7291abcc7..b841d617f0 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh", "description": "dsh CLI: profile launch, plugin management, and configuration inspection", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/apps/desktop-host/package.json b/apps/desktop-host/package.json index f2a75bfa51..c442c84f56 100644 --- a/apps/desktop-host/package.json +++ b/apps/desktop-host/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-desktop-host", "description": "Private Node-mode host process for the Electron desktop application", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "private": true, "license": "MIT", "type": "module", @@ -11,7 +11,7 @@ "lib/cli.js" ], "dependencies": { - "koffi": "^3.1.0", + "koffi": "3.1.1", "@deepseek-ai/libreoffice-kit": "^0.1.5", "@deepseek-ai/cordis": "workspace:~", "@deepseek-ai/dsh": "workspace:*", diff --git a/apps/desktop-host/src/cli.ts b/apps/desktop-host/src/cli.ts index 97a23b88a2..6c890f8990 100644 --- a/apps/desktop-host/src/cli.ts +++ b/apps/desktop-host/src/cli.ts @@ -7,7 +7,7 @@ import { installOfficeEngineResolution, runtimeArchivePath } from './office-engi /** * Run the ordinary CLI with Desktop's bundled package manager and reserved-profile plugin access. * @param runtimeDir - Prepared or ASAR-contained production DSH package tree. - * @param supportDir - Physical Desktop runtime directory containing pnpm. + * @param supportDir - Physical Desktop runtime directory containing the primary-runtime payload and Node launchers. * @returns Completion of the selected CLI command; profile plugins own their process lifetime. */ export async function runDesktopCli(runtimeDir: string, supportDir: string): Promise { @@ -16,7 +16,7 @@ export async function runDesktopCli(runtimeDir: string, supportDir: string): Pro manageDesktopProfile: true, packageManager: { command: process.execPath, - args: ['--expose-internals', join(supportDir, 'pnpm', 'bin', 'pnpm.mjs')], + args: ['--expose-internals', join(supportDir, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')], env: { ELECTRON_RUN_AS_NODE: '1', DSH_DESKTOP_NODE_EXECUTABLE: process.execPath, diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 2d82f686ea..f8979dccb9 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/desktop/README.md /deepseek-harness-desktop: - en: 001b7d11c237078b - zh: eda6b580dc224c7f + en: 5b8518fd5e0030ef + zh: 02efee59c39647dc /deepseek-harness-desktop/terminal-command: en: bef92953c237363f zh: d21d6effd21b8c00 @@ -15,8 +15,8 @@ en: 069fdfd72d2ea26a zh: 76ae549d43c99bb4 /deepseek-harness-desktop/key-technical-decisions/bundled-workspace-dependencies: - en: 0f9b6c1d6761d108 - zh: 77127063f244c361 + en: 2b049b4f104be1c2 + zh: 1e8f3a9f7eb15e2b /deepseek-harness-desktop/bundled-command-runtime: en: 09f0b75af9f0ed14 zh: 60bbc2e67d5e87a8 @@ -51,8 +51,8 @@ en: 5f05072ec82cc62d zh: a3d63b58d1ef2db7 /deepseek-harness-desktop/package/runtime-file-selection: - en: e7c1eb8e78fe46e3 - zh: b7ea497dd75be524 + en: 4c83b25ef03d0c1b + zh: 6c97d8f485e2d6d0 /deepseek-harness-desktop/package/upload-updates: en: f48baa680e841b0d zh: 25ded1aeaac6dad1 @@ -66,8 +66,8 @@ en: 64836513eb7eda6e zh: 6ebb9184a7aa51de /deepseek-harness-desktop/package/desktop-runtime-preparation: - en: 32ef30fc38462109 - zh: 093b1b57d084f5bb + en: 9a71990bee9867a7 + zh: 76c5c1fdff6e6908 /deepseek-harness-desktop/updates: en: 41d0f50eb7173342 zh: ce940ef3cbcc6fa5 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index c9d14c4511..e60aa3fa46 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -12,7 +12,7 @@ The first application-menu command, **About DeepSeek Harness**, opens Electron's Desktop’s local native directory flow opens an Electron folder dialog attached to the application window, restoring, showing, and focusing that window first. Concurrent requests share one dialog; cancellation returns no path and failures remain retryable. Ordinary Web uses the Host chooser. Browse mode lists Host directories. On Linux without zenity or kdialog, automatic selection uses browse instead of the Electron dialog. -Creator and the Web Plugin Manager use Desktop’s bundled pnpm under Electron Node mode without requiring pnpm on PATH. The private Node launcher environment applies only to package operations. +Desktop ships one pnpm distribution at `resources/runtime/primary-runtime/dependencies/pnpm`. Build-time production installation, the installed `dsh` CLI, Creator and the Web Plugin Manager execute its `bin/pnpm.mjs` with Electron Node mode; package operations do not require pnpm on PATH. The private Node launcher environment applies only to package operations. Agent workspace dependencies copy the same self-contained primary-runtime payload into the Harness home and execute pnpm with its standalone Node. Embedded Platform documents use a persistent WebContentsView partition keyed by a hash of the Platform origin and stable account ID. localStorage preferences, including dismissed notices, survive view closure and application restart; different accounts and origins use separate storage. The Host supplies the account ID from its latest successful profile read; while no ID is known, the document opens in a disposable partition that keeps no preference across opens. Opening a persistent partition clears cookies, filesystem, IndexedDB, Cache Storage, the HTTP and shader caches, service workers, and HTTP authentication before the document loads; a disposable partition clears all of its storage. Closing a view destroys the document, removes its request interceptors, and schedules the same cleanup, so authentication left by an unclean exit is cleared before the next document loads. The next open and application shutdown await that cleanup, and an update install waits for it before the installer takes over the exit. A failed cleanup rejects that open and blocks the same account until a later cleanup succeeds; other accounts keep opening. Sign-out destroys the document but retains account-scoped preferences for the next sign-in. A document already open in a disposable partition stays mounted when the account ID arrives for the same credential; the next open uses the account-scoped partition. The [storage decision](../../.agents/notes/implemented/architecture/2026-09-22-platform-browser-storage.md) describes the retention policy. Host sends account credentials through private Node IPC; account RPC and the Harness renderer receive no token. Before page scripts execute, the Platform preload performs one synchronous IPC to read the prepared main-process credential. It exposes displayMode, synchronous getAuthToken() and getLocale() getters, and onLocaleChange(listener), which returns an unsubscribe function. Both getters read preload memory without further IPC. The bootstrap includes the resolved Desktop language (`zh_CN` or `en_US`); Settings language changes update the preload cache and notify the open Platform document without reloading. Platform applies this language before its first render and does not persist it as a browser preference. The main-process handler performs only sender validation and an in-memory read; it never waits for Host, disk, or network. Trusted-page initialization failures retain embedded mode and make the getter throw, preventing browser-credential fallback. Only the owned Platform main frame on the configured issuer origin can initialize. Sign-out, credential replacement, Host shutdown, and view closure destroy the document. Cross-origin document navigation is blocked. HTTPS links requesting a new window open in the system browser without the embedded session or token; other schemes and URL credentials are rejected. Native views occupy the viewport below the Account feature’s return bar. @@ -72,6 +72,8 @@ The private Desktop `runtime/bin` directory is added only to package-installatio Node prepares the bundled interpreters and Python libraries without a system Python or pip. [The download lock](../../scripts/primary-runtime/lock.json) pins interpreter archives, Python distribution versions and target-specific wheel URLs and hashes; the shared builder resolves pnpm from the root development dependency pin. The root package-manager version and the Desktop pin are checked for agreement. Wheel filenames and distribution versions must agree for every target. Preserve key order within the selected target, wheel records and distribution map, plus wheel-entry order; these affect payload identity, while top-level lock key order does not. Library wheels unpack into site-packages, retaining auxiliary files under each wheel's `.data/scripts` directory without generating command wrappers. Other installation schemes are rejected. Native-target checks verify the locked wheel set and versions, permit the interpreter's bundled pip, and check the Python version, Office document read/write operations and dependency completeness without writing bytecode after staging cleanup and again after macOS signing. The standalone Node executable receives the JIT entitlement required by V8; the x64 target also receives `com.apple.security.cs.allow-unsigned-executable-memory` for executable code pages, including under Rosetta. ARM64 Node and Office helpers retain JIT-only permissions. Cross-target execution and signed installation require the target release host. Both `dev:desktop` and `start:desktop` prepare `.desktop-build/targets//runtime/primary-runtime` before launching Electron; first use may download locked dependencies. An unfinished preparation cannot report a successful launcher exit. +The shared Desktop/SDK runtime builder omits directories named `tests` beneath NumPy and pandas before copying or signing the payload, and removes their rows from each distribution’s `RECORD`. It retains `numpy.testing`, `pandas.testing`, `pandas._testing`, native extensions and licenses. Complete upstream suites are unavailable; native checks verify the retained testing APIs, absence of test directories and existence of recorded files. + | Decision | Why | Direct consequence | |---|---|---| | Release identity | The shell API, Web client, backend, and plugin graph are qualified as one combination; independent versions would create untested combinations and ambiguous update availability. | Electron and `@deepseek-ai/dsh` always have the same exact version. A dsh upgrade is a Desktop release, even when the shell code is unchanged. | @@ -272,7 +274,9 @@ The served `time` is the real packing time, so pnpm 11's default 24-hour `minimu ### Runtime file selection -Desktop packs workspace packages locally and installs external dependencies through the target's bundled Node and pnpm. [Desktop's file policy](scripts/runtime-file-policy.ts) then filters the immutable `resources/app.asar/dsh/node_modules` copy before signing and integrity sealing. It omits TypeScript declarations, recognized JavaScript/CSS/TypeScript source maps, TypeScript build caches, Domino's test directory, selected native compiler outputs, and node-pty prebuilds for other platforms. It preserves runtime JavaScript, native modules and their DLL/EXE helpers, WASM, unknown assets, licenses, and notices. Dependency manifests pass through electron-builder's metadata cleanup before integrity sealing, so archiving preserves their recorded bytes. The policy does not alter npm tarballs, the bundled package manager, or user-installed plugin files. +Desktop packs workspace packages locally and installs external dependencies through the target's bundled Node and pnpm. [Desktop's file policy](scripts/runtime-file-policy.ts) then filters the immutable `resources/app.asar/dsh/node_modules` copy before signing and integrity sealing. It omits TypeScript declarations, recognized JavaScript/CSS/TypeScript source maps, TypeScript build caches, Domino's test directory, selected native compiler outputs, and node-pty prebuilds for other platforms. Non-Windows targets also omit `node-pty/third_party/conpty`; Windows targets retain that directory. It preserves runtime JavaScript, target native modules and their DLL/EXE helpers, WASM, unknown assets, licenses, and notices. Dependency manifests pass through electron-builder's metadata cleanup before integrity sealing, so archiving preserves their recorded bytes. The policy does not alter npm tarballs, the bundled package manager, or user-installed plugin files. + +The [Electron shell file list](scripts/electron-builder-config.mjs) excludes recognized JavaScript/CSS/TypeScript source maps and TypeScript build caches from the separate `resources/app.asar/node_modules` dependency collection. These exclusions preserve runtime code, native modules, WASM, data files (including ordinary `.map` assets), licenses, and notices; [shell archive tests](tests/shell-file-selection.spec.ts) check retained bytes and exclusions. The [Office conversion provider](../../packages/document/office-to-pdf/README.md) carries the target’s declared native engine, or WASM when the kit declares no matching native target. Preparation rejects a missing target engine before packaging. The complete Office dependencies, including CLI, JavaScript libraries, and the selected engine’s executables, data, licenses, and notices, are unpacked under `resources/app.asar.unpacked/dsh/node_modules/`. The Desktop Host resolves engine manifests to these physical directories and supplies loaded skills with absolute standalone Node and unpacked CLI paths. Node lives in `resources/runtime/primary-runtime/dependencies/node/bin/`; the CLI is the unpacked `@deepseek-ai/libreoffice-kit/lib/cli.js`. On macOS, the native helper receives the JIT entitlement required by [LibreOffice’s UNO bridge](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in). @@ -443,7 +447,7 @@ Desktop [patch policy](scripts/runtime-patch-policy.ts) explicitly classifies ev Desktop dependency overrides are declared separately in the patch-policy module, not inferred from patch versions. The pi-ai constraint keeps its runtime version at the version qualified by the shared patch. Desktop still resolves a fresh runtime lockfile for each build, then installs with `--frozen-lockfile`; dependency resolution across separate builds is not yet reproducible. CLI packed-installation tests and other distribution flows retain their own configuration. -Every package command builds the repository, packs the first-party production closures rooted at dsh and the private Desktop Host, and prepares the target Electron distribution and pnpm CLI. `prepare:dsh` installs the production graph once at build time, prepares materialized packages for electron-builder to archive under `app.asar/dsh`, removes package-manager metadata, and writes `desktop-runtime.json` with shared package versions and final file hashes. Signed macOS builds sign and verify native files before inventory generation; electron-builder excludes this already-signed tree from nested re-signing. Resource mappings explicitly include `dsh/node_modules`, which the default root-directory filter omits; the prepared runtime inventory is checked after native signing. Native executables and libraries are unpacked beside ASAR; Python, standalone Node and pnpm remain in external runtime resources. Windows packaging checks every prepared PE against its ASAR unpacked entry and byte-identical disk copy, including unsigned builds. Builder glob rules match braces in PE filenames as single-character wildcards, so a matching neighboring file may also be unpacked. Prepared runtime smoke uses the verified target descriptor rather than the build host architecture. Signed installer, notarization, installed upgrade, and target-specific native-module qualification require the release environment. +Every package command builds the repository, packs the first-party production closures rooted at dsh and the private Desktop Host, and prepares the target Electron distribution plus primary-runtime with its shared pnpm CLI. `prepare:dsh` installs the production graph once at build time, prepares materialized packages for electron-builder to archive under `app.asar/dsh`, removes package-manager metadata, and writes `desktop-runtime.json` with shared package versions and final file hashes. Signed macOS builds sign and verify native files before inventory generation; electron-builder excludes this already-signed tree from nested re-signing. Resource mappings explicitly include `dsh/node_modules`, which the default root-directory filter omits; the prepared runtime inventory is checked after native signing. Native executables and libraries are unpacked beside ASAR; Python, standalone Node and pnpm remain in external runtime resources. Windows packaging checks every prepared PE against its ASAR unpacked entry and byte-identical disk copy, including unsigned builds. Builder glob rules match braces in PE filenames as single-character wildcards, so a matching neighboring file may also be unpacked. Prepared runtime smoke uses the verified target descriptor rather than the build host architecture. Signed installer, notarization, installed upgrade, and target-specific native-module qualification require the release environment. Signed macOS packaging writes `Contents/Resources/app-update.yml` while assembling the App and before code signing, including the directory build that feeds the parallel ZIP and DMG lanes. The signing hook verifies the exact feed and updater cache directory. Both lane copies and the promoted App are checked again before the release completion record is written; a missing or mismatched configuration prevents artifact promotion and therefore prevents upload. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index d5c52ebc1b..8379409ebf 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -12,7 +12,7 @@ Desktop 绑定 `127.0.0.1`,即 Electron 为就绪 URL 与 WebSocket 凭据过 Desktop 的本地原生目录流程打开绑定应用窗口的 Electron 文件夹对话框,并先恢复、显示和聚焦该窗口。并发请求共用一个对话框;取消不返回路径,失败后可以重试。普通 Web 使用 Host 选择器。浏览模式列出 Host 目录。Linux 缺少 zenity 或 kdialog 时,自动选择使用浏览模式,不使用 Electron 对话框。 -Creator 和 Web Plugin Manager 在 Electron Node 模式下使用 Desktop 内置 pnpm,无需 PATH 中存在 pnpm。私有 Node 启动器环境仅应用于包操作。 +Desktop 在 `resources/runtime/primary-runtime/dependencies/pnpm` 中携带一份 pnpm 分发包。构建时的生产依赖安装、安装版 `dsh` CLI、Creator 和 Web 插件管理器通过 Electron Node 模式执行其中的 `bin/pnpm.mjs`;包操作不要求 PATH 上存在 pnpm。私有 Node 启动器环境仅用于包操作。agent(智能体)的工作区依赖将同一份完整 primary-runtime 复制到 Harness 主目录,并使用其中的独立 Node 执行 pnpm。 Platform 内嵌文档使用持久化 WebContentsView 分区,分区名由 Platform 来源和稳定账号 ID 的哈希决定。localStorage 中的页面偏好(包括已关闭的通知)在关闭视图和重启应用后保留;不同账号和来源使用独立存储。账号 ID 来自 Host 最近一次成功的资料读取;尚无该 ID 时,文档在一次性分区中打开,该分区不跨次保留偏好。打开持久分区会先清理 Cookie、文件系统、IndexedDB、Cache Storage、HTTP 与着色器缓存、Service Worker 及 HTTP 认证状态;一次性分区则清理其全部存储。关闭视图会销毁文档、移除请求拦截器并安排相同的清理,因此异常退出遗留的认证会在下一个文档加载前被清除。下次打开和应用退出都会等待该清理完成,更新安装也会在安装器接管退出前等待。清理失败会使该次打开失败,并在后续清理成功前阻止同一账号打开;其他账号不受影响。退出登录会销毁文档,但保留账号偏好供下次登录使用。同一凭证下账号 ID 迟到时,已以一次性分区打开的文档保持挂载;下次打开使用账号分区。[存储决策](../../.agents/notes/implemented/architecture/2026-09-22-platform-browser-storage.zh.md)说明保留策略。Host 通过私有 Node IPC 发送账号凭证;账号 RPC 和 Harness 渲染进程不接收 token。Platform preload 在页面脚本执行前通过一次同步 IPC 读取主进程中已准备的凭证。它暴露 displayMode、同步的 getAuthToken() 和 getLocale() getter,以及返回取消订阅函数的 onLocaleChange(listener)。两个 getter 都只读取 preload 内存,不再调用 IPC。bootstrap 包含 Desktop 已解析的语言(`zh_CN` 或 `en_US`);Settings 语言变更会更新 preload 缓存并通知已打开的 Platform 文档,无需重载。Platform 在首屏渲染前应用该语言,且不将其持久化为浏览器偏好。主进程处理器仅校验调用来源并读取内存,不等待 Host、磁盘或网络。可信页面初始化失败时保留内嵌模式,由 getter 抛错,避免回退到浏览器凭证。只有受控 Platform 页面中、位于所配置签发来源的主 frame 能完成初始化。退登、凭证替换、Host 关闭及视图关闭都会销毁文档。跨来源文档导航被阻止。请求新窗口的 HTTPS 链接在系统浏览器中打开,不携带内嵌会话或 token;其他协议及带 URL 凭证的链接被拒绝。原生视图占据 Account 功能返回栏下方的视口。 @@ -74,6 +74,8 @@ Desktop 私有的 `runtime/bin` 目录仅添加到包安装进程,不进入 PT Node 准备内置解释器和 Python 库,无需系统 Python 或 pip。[下载锁](../../scripts/primary-runtime/lock.json)固定解释器压缩包、Python 分发包版本及目标平台 wheel 的 URL 和哈希;共享构建器从根开发依赖中解析 pnpm 固定版本。测试检查根 package-manager 版本和 Desktop 固定版本保持一致。每个目标的 wheel 文件名必须与分发包版本一致。所选目标、wheel 记录及分发包映射内部的键顺序,以及 wheel 条目顺序都会影响产物身份,编辑时须保留;锁文件顶层键的顺序不影响该身份。库 wheel 解压到 site-packages,各 wheel 的 `.data/scripts` 目录保留辅助文件,不生成命令行包装器。其他安装方案会被拒绝。本机目标检查在清理暂存目录后以及 macOS 签名后验证锁定 wheel 的集合与版本,允许解释器自带的 pip,并检查 Python 版本、Office 文档读写和依赖完整性,不写入字节码。独立 Node 可执行文件获得 V8 所需的 JIT 权限;x64 目标还获得 `com.apple.security.cs.allow-unsigned-executable-memory`,允许可执行代码页,包括在 Rosetta 下运行时。ARM64 Node 和 Office 辅助程序保留仅 JIT 权限。跨目标执行和签名安装需要对应的发布主机。`dev:desktop` 和 `start:desktop` 都会在启动 Electron 前准备 `.desktop-build/targets//runtime/primary-runtime`;首次准备可能需要下载锁定的依赖。准备未完成时,启动命令不能报告成功退出。 +Desktop/SDK 共享运行时构建器在复制或签名产物前,裁剪 NumPy 和 pandas 下名为 `tests` 的目录,并移除各分发包 `RECORD` 中对应的记录。它保留 `numpy.testing`、`pandas.testing`、`pandas._testing`、原生扩展和许可证。完整上游测试套件不可用;本机检查验证保留的 testing API、测试目录已移除,以及清单所列文件存在。 + | 决策 | 原因 | 直接结果 | |---|---|---| | 发布身份 | 桌面壳 API、Web 客户端、后端与插件依赖图作为一个组合完成验证;独立版本会产生未经验证的组合,并让更新可用性含糊不清。 | Electron 与 `@deepseek-ai/dsh` 始终使用同一精确版本。即使桌面壳代码不变,升级 dsh 也必须发布新 Desktop 版本。 | @@ -274,7 +276,9 @@ pnpm run dev:bundle-registry ### 运行时文件筛选 -Desktop 在本地打包工作区包,并通过目标捆绑的 Node 和 pnpm 安装外部依赖。[Desktop 文件策略](scripts/runtime-file-policy.ts)随后在签名和完整性封装前过滤不可变的 `resources/app.asar/dsh/node_modules` 副本。它排除 TypeScript 声明、已识别的 JavaScript/CSS/TypeScript source map、TypeScript 构建缓存、Domino 测试目录、选定的原生编译器输出和其他平台的 node-pty 预构建文件。它保留运行时 JavaScript、原生模块及其 DLL/EXE 辅助文件、WASM、未知资源、许可证和 notices。依赖清单在完整性封装前经过 electron-builder 的元数据清理,确保归档保持已记录的字节。该策略不修改 npm tarball、捆绑的包管理器或用户安装的插件文件。 +Desktop 在本地打包工作区包,并通过目标捆绑的 Node 和 pnpm 安装外部依赖。[Desktop 文件策略](scripts/runtime-file-policy.ts)随后在签名和完整性封装前过滤不可变的 `resources/app.asar/dsh/node_modules` 副本。它排除 TypeScript 声明、已识别的 JavaScript/CSS/TypeScript source map、TypeScript 构建缓存、Domino 测试目录、选定的原生编译器输出和其他平台的 node-pty 预构建文件。非 Windows 目标还会排除 `node-pty/third_party/conpty`;Windows 目标保留该目录。它保留运行时 JavaScript、目标平台的原生模块及其 DLL/EXE 辅助文件、WASM、未知资源、许可证和 notices。依赖清单在完整性封装前经过 electron-builder 的元数据清理,确保归档保持已记录的字节。该策略不修改 npm tarball、捆绑的包管理器或用户安装的插件文件。 + +[Electron 外壳文件清单](scripts/electron-builder-config.mjs)从单独收集的 `resources/app.asar/node_modules` 依赖中排除已识别的 JavaScript/CSS/TypeScript source map 和 TypeScript 构建缓存。这些排除规则保留运行时代码、原生模块、WASM、数据文件(包括普通 `.map` 资源)、许可证和 notices;[外壳归档测试](tests/shell-file-selection.spec.ts)检查保留文件的字节和排除项。 [Office 转换提供方](../../packages/document/office-to-pdf/README.zh.md)携带目标已声明的原生引擎;kit 未声明匹配原生目标时携带 WASM 引擎。准备阶段在打包前拒绝缺少目标引擎的情况。完整 Office 依赖(CLI、JavaScript 库和选定引擎的可执行文件、数据、许可证及 notices)解包到 `resources/app.asar.unpacked/dsh/node_modules/` 下。Desktop Host 将引擎清单解析到这些物理目录,并向加载的技能提供独立 Node 和解包后 CLI 的绝对路径。Node 位于 `resources/runtime/primary-runtime/dependencies/node/bin/`;CLI 位于解包后的 `@deepseek-ai/libreoffice-kit/lib/cli.js`。macOS 上的原生辅助程序获得 [LibreOffice UNO 桥](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in)所需的 JIT entitlement。 @@ -445,7 +449,7 @@ Desktop [补丁策略](scripts/runtime-patch-policy.ts) 将每份工作区补丁 Desktop 依赖 overrides 在补丁策略模块中单独声明,不从补丁版本推导。pi-ai 约束使运行时版本保持在共享补丁已验证的版本。Desktop 仍会在每次构建时重新解析运行时锁文件,再通过 `--frozen-lockfile` 安装;不同构建之间的依赖解析尚不保证可复现。CLI 打包安装测试及其他交付流程保留各自的配置。 -每条打包命令都会构建仓库,打包以 dsh 和私有 Desktop Host 为根的第一方生产依赖闭包,并准备目标专用的 Electron 分发包与 pnpm CLI。`prepare:dsh` 在构建时安装一次生产依赖图,准备物化包供 electron-builder 归档到 `app.asar/dsh`,移除包管理器元数据,并生成包含共享包版本和最终文件哈希的 `desktop-runtime.json`。macOS 签名构建先签名并验证原生文件,再生成清单;electron-builder 不对已签名的此目录重复进行嵌套签名。资源映射明确包含默认根目录过滤器会忽略的 `dsh/node_modules`;准备完成的运行时清单在原生签名后检查。原生可执行文件及库解包到 ASAR 旁;Python、独立 Node 和 pnpm 保留在外部 runtime 资源中。Windows 打包逐项检查准备好的 PE,确认其 ASAR 条目已标记为解包,且磁盘副本字节一致;未签名构建也执行此检查。Builder glob 规则用单字符通配符匹配 PE 文件名中的花括号,因此同目录中名称匹配的文件也可能被解包。准备好的运行时 smoke 沿用已验证的目标描述符,不使用构建宿主的架构。签名安装包、公证、已安装应用升级和各目标原生模块的验收需要发布环境。 +每条打包命令都会构建仓库,打包以 dsh 和私有 Desktop Host 为根的第一方生产依赖闭包,并准备目标专用的 Electron 分发包和包含共享 pnpm CLI 的 primary-runtime。`prepare:dsh` 在构建时安装一次生产依赖图,准备物化包供 electron-builder 归档到 `app.asar/dsh`,移除包管理器元数据,并生成包含共享包版本和最终文件哈希的 `desktop-runtime.json`。macOS 签名构建先签名并验证原生文件,再生成清单;electron-builder 不对已签名的此目录重复进行嵌套签名。资源映射明确包含默认根目录过滤器会忽略的 `dsh/node_modules`;准备完成的运行时清单在原生签名后检查。原生可执行文件及库解包到 ASAR 旁;Python、独立 Node 和 pnpm 保留在外部 runtime 资源中。Windows 打包逐项检查准备好的 PE,确认其 ASAR 条目已标记为解包,且磁盘副本字节一致;未签名构建也执行此检查。Builder glob 规则用单字符通配符匹配 PE 文件名中的花括号,因此同目录中名称匹配的文件也可能被解包。准备好的运行时 smoke 沿用已验证的目标描述符,不使用构建宿主的架构。签名安装包、公证、已安装应用升级和各目标原生模块的验收需要发布环境。 macOS 签名打包在组装 App 时、代码签名前写入 `Contents/Resources/app-update.yml`,供并行 ZIP 与 DMG 路线使用的目录构建也执行此操作。签名钩子验证准确的更新源和 updater 缓存目录。写入发布完成记录前,流程会再次检查两条路线的副本和最终移入的 App;配置缺失或不匹配会阻止移入产物,因而也会阻止上传。 diff --git a/apps/desktop/electron-builder.config.d.mts b/apps/desktop/electron-builder.config.d.mts index 4e978ec00f..a4d0739791 100644 --- a/apps/desktop/electron-builder.config.d.mts +++ b/apps/desktop/electron-builder.config.d.mts @@ -9,10 +9,7 @@ export interface DesktopElectronBuilderConfig { readonly output: string } readonly files: readonly [ - string, - string, - string, - string, + ...string[], { readonly from: string, readonly to: 'dsh', readonly filter: readonly ['**/*'] }, { readonly from: string, readonly to: 'dsh/node_modules', readonly filter: readonly ['**/*'] }, ] diff --git a/apps/desktop/package.json b/apps/desktop/package.json index fb8cee6fdd..563f60a858 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-desktop", "description": "Electron desktop shell for a bundled dsh runtime and external plugins", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "private": true, "license": "MIT", "type": "module", @@ -68,7 +68,7 @@ "js-yaml": "^4.3.2", "koffi": "3.1.1", "pnpm": "11.28.5", - "sharp": "^0.35.5", + "sharp": "0.35.5", "typescript": "^6.0.3", "@deepseek-ai/dsh-deepseek-account": "workspace:*", "@types/ws": "^8.18.1", diff --git a/apps/desktop/scripts/electron-builder-config.mjs b/apps/desktop/scripts/electron-builder-config.mjs index f08b783865..747d20d841 100644 --- a/apps/desktop/scripts/electron-builder-config.mjs +++ b/apps/desktop/scripts/electron-builder-config.mjs @@ -135,6 +135,8 @@ export function createElectronBuilderConfig( 'lib/preload-welcome.cjs', 'renderer/**/*', 'package.json', + '!node_modules/**/*.{js,cjs,mjs,ts,cts,mts,css}.map', + '!node_modules/**/*.tsbuildinfo', { from: buildPaths.dsh, to: 'dsh', filter: ['**/*'] }, // electron-builder excludes a source directory's root node_modules. { from: join(buildPaths.dsh, 'node_modules'), to: 'dsh/node_modules', filter: ['**/*'] }, diff --git a/apps/desktop/scripts/installed-update-builder.ts b/apps/desktop/scripts/installed-update-builder.ts index 2f0cbefe6c..42c665c430 100644 --- a/apps/desktop/scripts/installed-update-builder.ts +++ b/apps/desktop/scripts/installed-update-builder.ts @@ -31,6 +31,7 @@ export async function createInstalledUpdateBuilderConfig(manifest: string, versi files: [ { from: application, to: '.', filter: ['lib/*.js', 'lib/*.cjs', 'renderer/**/*', 'qualification-bootstrap.mjs', 'installed-update-identity.mjs'] }, 'package.json', + ...config.files.filter((file): file is string => typeof file === 'string' && file.startsWith('!')), { from: dsh, to: 'dsh', filter: ['**/*'] }, { from: join(dsh, 'node_modules'), to: 'dsh/node_modules', filter: ['**/*'] }, ], diff --git a/apps/desktop/scripts/prepare-dsh.ts b/apps/desktop/scripts/prepare-dsh.ts index e40cac98c4..7d290b945a 100644 --- a/apps/desktop/scripts/prepare-dsh.ts +++ b/apps/desktop/scripts/prepare-dsh.ts @@ -43,7 +43,7 @@ const RUNTIME_ROOT = BUILD_PATHS.runtime const PNPM_BUILD_STATE = BUILD_PATHS.dshPnpm const PACKAGE_SET_ROOT = BUILD_PATHS.packageSet const NODE = join(BUILD_PATHS.electron, process.platform === 'win32' ? 'electron.exe' : 'Electron.app/Contents/MacOS/Electron') -const PNPM = join(RUNTIME_ROOT, 'pnpm', 'bin', 'pnpm.mjs') +const PNPM = join(RUNTIME_ROOT, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs') function manifestVersion(path: string, subject: string): string { const manifest = JSON.parse(readFileSync(path, 'utf8')) as { version?: unknown } diff --git a/apps/desktop/scripts/prepare-runtime.ts b/apps/desktop/scripts/prepare-runtime.ts index 1b88f091bc..ba92fef0e6 100644 --- a/apps/desktop/scripts/prepare-runtime.ts +++ b/apps/desktop/scripts/prepare-runtime.ts @@ -1,14 +1,15 @@ -/** Prepare the target Electron distribution and pinned pnpm CLI. */ +/** Prepare Desktop resources and publish their versions after every preparation stage succeeds. */ -import { packagingStep } from './packaging-step.mjs' import { execFileSync } from 'node:child_process' -import { chmodSync, cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { chmodSync, cpSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' import { createRequire } from 'node:module' -import { dirname, join } from 'node:path' +import { join } from 'node:path' import { parseArgs } from 'node:util' import { downloadArtifact } from '@electron/get' import extractZip from 'extract-zip' -import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs' +import { readPrimaryRuntime } from '../../../packages/skill/tool-workspace-dependencies/src/index.ts' +import { desktopTargetPlatform, resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs' +import { packagingStep } from './packaging-step.mjs' import { preparePrimaryRuntime } from './prepare-primary-runtime.ts' import { prepareDesktopCli } from './prepare-cli.ts' import { prepareCommandLink } from './prepare-command-link.ts' @@ -16,23 +17,10 @@ import { prepareCommandLink } from './prepare-command-link.ts' const BUILD_PATHS = resolveDesktopTargetBuildPaths() const RUNTIME_ROOT = BUILD_PATHS.runtime -function preparePnpm(): string { - const require = createRequire(import.meta.url) - const manifestPath = require.resolve('pnpm') - const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { version?: unknown } - if (typeof manifest.version !== 'string') throw new Error('desktop runtime: pnpm manifest has no version') - const packageDir = dirname(manifestPath) - const destination = join(RUNTIME_ROOT, 'pnpm') - rmSync(destination, { recursive: true, force: true }) - cpSync(packageDir, destination, { recursive: true }) - return manifest.version -} +type DesktopTarget = ReturnType -async function main(): Promise { - const { values } = parseArgs({ options: { 'defer-primary-runtime-smoke': { type: 'boolean', default: false } } }) - const target = resolveDesktopBuildTarget() - const platform = target.startsWith('mac-') ? 'darwin' : 'win32' - const arch = target.endsWith('arm64') ? 'arm64' : 'x64' +/** Extract the target Electron distribution and return its embedded Node version. */ +async function prepareElectron({ platform, arch }: DesktopTarget): Promise { const require = createRequire(import.meta.url) const { version } = require('electron/package.json') as { version: string } const archive = await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'download:electron', @@ -40,28 +28,42 @@ async function main(): Promise { rmSync(BUILD_PATHS.electron, { recursive: true, force: true }) await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'extract:electron', () => extractZip(archive, { dir: BUILD_PATHS.electron })) const executable = join(BUILD_PATHS.electron, platform === 'win32' ? 'electron.exe' : 'Electron.app/Contents/MacOS/Electron') - const nodeVersion = execFileSync(executable, ['-p', 'process.versions.node'], { + return execFileSync(executable, ['-p', 'process.versions.node'], { encoding: 'utf8', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }, }).trim() - const macosMinimumVersion = platform === 'darwin' ? execFileSync('/usr/libexec/PlistBuddy', - ['-c', 'Print LSMinimumSystemVersion', join(BUILD_PATHS.electron, 'Electron.app', 'Contents', 'Info.plist')], { encoding: 'utf8' }).trim() : undefined - rmSync(RUNTIME_ROOT, { recursive: true, force: true }) - mkdirSync(RUNTIME_ROOT, { recursive: true }) - const pnpmVersion = preparePnpm() +} + +function prepareCli({ platform, arch }: DesktopTarget): void { cpSync(join(import.meta.dirname, 'node-bin'), join(RUNTIME_ROOT, 'bin'), { recursive: true }) chmodSync(join(RUNTIME_ROOT, 'bin', 'node'), 0o755) + const cli = join(RUNTIME_ROOT, 'cli') + prepareDesktopCli(cli, platform) + if (platform === 'darwin') { + const minimumVersion = execFileSync('/usr/libexec/PlistBuddy', [ + '-c', 'Print LSMinimumSystemVersion', join(BUILD_PATHS.electron, 'Electron.app', 'Contents', 'Info.plist'), + ], { encoding: 'utf8' }).trim() + prepareCommandLink(cli, arch, minimumVersion) + } + cpSync(join(import.meta.dirname, '..', 'lib', 'command-manager-entry.js'), join(cli, 'command-manager.js')) + cpSync(join(import.meta.dirname, 'command-path.ps1'), join(cli, 'command-path.ps1')) +} + +async function main(): Promise { + const { values } = parseArgs({ options: { 'defer-primary-runtime-smoke': { type: 'boolean', default: false } } }) + const target = desktopTargetPlatform(resolveDesktopBuildTarget()) + rmSync(RUNTIME_ROOT, { recursive: true, force: true }) + mkdirSync(RUNTIME_ROOT, { recursive: true }) + const nodeVersion = await prepareElectron(target) + await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:primary-runtime', + () => preparePrimaryRuntime({ deferSmoke: values['defer-primary-runtime-smoke'] })) + const { pnpm } = await readPrimaryRuntime(join(RUNTIME_ROOT, 'primary-runtime')) + if (pnpm === undefined) throw new Error('desktop runtime: primary-runtime manifest has no pnpm version') + await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:cli', async () => { prepareCli(target) }) writeFileSync(join(RUNTIME_ROOT, 'versions.json'), `${JSON.stringify({ schemaVersion: 1, node: nodeVersion, - pnpm: pnpmVersion, + pnpm, }, undefined, 2)}\n`) - await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:cli', - async () => prepareDesktopCli(join(RUNTIME_ROOT, 'cli'), platform)) - if (macosMinimumVersion !== undefined) prepareCommandLink(join(RUNTIME_ROOT, 'cli'), arch, macosMinimumVersion) - cpSync(join(import.meta.dirname, '..', 'lib', 'command-manager-entry.js'), join(RUNTIME_ROOT, 'cli', 'command-manager.js')) - cpSync(join(import.meta.dirname, 'command-path.ps1'), join(RUNTIME_ROOT, 'cli', 'command-path.ps1')) - await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:primary-runtime', - () => preparePrimaryRuntime({ deferSmoke: values['defer-primary-runtime-smoke'] })) } await main() diff --git a/apps/desktop/scripts/runtime-file-policy.ts b/apps/desktop/scripts/runtime-file-policy.ts index 1bc16b2a1c..a292e7d80f 100644 --- a/apps/desktop/scripts/runtime-file-policy.ts +++ b/apps/desktop/scripts/runtime-file-policy.ts @@ -1,7 +1,7 @@ /** Desktop-only omissions from already installed production npm packages. */ /** - * Identify build and diagnostic files omitted from the immutable Desktop runtime. + * Identify build, diagnostic and other-platform files omitted from the immutable Desktop runtime. * Unrecognized assets and target runtime binaries are retained. Paths name the copied * node_modules tree, including nested package containers. * @param path - Path relative to the production node_modules directory. @@ -16,7 +16,7 @@ export function desktopRuntimeFileExclusion( if (parts.some(part => ['.bin', '.pnpm', '.modules.yaml', '.pnpm-workspace-state-v1.json'].includes(part))) { return 'package-manager metadata' } - const file = parts.at(-1) ?? '' + const file = parts.at(-1)! if (/\.(?:[cm]?[jt]s|css)\.map$/u.test(file)) return 'source map' if (/\.d\.[cm]?ts$/u.test(file)) return 'TypeScript declaration' if (/\.tsbuildinfo$/u.test(file)) return 'TypeScript build cache' @@ -34,6 +34,9 @@ export function desktopRuntimeFileExclusion( return 'fs-ext build configuration' } if (name === '@mixmark-io/domino' && (entry === 'test' || entry.startsWith('test/'))) return 'Domino test fixtures' + if (name === 'node-pty' && target.platform !== 'win32' && /^third_party\/conpty(?:\/|$)/u.test(entry)) { + return 'node-pty Windows ConPTY sources' + } if (name === 'node-pty' && entry.startsWith('prebuilds/')) { const platform = packageParts[nameParts + 1] if (platform !== undefined && platform !== `${target.platform}-${target.arch}`) return 'node-pty other platform' diff --git a/apps/desktop/scripts/smoke-runtime.ts b/apps/desktop/scripts/smoke-runtime.ts index f15f10a928..dec4f8fbc6 100644 --- a/apps/desktop/scripts/smoke-runtime.ts +++ b/apps/desktop/scripts/smoke-runtime.ts @@ -1,6 +1,7 @@ /** Boot the materialized target runtime without access to a user's Harness profile. */ -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { execFile } from 'node:child_process' @@ -12,7 +13,7 @@ import { createPluginProfile } from '../src/project-manager.ts' import type { DesktopRuntimeDescriptor } from '../src/runtime-tree.ts' /** - * Check Host startup, its matching frontend, external plugins and real Office-to-PDF conversion. + * Check CLI package operations, Host startup, its frontend, external plugins and real Office-to-PDF conversion. * @param root - Materialized dsh resources. * @param node - Prepared target Electron executable. * @param runtime - Verified resource descriptor. @@ -25,15 +26,50 @@ export async function smokeDesktopRuntime( ): Promise { const home = mkdtempSync(join(tmpdir(), 'dsh-desktop-smoke-')) const profile = join(home, 'profiles', 'desktop') - const host = new DesktopHostProcess(node, root, profile, undefined, { ...environment, DSH_HOME: home }, + const hostEnvironment = { + ...Object.fromEntries(Object.entries(environment).filter(([name]) => !/^(?:npm|pnpm|corepack)_/iu.test(name))), + DSH_HOME: home, HOME: home, USERPROFILE: home, + XDG_CONFIG_HOME: home, XDG_CACHE_HOME: home, XDG_STATE_HOME: home, + APPDATA: home, LOCALAPPDATA: home, + npm_config_userconfig: join(home, 'npmrc'), npm_config_globalconfig: join(home, 'npmrc'), + npm_config_store_dir: join(home, 'store'), npm_config_enable_global_virtual_store: 'false', + } + const host = new DesktopHostProcess(node, root, profile, undefined, hostEnvironment, undefined, join(resourcesRuntime, 'primary-runtime'), - { pnpm: join(resourcesRuntime, 'pnpm', 'bin', 'pnpm.cjs'), nodeBin: join(resourcesRuntime, 'bin') }) + { pnpm: join(resourcesRuntime, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs'), nodeBin: join(resourcesRuntime, 'bin') }) let timer: ReturnType | undefined try { + writeFileSync(join(home, 'npmrc'), '') createPluginProfile(profile) const pluginName = 'desktop-runtime-smoke-plugin' - const plugin = join(profile, 'node_modules', pluginName) + const plugin = join(home, pluginName) mkdirSync(plugin, { recursive: true }) + const installedName = 'desktop-installed-smoke-plugin' + const installedPlugin = join(home, installedName) + mkdirSync(installedPlugin) + writeFileSync(join(installedPlugin, 'package.json'), JSON.stringify({ + name: installedName, version: '1.0.0', type: 'module', exports: './index.js', dsh: { bundle: { patch: './bundle.yml' } }, + })) + writeFileSync(join(installedPlugin, 'index.js'), ` +export function apply(ctx) { + ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/desktop-smoke-installed', + handler(_request, response) { response.end('installed plugin ready') } })) +} +`) + writeFileSync(join(installedPlugin, 'bundle.yml'), `- insert:\n - id: ${installedName}\n name: ${installedName}\n inject: [webServer]\n`) + appendFileSync(join(profile, 'pnpm-workspace.yaml'), 'offline: true\nupdateNotifier: false\n') + const cli = join(root, 'node_modules', '@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js') + const runCli = (args: string[]) => promisify(execFile)(node, + ['--expose-internals', cli, 'plugin', '--profile', 'desktop', ...args], + { cwd: home, env: { ...hostEnvironment, ELECTRON_RUN_AS_NODE: '1' }, timeout: 120_000, windowsHide: true }) + await runCli(['add', `file:${installedPlugin}`, '--offline', '--ignore-scripts']) + const installedEntry = join(profile, 'node_modules', installedName, 'index.js') + if (readFileSync(installedEntry, 'utf8') !== readFileSync(join(installedPlugin, 'index.js'), 'utf8')) { + throw new Error('desktop runtime: CLI did not install the local plugin') + } + await runCli(['remove', installedName]) + if (existsSync(installedEntry)) throw new Error('desktop runtime: CLI did not remove the local plugin') + console.log('desktop runtime: CLI offline plugin installation and removal passed') const primary = join(resourcesRuntime, 'primary-runtime') const dependencies = workspaceDependencyPaths(primary, await readPrimaryRuntime(primary)) await promisify(execFile)(dependencies.python, ['-I', '-B', @@ -56,6 +92,15 @@ export function apply(ctx) { if (!(ctx instanceof Context)) throw new Error('desktop runtime: external plugin loaded another Cordis instance') ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/desktop-smoke', handler(_request, response) { response.end('plugin route ready') } })) + ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/desktop-smoke-install', + async handler(_request, response) { + try { + response.end(JSON.stringify(await ctx.pluginManager.installBundle(${JSON.stringify(installedPlugin)}))) + } catch (error) { + response.statusCode = 500 + response.end(inspect(error, { depth: 5 })) + } + } })) ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/desktop-smoke-office-cli', async handler(_request, response) { try { @@ -90,12 +135,13 @@ export function apply(ctx) { } } `) - writeFileSync(join(plugin, 'bundle.yml'), '- insert:\n - id: desktop-runtime-smoke-plugin\n name: desktop-runtime-smoke-plugin\n inject: [webServer, officeToPdf, skills]\n') + writeFileSync(join(plugin, 'bundle.yml'), '- insert:\n - id: desktop-runtime-smoke-plugin\n name: desktop-runtime-smoke-plugin\n inject: [webServer, officeToPdf, skills, pluginManager]\n') + cpSync(plugin, join(profile, 'node_modules', pluginName), { recursive: true }) const manifest = JSON.parse(readFileSync(join(profile, 'package.json'), 'utf8')) as { - dependencies: Record + dependencies?: Record dsh: { profile: { bundles: string[] } } } - manifest.dependencies[pluginName] = '1.0.0' + manifest.dependencies = { ...manifest.dependencies, [pluginName]: `file:${plugin}` } manifest.dsh.profile.bundles.push(pluginName) writeFileSync(join(profile, 'package.json'), JSON.stringify(manifest)) writeFileSync(join(profile, 'cordis.patch.yml'), '- id: webserver\n config:\n host: 127.0.0.1\n port: 0\n') @@ -111,6 +157,14 @@ export function apply(ctx) { } const pluginResponse = await fetch(new URL('/desktop-smoke', ready.url), { headers: { cookie } }) if (await pluginResponse.text() !== 'plugin route ready') throw new Error('desktop runtime: plugin HTTP route failed') + const installation = await fetch(new URL('/desktop-smoke-install', ready.url), { + headers: { cookie }, signal: AbortSignal.timeout(120_000), + }) + const installationResult = await installation.text() + const installed = await fetch(new URL('/desktop-smoke-installed', ready.url), { headers: { cookie } }) + if (!installation.ok || !installed.ok || await installed.text() !== 'installed plugin ready') { + throw new Error(`desktop runtime: bundled pnpm plugin installation failed: ${installationResult}`) + } for (const { extension } of inputs) { const converted = await fetch(new URL(`/desktop-smoke-office/${extension}`, ready.url), { headers: { cookie }, signal: AbortSignal.timeout(120_000), @@ -133,7 +187,8 @@ export function apply(ctx) { console.log('desktop runtime: DOCX, XLSX, PPTX to PDF and skill CLI discovery passed') } finally { clearTimeout(timer) - await host.stop() - rmSync(home, { recursive: true, force: true }) + try { await host.stop() } finally { + await rm(home, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 }) + } } } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 38585675b1..1202670a3e 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -155,7 +155,7 @@ function runtimeResources(): RuntimeResources { const nodeBin = development ? join(app.getAppPath(), 'scripts', 'node-bin') : join(process.resourcesPath, 'runtime', 'bin') const pnpm = (development ? process.env.DSH_DESKTOP_PNPM_ENTRY : undefined) ?? (development ? join(app.getAppPath(), 'node_modules', 'pnpm', 'bin', 'pnpm.mjs') - : join(process.resourcesPath, 'runtime', 'pnpm', 'bin', 'pnpm.mjs')) + : join(process.resourcesPath, 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')) const dsh = (development ? process.env.DSH_DESKTOP_DSH_DIR : undefined) ?? (development ? join(app.getAppPath(), '.desktop-build', 'development', 'project') : join(app.getAppPath(), 'dsh')) return { node, nodeBin, pnpm, dsh } diff --git a/apps/desktop/tests/desktop-cli.spec.ts b/apps/desktop/tests/desktop-cli.spec.ts new file mode 100644 index 0000000000..e358df8657 --- /dev/null +++ b/apps/desktop/tests/desktop-cli.spec.ts @@ -0,0 +1,37 @@ +/** The installed CLI uses the shared pnpm distribution with Electron's package-operation environment. */ + +import { delimiter, join } from 'node:path' +import { afterEach, expect, it, vi } from 'vitest' +import { runCli } from '@deepseek-ai/dsh/lib/bin.js' +import { runDesktopCli } from '../../desktop-host/src/cli.ts' +import { installOfficeEngineResolution } from '../../desktop-host/src/office-engine.ts' + +vi.mock('@deepseek-ai/dsh/lib/bin.js', () => ({ runCli: vi.fn() })) +vi.mock('../../desktop-host/src/office-engine.ts', () => ({ installOfficeEngineResolution: vi.fn() })) + +afterEach(() => { + vi.unstubAllEnvs() + vi.clearAllMocks() +}) + +it.each(['system-bin', undefined])('dispatches with bundled pnpm when PATH is %s', async (path) => { + vi.stubEnv('PATH', path) + const runtime = join('Application 中文 with spaces', 'resources', 'app.asar', 'dsh') + const support = join('Application 中文 with spaces', 'resources', 'runtime') + + await runDesktopCli(runtime, support) + + expect(installOfficeEngineResolution).toHaveBeenCalledExactlyOnceWith(runtime) + expect(runCli).toHaveBeenCalledExactlyOnceWith({ + manageDesktopProfile: true, + packageManager: { + command: process.execPath, + args: ['--expose-internals', join(support, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')], + env: { + ELECTRON_RUN_AS_NODE: '1', + DSH_DESKTOP_NODE_EXECUTABLE: process.execPath, + PATH: `${join(support, 'bin')}${delimiter}${path ?? ''}`, + }, + }, + }) +}) diff --git a/apps/desktop/tests/fixtures/pnpm-smoke.mjs b/apps/desktop/tests/fixtures/pnpm-smoke.mjs new file mode 100644 index 0000000000..5d16769ab4 --- /dev/null +++ b/apps/desktop/tests/fixtures/pnpm-smoke.mjs @@ -0,0 +1,69 @@ +/** Install and use a local dependency with the packaged pnpm and Electron Node launcher. */ + +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { delimiter, join } from 'node:path' + +/** + * Exercise offline installation and package scripts without a system pnpm or Node installation. + * @param {string} resourcesRuntime - Packaged runtime directory outside ASAR. + * @returns {Promise} Resolves after execution and removal of the private installation. + */ +export async function checkPnpm(resourcesRuntime) { + assert.ok(process.versions.electron, 'Run the package-manager smoke with Electron Node mode') + assert.equal(existsSync(join(resourcesRuntime, 'pnpm')), false, 'Desktop must ship only primary-runtime pnpm') + const primary = join(resourcesRuntime, 'primary-runtime') + const pnpm = join(primary, 'dependencies', 'pnpm', 'bin', 'pnpm.mjs') + const versions = JSON.parse(readFileSync(join(resourcesRuntime, 'versions.json'), 'utf8')) + const manifest = JSON.parse(readFileSync(join(primary, 'runtime.json'), 'utf8')) + assert.equal(versions.pnpm, manifest.pnpm) + const scratch = mkdtempSync(join(tmpdir(), 'desktop-pnpm-smoke-')) + try { + mkdirSync(join(scratch, 'dependency')) + writeFileSync(join(scratch, 'dependency', 'package.json'), JSON.stringify({ + name: 'desktop-pnpm-smoke-dependency', version: '1.0.0', main: 'index.cjs', + })) + writeFileSync(join(scratch, 'dependency', 'index.cjs'), 'exports.answer = 42\n') + writeFileSync(join(scratch, 'package.json'), JSON.stringify({ + name: 'desktop-node-script-smoke', private: true, + dependencies: { 'desktop-pnpm-smoke-dependency': 'file:./dependency' }, scripts: { check: 'node check.cjs' }, + })) + writeFileSync(join(scratch, 'npmrc'), '') + writeFileSync(join(scratch, 'check.cjs'), ` +const assert = require('node:assert/strict') +assert.equal(require('desktop-pnpm-smoke-dependency').answer, 42) +assert.equal(process.execPath, ${JSON.stringify(process.execPath)}) +assert.ok(process.versions.electron) +assert.ok(process.execArgv.includes('--expose-internals')) +assert.equal(typeof require('internal/modules/esm/loader').getOrInitializeCascadedLoader, 'function') +console.log('desktop-node-script-ok') +`) + const environment = Object.fromEntries(Object.entries(process.env).filter(([name]) => /^(?:systemroot|windir|comspec|LD_LIBRARY_PATH)$/iu.test(name))) + const systemBin = process.platform === 'win32' ? join(process.env.SystemRoot, 'System32') : '/usr/bin:/bin' + const run = args => execFileSync(process.execPath, ['--expose-internals', pnpm, ...args], { + cwd: scratch, encoding: 'utf8', timeout: 45_000, windowsHide: true, + env: { ...environment, pnpm_config_verify_deps_before_run: 'false', + ELECTRON_RUN_AS_NODE: '1', DSH_DESKTOP_NODE_EXECUTABLE: process.execPath, + PATH: `${join(resourcesRuntime, 'bin')}${delimiter}${systemBin}`, + HOME: scratch, USERPROFILE: scratch, TMP: scratch, TEMP: scratch, TMPDIR: scratch, + XDG_CONFIG_HOME: scratch, XDG_CACHE_HOME: scratch, XDG_STATE_HOME: scratch, + npm_config_userconfig: join(scratch, 'npmrc'), npm_config_globalconfig: join(scratch, 'npmrc'), + npm_config_store_dir: join(scratch, 'store'), npm_config_enable_global_virtual_store: 'false', + npm_config_update_notifier: 'false' }, + }) + assert.equal(run(['--version']).trim(), versions.pnpm) + run(['install', '--offline', '--ignore-scripts']) + assert.match(run(['run', 'check']), /desktop-node-script-ok/u) + } finally { + await rm(scratch, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 }) + } +} + +if (import.meta.main) { + assert.ok(process.argv[2], 'Pass the packaged runtime directory') + await checkPnpm(process.argv[2]) + console.log('desktop pnpm: offline installation and Electron Node package script passed') +} diff --git a/apps/desktop/tests/fixtures/runtime-payload-smoke.mjs b/apps/desktop/tests/fixtures/runtime-payload-smoke.mjs index f9b6ca9279..3cda38c620 100644 --- a/apps/desktop/tests/fixtures/runtime-payload-smoke.mjs +++ b/apps/desktop/tests/fixtures/runtime-payload-smoke.mjs @@ -8,6 +8,7 @@ import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { delimiter, dirname, join, resolve } from 'node:path' import { pathToFileURL } from 'node:url' +import { checkPnpm } from './pnpm-smoke.mjs' const runtime = process.argv[2] assert.ok(runtime, 'Pass the filtered resources/dsh directory') @@ -20,33 +21,6 @@ const resourcesRuntime = process.argv[3] ?? join(dirname(root), 'runtime') const requireRuntime = createRequire(join(root, 'package.json')) const scratch = mkdtempSync(join(tmpdir(), 'dsh-runtime-payload-')) -/** Run a package script with only the shipped node launcher available on PATH. */ -function checkPnpm() { - const bin = join(resourcesRuntime, 'bin') - const pnpm = join(resourcesRuntime, 'pnpm', 'bin', 'pnpm.mjs') - writeFileSync(join(scratch, 'package.json'), JSON.stringify({ - name: 'desktop-node-script-smoke', private: true, scripts: { check: 'node check.cjs' }, - })) - writeFileSync(join(scratch, 'check.cjs'), ` -const assert = require('node:assert/strict') -assert.equal(process.execPath, ${JSON.stringify(process.execPath)}) -assert.ok(process.versions.electron) -assert.ok(process.execArgv.includes('--expose-internals')) -assert.equal(typeof require('internal/modules/esm/loader').getOrInitializeCascadedLoader, 'function') -console.log('desktop-node-script-ok') -`) - const environment = Object.fromEntries(Object.entries(process.env).filter(([name]) => /^(?:systemroot|windir|comspec)$/iu.test(name))) - const systemBin = process.platform === 'win32' ? join(process.env.SystemRoot, 'System32') : '/usr/bin:/bin' - // This dependency-free fixture checks script launch, without pnpm's implicit install and update-network check. - const output = execFileSync(process.execPath, ['--expose-internals', pnpm, 'run', 'check'], { - cwd: scratch, encoding: 'utf8', timeout: 45_000, - env: { ...environment, pnpm_config_verify_deps_before_run: 'false', - ELECTRON_RUN_AS_NODE: '1', DSH_DESKTOP_NODE_EXECUTABLE: process.execPath, - PATH: `${bin}${delimiter}${systemBin}`, HOME: scratch, USERPROFILE: scratch, TMP: scratch, TEMP: scratch, TMPDIR: scratch }, - }) - assert.match(output, /desktop-node-script-ok/u) -} - /** Spawn only a fixed Node program and await the terminal's drained exit event. */ async function checkPty() { const pty = requireRuntime('node-pty') @@ -162,7 +136,7 @@ try { const builtin = requireRuntime('node-addon-require-builtin') assert.equal(typeof builtin.requireBuiltin('internal/modules/esm/loader').getOrInitializeCascadedLoader, 'function') console.error('runtime payload: pnpm') - checkPnpm() + await checkPnpm(resourcesRuntime) console.error('runtime payload: koffi') checkKoffi() console.error('runtime payload: sharp') diff --git a/apps/desktop/tests/installed-update-builder.spec.ts b/apps/desktop/tests/installed-update-builder.spec.ts index 089b04deef..12e5a82258 100644 --- a/apps/desktop/tests/installed-update-builder.spec.ts +++ b/apps/desktop/tests/installed-update-builder.spec.ts @@ -88,8 +88,10 @@ describe('installed-update application inputs and builder configuration', () => expect(config.extraMetadata.main).toBe('qualification-bootstrap.mjs') expect(config.directories.output).toBe(join(manifest, '..', versions[index]!, 'installer')) const dsh = join(manifest, '..', versions[index]!, 'dsh') - expect(config.files[2]).toMatchObject({ from: dsh, to: 'dsh' }) - expect(config.files[3]).toMatchObject({ from: join(dsh, 'node_modules'), to: 'dsh/node_modules' }) + expect(config.files.at(-2)).toMatchObject({ from: dsh, to: 'dsh' }) + expect(config.files.at(-1)).toMatchObject({ from: join(dsh, 'node_modules'), to: 'dsh/node_modules' }) + expect(config.files).toContain('!node_modules/**/*.{js,cjs,mjs,ts,cts,mts,css}.map') + expect(config.files).toContain('!node_modules/**/*.tsbuildinfo') expect(config.win.forceCodeSigning).toBe(true) expect(config.win.signtoolOptions.publisherName).toBe('CN=Fixture,O=Fixture,C=CN') expect(typeof config.win.signtoolOptions.sign).toBe('function') diff --git a/apps/desktop/tests/macos-signature.spec.ts b/apps/desktop/tests/macos-signature.spec.ts index d39ee99cb9..186b597b07 100644 --- a/apps/desktop/tests/macos-signature.spec.ts +++ b/apps/desktop/tests/macos-signature.spec.ts @@ -93,9 +93,9 @@ describe('desktop macOS release signature', () => { const ignored = (path: string): boolean => config.mac.signIgnore.some(pattern => new RegExp(pattern).test(path)) expect(ignored('/App.app/Contents/Frameworks/Electron.framework/Versions/A/Resources/en.lproj/locale.pak')).toBe(true) expect(ignored('/App.app/Contents/Frameworks/Electron.framework/Versions/A/Resources/resources.pak')).toBe(true) + expect(ignored('/App.app/Contents/Resources/runtime/primary-runtime/dependencies/pnpm/addon.node')).toBe(true) for (const path of [ '/App.app/Contents/Resources/runtime/node/node', - '/App.app/Contents/Resources/runtime/pnpm/addon.node', '/App.app/Contents/Frameworks/Electron.framework/Versions/A/library.dylib', '/App.app/Contents/Frameworks/Electron.framework', '/App.app', diff --git a/apps/desktop/tests/main-startup.spec.ts b/apps/desktop/tests/main-startup.spec.ts index fff39c38ef..d9a8f3103b 100644 --- a/apps/desktop/tests/main-startup.spec.ts +++ b/apps/desktop/tests/main-startup.spec.ts @@ -443,6 +443,21 @@ afterEach(async () => { }) describe('desktop main startup', () => { + it.each([true, false])('selects the package manager before Host startup (packaged=%s)', async (packaged) => { + harness.app.isPackaged = packaged + await readyForUpdate() + const host = harness.hosts[0]! + expect(host.node).toBe(process.execPath) + expect(host.primaryRuntime).toBe(packaged + ? join('desktop-test-resources', 'runtime', 'primary-runtime') : 'test-primary-runtime') + expect(host.packageManager).toMatchObject({ + pnpm: packaged + ? join('desktop-test-resources', 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs') + : 'test-pnpm', + nodeBin: packaged ? join('desktop-test-resources', 'runtime', 'bin') : join('desktop-test-app', 'scripts', 'node-bin'), + }) + }) + it('starts the Host with authentication popups disabled when local settings are invalid', async () => { configureTestAuthPopup(false) const path = join(harness.app.getPath('userData'), 'desktop', 'settings.json') diff --git a/apps/desktop/tests/runtime-file-policy.spec.ts b/apps/desktop/tests/runtime-file-policy.spec.ts index 76f5c8d95e..a0b392f41e 100644 --- a/apps/desktop/tests/runtime-file-policy.spec.ts +++ b/apps/desktop/tests/runtime-file-policy.spec.ts @@ -85,6 +85,62 @@ it('retains native prebuilds for the selected macOS architecture', () => { expect(desktopRuntimeFileExclusion('node-pty/prebuilds/win32-x64/conpty.node', mac, 'darwin-arm64')).toBeDefined() }) +it.each([ + ['darwin', 'arm64'], ['darwin', 'x64'], + ['linux', 'arm64'], ['linux', 'x64'], + ['win32', 'arm64'], ['win32', 'x64'], +] as const)('copies ConPTY sources only for Windows targets (%s/%s)', (platform, arch) => { + const root = mkdtempSync(join(tmpdir(), 'desktop-conpty-policy-')) + try { + const source = join(root, 'source') + const output = join(root, 'output') + const prefixes = ['', '@example/outer/node_modules/'] + const conpty = ['arm64', 'x64'].flatMap(cpu => ['conpty.dll', 'OpenConsole.exe'].map( + file => `node-pty/third_party/conpty/1.25.260303002/win10-${cpu}/${file}`, + )) + const native = platform === 'win32' + ? ['conpty.node', 'conpty_console_list.node', 'conpty/conpty.dll', 'conpty/OpenConsole.exe'] + : ['pty.node', 'spawn-helper'] + const retained = [ + ...native.map(file => `node-pty/prebuilds/${platform}-${arch}/${file}`), + ...native.map(file => `node-pty/build/Release/${file}`), + 'node-pty/lib/index.js', 'node-pty/LICENSE', + 'node-pty/third_party/conpty-notes/README.md', 'node-pty/third_party/conpty.dll', + 'other/third_party/conpty/conpty.dll', + ] + for (const prefix of prefixes) { + for (const path of [...conpty, ...retained]) { + mkdirSync(join(source, prefix, path, '..'), { recursive: true }) + writeFileSync(join(source, prefix, path), `payload:${prefix}${path}`) + } + } + cpSync(source, output, { + recursive: true, dereference: true, + filter: (path) => { + const entry = relative(source, path) + return desktopRuntimeFileExclusion( + platform === 'win32' ? entry.replaceAll('/', '\\') : entry, + { platform, arch }, `${platform}-${arch}`, + ) === undefined + }, + }) + for (const prefix of prefixes) { + expect(existsSync(join(output, prefix, 'node-pty/third_party/conpty'))).toBe(platform === 'win32') + for (const path of conpty) { + expect(existsSync(join(output, prefix, path)), `${prefix}${path}`).toBe(platform === 'win32') + } + for (const path of [...retained, ...(platform === 'win32' ? conpty : [])]) { + expect(readFileSync(join(output, prefix, path), 'utf8')).toBe(`payload:${prefix}${path}`) + } + for (const path of conpty) { + expect(readFileSync(join(source, prefix, path), 'utf8')).toBe(`payload:${prefix}${path}`) + } + } + } finally { + rmSync(root, { recursive: true, force: true }) + } +}) + it.each([ ['linux', 'x64', 'linux-x64'], ['linux', 'arm64', 'wasm'], diff --git a/apps/desktop/tests/runtime-preparation.spec.ts b/apps/desktop/tests/runtime-preparation.spec.ts new file mode 100644 index 0000000000..d16dd592c2 --- /dev/null +++ b/apps/desktop/tests/runtime-preparation.spec.ts @@ -0,0 +1,120 @@ +/** Desktop resource preparation publishes versions only for complete resources. */ + +import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' + +const state = vi.hoisted(() => ({ + root: '', + target: 'mac-arm64' as 'mac-arm64' | 'mac-x64' | 'win-x64', + pnpm: '11.7.0' as string | undefined, + failure: undefined as 'electron' | 'primary' | 'cli' | undefined, + preparePrimary: vi.fn(), + commandLink: vi.fn(), +})) +vi.mock('../scripts/desktop-build-paths.mjs', async importOriginal => ({ + ...await importOriginal(), + resolveDesktopBuildTarget: () => state.target, + resolveDesktopTargetBuildPaths: () => ({ + runtime: join(state.root, 'runtime'), electron: join(state.root, 'electron'), downloads: join(state.root, 'downloads'), + }), +})) +vi.mock('@electron/get', () => ({ downloadArtifact: async () => 'electron.zip' })) +vi.mock('extract-zip', () => ({ default: async () => { + if (state.failure === 'electron') throw new Error('Electron extraction failed') +} })) +vi.mock('node:child_process', async importOriginal => ({ + ...await importOriginal(), + execFileSync: (file: string) => file === '/usr/libexec/PlistBuddy' ? '12.0\n' : '24.18.1\n', +})) +vi.mock('../scripts/prepare-command-link.ts', () => ({ prepareCommandLink: state.commandLink })) +// The compiled command manager is a build input; all source resource copies use real files. +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, cpSync: (...args: Parameters) => { + if (String(args[0]).endsWith(join('lib', 'command-manager-entry.js'))) { + if (state.failure === 'cli') throw new Error('command manager copy failed') + actual.writeFileSync(args[1], 'command manager fixture') + } else actual.cpSync(...args) + } } +}) +vi.mock('../scripts/prepare-primary-runtime.ts', () => ({ + preparePrimaryRuntime: state.preparePrimary, +})) + +const originalArgv = process.argv +beforeEach(() => { + vi.resetModules() + vi.clearAllMocks() + state.root = mkdtempSync(join(tmpdir(), 'desktop-runtime-preparation-')) + state.target = 'mac-arm64' + state.pnpm = '11.7.0' + state.failure = undefined + process.argv = [process.execPath, 'prepare-runtime.ts'] + vi.stubEnv('DSH_DESKTOP_PACKAGING_RUN_DIR', undefined) + state.preparePrimary.mockImplementation(async () => { + if (state.failure === 'primary') throw new Error('primary preparation failed') + const root = join(state.root, 'runtime', 'primary-runtime') + const pnpm = join(root, 'dependencies', 'pnpm', 'bin') + mkdirSync(pnpm, { recursive: true }) + writeFileSync(join(pnpm, 'pnpm.mjs'), '') + writeFileSync(join(root, 'runtime.json'), JSON.stringify({ + desktopVersion: '1.0.0', platform: state.target === 'win-x64' ? 'win32' : 'darwin', + arch: state.target === 'mac-arm64' ? 'arm64' : 'x64', + python: '3.12.14', node: '24.17.0', pnpm: state.pnpm, pythonPackages: { numpy: '2.3.5' }, + })) + }) +}) + +afterEach(async () => { + process.argv = originalArgv + vi.unstubAllEnvs() + await rm(state.root, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 }) +}) + +it.each(['mac-arm64', 'mac-x64', 'win-x64'] as const)('prepares %s with one pnpm distribution and its CLI resources', async (target) => { + state.target = target + const windows = target === 'win-x64' + if (windows) process.argv.push('--defer-primary-runtime-smoke') + const runtime = join(state.root, 'runtime') + mkdirSync(join(runtime, 'pnpm'), { recursive: true }) + await import('../scripts/prepare-runtime.ts') + expect(existsSync(join(runtime, 'pnpm'))).toBe(false) + expect(existsSync(join(runtime, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs'))).toBe(true) + expect(existsSync(join(runtime, 'bin', 'node'))).toBe(true) + expect(existsSync(join(runtime, 'cli', 'bin', windows ? 'dsh.cmd' : 'dsh'))).toBe(true) + expect(readFileSync(join(runtime, 'cli', 'command-manager.js'), 'utf8')).toBe('command manager fixture') + expect(existsSync(join(runtime, 'cli', 'command-path.ps1'))).toBe(true) + expect(JSON.parse(readFileSync(join(runtime, 'versions.json'), 'utf8'))).toEqual({ + schemaVersion: 1, node: '24.18.1', pnpm: '11.7.0', + }) + expect(state.preparePrimary).toHaveBeenCalledWith({ deferSmoke: windows }) + if (windows) expect(state.commandLink).not.toHaveBeenCalled() + else expect(state.commandLink).toHaveBeenCalledWith(join(runtime, 'cli'), target === 'mac-arm64' ? 'arm64' : 'x64', '12.0') +}) + +it.each([ + ['electron', 'Electron extraction failed'], + ['primary', 'primary preparation failed'], + ['cli', 'command manager copy failed'], +] as const)('does not publish versions when %s preparation fails', async (failure, error) => { + state.failure = failure + const runtime = join(state.root, 'runtime') + mkdirSync(runtime) + writeFileSync(join(runtime, 'versions.json'), 'stale versions') + await expect(import('../scripts/prepare-runtime.ts')).rejects.toThrow(error) + expect(existsSync(join(runtime, 'versions.json'))).toBe(false) +}) + +it.each([ + ['not-a-version', 'primary runtime: invalid metadata'], + [undefined, 'primary-runtime manifest has no pnpm version'], +])('rejects pnpm metadata %s even when Windows smoke is deferred', async (pnpm, error) => { + state.target = 'win-x64' + process.argv.push('--defer-primary-runtime-smoke') + state.pnpm = pnpm + await expect(import('../scripts/prepare-runtime.ts')).rejects.toThrow(error) + expect(existsSync(join(state.root, 'runtime', 'versions.json'))).toBe(false) +}) diff --git a/apps/desktop/tests/shell-file-selection.spec.ts b/apps/desktop/tests/shell-file-selection.spec.ts new file mode 100644 index 0000000000..115d75392a --- /dev/null +++ b/apps/desktop/tests/shell-file-selection.spec.ts @@ -0,0 +1,110 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { dirname, join, relative } from 'node:path' +import { Packager, type Configuration, type PlatformSpecificBuildOptions } from 'app-builder-lib' +import { readAsar } from 'app-builder-lib/out/asar/asar.js' +import type { FileMatcher } from 'app-builder-lib/out/fileMatcher.js' +import { MacPackager } from 'app-builder-lib/out/macPackager.js' +import { PM } from 'app-builder-lib/out/node-module-collector/index.js' +import type { PlatformPackager } from 'app-builder-lib/out/platformPackager.js' +import type { ResolvedFileSet } from 'app-builder-lib/out/util/appFileCopier.js' +import { WinPackager } from 'app-builder-lib/out/winPackager.js' +import { expect, it, onTestFinished, vi } from 'vitest' +import { createElectronBuilderConfig } from '../scripts/electron-builder-config.mjs' + +const require = createRequire(import.meta.url) +// The locked builder exports these packaging operations without declarations. +const { getNodeModuleFileMatcher } = require('app-builder-lib/out/fileMatcher.js') as { + getNodeModuleFileMatcher: (app: string, destination: string, expand: (value: string) => string, + options: PlatformSpecificBuildOptions, packager: Packager) => FileMatcher +} +const { computeNodeModuleFileSets } = require('app-builder-lib/out/util/appFileCopier.js') as { + computeNodeModuleFileSets: (packager: PlatformPackager, matcher: FileMatcher) => Promise +} +const { AsarPackager } = require('app-builder-lib/out/asar/asarUtil.js') as { + AsarPackager: new (packager: object, configuration: object) => { pack(files: ResolvedFileSet[]): Promise } +} + +it.each([ + ['darwin', 'arm64'], ['darwin', 'x64'], ['win32', 'x64'], +] as const)('omits shell build files from %s/%s ASAR while retaining runtime assets', async (platform, arch) => { + const root = await mkdtemp(join(tmpdir(), 'desktop-shell-files-')) + onTestFinished(() => rm(root, { recursive: true, force: true })) + const appDir = join(root, 'app') + const resources = join(root, 'resources') + const destination = join(resources, 'app') + await mkdir(resources, { recursive: true }) + const manifest = { name: 'fixture', version: '1.0.0', dependencies: { shell: '1.0.0', '@fixture/shared': '2.0.0' } } + const packages = [ + ['node_modules/shell', { name: 'shell', version: '1.0.0', dependencies: { '@fixture/shared': '1.0.0' } }], + ['node_modules/@fixture/shared', { name: '@fixture/shared', version: '2.0.0' }], + ['node_modules/shell/node_modules/@fixture/shared', { name: '@fixture/shared', version: '1.0.0' }], + ] as const + const omitted = [ + 'index.js.map', 'lib/index.cjs.map', 'lib/index.mjs.map', 'lib/index.ts.map', + 'lib/index.cts.map', 'lib/index.mts.map', 'lib/style.css.map', 'tsconfig.tsbuildinfo', 'lib/types/tsconfig.tsbuildinfo', + ] + const retained = [ + 'index.js', 'lib/index.cjs', 'lib/index.mjs', 'lib/index.ts', 'lib/index.cts', 'lib/index.mts', + 'lib/style.css', 'lib/types/runtime.js', 'data.map', 'lib/data.map', 'lib/addon.node', 'lib/engine.wasm', 'LICENSE', 'NOTICE', + ] + const originals = new Map() + for (const [directory, metadata] of packages) { + for (const name of [...retained, ...omitted, 'package.json']) { + const file = join(appDir, directory, name) + const contents = Buffer.from(name === 'package.json' ? JSON.stringify(metadata) : `${directory}/${name}\n`) + await mkdir(dirname(file), { recursive: true }) + await writeFile(file, contents) + originals.set(file, contents) + } + } + await writeFile(join(appDir, 'package.json'), JSON.stringify(manifest)) + const desktopConfig = createElectronBuilderConfig({ + DSH_DESKTOP_APP_ID: 'com.example.shell-files', + DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN: 'https://policy.example.com', + DSH_DESKTOP_MANDATORY_UPDATE_CONFIG: JSON.stringify({ allowedAuthOrigins: ['https://login.example.com'] }), + DSH_DESKTOP_UNSIGNED: platform === 'win32' ? '1' : '0', + DSH_DESKTOP_MACOS_SIGNING_IDENTITY: 'Example Company (TEAMID1234)', + DSH_DESKTOP_MACOS_TEAM_ID: 'TEAMID1234', + APPLE_KEYCHAIN_PROFILE: 'shell-files-test', + DOWNLOAD_TEST_ORIGIN: 'https://desktop-updates.example.com', + DOWNLOAD_TEST_RELEASE_ID: '0123456789abcdef0123456789abcdef', + }, platform, arch) + const config: Configuration = { + files: desktopConfig.files.map(file => typeof file === 'string' ? file : { ...file, filter: [...file.filter] }), + } + const info = new Packager({ projectDir: appDir }) + onTestFinished(() => info.tempDirManager.cleanup()) + Object.defineProperties(info, { + appDir: { value: appDir }, config: { value: config }, + metadata: { value: manifest }, originalMetadata: { value: manifest }, + appInfo: { value: { type: 'commonjs' } }, + framework: { value: { name: 'electron' } }, + }) + // Traverse the installed fixture without invoking a host package manager. + vi.spyOn(info, 'getPackageManager').mockResolvedValue(PM.TRAVERSAL) + vi.spyOn(info, 'getWorkspaceRoot').mockResolvedValue(appDir) + const packager = platform === 'darwin' ? new MacPackager(info) : new WinPackager(info) + const matcher = getNodeModuleFileMatcher(appDir, destination, value => value, packager.platformSpecificBuildOptions, info) + const fileSets = await computeNodeModuleFileSets(packager, matcher) + expect(fileSets.map(set => relative(destination, set.destination).replaceAll('\\', '/')).sort()) + .toEqual(packages.map(([directory]) => directory).sort()) + const consoleLog = console.log + try { + await new AsarPackager(packager, { + defaultDestination: destination, resourcePath: resources, options: { smartUnpack: false }, + }).pack(fileSets) + } finally { + // Builder temporarily intercepts console.log while writing ASAR. + console.log = consoleLog + } + const archive = await readAsar(join(resources, 'app.asar')) + for (const [directory] of packages) { + for (const name of [...retained, 'package.json']) { + expect(await archive.readFile(join(directory, name))).toEqual(originals.get(join(appDir, directory, name))) + } + for (const name of omitted) expect(archive.getNode(join(directory, name))).toBeUndefined() + } + for (const [file, contents] of originals) expect(await readFile(file)).toEqual(contents) +}) diff --git a/apps/web/package.json b/apps/web/package.json index f44baf5c4c..0b44c5cd00 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-frontend", "description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/apps/web/tests/access-confirmation.e2e.ts b/apps/web/tests/access-confirmation.e2e.ts index a47cad63f6..f97978d346 100644 --- a/apps/web/tests/access-confirmation.e2e.ts +++ b/apps/web/tests/access-confirmation.e2e.ts @@ -64,7 +64,7 @@ describe('web e2e: experimental Auto and Full access confirmation', () => { const currentMenu = page.getByRole('menu') await currentMenu.waitFor({ timeout: 10_000 }) expect(await currentMenu.getByRole('menuitem').allTextContents()) - .toEqual(['仅可查看', '工作区内修改', '完全权限', 'Auto reviewEXP']) + .toEqual(['仅可查看', '工作区内修改', '完全权限', '自动审查EXP']) await captureAutoReviewState(page, 'experimental-current-session-picker') const currentSnapshot = await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd) await compareOrRefreshGolden(CURRENT_SESSION_PICKER_EXPECTED, currentSnapshot, MODE) @@ -77,10 +77,10 @@ describe('web e2e: experimental Auto and Full access confirmation', () => { expect(currentMenuBox!.width).toBeGreaterThan(currentTriggerBox!.width) expect(Math.abs(currentTriggerBox!.y - currentMenuBox!.y - currentMenuBox!.height - 4)).toBeLessThan(1) - await currentMenu.getByRole('menuitem', { name: 'Auto review EXP' }).click() - const autoDialog = page.getByRole('dialog', { name: '确认启用 Auto review(实验)?' }) + await currentMenu.getByRole('menuitem', { name: '自动审查 EXP' }).click() + const autoDialog = page.getByRole('dialog', { name: '确认启用自动审查(实验)?' }) await autoDialog.waitFor({ timeout: 10_000 }) - const autoEnable = autoDialog.getByRole('button', { name: '启用 Auto review' }) + const autoEnable = autoDialog.getByRole('button', { name: '启用自动审查' }) expect(await autoEnable.isDisabled()).toBe(true) expect(await autoDialog.getByText(/不使用沙箱/).count()).toBe(1) expect(await autoDialog.getByText(/误放行或误拒绝/).count()).toBe(1) @@ -105,7 +105,7 @@ describe('web e2e: experimental Auto and Full access confirmation', () => { await slashPicker.waitFor({ timeout: 10_000 }) const slashRows = slashPicker.getByRole('option') expect(await slashRows.count()).toBe(4) - expect(await slashPicker.getByRole('option', { name: 'Auto review EXP' }).count()).toBe(1) + expect(await slashPicker.getByRole('option', { name: '自动审查 EXP' }).count()).toBe(1) await captureAutoReviewState(page, 'experimental-slash-picker') const slashSnapshot = await captureStableAria(page, '[aria-label="/permission 选项"]', scaffold.workspaceCwd) await compareOrRefreshGolden(SLASH_PICKER_EXPECTED, slashSnapshot, MODE) @@ -119,15 +119,15 @@ describe('web e2e: experimental Auto and Full access confirmation', () => { expect(slashPickerBox!.x).toBeCloseTo(composerBox!.x) expect(Math.abs(composerBox!.y - slashPickerBox!.y - slashPickerBox!.height - 4)).toBeLessThan(1) - await slashPicker.getByRole('option', { name: 'Auto review EXP' }).click() - const slashDialog = page.getByRole('dialog', { name: '确认启用 Auto review(实验)?' }) + await slashPicker.getByRole('option', { name: '自动审查 EXP' }).click() + const slashDialog = page.getByRole('dialog', { name: '确认启用自动审查(实验)?' }) await slashDialog.waitFor({ timeout: 10_000 }) expect(await slashDialog.count()).toBe(1) expect(await slashPicker.count()).toBe(0) await slashDialog.getByRole('checkbox', { name: '我已了解这些风险,并愿意继续' }).check() - await slashDialog.getByRole('button', { name: '启用 Auto review' }).click() + await slashDialog.getByRole('button', { name: '启用自动审查' }).click() await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 }) - .toBe('访问模式,当前:Auto review EXP') + .toBe('访问模式,当前:自动审查') expect(await slashDialog.count()).toBe(0) expect(await input.textContent()).toBe('') await captureAutoReviewState(page, 'experimental-current-session') @@ -188,12 +188,18 @@ describe('web e2e: experimental Auto and Full access confirmation', () => { expect(menuBox.width).toBeLessThanOrEqual(360) expect(menuBox.x).toBeGreaterThanOrEqual(0) expect(menuBox.x + menuBox.width).toBeLessThanOrEqual(420) - for (const name of ['仅可查看', '工作区内修改', '完全权限', 'Auto review']) { + for (const name of ['仅可查看', '工作区内修改', '完全权限', '自动审查']) { expect(await menu.getByText(name, { exact: true }).evaluate(node => node.scrollWidth <= node.clientWidth)).toBe(true) } await page.keyboard.press('Escape') const input = page.locator('[data-composer-input]').first() + await writeComposerDraft(page, input, '/permission auto') + await input.press('Enter') + await expect.poll(() => access.getAttribute('aria-label')).toBe('访问模式,当前:自动审查') + expect(await access.getByText('自动审查', { exact: true }).isVisible()).toBe(false) + expect(await access.getByText('EXP', { exact: true }).count()).toBe(0) + expect(await access.getAttribute('title')).toBe('无沙箱运行;每次原生工具调用和 PTC 内层调用前由同一模型进行实验性审查。') await writeComposerDraft(page, input, '/permission') await input.press('Escape') await input.press('Enter') @@ -206,10 +212,13 @@ describe('web e2e: experimental Auto and Full access confirmation', () => { expect(slashBox.x + slashBox.width).toBeLessThanOrEqual(420) expect(await slash.evaluate(node => getComputedStyle(node).minWidth)).toBe('min(220px, 100%)') expect(await slash.evaluate(node => getComputedStyle(node).maxWidth)).toBe('100%') - for (const name of ['仅可查看', '工作区内修改', '完全权限', 'Auto review']) { + for (const name of ['仅可查看', '工作区内修改', '完全权限', '自动审查']) { expect(await slash.getByText(name, { exact: true }).evaluate(node => node.scrollWidth <= node.clientWidth)).toBe(true) } await page.keyboard.press('Escape') + await writeComposerDraft(page, input, '/permission danger-full-access') + await input.press('Enter') + await expect.poll(() => access.getAttribute('aria-label')).toBe('访问模式,当前:完全权限') expect(tripwire.pageErrors).toEqual([]) }) @@ -219,7 +228,7 @@ describe('web e2e: experimental Auto and Full access confirmation', () => { const access = page.locator('button[aria-label^="访问模式"]').first() await writeComposerDraft(page, input, '/permission auto') await input.press('Enter') - await expect.poll(() => access.getAttribute('aria-label')).toBe('访问模式,当前:Auto review EXP') + await expect.poll(() => access.getAttribute('aria-label')).toBe('访问模式,当前:自动审查') expect(await page.getByRole('dialog').count()).toBe(0) await writeComposerDraft(page, input, '/permission') await input.press('Escape') @@ -237,7 +246,7 @@ describe('web e2e: experimental Auto and Full access confirmation', () => { await input.press('Enter') await slash.waitFor() expect(await slash.getByRole('option').count()).toBe(3) - expect(await slash.getByText('Auto review', { exact: true }).count()).toBe(0) + expect(await slash.getByText('自动审查', { exact: true }).count()).toBe(0) await page.keyboard.press('Escape') await access.click() await expect.poll(() => page.getByRole('menuitem').allTextContents()) @@ -248,7 +257,7 @@ describe('web e2e: experimental Auto and Full access confirmation', () => { await scaffold.ctx.loader.await() await access.click() await expect.poll(() => page.getByRole('menuitem').allTextContents()) - .toEqual(['仅可查看', '工作区内修改', '完全权限', 'Auto reviewEXP']) + .toEqual(['仅可查看', '工作区内修改', '完全权限', '自动审查EXP']) expect(await access.getAttribute('aria-label')).toBe('访问模式,当前:完全权限') await captureAutoReviewState(page, 'reinstalled-current-session-picker') await page.keyboard.press('Escape') @@ -302,7 +311,7 @@ describe('web e2e: default permission choices', () => { const slash = page.locator('[aria-label="/permission 选项"]') await slash.waitFor() expect(await slash.getByRole('option').count()).toBe(3) - expect(await slash.getByText('Auto review', { exact: true }).count()).toBe(0) + expect(await slash.getByText('自动审查', { exact: true }).count()).toBe(0) await captureAutoReviewState(page, 'default-slash-picker') }) }) diff --git a/apps/web/tests/auto-review-approval.e2e.ts b/apps/web/tests/auto-review-approval.e2e.ts index b430de35f4..890ed25e0e 100644 --- a/apps/web/tests/auto-review-approval.e2e.ts +++ b/apps/web/tests/auto-review-approval.e2e.ts @@ -63,7 +63,7 @@ describe.skipIf(MODE === 'record')('web e2e: cold Auto-review user rejection and const access = page.locator('button[aria-label^="Access mode"]').first() await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 }) - .toBe('Access mode, current: Auto review EXP') + .toBe('Access mode, current: Auto review') const collapsed = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) .split(SEED_ID).join('{{seededId}}') diff --git a/apps/web/tests/auto-review-denial.e2e.ts b/apps/web/tests/auto-review-denial.e2e.ts index b792361be5..58371631a9 100644 --- a/apps/web/tests/auto-review-denial.e2e.ts +++ b/apps/web/tests/auto-review-denial.e2e.ts @@ -92,7 +92,7 @@ describe.skipIf(MODE === 'record')('web e2e: cold Auto-review denial', () => { const access = page.locator('button[aria-label^="Access mode"]').first() await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 }) - .toBe('Access mode, current: Auto review EXP') + .toBe('Access mode, current: Auto review') await captureAutoReviewState(page, 'deny-collapsed') const collapsed = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) diff --git a/apps/web/tests/desktop-onboarding.e2e.ts b/apps/web/tests/desktop-onboarding.e2e.ts index e512813ffc..4f0b527885 100644 --- a/apps/web/tests/desktop-onboarding.e2e.ts +++ b/apps/web/tests/desktop-onboarding.e2e.ts @@ -100,6 +100,16 @@ describe.skipIf(MODE === 'record')('web e2e: App-only desktop onboarding', () => async function snapshot(name: string): Promise { await expect.poll(() => page.locator('[data-desktop-onboarding]').getAttribute('aria-busy')).toBe('false') + const hasIllustration = name.startsWith('welcome') || name.startsWith('credit') + if (hasIllustration) { + const decoded = await page.locator('[class*="illustration"] > img').evaluateAll(async elements => + Promise.all(elements.map(async (element) => { + const image = element as HTMLImageElement + await image.decode() + return image.naturalWidth > 0 && image.naturalHeight > 0 + }))) + expect(decoded).toEqual([true, true]) + } const aria = await captureStableAria(page, '[data-desktop-onboarding]', scaffold.workspaceCwd) await mkdir(EXPECTED, { recursive: true }) await compareOrRefreshGolden(join(EXPECTED, `${name}.expected.md`), aria, MODE) @@ -171,7 +181,7 @@ describe.skipIf(MODE === 'record')('web e2e: App-only desktop onboarding', () => expect(action?.width).toBeCloseTo(240, 0) expect(action!.y - (card!.y + card!.height)).toBeCloseTo(40, 0) } - if (name === 'welcome' || name === 'credit') { + if (hasIllustration) { const illustrations = await page.locator('[class*="illustration"] > img:visible').evaluateAll(elements => elements.map((element) => { const image = element as HTMLImageElement const box = image.getBoundingClientRect() diff --git a/apps/web/tests/expected/access-confirmation/auto-confirmation.expected.md b/apps/web/tests/expected/access-confirmation/auto-confirmation.expected.md index b9967c9722..6c7320a3da 100644 --- a/apps/web/tests/expected/access-confirmation/auto-confirmation.expected.md +++ b/apps/web/tests/expected/access-confirmation/auto-confirmation.expected.md @@ -1,8 +1,8 @@ -- dialog "确认启用 Auto review(实验)?": - - heading "确认启用 Auto review(实验)?" [level=2] +- dialog "确认启用自动审查(实验)?": + - heading "确认启用自动审查(实验)?" [level=2] - button "关闭" - - paragraph: Auto review 不使用沙箱。每次原生工具调用和 PTC 内层调用前,都会由与当前 agent 相同的模型进行审查;审查拒绝的调用由你批准或拒绝。此功能仍属实验性,可能误放行或误拒绝,并会消耗额外 token。 + - paragraph: 自动审查不使用沙箱。每次原生工具调用和 PTC 内层调用前,都会由与当前 agent 相同的模型进行审查;审查拒绝的调用由你批准或拒绝。此功能仍属实验性,可能误放行或误拒绝,并会消耗额外 token。 - checkbox "我已了解这些风险,并愿意继续" - text: 我已了解这些风险,并愿意继续 - button "取消" - - button "启用 Auto review" [disabled] + - button "启用自动审查" [disabled] diff --git a/apps/web/tests/expected/access-confirmation/current-session-picker.expected.md b/apps/web/tests/expected/access-confirmation/current-session-picker.expected.md index 9e942e44b5..688540b20e 100644 --- a/apps/web/tests/expected/access-confirmation/current-session-picker.expected.md +++ b/apps/web/tests/expected/access-confirmation/current-session-picker.expected.md @@ -2,6 +2,4 @@ - menuitem "仅可查看" - menuitem "工作区内修改" - menuitem "完全权限" - - menuitem "Auto review EXP": - - text: Auto review - - superscript: EXP + - menuitem "自动审查 EXP" diff --git a/apps/web/tests/expected/access-confirmation/slash-picker.expected.md b/apps/web/tests/expected/access-confirmation/slash-picker.expected.md index f1b12d28b0..cf54b54669 100644 --- a/apps/web/tests/expected/access-confirmation/slash-picker.expected.md +++ b/apps/web/tests/expected/access-confirmation/slash-picker.expected.md @@ -4,7 +4,7 @@ - option "仅可查看" - option "工作区内修改" [selected] - option "完全权限" - - option "Auto review EXP": - - text: Auto review + - option "自动审查 EXP": + - text: 自动审查 - superscript: EXP - text: 无沙箱运行;每次原生工具调用和 PTC 内层调用前由同一模型进行实验性审查。 diff --git a/apps/web/tests/expected/cot-translation-paid/paid-choices.expected.md b/apps/web/tests/expected/cot-translation-paid/paid-choices.expected.md index 7275ed50b2..44be4fbdaf 100644 --- a/apps/web/tests/expected/cot-translation-paid/paid-choices.expected.md +++ b/apps/web/tests/expected/cot-translation-paid/paid-choices.expected.md @@ -2,7 +2,7 @@ - button "卸载 思考过程机器翻译" [disabled]: 卸载 - switch "启用 思考过程机器翻译" [checked] - heading "思考过程机器翻译" [level=3] -- text: v0.2.1-alpha.1 实验性 +- text: v0.2.1-alpha.2 实验性 - paragraph: - code: "@deepseek-ai/dsh-experimental-cot-translation-bundle" - paragraph: 机器翻译展开的思考内容;原文会发送给所选服务 @@ -39,4 +39,4 @@ - term: 代码来源 - definition: 内置 - term: 当前版本 -- definition: 0.2.1-alpha.1 +- definition: 0.2.1-alpha.2 diff --git a/apps/web/tests/expected/cot-translation/preferences.expected.md b/apps/web/tests/expected/cot-translation/preferences.expected.md index f4735cc02e..14f544b50b 100644 --- a/apps/web/tests/expected/cot-translation/preferences.expected.md +++ b/apps/web/tests/expected/cot-translation/preferences.expected.md @@ -2,7 +2,7 @@ - button "卸载 思考过程机器翻译" [disabled]: 卸载 - switch "启用 思考过程机器翻译" [checked] - heading "思考过程机器翻译" [level=3] -- text: v0.2.1-alpha.1 实验性 +- text: v0.2.1-alpha.2 实验性 - paragraph: - code: "@deepseek-ai/dsh-experimental-cot-translation-bundle" - paragraph: 机器翻译展开的思考内容;原文会发送给所选服务 @@ -36,4 +36,4 @@ - term: 代码来源 - definition: 内置 - term: 当前版本 -- definition: 0.2.1-alpha.1 +- definition: 0.2.1-alpha.2 diff --git a/apps/web/tests/remote-settings.e2e.ts b/apps/web/tests/remote-settings.e2e.ts new file mode 100644 index 0000000000..8f7ea86e1a --- /dev/null +++ b/apps/web/tests/remote-settings.e2e.ts @@ -0,0 +1,129 @@ +// A trusted remote browser uses the same Host settings service as loopback: +// welcome acknowledgement, model configuration and appearance persist in the +// Host profile patch across reloads and independent browser contexts. +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import type { Browser, Locator, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { + acknowledgeReloadConnectionLoss, + launchWebScaffold, watchConsole, webSnapshotMode, WELCOME_NOTICE_COPY, + type WebScaffold, +} from './scaffold.ts' +import { openSettings, ZH_BROWSER_LOCALE } from './support.ts' + +const MODE = webSnapshotMode() + +/** + * A loading welcome store hides the notice, so absence is conclusive only + * after the shared settings read has enabled the Models view. + * @param page - the loaded page. + * @returns the open settings dialog on its Models view. + */ +async function expectOnboardingSettled(page: Page): Promise { + await page.waitForSelector('#root', { timeout: 30_000 }) + await page.getByRole('textbox', { name: '选择工作区' }).waitFor({ timeout: 30_000 }) + await openSettings(page, 'zh') + const dialog = page.getByRole('dialog', { name: '设置' }) + await dialog.getByRole('button', { name: '模型', exact: true }).click() + await dialog.getByRole('button', { name: '添加模型提供商', exact: true }).waitFor({ timeout: 15_000 }) + expect(await page.getByRole('dialog', { name: WELCOME_NOTICE_COPY.zh.title }).count()).toBe(0) + return dialog +} + +describe.skipIf(MODE === 'record')('web e2e: trusted remote host preferences', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + scaffold = await launchWebScaffold({ + remoteAuthority: 'remote.localhost', + welcomeNoticePending: true, + }) + browser = await chromium.launch() + page = await browser.newPage({ + viewport: { width: 1440, height: 960 }, + locale: ZH_BROWSER_LOCALE, + }) + tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await page.waitForSelector('#root', { timeout: 30_000 }) + }, 120_000) + + afterAll(async () => { + await browser?.close() + await scaffold?.close() + }) + + /** Reload the page and absorb the one expected reload-time connection loss. */ + async function reloadPage(): Promise { + const warnings = tripwire.warnings.length + await page.reload({ waitUntil: 'load' }) + acknowledgeReloadConnectionLoss(tripwire, warnings) + } + + it('persists welcome acknowledgement, model configuration, and appearance across reloads and browser contexts', async () => { + const welcome = page.getByRole('dialog', { name: WELCOME_NOTICE_COPY.zh.title }) + await welcome.waitFor({ timeout: 15_000 }) + expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true) + + await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click() + await welcome.waitFor({ state: 'detached', timeout: 15_000 }) + await expect.poll( + () => page.locator('#root').evaluate(root => (root as HTMLElement).inert), + { timeout: 15_000 }, + ).toBe(false) + const settingsPath = join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml') + await expect.poll(() => readFile(settingsPath, 'utf8'), { timeout: 10_000 }) + .toContain('welcomeNoticeVersion:') + + // The acknowledged version is durable, so the notice stays away after a reload. + await reloadPage() + const dialog = await expectOnboardingSettled(page) + + await page.emulateMedia({ colorScheme: 'light' }) + await dialog.getByRole('button', { name: '添加模型提供商', exact: true }).click() + await dialog.getByLabel('提供商', { exact: true }).selectOption('minimax-cn') + await dialog.getByRole('button', { name: '保存', exact: true }).click() + await dialog.getByText('已保存 minimax-cn。', { exact: true }).waitFor({ timeout: 15_000 }) + await expect.poll(() => readFile(settingsPath, 'utf8'), { timeout: 10_000 }) + .toContain('minimax-cn: {}') + + await dialog.getByRole('button', { name: '通用设置', exact: true }).click() + await dialog.getByRole('button', { name: '深色', exact: true }).click() + await expect.poll(() => page.locator('body').getAttribute('data-ds-dark-theme')).toBe('') + await expect.poll(() => readFile(settingsPath, 'utf8'), { timeout: 10_000 }) + .toContain('preference: dark') + + await reloadPage() + await expectOnboardingSettled(page) + await dialog.getByRole('button', { name: '编辑 minimax-cn', exact: true }).waitFor({ timeout: 15_000 }) + await expect.poll(() => page.locator('body').getAttribute('data-ds-dark-theme'), { timeout: 15_000 }).toBe('') + expect(tripwire.warnings).toEqual([]) + expect(tripwire.pageErrors).toEqual([]) + + const context = await browser.newContext({ + viewport: { width: 1440, height: 960 }, + locale: ZH_BROWSER_LOCALE, + colorScheme: 'light', + }) + try { + const freshPage = await context.newPage() + const freshTripwire = watchConsole(freshPage) + await freshPage.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + const freshDialog = await expectOnboardingSettled(freshPage) + await freshDialog.getByRole('button', { name: '编辑 minimax-cn', exact: true }).waitFor({ timeout: 15_000 }) + await expect.poll( + () => freshPage.locator('body').getAttribute('data-ds-dark-theme'), + { timeout: 15_000 }, + ).toBe('') + expect(freshTripwire.warnings).toEqual([]) + expect(freshTripwire.pageErrors).toEqual([]) + } finally { + await context.close() + } + }, 180_000) +}) diff --git a/apps/web/tests/remote-welcome.e2e.ts b/apps/web/tests/remote-welcome.e2e.ts deleted file mode 100644 index b7b013dc2d..0000000000 --- a/apps/web/tests/remote-welcome.e2e.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Trusted non-loopback Web access cannot call the loopback-only settings API; -// the notice therefore advances for this browser process and returns on reload. -import type { Browser, Page } from 'playwright' -import { chromium } from 'playwright' -import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { - acknowledgeReloadConnectionLoss, launchWebScaffold, watchConsole, webSnapshotMode, - WELCOME_NOTICE_COPY, - type WebScaffold, -} from './scaffold.ts' -import { ZH_BROWSER_LOCALE } from './support.ts' - -const MODE = webSnapshotMode() - -describe.skipIf(MODE === 'record')('web e2e: remote welcome notice', () => { - let scaffold: WebScaffold - let browser: Browser - let page: Page - let tripwire: ReturnType - - beforeAll(async () => { - scaffold = await launchWebScaffold({ - remoteAuthority: 'remote.localhost', - welcomeNoticePending: true, - }) - browser = await chromium.launch() - page = await browser.newPage({ - viewport: { width: 1440, height: 960 }, - locale: ZH_BROWSER_LOCALE, - }) - tripwire = watchConsole(page) - await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) - await page.waitForSelector('#root', { timeout: 30_000 }) - }, 120_000) - - afterAll(async () => { - await browser?.close() - await scaffold?.close() - }) - - it('advances process-locally and presents the notice again after reload', async () => { - const welcome = page.getByRole('dialog', { name: WELCOME_NOTICE_COPY.zh.title }) - await welcome.waitFor({ timeout: 15_000 }) - expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true) - - await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click() - await welcome.waitFor({ state: 'detached', timeout: 15_000 }) - await expect.poll( - () => page.locator('#root').evaluate(root => (root as HTMLElement).inert), - { timeout: 15_000 }, - ).toBe(false) - - const reloadWarnings = tripwire.warnings.length - await page.reload({ waitUntil: 'load' }) - acknowledgeReloadConnectionLoss(tripwire, reloadWarnings) - await welcome.waitFor({ timeout: 15_000 }) - expect(tripwire.warnings).toEqual([]) - expect(tripwire.pageErrors).toEqual([]) - }, 60_000) -}) diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 8009cf4453..f1524ed0a5 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -887,10 +887,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise Implementation internals — click to expand -`watchConfig()` registers an awaited configuration handler. `runExclusive()` serializes configuration changes and Loader updates with automatic reloads and rejects nested transactions. Package installation and removal run outside this queue. HMR does not acquire the package writer lock; manifest notifications reload only when the ordered `dsh.profile.bundles` list changes. Profile and home patch changes also trigger recomposition. File events received during a configuration transaction are processed afterward. Include refreshes and profile reconciliation reach plugins through ordinary Loader entry updates; Loader commits volatile-only changes in place. +`watchConfig()` registers an awaited configuration handler and watches only its target path and required parent directories; sibling files, including temporary writer locks, are excluded before native watcher creation. `runExclusive()` serializes configuration changes and Loader updates with automatic reloads and rejects nested transactions. Package installation and removal run outside this queue. HMR does not acquire the package writer lock; manifest notifications reload only when the ordered `dsh.profile.bundles` list changes. Profile and home patch changes also trigger recomposition. File events received during a configuration transaction are processed afterward. Include refreshes and profile reconciliation reach plugins through ordinary Loader entry updates; Loader commits volatile-only changes in place. App-boot owns profile parsing and patch precedence. HMR reads the launcher’s data-only `profileContext`, registers the profile manifest and both user patch watches during initialization, and waits for application readiness before processing changes. Its disposal closes the watchers and cancels reloads waiting for startup. Configuration watchers start outside the active transaction context so later notifications can enter the queue. diff --git a/packages/boot/hmr/README.zh.md b/packages/boot/hmr/README.zh.md index ba99733ccf..a7a9f84b1b 100644 --- a/packages/boot/hmr/README.zh.md +++ b/packages/boot/hmr/README.zh.md @@ -54,7 +54,7 @@ Chokidar 选项(包括轮询)保持原有含义。精确配置监听同时
实现细节——点击展开 -`watchConfig()` 注册会被等待的配置处理器。`runExclusive()` 将配置变更、Loader 更新与自动重载串行化,并拒绝嵌套事务。包安装和删除在该队列之外执行。HMR 不获取包操作写锁;manifest 通知仅在有序的 `dsh.profile.bundles` 列表变化时触发重载。profile 与 home patch 变化也会触发重新组合。配置事务期间收到的文件事件在事务结束后处理。 Include 刷新和 profile 重载都通过普通的 Loader 条目更新到达插件;仅 volatile 变化由 Loader 就地提交。 +`watchConfig()` 注册会被等待的配置处理器,仅监听目标路径及必要的父目录;同级文件(包括临时写锁)在创建原生 watcher 前即被排除。`runExclusive()` 将配置变更、Loader 更新与自动重载串行化,并拒绝嵌套事务。包安装和删除在该队列之外执行。HMR 不获取包操作写锁;manifest 通知仅在有序的 `dsh.profile.bundles` 列表变化时触发重载。profile 与 home patch 变化也会触发重新组合。配置事务期间收到的文件事件在事务结束后处理。 Include 刷新和 profile 重载都通过普通的 Loader 条目更新到达插件;仅 volatile 变化由 Loader 就地提交。 App-boot 负责 profile 解析和 patch 优先级规则。HMR 读取启动器提供的纯数据 `profileContext`,在初始化时注册 profile manifest 和两份用户 patch 的监听,并等待应用就绪后处理更改。销毁 HMR 时会关闭监听器并取消等待启动的重载。配置监听器在当前事务上下文之外启动,使后续通知可以进入队列。 diff --git a/packages/boot/hmr/package.json b/packages/boot/hmr/package.json index 9460462734..403fe5ed76 100644 --- a/packages/boot/hmr/package.json +++ b/packages/boot/hmr/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hmr", "description": "Coordinated module and profile configuration hot reload", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -37,7 +37,7 @@ }, "dependencies": { "@babel/code-frame": "^7.29.0", - "chokidar": "^4.0.3", + "chokidar": "5.0.0", "node-addon-require-builtin": "^0.1.9", "picomatch": "^4.0.7", "@deepseek-ai/schemastery": "workspace:~" diff --git a/packages/boot/hmr/src/watch-config.ts b/packages/boot/hmr/src/watch-config.ts index 6c62610dec..41815b5d8d 100644 --- a/packages/boot/hmr/src/watch-config.ts +++ b/packages/boot/hmr/src/watch-config.ts @@ -1,5 +1,5 @@ /** Exact-path watching for live profile patch files outside Cordis module roots. */ -import { dirname, relative, resolve } from 'node:path' +import { dirname, relative, resolve, sep } from 'node:path' import { realpath, stat } from 'node:fs/promises' import { watch, type ChokidarOptions } from 'chokidar' import type { Context } from '@deepseek-ai/cordis' @@ -25,7 +25,8 @@ async function findWatchRoot(filename: string): Promise<{ filename: string; root } /** - * Watch one patch path, including missing parents, and serialize refresh callbacks. + * Watch one patch path and its parent chain without subscribing to sibling files or directories. + * Missing parents are observed as they appear; refresh callbacks run serially. * @param ctx Context that owns watcher disposal and receives refresh failures. * @param filename Absolute patch-file path. * @param options Deployment watcher options; configuration watches enable write stabilization by default. @@ -46,6 +47,10 @@ export async function watchConfig( const watcher = watch(target.root, { // Stabilized events bypass Chokidar's lossy 50 ms change-event throttle. awaitWriteFinish: true, ...watchOptions, depth: target.depth, ignoreInitial: false, + ignored: (path) => { + const remainder = relative(resolve(path), target.filename) + return remainder === '..' || remainder.startsWith(`..${sep}`) + }, }) paths.add(target.filename) const state = { dirty: false } diff --git a/packages/boot/hmr/tests/watch-config.spec.ts b/packages/boot/hmr/tests/watch-config.spec.ts index 7d0caf035b..e187d20a07 100644 --- a/packages/boot/hmr/tests/watch-config.spec.ts +++ b/packages/boot/hmr/tests/watch-config.spec.ts @@ -10,10 +10,13 @@ import { watchConfig } from '../src/watch-config.ts' import Hmr from '../src/index.ts' import Loader from '@deepseek-ai/cordis-plugin-loader' import Timer from '@deepseek-ai/cordis-plugin-timer' +import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write' import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest' import { FSWatcher, type ChokidarOptions } from 'chokidar' -const configWatch = vi.hoisted(() => ({ create: undefined as ((options?: ChokidarOptions) => FSWatcher) | undefined })) +const configWatch = vi.hoisted(() => ({ + create: undefined as ((paths: string | string[], options?: ChokidarOptions) => FSWatcher) | undefined, +})) vi.mock('node:fs', async (importOriginal) => { const native = await importOriginal() return { ...native, realpathSync: vi.fn(native.realpathSync) } @@ -25,7 +28,7 @@ vi.mock('node:fs/promises', async (importOriginal) => { vi.mock('chokidar', async (importOriginal) => { const native = await importOriginal() return { ...native, watch: (paths: string | string[], options?: ChokidarOptions) => - configWatch.create === undefined ? native.watch(paths, options) : configWatch.create(options) } + configWatch.create === undefined ? native.watch(paths, options) : configWatch.create(paths, options) } }) /** Every per-test tree root, removed once the booted watcher has been disposed. */ @@ -80,6 +83,42 @@ describe('HMR exact config paths', () => { for (const root of hmrRoots.splice(0)) rmSync(root, { recursive: true, force: true }) }) + it.for([false, true])('excludes siblings while observing config replacement through an alias (missing parents: %s)', async (missingParents, test) => { + const root = mkdtempSync(join(tmpdir(), 'dsh-hmr-config-scope-')) + onTestFinished(() => { rmSync(root, { recursive: true, force: true }) }) + const target = join(root, 'target') + const dir = join(root, 'alias') + mkdirSync(target) + symlinkSync(target, dir, process.platform === 'win32' ? 'junction' : 'dir') + const filename = join(dir, ...(missingParents ? ['later', 'nested'] : []), 'package.json') + writeFileSync(join(dir, 'package.json.lock'), 'held') + mkdirSync(join(dir, 'unrelated')) + writeFileSync(join(dir, 'unrelated', 'config.yml'), 'ignored') + if (!missingParents) writeFileSync(filename, 'initial') + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + let watcher!: FSWatcher + const previousFactory = configWatch.create + onTestFinished(() => { configWatch.create = previousFactory }) + configWatch.create = (paths, options) => { + watcher = new FSWatcher(options) + return watcher.add(paths) + } + const observed: string[] = [] + await watchConfig(ctx, filename, {}, () => { observed.push(readFileSync(filename, 'utf8')) }) + const watchedEntries = () => Object.values(watcher.getWatched()).flat() + expect(watchedEntries()).not.toContain('package.json.lock') + expect(watchedEntries()).not.toContain('unrelated') + ensureNativeWatchLive(dir) + await writeFileAtomic(filename, 'created', { mode: 0o600 }) + await eventually(() => observed.includes('created'), 'created config was not observed', test.task.timeout) + await writeFileAtomic(filename, 'replaced', { mode: 0o600 }) + await eventually(() => observed.includes('replaced'), 'replaced config was not observed', test.task.timeout) + expect(watchedEntries()).toContain('package.json') + expect(watchedEntries()).not.toContain('package.json.lock') + expect(watchedEntries()).not.toContain('unrelated') + }) + it.each(['relative', 'absolute'])('observes %s module roots through a filesystem alias', { timeout: 30_000 }, async (rootKind) => { const target = mkdtempSync(join(tmpdir(), 'dsh-hmr-module-canonical-')) const alias = `${target}-alias` @@ -252,7 +291,7 @@ describe('HMR exact config paths', () => { let watcher!: FSWatcher const previousFactory = configWatch.create onTestFinished(() => { configWatch.create = previousFactory }) - configWatch.create = (options) => { + configWatch.create = (_paths, options) => { watcher = new FSWatcher(options) // Use Chokidar's real normalization with deterministic event delivery. Reflect.set(watcher, '_readyEmitted', true) diff --git a/packages/boot/plugin-manager/package.json b/packages/boot/plugin-manager/package.json index c6fc6af78a..d75e0ce99f 100644 --- a/packages/boot/plugin-manager/package.json +++ b/packages/boot/plugin-manager/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plugin-manager", "description": "Current-profile plugin and bundle management shared by dsh CLI, Web and agent tools", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -61,7 +61,7 @@ "@deepseek-ai/schemastery": "workspace:~", "execa": "^10.0.1", "yaml": "^2.9.0", - "zod": "^4.4.3", + "zod": "4.4.3", "@deepseek-ai/dsh-util-values": "workspace:*" }, "peerDependencies": { diff --git a/packages/browser-use/browser-use/package.json b/packages/browser-use/browser-use/package.json index 8178f5d01d..88f9365e81 100644 --- a/packages/browser-use/browser-use/package.json +++ b/packages/browser-use/browser-use/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-browser-use", "description": "Exclusive named browser-use provider registration", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/acp-app/package.json b/packages/bundle/acp-app/package.json index d1d12860c2..525414a75b 100644 --- a/packages/bundle/acp-app/package.json +++ b/packages/bundle/acp-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-acp-app", "description": "The dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-base", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index be152900d3..fa80fcfb55 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-base", "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.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/headless/package.json b/packages/bundle/headless/package.json index cf2dc9f38f..1eea38a9f0 100644 --- a/packages/bundle/headless/package.json +++ b/packages/bundle/headless/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-headless", "description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/sdk-app/README.i18n.yaml b/packages/bundle/sdk-app/README.i18n.yaml index ca0a12fc89..fb39b99573 100644 --- a/packages/bundle/sdk-app/README.i18n.yaml +++ b/packages/bundle/sdk-app/README.i18n.yaml @@ -15,8 +15,8 @@ en: 4e53164159b5ce41 zh: 89582dc0294d0776 /deepseek-ai-dsh-sdk-app/use-this-package: - en: 465be8e510a86913 - zh: 07e2e1efba1d108b + en: 63573087d1db9aff + zh: 638612ac3f0da2c0 /deepseek-ai-dsh-sdk-app/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 diff --git a/packages/bundle/sdk-app/README.md b/packages/bundle/sdk-app/README.md index e2615fc435..a149ec1254 100644 --- a/packages/bundle/sdk-app/README.md +++ b/packages/bundle/sdk-app/README.md @@ -33,7 +33,7 @@ The startup provider binds stdin EOF to the launcher's bounded successful shutdo The SDK uses the base `read`, `write`, and `edit` defaults. To add `str_replace_editor`, use the explicit insertion patch in the [base configuration guide](../base/README.md#use-this-package). The standalone `sdk-minimal` profile owns its separate tool selection. -The packaged Python runtime enables the [Office runtime query and skills](../../skill/tool-workspace-dependencies/README.md#use-this-package) by default. `DSH_PRIMARY_RUNTIME` overrides its bundled payload; an empty value disables both rows. Source launches without a carrier default remain opt-in. The tool reads the payload in place. Profile patches can disable `skill-office` or replace its `assetRoot` independently of `workspace-dependencies`; filesystem skills with the same name take precedence over bundled skills. See the [runtime package](../../../python/sdk-runtime/README.md) for packaging and configuration. +The Python runtime exposes explicitly downloaded authoring resources and Office sidecars to this profile. `DSH_PRIMARY_RUNTIME` overrides the authoring payload; an empty value disables its query. Office skills can use either resource independently. Profile patches can disable `skill-office` or replace its `assetRoot` independently of `workspace-dependencies`; filesystem skills with the same name take precedence. See the [runtime package](../../../python/sdk-runtime/README.md) for downloads and configuration. ----- diff --git a/packages/bundle/sdk-app/README.zh.md b/packages/bundle/sdk-app/README.zh.md index 610f4bef5c..f244072935 100644 --- a/packages/bundle/sdk-app/README.zh.md +++ b/packages/bundle/sdk-app/README.zh.md @@ -33,7 +33,7 @@ kind: "package-bundle" SDK 使用 base 默认提供的 `read`、`write` 和 `edit`。要添加 `str_replace_editor`,请使用 [base 配置指南](../base/README.zh.md#use-this-package)中的显式插入 patch。独立的 `sdk-minimal` profile 自行决定其工具选择。 -打包的 Python runtime 默认启用 [Office 运行时查询与 skills](../../skill/tool-workspace-dependencies/README.zh.md#use-this-package)。`DSH_PRIMARY_RUNTIME` 覆盖随包 payload;空值禁用两行。没有载体默认路径的源码启动仍需显式启用。工具原位读取 payload。profile patch 可独立于 `workspace-dependencies` 禁用 `skill-office` 或替换其 `assetRoot`;同名文件系统 skills 优先于随包 skills。打包与配置见 [runtime 包](../../../python/sdk-runtime/README.zh.md)。 +Python 运行时向本 profile 提供显式下载的创作资源和 Office sidecar。`DSH_PRIMARY_RUNTIME` 覆盖创作载荷;空值禁用其查询。Office skills 可独立使用任一种资源。Profile patch 可独立于 `workspace-dependencies` 禁用 `skill-office` 或替换其 `assetRoot`;同名文件系统 skills 优先。下载与配置见[运行时包](../../../python/sdk-runtime/README.zh.md)。 ----- diff --git a/packages/bundle/sdk-app/cordis.patch.yml b/packages/bundle/sdk-app/cordis.patch.yml index 1c0beaae95..004a81ce9b 100644 --- a/packages/bundle/sdk-app/cordis.patch.yml +++ b/packages/bundle/sdk-app/cordis.patch.yml @@ -29,7 +29,8 @@ - id: skill-office name: '@deepseek-ai/dsh-skill-office' - disabled: !!js "!(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME)" + disabled: !!js "!(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME) && !process.env.DSH_BUNDLED_OFFICE_CLI" config: - assetRoot: !!js "process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', '..', 'office-skills')" - node: !!js "process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', 'dependencies', 'node', 'bin', process.platform === 'win32' ? 'node.exe' : 'node')" + assetRoot: !!js "process.env.DSH_BUNDLED_OFFICE_SKILLS ?? process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', '..', 'office-skills')" + cli: !!js "process.env.DSH_BUNDLED_OFFICE_CLI === '' ? false : process.env.DSH_BUNDLED_OFFICE_CLI" + node: !!js "process.env.DSH_BUNDLED_OFFICE_NODE ?? process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', 'dependencies', 'node', 'bin', process.platform === 'win32' ? 'node.exe' : 'node')" diff --git a/packages/bundle/sdk-app/package.json b/packages/bundle/sdk-app/package.json index fdfc1d2ad3..389582f50c 100644 --- a/packages/bundle/sdk-app/package.json +++ b/packages/bundle/sdk-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-app", "description": "The dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-base", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json index df59eaae4d..e952695405 100644 --- a/packages/bundle/sdk-minimal/package.json +++ b/packages/bundle/sdk-minimal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-minimal", "description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessions", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index 81c3782121..deb1a2ac05 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-app", "description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/AGENTS.md b/packages/client/AGENTS.md index 2d39d6b3ca..c6f92acce6 100644 --- a/packages/client/AGENTS.md +++ b/packages/client/AGENTS.md @@ -73,11 +73,11 @@ Client business code may statically read `process.env.DSH_CLIENT_*`; every refer ## Shared modules and the module graph -A dynamic browser half either carries a module privately or requests the shared module-table identity. The client baseline is centralized in [`web/src/platform.ts`](web/src/platform.ts): `PLATFORM_MODULES` names shell-seeded React, Cordis, and static Client libraries; `PRELOADED_CLIENT_EXTERNALS` is reserved for dynamic rows whose factories must arrive before shell boot and is empty when no such row exists. +A dynamic browser half either carries a module privately or requests the shared module-table identity. The client baseline is centralized in [`web/src/platform.ts`](web/src/platform.ts): `PLATFORM_MODULES` names shell-seeded React, Cordis, selected third-party libraries, and static Client libraries; `PRELOADED_CLIENT_EXTERNALS` is reserved for dynamic rows whose factories must arrive before shell boot and is empty when no such row exists. -1. **Baseline externals are implicit for every dynamic bundle.** Do not repeat React, Cordis, `client/store`, `ui-primitives`, `ui-slots`, or `ui-dockkit` in package manifests. +1. **Baseline externals are implicit for every dynamic bundle.** Do not repeat entries from `PLATFORM_MODULES` in `dsh.client.external`; npm dependency declarations still follow the dependency rules above. 2. **`dsh.client.external` is not a feature-plugin dependency mechanism.** Only infrastructure, transport, or generated assembly may add a package-specific non-baseline value request whose dynamic row must be materialized through the module table. Declare the exact import specifier; only a trailing `/client` aliases the package row. -3. **Silence means a private copy.** Ordinary third-party implementation libraries may be bundled independently. A value reached only through `import type` is erased and creates no request. +3. **Silence means a private copy.** Third-party implementation libraries outside the baseline may be bundled independently. A value reached only through `import type` is erased and creates no request. 4. **A request has two possible suppliers.** A dynamic package supplies its own row; `PLATFORM_MODULES` supplies an exact static-table key. There is no `dsh.client.provide` alias protocol. 5. **Validate both sides.** The dynamic build preset externalizes the baseline and rejects undeclared workspace value imports; [`verify-client-packages`](../../scripts/verify-client-packages.ts) rejects malformed or redundant requests, missing suppliers, and synchronous request cycles. 6. **Package-local dynamic chunks are self-contained.** Source uses `import()` and the shared preset emits `require.async("./client..js")`; an entry or chunk must not retain a synchronous relative `require("./client*.js")`. Keep every static relative dependency inside its owning output chunk rather than relying on a sibling-chunk graph the runtime does not support. diff --git a/packages/client/connection/package.json b/packages/client/connection/package.json index b233722f10..bcd37df919 100644 --- a/packages/client/connection/package.json +++ b/packages/client/connection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-connection", "description": "Authenticated RPC transport and generation lifecycle", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -36,7 +36,7 @@ "dependencies": { "@deepseek-ai/dsh-credentials": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "files": [ "lib/index.js", diff --git a/packages/client/file-upload/package.json b/packages/client/file-upload/package.json index eecaa28d28..027de0b95a 100644 --- a/packages/client/file-upload/package.json +++ b/packages/client/file-upload/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-file-upload", "description": "Agent-scoped browser file upload, streaming intake, and staged receipt service", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -62,7 +62,7 @@ "lib/typert.remote-client.d.ts" ], "dependencies": { - "zod": "^4.4.3", + "zod": "4.4.3", "@deepseek-ai/dsh-typert-protocol": "workspace:*", "@deepseek-ai/dsh-brand": "workspace:*" }, diff --git a/packages/client/hmr/package.json b/packages/client/hmr/package.json index 373c176123..f87c12fca4 100644 --- a/packages/client/hmr/package.json +++ b/packages/client/hmr/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-hmr", "description": "Web client graph synchronization and rebuilt-bundle reload transport", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/locale/README.i18n.yaml b/packages/client/locale/README.i18n.yaml index 4b363e6971..2e2736659e 100644 --- a/packages/client/locale/README.i18n.yaml +++ b/packages/client/locale/README.i18n.yaml @@ -9,8 +9,8 @@ en: 9365090d78467dea zh: 74b6fbb9769d795f /deepseek-ai-dsh-client-locale/summary: - en: a548ff3095e8d7d8 - zh: fec6ad47291018f8 + en: d8cc5ae25160ca20 + zh: e8fc2b9fb663cdee /deepseek-ai-dsh-client-locale/table-of-contents: en: d152484eb41ac6b4 zh: bda879a892f240f0 @@ -30,8 +30,8 @@ en: aa4e9c70523339cc zh: 3359eddc4e9ef15e /deepseek-ai-dsh-client-locale/use-this-package/what-the-host-half-does: - en: 8fa91333f7fc3387 - zh: 789a9cbb3eace3b0 + en: 784035ca4b4838b8 + zh: 4a258795f408528f /deepseek-ai-dsh-client-locale/understand-the-implementation: en: 288d8db39f9acfc5 zh: d65cf2ffae8ad35c @@ -39,8 +39,8 @@ en: c2e0ab8bb6bcb26a zh: 508dec4db2c0fde6 /deepseek-ai-dsh-client-locale/understand-the-implementation/preference-resolution: - en: 4bc9dffa4629f537 - zh: f51633c658b8d25b + en: d602d957654641c6 + zh: 2db822b746a6471d /deepseek-ai-dsh-client-locale/understand-the-implementation/dictionary-lookup: en: f8dcc4035830e738 zh: 8452ac63ee148c61 diff --git a/packages/client/locale/README.md b/packages/client/locale/README.md index 930c715a47..8c52703748 100644 --- a/packages/client/locale/README.md +++ b/packages/client/locale/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Use `dsh-client-locale` to switch the web GUI between the shipped English and Chinese locales or languages added by client plugins. User selections take effect immediately; loopback pages persist them in `$DSH_HOME/cordis.patch.yml`, while non-loopback pages keep them only for the current process. New browsers use the first supported language requested by the browser until an allowed stored preference arrives. Plugin authors add typed namespace dictionaries and translate through the public locale API; slot-rendered copy updates without a reload. +Use `dsh-client-locale` to switch the web GUI between the shipped English and Chinese locales or languages added by client plugins. User selections take effect immediately; every authenticated browser persists them as `locale.preference` in `$DSH_HOME/cordis.patch.yml` through the Host settings service. New browsers use the first supported language requested by the browser until the Host read delivers a stored preference. Plugin authors add typed namespace dictionaries and translate through the public locale API; slot-rendered copy updates without a reload. ## Table of Contents @@ -67,7 +67,7 @@ An external id is a non-empty ASCII BCP 47-style tag. Its fallback must already ### What the Host half does -The Host persists the preference through the settings service on loopback pages. The Client deliberately withholds that settings scope on non-loopback pages, so their locale selection remains process-local even though Connection authenticates every API method. +Every authenticated browser persists the preference through the Host settings service under the shared [settings scope policy](../ui-settings/README.md). ----- @@ -85,7 +85,7 @@ One `LocaleRuntime` owns the preference and the dictionary registry, and is itse ### Preference resolution -The provisional locale comes from the browser (`navigator.languages` matched by full tag and then primary subtag, English as the fallback), standing in until the allowed Host-backed settings scope delivers its stored preference. The Host read runs after plugin activation so an unavailable or withheld settings scope cannot block the page, and the result replaces the provisional value live. A stored external locale waits for its definition to register. `setLocale` is the only write entry; it persists even when the id already matches the active locale, because the active value may be provisional and must survive a different browser sharing the same home. +The provisional locale comes from the browser (`navigator.languages` matched by full tag and then primary subtag, English as the fallback), standing in until the Host settings scope delivers its stored preference. The Host read runs after plugin activation so an unavailable settings scope cannot block the page, and the result replaces the provisional value live. A stored external locale waits for its definition to register. `setLocale` is the only write entry; it persists even when the id already matches the active locale, because the active value may be provisional and must survive a different browser sharing the same home. ### Dictionary lookup diff --git a/packages/client/locale/README.zh.md b/packages/client/locale/README.zh.md index 6298fac88f..0527984591 100644 --- a/packages/client/locale/README.zh.md +++ b/packages/client/locale/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -使用 `dsh-client-locale` 可在 web GUI 中切换内置的英文和中文 locale,或 client 插件添加的语言。用户选择会立即生效;loopback 页面把选择持久化到 `$DSH_HOME/cordis.patch.yml`,非 loopback 页面则只为当前进程保留选择。全新浏览器会使用浏览器请求的第一个受支持语言,直到允许读取的已存储偏好到达。插件作者可添加类型化命名空间字典,并通过公开 locale API 翻译;经 slot 渲染的文案无需重新加载即可随语言切换更新。 +使用 `dsh-client-locale` 可在 web GUI 中切换内置的英文和中文 locale,或 client 插件添加的语言。用户选择会立即生效;每个已认证浏览器都通过 Host settings 服务把选择以 `locale.preference` 持久化到 `$DSH_HOME/cordis.patch.yml`。全新浏览器会使用浏览器请求的第一个受支持语言,直到 Host 读取送达已存储偏好。插件作者可添加类型化命名空间字典,并通过公开 locale API 翻译;经 slot 渲染的文案无需重新加载即可随语言切换更新。 ## 目录 @@ -67,7 +67,7 @@ export function apply(ctx) { ### Host 半侧做什么 -Host 通过 settings 服务为 loopback 页面持久化偏好。Client 会刻意拒绝非 loopback 页面使用该 settings scope,因此即使 Connection 认证所有 API 方法,它们的 locale 选择仍只存在于进程内。 +每个已认证浏览器都遵循共享的 [settings scope 策略](../ui-settings/README.zh.md),通过 Host settings 服务持久化偏好。 ----- @@ -85,7 +85,7 @@ Host 通过 settings 服务为 loopback 页面持久化偏好。Client 会刻意 ### 偏好解析 -临时 locale 来自浏览器(`navigator.languages` 先按完整标签、再按主语言子标签匹配,以英文作为回退),在允许使用的 Host-backed settings scope 送达其存储偏好之前生效。Host 读取在插件激活后运行,因此 settings scope 不可用或被拒绝都不会阻塞页面,结果会实时替换临时值。已存储的外部 locale 会等待其定义注册。`setLocale` 是唯一写入入口;即使 id 已与生效中的 locale 匹配也会持久化,因为生效中的值可能是临时的,必须能供共享同一 home 的其他浏览器继续使用。 +临时 locale 来自浏览器(`navigator.languages` 先按完整标签、再按主语言子标签匹配,以英文作为回退),在 Host settings scope 送达其存储偏好之前生效。Host 读取在插件激活后运行,因此 settings scope 不可用不会阻塞页面,结果会实时替换临时值。已存储的外部 locale 会等待其定义注册。`setLocale` 是唯一写入入口;即使 id 已与生效中的 locale 匹配也会持久化,因为生效中的值可能是临时的,必须能在共享同一 home 的其他浏览器上存活。 ### 字典查找 diff --git a/packages/client/locale/package.json b/packages/client/locale/package.json index a1577330a2..7027a879cb 100644 --- a/packages/client/locale/package.json +++ b/packages/client/locale/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-locale", "description": "Locale plugin: Host-backed preference, extensible language catalog, browser fallback, and typed built-in dictionaries", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/modules/README.i18n.yaml b/packages/client/modules/README.i18n.yaml index 4ab13cc6e9..ebb8ecbb3a 100644 --- a/packages/client/modules/README.i18n.yaml +++ b/packages/client/modules/README.i18n.yaml @@ -27,8 +27,8 @@ en: 45740a56e931d094 zh: b3362af77c804459 /deepseek-ai-dsh-client-modules/use-this-package/sharing-modules: - en: 0aaf0693db8bb203 - zh: 9f4738cdc6b55c26 + en: 1778b5a63ecde5e8 + zh: 537b4f8c755af8ad /deepseek-ai-dsh-client-modules/use-this-package/build-requirements: en: b6a4d3b901c89efe zh: 615b5f3eec41f281 diff --git a/packages/client/modules/README.md b/packages/client/modules/README.md index 3388eb2ae4..8224ab0ac7 100644 --- a/packages/client/modules/README.md +++ b/packages/client/modules/README.md @@ -43,7 +43,7 @@ An open Web page follows the Host's complete module graph through the HMR transp ### Sharing modules -The shell seeds a frozen module table (`PLATFORM_MODULES`: React, Cordis, and static UI libraries); every dynamic bundle resolves its externals against exactly that baseline. `dsh.client.external` adds only exact non-baseline requests, each answered by the dynamic package row it names or an exact static-table key. Type-only imports are erased and create no request. Composition rejects malformed requests, missing suppliers, self-requests, and synchronous request cycles. +The shell seeds a frozen module table ([`PLATFORM_MODULES`](../web/README.md#the-shared-module-table)); every dynamic bundle resolves its externals against exactly that baseline. `dsh.client.external` adds only exact non-baseline requests, each answered by the dynamic package row it names or an exact static-table key. Type-only imports are erased and create no request. Composition rejects malformed requests, missing suppliers, self-requests, and synchronous request cycles. ### Build requirements diff --git a/packages/client/modules/README.zh.md b/packages/client/modules/README.zh.md index 3cbc600ee5..c972fe38d4 100644 --- a/packages/client/modules/README.zh.md +++ b/packages/client/modules/README.zh.md @@ -43,7 +43,7 @@ application combo 脚本只携带每个插件的 `client.js` 入口,并在启 ### 共享模块 -外壳初始化一张冻结的模块表(`PLATFORM_MODULES`:React、Cordis 与静态 UI 库);每个动态 bundle 都精确针对该基座解析其 external。`dsh.client.external` 只添加基座之外的精确请求;系统会将每个请求解析到其指定的动态包 row 或完全匹配的静态表键。纯类型 import 会被擦除,不产生请求。组合阶段会拒绝畸形请求、缺失提供方、自请求与同步请求环。 +外壳初始化一张冻结的模块表([`PLATFORM_MODULES`](../web/README.zh.md#the-shared-module-table));每个动态 bundle 都精确针对该基座解析其 external。`dsh.client.external` 只添加基座之外的精确请求;系统会将每个请求解析到其指定的动态包 row 或完全匹配的静态表键。纯类型 import 会被擦除,不产生请求。组合阶段会拒绝畸形请求、缺失提供方、自请求与同步请求环。 ### 构建要求 diff --git a/packages/client/modules/package.json b/packages/client/modules/package.json index 497adf5366..c5235aa379 100644 --- a/packages/client/modules/package.json +++ b/packages/client/modules/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-modules", "description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/product-analytics/package.json b/packages/client/product-analytics/package.json index a7a8b4cddc..0b35b21571 100644 --- a/packages/client/product-analytics/package.json +++ b/packages/client/product-analytics/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-product-analytics", "description": "Desktop product event collection and authenticated Host reporting", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -77,6 +77,6 @@ "dependencies": { "@deepseek-ai/dsh-typert-protocol": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" } } diff --git a/packages/client/resources/package.json b/packages/client/resources/package.json index 531d64def9..9664423589 100644 --- a/packages/client/resources/package.json +++ b/packages/client/resources/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-resources", "description": "Unified client resource model: protocol-registered providers turn URL addresses into live values, consumed through the useResource global standard hook", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/shortcuts/package.json b/packages/client/shortcuts/package.json index 76f34297ec..0d9165439d 100644 --- a/packages/client/shortcuts/package.json +++ b/packages/client/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-shortcuts", "description": "Application keyboard command registry and physical-key routing", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/store/package.json b/packages/client/store/package.json index e6284b5e8c..a0b08df37f 100644 --- a/packages/client/store/package.json +++ b/packages/client/store/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-store", "description": "React-free observable and snapshot-store contracts with the shared Zustand/Immer engine", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/tsdown.client.ts b/packages/client/tsdown.client.ts index 2e8c06b18e..9d9053ded3 100644 --- a/packages/client/tsdown.client.ts +++ b/packages/client/tsdown.client.ts @@ -490,7 +490,7 @@ function clientConfig(id: string, entry: string, clientBanner?: (fileName: strin deps: { neverBundle: isRequested, // Anything NOT requested from the loader module table must inline - // (wire/type layers, zod, clsx — every non-shared dep). A require() the + // (wire/type layers, clsx — every non-shared dep). A require() the // table cannot answer is a guaranteed runtime throw, so the rule is the // package's own request list: requested specifiers stay imports, // everything else is bundled. diff --git a/packages/client/ui-agent-preset/README.i18n.yaml b/packages/client/ui-agent-preset/README.i18n.yaml index 7181942309..4205a9cf81 100644 --- a/packages/client/ui-agent-preset/README.i18n.yaml +++ b/packages/client/ui-agent-preset/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: b3ad7867f23dad1c /deepseek-ai-dsh-client-ui-agent-preset/use-this-package: - en: d616984e840f036e - zh: 3e7c97c7d10669eb + en: a21296842d6e18da + zh: 75f3a6a79691884f /deepseek-ai-dsh-client-ui-agent-preset/understand-the-implementation: en: b3221ef8bc6d8096 zh: 68cb05566249da10 diff --git a/packages/client/ui-agent-preset/README.md b/packages/client/ui-agent-preset/README.md index 09df3e46f3..75f778ff62 100644 --- a/packages/client/ui-agent-preset/README.md +++ b/packages/client/ui-agent-preset/README.md @@ -29,7 +29,7 @@ Settings shows the built-in and custom card groups with default highlighting and With Coding Tools off in General Settings, the new-session picker and Settings roster offer Standard, Creator, and custom presets; turning them on also offers PTC and Minimal. Default selection and the Creator entry remain available in both states. Choosing a healthy default also synchronizes the blank session on the current new-task surface. Creator starts a new task using the `cordis` preset. -Turning Coding Tools off clears all unapplied choices through the existing Developer tools subscription. Once the off value is accepted, a host-backed connection resets a saved built-in PTC or Minimal default to Standard. Saved Standard, Creator, and custom defaults (including named PTC or Minimal overrides) stay unchanged. The user can select an available preset again after turning Coding Tools off. Existing sessions keep their selected mode; New Session uses the current default. An existing mode's label remains visible even when that mode is absent from the menu. Turning Coding Tools back on does not restore the old default or cleared choice. A remote connection using in-memory settings does not rewrite the Host's default. If Standard is missing or broken, the default remains unchanged and an error is shown. +Turning Coding Tools off clears all unapplied choices through the existing Developer tools subscription. Once the off value is accepted, every authenticated browser resets a saved built-in PTC or Minimal default to Standard. Saved Standard, Creator, and custom defaults (including named PTC or Minimal overrides) stay unchanged. The user can select an available preset again after turning Coding Tools off. Existing sessions keep their selected mode; New Session uses the current default. An existing mode's label remains visible even when that mode is absent from the menu. Turning Coding Tools back on does not restore the old default or cleared choice. If Standard is missing or broken, the default remains unchanged and an error is shown. Known shipped presets offer mode details and usage examples in a read-only dialog. Its tabs preserve each page's scroll position; closing returns focus to the opening action. Help does not change the new-task default. The default badge replaces the card's group badge, and the preset id appears beside the title. Guide copy and examples belong to this package. diff --git a/packages/client/ui-agent-preset/README.zh.md b/packages/client/ui-agent-preset/README.zh.md index 7514fc9953..735d008239 100644 --- a/packages/client/ui-agent-preset/README.zh.md +++ b/packages/client/ui-agent-preset/README.zh.md @@ -29,7 +29,7 @@ kind: "package-reference" 关闭通用设置中的代码工作工具后,新会话选择器和设置页列表提供标准、创造和自定义预设;开启后还提供 PTC 和极简模式。两种状态下均保留默认值选择和创造入口。选择健康定义作为默认值也会同步当前新任务页面的空白会话。Creator 入口开启一个使用 `cordis` preset 的新任务。 -关闭代码工作工具时,沿用现有 Developer tools 订阅清空所有尚未应用的选择。关闭状态被接受后,使用 Host 持久化设置的连接会将已保存的内置 PTC 或极简默认值回退为标准模式。已保存的标准、创造和自定义默认值(包括有名称的 PTC 或极简覆盖项)保持不变。关闭后用户仍可重新选择可用预设。已有会话保留所选模式;新会话使用当前默认值。即使已有会话的模式不在菜单中,其标签仍会显示。重新开启代码工作工具不会恢复原默认值或已清除的选择。使用内存设置的远程连接不会改写 Host 的默认值。标准模式缺失或损坏时,默认值保持不变,并显示错误。 +关闭代码工作工具时,沿用现有 Developer tools 订阅清空所有尚未应用的选择。关闭状态被接受后,每个已认证浏览器都会将已保存的内置 PTC 或极简默认值回退为标准模式。已保存的标准、创造和自定义默认值(包括有名称的 PTC 或极简覆盖项)保持不变。关闭后用户仍可重新选择可用预设。已有会话保留所选模式;新会话使用当前默认值。即使已有会话的模式不在菜单中,其标签仍会显示。重新开启代码工作工具不会恢复原默认值或已清除的选择。标准模式缺失或损坏时,默认值保持不变,并显示错误。 已知的内置预设提供只读的模式说明与使用示例对话框。各页签保留各自的滚动位置;关闭后焦点回到打开它的操作。帮助不会改变新任务默认值。默认徽标取代卡片的分组徽标,预设 id 显示在标题旁。指南文案与示例归本包所有。 diff --git a/packages/client/ui-agent-preset/package.json b/packages/client/ui-agent-preset/package.json index d65a284667..10eab5b9d8 100644 --- a/packages/client/ui-agent-preset/package.json +++ b/packages/client/ui-agent-preset/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-agent-preset", "description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-agent-preset/src/client/index.ts b/packages/client/ui-agent-preset/src/client/index.ts index 184b7fd791..1abeb855be 100644 --- a/packages/client/ui-agent-preset/src/client/index.ts +++ b/packages/client/ui-agent-preset/src/client/index.ts @@ -74,8 +74,7 @@ export function apply(ctx: ClientContext): void { const presetSettings = ctx.configForms.get(AGENT_PRESET_SETTINGS_NS) let active = true // Host preferences start at false before their first accepted document. - const codingToolsDisabled = (): boolean => active && toolsSettings.getSnapshot().mode === 'host' - && toolsSettings.getSnapshot().value?.enabled === false + const codingToolsDisabled = (): boolean => active && toolsSettings.getSnapshot().value?.enabled === false const controller = new AgentPresetSettingsController(ctx) const staged: AgentPresetStage = { id: undefined, introduce: false } const seats = new WeakMapWithValues() diff --git a/packages/client/ui-agent-preset/src/client/section-store.ts b/packages/client/ui-agent-preset/src/client/section-store.ts index 3ab2bdd2aa..317543daf8 100644 --- a/packages/client/ui-agent-preset/src/client/section-store.ts +++ b/packages/client/ui-agent-preset/src/client/section-store.ts @@ -86,7 +86,7 @@ export class AgentPresetSectionController { while (this.pendingSave !== undefined) await this.pendingSave if (!shouldReset()) return const settings = this.ctx.configForms.get(AGENT_PRESET_SETTINGS_NS).getSnapshot() - if (settings.mode !== 'host' || settings.status !== 'ready' || !settings.writable || settings.revision === undefined) return + if (settings.status !== 'ready' || !settings.writable || settings.revision === undefined) return await this.load() if (!shouldReset() || this.store.getSnapshot().status !== 'ready') return const rows = this.store.getSnapshot().rows diff --git a/packages/client/ui-agent-preset/tests/apply.client.spec.ts b/packages/client/ui-agent-preset/tests/apply.client.spec.ts index f23bde9a6e..aa52b2c3d7 100644 --- a/packages/client/ui-agent-preset/tests/apply.client.spec.ts +++ b/packages/client/ui-agent-preset/tests/apply.client.spec.ts @@ -72,7 +72,6 @@ async function bench(options: { describeGate?: Promise presets?: AgentPresetRow[] developerToolsEnabled?: boolean - memory?: boolean } = {}) { const ctx = new Context() // The host's answer, mutable so a spec can move the default the way the @@ -138,7 +137,6 @@ async function bench(options: { } const remote = new TestRemote(ctx, { settings }) - if (options.memory === true) remote.$host = { home: undefined, isLoopback: false } // The roster and the switch are the AgentPresets Remote namespace; the // shared double carries no generated namespaces, so this spec stages its // own. Registered twice on purpose: the nested key satisfies the plugin's @@ -174,9 +172,9 @@ async function bench(options: { await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await() if (options.describeGate === undefined) { await vi.waitFor(() => { - expect(ctx.configForms.developerTools.enabled.getSnapshot()).toBe(options.memory === true ? true : developerToolsEnabled) + expect(ctx.configForms.developerTools.enabled.getSnapshot()).toBe(developerToolsEnabled) }) - if (options.memory !== true) await vi.waitFor(() => { expect(ctx.configForms.get('ui-settings').getSnapshot().status).toBe('ready') }) + await vi.waitFor(() => { expect(ctx.configForms.get('ui-settings').getSnapshot().status).toBe('ready') }) } /** Publish a new Developer tools choice the way the settings mirror does. */ const setDeveloperTools = async (enabled: boolean): Promise => { @@ -933,7 +931,6 @@ describe('ui-agent-preset apply', () => { await b.ctx.plugin({ inject: [...inject], apply }).await() expect(b.calls.filter(call => call.startsWith('settings:'))).toEqual([]) await b.setDeveloperTools(false) - expect(b.ctx.configForms.get('agent-preset-registry').getSnapshot()).toMatchObject({ status: 'ready', mode: 'host', writable: true, revision: 0 }) await vi.waitFor(() => { expect(b.calls.filter(call => call.startsWith('settings:'))).toEqual(['settings:{"selectedDefault":"standard"}']) }) await b.ctx.fiber.dispose() }) @@ -1030,36 +1027,6 @@ describe('ui-agent-preset apply', () => { await b.ctx.fiber.dispose() }) - it.each(['ptc', 'minimal'])('clears a browser-local staged %s choice without rewriting the Host default', async (preset) => { - const b = await bench({ memory: true, presets: [ - ...ROSTER_MOVED.value.presets, { id: 'ptc', isDefault: false }, - ] }) - declareRoot(b.slots) - declareConversation(b.slots) - b.ctx.provide('conversation', {} as never) - const state: { current?: string; byId: Record } = { - byId: { s1: { id: 's1', blank: true } }, - } - const sessions = sessionsDouble(b.ctx, state) - b.ctx.provide('sessions', sessions as never) - b.ctx.provide('uiWorkspace', uiWorkspaceDouble() as never) - await b.ctx.plugin({ inject: [...inject], apply }).await() - const injectSeat = b.slots.entries('conversation.hero.agentPreset')[0]! - .inject as (sessionId?: SessionId) => AgentPresetSeatInjected & Record - const chip = injectSeat() - await chip.load() - await chip.select(preset) - await b.ctx.configForms.developerTools.setEnabled(false) - const section = (b.slots.entries('settings.section')[0]!.inject as () => AgentPresetSectionInjected & Record)() - await section.load() - expect(section.hooks.agentPresetSection.getSnapshot().rows.find(row => row.isDefault)?.id).toBe('minimal') - expect(b.calls.filter(call => call.startsWith('settings:'))).toEqual([]) - state.current = 's1' - await injectSeat(SessionId('s1')).load() - sessions.notify() - expect(b.calls.filter(call => call.startsWith('select:'))).toEqual([]) - await b.ctx.fiber.dispose() - }) it('loads the header label from the shared roster store', async () => { const { ctx, slots } = await bench() diff --git a/packages/client/ui-agent-preset/tests/section-store.client.spec.ts b/packages/client/ui-agent-preset/tests/section-store.client.spec.ts index ba2d1841b9..f1a88d720a 100644 --- a/packages/client/ui-agent-preset/tests/section-store.client.spec.ts +++ b/packages/client/ui-agent-preset/tests/section-store.client.spec.ts @@ -7,7 +7,7 @@ import { en } from '../src/client/locales.ts' function fixture() { const form = createSnapshotStore>({ - status: 'ready', mode: 'host', writable: true, revision: 7, value: { selectedDefault: 'minimal' }, base: {}, user: {}, + status: 'ready', writable: true, revision: 7, value: { selectedDefault: 'minimal' }, base: {}, user: {}, }) const remote = { agentPresets: { list: vi.fn(async (): Promise<{ ok: true; value: { presets: AgentPresetRow[] } }> => ({ @@ -149,7 +149,7 @@ describe('Coding Tools default reconciliation', () => { it.each([ { status: 'loading' as const }, { status: 'unavailable' as const }, - { mode: 'memory' as const }, { writable: false }, { revision: undefined }, + { writable: false }, { revision: undefined }, ])('does not write without a writable accepted Host form: %j', async (patch) => { const f = setup() f.form.set({ ...f.form.getSnapshot(), ...patch }) diff --git a/packages/client/ui-approval/package.json b/packages/client/ui-approval/package.json index 4d8ef679e1..3d34e53fbf 100644 --- a/packages/client/ui-approval/package.json +++ b/packages/client/ui-approval/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-approval", "description": "Approval composer takeover over the scoped Remote Event waterfall", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-attachment/package.json b/packages/client/ui-attachment/package.json index e37afaf50d..8e18afbe65 100644 --- a/packages/client/ui-attachment/package.json +++ b/packages/client/ui-attachment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-attachment", "description": "Dynamic attachment presentation plugin for conversation input, message-image, and trajectory image slots", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-brand-official/package.json b/packages/client/ui-brand-official/package.json index b4587a84ac..deadad2ad5 100644 --- a/packages/client/ui-brand-official/package.json +++ b/packages/client/ui-brand-official/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-brand-official", "description": "Official DeepSeek Harness brand occupants for the Web client's sidebar slots", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index 9bfd71270b..3afec610af 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -15,8 +15,8 @@ en: 41cd21266011d571 zh: 3172d1a2acbb27b7 /deepseek-ai-dsh-client-ui-chat/reference-previews: - en: 0e9c004b62fa2f77 - zh: d0b0e27f74747253 + en: 07a55c307ac68b95 + zh: b2dd3f47008929c2 /deepseek-ai-dsh-client-ui-chat/hidden-chat-rows: en: cbf1f0a2205046a5 zh: 2e59d0815351349f @@ -24,8 +24,8 @@ en: 2157fcd6b0d100b5 zh: 9e865ac3953ebe30 /deepseek-ai-dsh-client-ui-chat/turn-token-usage: - en: 53fd2db200c9ea16 - zh: 65c7b5ef1eb4159c + en: bb20da372b144302 + zh: 75a8c5ab7ec8d665 /deepseek-ai-dsh-client-ui-chat/completed-turn-footer: en: b4cd36e7ab91281e zh: 8ccf412b9dde61d5 diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index bea62104b2..1bf489d09e 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -39,7 +39,7 @@ Chat supplies file and HTTP(S) navigation through one `MarkdownDelegateProvider` Standalone Markdown images show contained previews and open the shared image lightbox; local paths resolve against the viewed workspace after settlement. Image file links keep their sidebar activation and show a thumbnail after hover dwell or keyboard focus. Escape dismisses the thumbnail. Failed images retain a localized status and their description; no duplicate-image filtering is applied. -Settings → General → Open chat links in selects the destination for ordinary clicks on Chat HTTP(S) links: In-App Sidebar (default) opens a new right-Sidebar Browser tab, while Default Browser opens an external tab. The setting follows Keyboard shortcuts and is shown only while the Sidebar Browser is available. If the Sidebar Browser is not registered, both choices use the external browser; modified clicks retain native behavior. The `ui-chat.linkOpening` preference persists on loopback browsers and stays process-local when settings cannot persist writes. Sent file references and skills confirmed by the message’s logged invocation also open in the right Sidebar. File paths use the viewed Session; skill names resolve through its current input-trigger source. Both use the prose file-link dotted underline on hover or focus. Sessions, directories, and command labels remain non-navigating references. +Settings → General → Open chat links in selects the destination for ordinary clicks on Chat HTTP(S) links: In-App Sidebar (default) opens a new right-Sidebar Browser tab, while Default Browser opens an external tab. The setting follows Keyboard shortcuts and is shown only while the Sidebar Browser is available. If the Sidebar Browser is not registered, both choices use the external browser; modified clicks retain native behavior. The `ui-chat.linkOpening` preference persists on the Host for authenticated browsers and stays process-local when settings cannot persist writes. Sent file references and skills confirmed by the message’s logged invocation also open in the right Sidebar. File paths use the viewed Session; skill names resolve through its current input-trigger source. Both use the prose file-link dotted underline on hover or focus. Sessions, directories, and command labels remain non-navigating references. ## Hidden Chat rows @@ -64,7 +64,7 @@ Settings → General → Performance & usage appears after Send behavior while b The composer statistics are two `conversation.composer.dock` list entries: `activity` (order 0, turn and step counts and output speed, with LLM time, tool time, and TTFT) and `usage` (order 1, token total and cache hit). The controls follow the shared [statistics-control radius](../../../docs/ui-radius.md#radius-scale). Another plugin replaces one control by registering the same id and the same `order`; the dock orders rows by `order`, while a lower `priority` picks the replacement: a dynamic plugin receives one automatically, and a statically composed plugin passes it explicitly. A new id adds a control. Each control carries its id in `data-composer-stat`. An open statistics dialog closes on Escape or on a pointerdown or click outside it, so activating another control by pointer or keyboard swaps dialogs instead of stacking them. -On non-loopback browsers, the preference remains process-local because the settings scope cannot persist writes. Explicit selections update every consumer immediately; accepted Host settings reconcile the live value on loopback browsers. +Explicit selections update every consumer immediately; accepted Host settings reconcile the live value, so a refused or pending write keeps the selected mode until the Host answers. Preference menus restore focus to their trigger without scrolling before publishing a new selection. diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index fc63cb364d..c08c2d5605 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -39,7 +39,7 @@ Chat 在节点列表外通过一个 `MarkdownDelegateProvider` 提供文件及 H 独立的 Markdown 图片以内嵌预览显示,点击打开共享图片浮层;本地路径在消息落定后基于当前查看的工作区解析。图片文件链接保持点击打开侧栏,鼠标停留或键盘聚焦时显示缩略图,Esc 关闭缩略图。图片加载失败时保留本地化状态与图片说明;不执行重复图片过滤。 -设置 → 通用设置 → 网页链接默认打开方式控制普通点击 Chat HTTP(S) 链接时的目标:「应用内侧边栏」(默认)打开新的右侧 Sidebar Browser tab,「默认浏览器」打开外部标签页。该设置项位于快捷键之后,仅在 Sidebar Browser 可用时显示。若 Sidebar Browser 未注册,两种选择均使用外部浏览器;带修饰键的点击保留原生行为。`ui-chat.linkOpening` 偏好在回环地址浏览器中持久化,设置无法持久化写入时仅在当前进程内生效。已发送的文件引用及消息日志确认调用的 skill 也可在右侧栏打开预览。文件路径使用当前查看的 Session;skill 名称由该 Session 当前的输入触发源解析。两者悬停或聚焦时均使用正文文件链接的虚线下划线。会话、目录和命令标签仍只作为引用展示。 +设置 → 通用设置 → 网页链接默认打开方式控制普通点击 Chat HTTP(S) 链接时的目标:「应用内侧边栏」(默认)打开新的右侧 Sidebar Browser tab,「默认浏览器」打开外部标签页。该设置项位于快捷键之后,仅在 Sidebar Browser 可用时显示。若 Sidebar Browser 未注册,两种选择均使用外部浏览器;带修饰键的点击保留原生行为。已认证浏览器的 `ui-chat.linkOpening` 偏好在 Host 上持久化,设置无法持久化写入时仅在当前进程内生效。已发送的文件引用及消息日志确认调用的 skill 也可在右侧栏打开预览。文件路径使用当前查看的 Session;skill 名称由该 Session 当前的输入触发源解析。两者悬停或聚焦时均使用正文文件链接的虚线下划线。会话、目录和命令标签仍只作为引用展示。 ## Chat 隐藏的行 @@ -64,7 +64,7 @@ Assistant 尝试结束且没有可见消息时,Chat 隐藏已发布的 Node, 输入框统计信息由两个 `conversation.composer.dock` 列表条目组成:`activity`(order 0,轮次数、步骤数与输出速度,以及 LLM 耗时、工具耗时与 TTFT)和 `usage`(order 1,token 总量与缓存命中)。这些控件遵循公共的[统计控件圆角规则](../../../docs/ui-radius.zh.md#radius-scale)。其他插件以相同的 id 和相同的 `order` 注册即可替换某个控件;dock 按 `order` 排列各行,更低的 `priority` 决定由替换者渲染:动态插件会自动获得,静态组合的插件需显式传入。以新 id 注册则新增控件。每个控件在 `data-composer-stat` 中携带其 id。按下 Escape,或在已打开的对话框之外发生 pointerdown 或点击时,该对话框会关闭,因此用指针或键盘激活另一个控件会切换对话框,而不会叠加两个。 -在非回环地址浏览器中,设置作用域无法持久化写入,因此该偏好仅在当前进程内生效。明确选择会立即更新所有使用方;回环地址浏览器收到 Host 已接受的设置后会同步当前值。 +明确选择会立即更新所有使用方;Host 接受的设置会校正实时值,因此写入被拒绝或尚未应答时保持所选模式,直到 Host 应答。 偏好菜单在发布新选择之前,先将焦点还给触发按钮,且不引起滚动。 diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json index 432346bf9d..a738710edb 100644 --- a/packages/client/ui-chat/package.json +++ b/packages/client/ui-chat/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-chat", "description": "Chat Conversation target, node definitions, renderers, and details surface", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -87,7 +87,7 @@ "@deepseek-ai/dsh-tools": "workspace:*", "@deepseek-ai/dsh-util-workspace-path": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", - "@tanstack/react-virtual": "^3.14.10", + "@tanstack/react-virtual": "3.14.10", "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", "react": "^18.2.0", diff --git a/packages/client/ui-chat/src/client/conversation-nodes/README.i18n.yaml b/packages/client/ui-chat/src/client/conversation-nodes/README.i18n.yaml index a26c18c63a..2c402fade5 100644 --- a/packages/client/ui-chat/src/client/conversation-nodes/README.i18n.yaml +++ b/packages/client/ui-chat/src/client/conversation-nodes/README.i18n.yaml @@ -30,8 +30,8 @@ en: ada5a60d2c46f6a8 zh: f3eaeb5cb4d9b3de /chat-conversation-node-rules/display-modes/defaults-and-saving-preferences: - en: 71249bd1ef74a307 - zh: 9c8273ffd8823afa + en: b25bf00ce3a6a119 + zh: 0ab47a4014ea5e1e /chat-conversation-node-rules/display-modes/collapse-timing: en: c31be5bb8cb77dc7 zh: 9550ab598119076f diff --git a/packages/client/ui-chat/src/client/conversation-nodes/README.md b/packages/client/ui-chat/src/client/conversation-nodes/README.md index f823ed9b65..91c8b42ef6 100644 --- a/packages/client/ui-chat/src/client/conversation-nodes/README.md +++ b/packages/client/ui-chat/src/client/conversation-nodes/README.md @@ -131,7 +131,7 @@ Work details uses the Host setting `ui-chat.transcriptView`. The client reads it - Legacy `normal` and `expanded` values display as `detailed`, without automatically rewriting the saved values on disk. - When the setting is missing, `null`, or invalid, Desktop uses `standard` and non-Desktop Web uses `detailed`. Each client also uses its default before Host settings arrive; invalid values in other settings still fail validation. -Reading a default does not automatically save configuration. When the user changes modes, the selected value is saved through Host settings; memory mode does not write to disk. Desktop onboarding writes preferences according to its rules when completed or skipped. +Reading a default does not automatically save configuration. When the user changes modes, the selected value is saved through Host settings and stays process-local when settings cannot persist writes. Desktop onboarding writes preferences according to its rules when completed or skipped. ### Collapse timing diff --git a/packages/client/ui-chat/src/client/conversation-nodes/README.zh.md b/packages/client/ui-chat/src/client/conversation-nodes/README.zh.md index 886427a49a..d3b44f86c0 100644 --- a/packages/client/ui-chat/src/client/conversation-nodes/README.zh.md +++ b/packages/client/ui-chat/src/client/conversation-nodes/README.zh.md @@ -131,7 +131,7 @@ description: "Chat 过程分组与活动摘要的明细业务规则。" - 旧值 `normal` 和 `expanded` 均按 `detailed` 展示,不自动改写磁盘中的值。 - 未配置、值为 `null` 或无效时,Desktop 使用 `standard`,非 Desktop Web 使用 `detailed`。Host 设置到达前也使用各自的默认值;其他设置的无效值仍会校验失败。 -读取默认值不会自动保存配置。用户切换模式时,通过 Host 设置保存所选值;内存模式不写磁盘。Desktop onboarding 完成或跳过时,按引导规则写入偏好。 +读取默认值不会自动保存配置。用户切换模式时,通过 Host 设置保存所选值;设置无法持久化写入时仅在当前进程内生效。Desktop onboarding 完成或跳过时,按引导规则写入偏好。 ### 收起时机 diff --git a/packages/client/ui-chat/src/client/performance-usage.ts b/packages/client/ui-chat/src/client/performance-usage.ts index b0313649a7..158860efb4 100644 --- a/packages/client/ui-chat/src/client/performance-usage.ts +++ b/packages/client/ui-chat/src/client/performance-usage.ts @@ -1,4 +1,4 @@ -/** Performance detail preference with process-local choices on memory-only settings scopes. */ +/** Performance detail preference, live immediately and reconciled with accepted Host settings. */ import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client' import { DEFAULT_PERFORMANCE_USAGE, type ChatSettings, type PerformanceUsageMode } from '../chat-settings.ts' @@ -9,7 +9,7 @@ export class PerformanceUsagePolicy { /** Current choice, reconciled with accepted Host settings when available. */ readonly mode = createSnapshotStore(DEFAULT_PERFORMANCE_USAGE) - /** @param host - Chat settings scope, durable on loopback and memory-only elsewhere. */ + /** @param host - Chat settings scope persisted through the Host. */ constructor(private readonly host: ConfigForm) { const adopt = (): void => { const accepted = host.getSnapshot().value?.performanceUsage @@ -23,7 +23,7 @@ export class PerformanceUsagePolicy { dispose(): void { this.unsubscribe() } /** - * Publish a choice immediately and persist it when the scope supports writes. + * Publish a choice immediately and queue its Host write. * @param mode - Statistics detail selected by the user. */ setMode(mode: PerformanceUsageMode): void { diff --git a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx index 7eee5bcff7..13bb94398b 100644 --- a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx @@ -368,7 +368,7 @@ describe('Chat inject API', () => { const open = vi.spyOn(window, 'open').mockImplementation(() => null) try { const { injected } = b.chatViewApi(b.rootReference) - b.chatSettings.publish({ mode: 'memory', status: 'unavailable' }) + b.chatSettings.publish({ status: 'unavailable' }) b.chatSettings.set.mockResolvedValueOnce(false) b.linkPreference.setLinkOpening('new-tab') b.chatSettings.publish({ value: undefined }) diff --git a/packages/client/ui-chat/tests/performance-usage.client.spec.ts b/packages/client/ui-chat/tests/performance-usage.client.spec.ts index 0ab9bde440..42a8e8f4e9 100644 --- a/packages/client/ui-chat/tests/performance-usage.client.spec.ts +++ b/packages/client/ui-chat/tests/performance-usage.client.spec.ts @@ -5,9 +5,9 @@ import type { ChatSettings } from '../src/chat-settings.ts' import { PerformanceUsagePolicy } from '../src/client/performance-usage.ts' describe('PerformanceUsagePolicy', () => { - it('keeps explicit choices live when a memory-only scope cannot persist them', () => { + it('keeps an explicit choice live while the Host write is pending or refused', () => { const host = stubConfigForm() - host.publish({ mode: 'memory', status: 'ready' }) + host.publish({ status: 'ready' }) const policy = new PerformanceUsagePolicy(host.scope) expect(policy.mode.getSnapshot()).toBe('detailed') policy.setMode('compact') diff --git a/packages/client/ui-commands/package.json b/packages/client/ui-commands/package.json index ecedb1d9f8..8fc1e03aea 100644 --- a/packages/client/ui-commands/package.json +++ b/packages/client/ui-commands/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-commands", "description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index af91420381..20d6dd56b6 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-conversation", "description": "Target-neutral Conversation assembly, shell, composer, queue, and view navigation", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -87,7 +87,7 @@ "@lexical/headless": "^0.49.0", "react-dom": "^18.2.0", "@types/react-dom": "~18.3.0", - "zod": "^4.4.3", + "zod": "4.4.3", "lexical": "^0.49.0", "clsx": "^2.0.0", "@lexical/utils": "^0.49.0", diff --git a/packages/client/ui-deliverables/package.json b/packages/client/ui-deliverables/package.json index 174fcf3f72..22a27a2cbf 100644 --- a/packages/client/ui-deliverables/package.json +++ b/packages/client/ui-deliverables/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-deliverables", "description": "Changed-files card with per-file comparison tabs, delivery cards, and clickable final-response file references for Web", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-directory-picker-browse/package.json b/packages/client/ui-directory-picker-browse/package.json index 557ca70801..5d6e0a6e40 100644 --- a/packages/client/ui-directory-picker-browse/package.json +++ b/packages/client/ui-directory-picker-browse/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-directory-picker-browse", "description": "In-app directory browsing surface: the workspace directory-flow owner rendering the host's listing and creation primitives", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-directory-picker-native/package.json b/packages/client/ui-directory-picker-native/package.json index 727c9f22b2..a8a691d11e 100644 --- a/packages/client/ui-directory-picker-native/package.json +++ b/packages/client/ui-directory-picker-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-directory-picker-native", "description": "Native directory-picker surface: the renderless workspace directory-flow occupant driving the local Desktop or Host OS chooser", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-dockkit/package.json b/packages/client/ui-dockkit/package.json index e42a8d828f..30b92e8b1b 100644 --- a/packages/client/ui-dockkit/package.json +++ b/packages/client/ui-dockkit/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-dockkit", "description": "Docking layout kit: split-tree engine with invertible operations, and the React components that render and drive it (zero cordis)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-goal/package.json b/packages/client/ui-goal/package.json index b1cac92875..b870e57a12 100644 --- a/packages/client/ui-goal/package.json +++ b/packages/client/ui-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-goal", "description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-input-trigger/package.json b/packages/client/ui-input-trigger/package.json index 280dbdaaad..20f69f5410 100644 --- a/packages/client/ui-input-trigger/package.json +++ b/packages/client/ui-input-trigger/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-input-trigger", "description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-jobs/package.json b/packages/client/ui-jobs/package.json index 7cd9351e30..6b4133b618 100644 --- a/packages/client/ui-jobs/package.json +++ b/packages/client/ui-jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-jobs", "description": "Session-header background-job list with on-demand streaming record panels", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "type": "module", "main": "lib/index.js", "types": "lib/types/index.d.ts", diff --git a/packages/client/ui-layout/package.json b/packages/client/ui-layout/package.json index 9c04a714ca..95ccfd9040 100644 --- a/packages/client/ui-layout/package.json +++ b/packages/client/ui-layout/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-layout", "description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-layout/tests/apply.client.spec.ts b/packages/client/ui-layout/tests/apply.client.spec.ts index e1b267dc4c..96aac84fd1 100644 --- a/packages/client/ui-layout/tests/apply.client.spec.ts +++ b/packages/client/ui-layout/tests/apply.client.spec.ts @@ -56,11 +56,8 @@ async function bench() { await owner.await() if (ctx === undefined) throw new Error('the fixture owner did not activate') const slotsFiber = ctx.plugin(SlotRegistry) - // Theme registers its Appearance settings row and requires the connection - // seam for persistence; model this bench as a remote, memory-only browser. ctx.provide('locale', new LocaleRuntime(ctx)) await ctx.plugin(ShortcutsService).await() - ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) // ui-theme's Appearance row binds a durable scope through these two. ctx.provide('remote', { $on: () => () => {} } as never) ctx.provide('configForms', { developerTools: { enabled: createSnapshotStore(true) }, get: () => stubConfigForm().scope } as never) diff --git a/packages/client/ui-message-feedback/package.json b/packages/client/ui-message-feedback/package.json index df6cb6895e..74db6f0f49 100644 --- a/packages/client/ui-message-feedback/package.json +++ b/packages/client/ui-message-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-message-feedback", "description": "The Web feedback surface: per-message Like/Dislike in the assistant-message action strip and the feedback dialog behind both ratings and /feedback, backed by the messageFeedback and sessionFeedback Host Remotes", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-model-selection/package.json b/packages/client/ui-model-selection/package.json index 90a4f1dbcb..9775cdb5e3 100644 --- a/packages/client/ui-model-selection/package.json +++ b/packages/client/ui-model-selection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-model-selection", "description": "Model selection over the shared model catalog, Session projection, and session.selectModel", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-open-in-app/package.json b/packages/client/ui-open-in-app/package.json index e1b15cdf72..ff8738627e 100644 --- a/packages/client/ui-open-in-app/package.json +++ b/packages/client/ui-open-in-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-open-in-app", "description": "Web \"Open In...\" controls: the Session-header split button opening the workspace directory in an installed application, and the document preview's default-application controls for one file", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-permission-presets/README.i18n.yaml b/packages/client/ui-permission-presets/README.i18n.yaml index 3f24ef0979..f9ae7348ed 100644 --- a/packages/client/ui-permission-presets/README.i18n.yaml +++ b/packages/client/ui-permission-presets/README.i18n.yaml @@ -10,7 +10,7 @@ zh: 74b8ded4da556e81 /deepseek-ai-dsh-client-ui-permission-presets/summary: en: 2591adc4721c0564 - zh: 311292f58621adb6 + zh: 558413d3f819d3af /deepseek-ai-dsh-client-ui-permission-presets/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb @@ -18,14 +18,14 @@ en: 91606bff7da02253 zh: ef68e813d64c89be /deepseek-ai-dsh-client-ui-permission-presets/use-this-package/the-picker: - en: d851eab85fe002bd - zh: 0fdf6eeb1cbd609b + en: 4e5a070ad4321548 + zh: 010063336bd2b8df /deepseek-ai-dsh-client-ui-permission-presets/use-this-package/the-settings-row: en: 5067f6aa12496225 zh: cfa6796684f97535 /deepseek-ai-dsh-client-ui-permission-presets/understand-the-implementation: en: 4089d5bebf3e8bf0 - zh: bc5c3652b0612784 + zh: 22071e6cd6de0ffc /deepseek-ai-dsh-client-ui-permission-presets/further-exploration: en: 9bdbdea03d0e2a96 zh: cc3bf480a0706ef2 @@ -37,7 +37,7 @@ zh: 4fcbc876952d240d /deepseek-ai-dsh-client-ui-permission-presets/known-limitations-and-deferred-work: en: 08cdc0ad196d12c1 - zh: 1b65f5d30e219197 + zh: 157bee9d5b29f470 /deepseek-ai-dsh-client-ui-permission-presets/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-permission-presets/README.md b/packages/client/ui-permission-presets/README.md index 6798cc7f47..50d8b4918c 100644 --- a/packages/client/ui-permission-presets/README.md +++ b/packages/client/ui-permission-presets/README.md @@ -29,7 +29,7 @@ Mount this plugin alongside the settings, commands, and conversation packages. G ### The picker -A pick submits the `/permission ` command line. The argument-bearing form (`/permission ` typed directly) still switches directly; the decoration replaces only the bare invocation. The built-in labels are `Read Only`, `Workspace Write`, `Full access`, and `Auto review` in English and `仅可查看`, `工作区内修改`, `完全权限`, and `Auto review` in Chinese. Explicit host labels remain unchanged, unknown kebab-case names render in title case, and `auto` carries an `EXP` badge plus an experimental-risk confirmation. `custom` is display state, never a target. +A pick submits the `/permission ` command line. The argument-bearing form (`/permission ` typed directly) still switches directly; the decoration replaces only the bare invocation. The built-in labels are `Read Only`, `Workspace Write`, `Full access`, and `Auto review` in English and `仅可查看`, `工作区内修改`, `完全权限`, and `自动审查` in Chinese. Explicit host labels remain unchanged, unknown kebab-case names render in title case, and `auto` carries an `EXP` badge in menu options plus an experimental-risk confirmation. The selected composer control shows the mode without the badge. `custom` is display state, never a target. When the live catalog withdraws a preset, the composer closes its pending confirmation and shows the Session's current value instead of an unavailable optimistic pick. Every catalog invalidation — the payload-free notification or a connection-generation change — closes an open slash picker or its confirmation without consuming the draft, while publishing the result of a read the picker waits for leaves it open with its failure and retry state; reopening reads the current catalog. A submitted command remains busy until its response settles. diff --git a/packages/client/ui-permission-presets/README.zh.md b/packages/client/ui-permission-presets/README.zh.md index 1c603b5eb5..df37cea7ec 100644 --- a/packages/client/ui-permission-presets/README.zh.md +++ b/packages/client/ui-permission-presets/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -为当前 Web 会话或未来会话选择权限预设。通用设置行只更改之后创建会话所用的默认值;composer 与 `/permission` 选择器切换当前会话。默认 Web 提供仅可查看、工作区内修改与完全权限。显式加载实验 Auto integration 后,当前会话选择器会增加带 `EXP` 标记的 Auto review。通过可见选项选择完全权限或 Auto 时,需要分别确认对应风险;完整的 `/permission ` 命令直接执行。宿主通过 Session 投影确认每次变更。 +为当前 Web 会话或未来会话选择权限预设。通用设置行只更改之后创建会话所用的默认值;composer 与 `/permission` 选择器切换当前会话。默认 Web 提供仅可查看、工作区内修改与完全权限。显式加载实验 Auto integration 后,当前会话选择器会增加带 `EXP` 标记的自动审查。通过可见选项选择完全权限或 Auto 时,需要分别确认对应风险;完整的 `/permission ` 命令直接执行。宿主通过 Session 投影确认每次变更。 ## 目录 @@ -29,7 +29,7 @@ kind: "package-reference" ### 选择器 -选中即提交 `/permission ` 命令行。带参形式(直接键入 `/permission `)仍直接切换;装饰只替换裸调用。内置标签在英文界面中是 `Read Only`、`Workspace Write`、`Full access` 和 `Auto review`,在中文界面中是「仅可查看」「工作区内修改」「完全权限」和 `Auto review`。显式 host 标签保持原样,未知 kebab-case 名称渲染为 Title Case;`auto` 带有 `EXP` badge,并在可见选择时要求实验风险确认。`custom` 只是显示状态,绝非目标。 +选中即提交 `/permission ` 命令行。带参形式(直接键入 `/permission `)仍直接切换;装饰只替换裸调用。内置标签在英文界面中是 `Read Only`、`Workspace Write`、`Full access` 和 `Auto review`,在中文界面中是「仅可查看」「工作区内修改」「完全权限」和「自动审查」。显式 host 标签保持原样,未知 kebab-case 名称渲染为 Title Case;`auto` 在菜单选项中带有 `EXP` 标记,并在可见选择时要求实验风险确认。选中后的 composer 控件显示模式,不展示该标记。`custom` 只是显示状态,绝非目标。 实时目录撤销某个预设时,composer 关闭对应的待确认对话框,并用 Session 的当前值替代已不可用的乐观选择。每次目录失效(无 payload 的通知或连接代际变更)会关闭已打开的 slash 选择器或其确认对话框,不消费草稿;而发布某个 picker 正在等待的读取结果时,该 picker 保持打开并保留失败与重试状态;重新打开时读取当前目录。已经提交的命令在响应结束前继续保持忙碌状态。 @@ -45,7 +45,7 @@ kind: "package-reference"
实现细节——点击展开 -General Settings 行经 `ctx.configForms` 读取显式暴露的 `permission` Settings 描述符,并携带描述符 revision 写入一条 `settings.mutate` 路径操作;其 observable 经 slot 系统的 `hooks` compartment 传递,因此 React 钩子绑定归渲染器,推送失效通知会重新获取描述符。该值只在之后创建会话时读取。当前会话界面是挂在宿主 `/permission` 命令上的 popupSelect 装饰(`ctx.commandUi.decorate`):宿主命令保留斜杠菜单行、带参形式与持久生命周期记账,装饰只把裸调用替换为选择器。一个进程级目录会在首次 Remote 读取前订阅无 payload 的目录通知,并且只发布当前连接代际中最新的完整成功结果。胜出的读取失败或连接 reset 会清空旧快照:composer seat 因此隐藏,直到后续既有触发重试,而 slash 选择器保持可用并在弹窗内显示失败与重试;旧连接代际与 dispose 后才返回的结果会被忽略。它的公共 observable 是 `{ value }` 与 `invalidations`(每次目录通知或连接代际变更打一个点);失败仅供命令式加载内部使用。slash popup 与 composer seat 共用这份目录,而 Session `permissions` 投影只提供 `currentValue`。Full access 与 Auto review 各自携带本地化确认文案;Auto 还携带由共享 popup 外壳渲染的 badge。 +General Settings 行经 `ctx.configForms` 读取显式暴露的 `permission` Settings 描述符,并携带描述符 revision 写入一条 `settings.mutate` 路径操作;其 observable 经 slot 系统的 `hooks` compartment 传递,因此 React 钩子绑定归渲染器,推送失效通知会重新获取描述符。该值只在之后创建会话时读取。当前会话界面是挂在宿主 `/permission` 命令上的 popupSelect 装饰(`ctx.commandUi.decorate`):宿主命令保留斜杠菜单行、带参形式与持久生命周期记账,装饰只把裸调用替换为选择器。一个进程级目录会在首次 Remote 读取前订阅无 payload 的目录通知,并且只发布当前连接代际中最新的完整成功结果。胜出的读取失败或连接 reset 会清空旧快照:composer seat 因此隐藏,直到后续既有触发重试,而 slash 选择器保持可用并在弹窗内显示失败与重试;旧连接代际与 dispose 后才返回的结果会被忽略。它的公共 observable 是 `{ value }` 与 `invalidations`(每次目录通知或连接代际变更打一个点);失败仅供命令式加载内部使用。slash popup 与 composer seat 共用这份目录,而 Session `permissions` 投影只提供 `currentValue`。完全权限与自动审查各自携带本地化确认文案;Auto 还携带由共享 popup 外壳渲染的 badge。
@@ -80,7 +80,7 @@ General Settings 行经 `ctx.configForms` 读取显式暴露的 `permission` Set 这些限制界定了当前权限界面。它们是当前包约束,不是通用策略对比或任务积压。 - **设置行仅限 Web**——非 Web 客户端仍可经 `/permission` 切换当前会话,但不会获得这项浏览器贡献。 -- **Auto review 仅限当前会话**——General Settings 行有意省略它,且只有通过可见选择器选择时才显示实验确认;显式键入 `/permission auto` 已经构成明确同意。 +- **自动审查仅限当前会话**——General Settings 行有意省略它,且只有通过可见选择器选择时才显示实验确认;显式键入 `/permission auto` 已经构成明确同意。 - **预设描述来自宿主**——本地化的内置标签旁边可能显示另一种语言编写的描述。 diff --git a/packages/client/ui-permission-presets/package.json b/packages/client/ui-permission-presets/package.json index 48e196a2eb..28f319f208 100644 --- a/packages/client/ui-permission-presets/package.json +++ b/packages/client/ui-permission-presets/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-permission-presets", "description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-permission-presets/src/client/PermissionSelect.module.css b/packages/client/ui-permission-presets/src/client/PermissionSelect.module.css index b40748e22e..a3add30afb 100644 --- a/packages/client/ui-permission-presets/src/client/PermissionSelect.module.css +++ b/packages/client/ui-permission-presets/src/client/PermissionSelect.module.css @@ -52,7 +52,7 @@ .optionLabel { display: inline-flex; - align-items: baseline; + align-items: center; gap: 4px; min-width: 0; max-width: 100%; @@ -67,12 +67,10 @@ .badge { flex: none; - align-self: flex-start; - margin-top: -1px; color: var(--dsw-alias-label-tertiary); font-size: 8px; line-height: 10px; - font-weight: 600; + font-weight: 500; letter-spacing: 0.2px; } @@ -85,8 +83,8 @@ transition: transform 120ms ease; } -/* Narrow composer: the trigger collapses to icon + chevron so the row keeps - fitting. Only triggers that actually carry a mode glyph drop their label — +/* Narrow composer: mode text collapses while icons stay visible. + Only triggers that actually carry a mode glyph drop their label — a host-configured mode without one keeps its text as the sole identifier. The 460px cut is the point where the row (attach + modes + model + send) starts squeezing labels; the container is the composer row (InputBar .row — diff --git a/packages/client/ui-permission-presets/src/client/PermissionSelect.tsx b/packages/client/ui-permission-presets/src/client/PermissionSelect.tsx index f34e2b16e9..0281c41a24 100644 --- a/packages/client/ui-permission-presets/src/client/PermissionSelect.tsx +++ b/packages/client/ui-permission-presets/src/client/PermissionSelect.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import type { ReactNode } from 'react' import clsx from 'clsx' import { - IconChevronDownOutlineRegular, Menu, PermissionIconFullAccessRegular, + IconChevronDownOutlineRegular, Menu, PermissionIconAutoReviewRegular, PermissionIconFullAccessRegular, PermissionIconReadOnlyRegular, PermissionIconWorkspaceWriteRegular, RiskConfirmation, } from '@deepseek-ai/dsh-client-ui-primitives' import type { MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives' @@ -29,6 +29,7 @@ const permissionGlyphs = new Map([ ['read-only', ], ['workspace-write', ], [FULL_ACCESS, ], + [AUTO_REVIEW, ], ]) /** Glyph for a permission option value; host-configured names outside the design set get none. */ @@ -112,7 +113,7 @@ export function PermissionSelect({ : ( {label} - {badge} + {badge} ), ...icon === undefined ? {} : { icon }, @@ -159,8 +160,6 @@ export function PermissionSelect({ const confirmationEnable = confirmation === AUTO_REVIEW ? t('auto.confirm.enable') : t('confirm.enable') - const currentBadge = optionBadge(currentValue, t) - const currentAccessibleLabel = currentBadge === undefined ? currentLabel : `${currentLabel} ${currentBadge}` return ( <> @@ -176,7 +175,7 @@ export function PermissionSelect({
diff --git a/packages/client/ui-settings-models/README.zh.md b/packages/client/ui-settings-models/README.zh.md index 0af8ee6abb..5509ac386b 100644 --- a/packages/client/ui-settings-models/README.zh.md +++ b/packages/client/ui-settings-models/README.zh.md @@ -89,7 +89,7 @@ Models 页面包含 **DeepSeek 账号**(`deepseek-account`,英文为 **DeepS ### 引导协调器 -预览版说明在 `src/client/locales.ts` 中持有精确文案,并在 `src/onboarding-copy.ts` 中持有确认版本;回环时它通过既有 settings API 比较并写入 `ui-settings-general.welcomeNoticeVersion`,且只有显式点击「继续」才会记录当前版本。已确认旧版的用户会再次看到当前说明。非回环浏览器无法使用这个仅限宿主的 namespace,因此确认只保留在进程内,刷新后说明会再次出现。DeepSeek 步骤面向 `llm-deepseek` 中的 `deepseek-official`,在共享引导模态框内以仅凭据模式渲染既有 `ProviderEditor`;`credentials.set` 仍是唯一的机密写入,且不改变任何提供商设置。 +预览版说明在 `src/client/locales.ts` 中持有精确文案,并在 `src/onboarding-copy.ts` 中持有确认版本。认证浏览器通过共享配置表单比较并写入 `ui-settings-general.welcomeNoticeVersion`;只有显式点击「继续」才会记录当前版本;命名空间不可用或写入被拒时,声明保持未确认状态。已确认旧版的用户会再次看到当前说明。DeepSeek 步骤面向 `llm-deepseek` 中的 `deepseek-official`,在共享引导模态框内以仅凭据模式渲染既有 `ProviderEditor`;`credentials.set` 仍是唯一的机密写入,且不改变任何提供商设置。 diff --git a/packages/client/ui-settings-models/package.json b/packages/client/ui-settings-models/package.json index 0f4aa17a27..69474742e2 100644 --- a/packages/client/ui-settings-models/package.json +++ b/packages/client/ui-settings-models/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-models", "description": "Models settings and shared product-onboarding dialogs over existing settings and credential joins", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-models/src/client/WelcomeNotice.tsx b/packages/client/ui-settings-models/src/client/WelcomeNotice.tsx index 51685beb44..798854ab32 100644 --- a/packages/client/ui-settings-models/src/client/WelcomeNotice.tsx +++ b/packages/client/ui-settings-models/src/client/WelcomeNotice.tsx @@ -13,7 +13,7 @@ import css from './WelcomeNotice.module.css' /** Registration-side dependencies of {@link WelcomeNotice}. */ export interface WelcomeNoticeInjected { hooks: { - /** Durable or process-local acknowledgement state. */ + /** Durable acknowledgement state. */ welcome: SnapshotStore } /** Welcome acknowledgement controller. */ diff --git a/packages/client/ui-settings-models/src/client/index.ts b/packages/client/ui-settings-models/src/client/index.ts index bb60baabe8..bf3c7e13e0 100644 --- a/packages/client/ui-settings-models/src/client/index.ts +++ b/packages/client/ui-settings-models/src/client/index.ts @@ -106,8 +106,6 @@ export function apply(ctx: ClientContext): void { schema, t, }) - // The scope's own memory mode is what keeps a remote browser process-local, - // so the store needs no isLoopback branch of its own. const welcomeController = new WelcomeNoticeStore(ctx.configForms.get>(WELCOME_NOTICE_SETTINGS_NAMESPACE)) const welcomeInjected = (): WelcomeNoticeInjected => ({ controller: welcomeController, diff --git a/packages/client/ui-settings-models/src/client/store.ts b/packages/client/ui-settings-models/src/client/store.ts index 52e00bd7eb..0b4d53fb8d 100644 --- a/packages/client/ui-settings-models/src/client/store.ts +++ b/packages/client/ui-settings-models/src/client/store.ts @@ -209,7 +209,9 @@ export class ModelsSettingsStore { if (!declared.ok) { this.failLoad(declared.error.message); return } const mirrored = this.describeFace.getSnapshot() if (mirrored.view === undefined) { - this.failLoad(mirrored.error ?? 'settings are unavailable in this browser') + /* v8 ignore next -- SettingsDescribeFace.ensure settles with a view or the read's error */ + if (mirrored.error === null) throw new Error('settings describe settled with neither a view nor an error') + this.failLoad(mirrored.error) return } const providers = joinProviderDirectory(registered.value, declared.value) diff --git a/packages/client/ui-settings-models/src/client/welcome-store.ts b/packages/client/ui-settings-models/src/client/welcome-store.ts index 1de2066ada..49cec92fc1 100644 --- a/packages/client/ui-settings-models/src/client/welcome-store.ts +++ b/packages/client/ui-settings-models/src/client/welcome-store.ts @@ -1,8 +1,6 @@ /** - * Welcome-notice state derived from the welcome settings scope. The scope is - * the transport: a loopback browser follows the durable Host section, while a - * remote browser's memory-mode scope never answers and the acknowledgement - * stays process-local here. + * Welcome-notice state derived from the welcome settings scope: the notice + * follows the Host section and records its acknowledgement through it. */ import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store' @@ -39,21 +37,16 @@ function assertNever(_value: never): never { throw new Error('unexpected welcome settings status') } -/** Coordinates durable Host acknowledgement or a process-local remote fallback. */ +/** Coordinates the durable Host acknowledgement. */ export class WelcomeNoticeStore { /** uSES-safe state source shared by the registered welcome step. */ readonly store: SnapshotStore = createSnapshotStore({ status: 'idle', acknowledged: false, error: null, }) - private localAcknowledged = false private saving = false private following: (() => void) | undefined - /** - * @param scope - the welcome settings namespace scope; its memory mode is - * what keeps a remote browser process-local. - */ constructor(private readonly scope: ConfigForm) {} /** @@ -67,17 +60,12 @@ export class WelcomeNoticeStore { } /** - * Persist this copy version, or advance only this process for a remote - * browser. Success is judged against the state the write left behind, so a - * refused or failed write reports false after its recovery read settles. - * @returns true when the selected persistence mode holds the acknowledgement. + * Persist this copy version through the scope. Success is judged against the + * state the write left behind, so a refused or failed write reports false + * after its recovery read settles. + * @returns true when the Host holds the acknowledgement. */ async acknowledge(): Promise { - if (this.scope.getSnapshot().mode === 'memory') { - this.localAcknowledged = true - this.derive() - return true - } this.saving = true this.store.update((state) => { state.status = 'saving'; state.error = null }) try { @@ -105,14 +93,6 @@ export class WelcomeNoticeStore { private derive(): void { if (this.saving) return const scope = this.scope.getSnapshot() - if (scope.mode === 'memory') { - this.store.update((state) => { - state.status = 'ready' - state.acknowledged = this.localAcknowledged - state.error = null - }) - return - } switch (scope.status) { case 'loading': this.store.update((state) => { state.status = 'loading'; state.error = null }) diff --git a/packages/client/ui-settings-models/tests/apply.client.spec.ts b/packages/client/ui-settings-models/tests/apply.client.spec.ts index 2372690ba2..f9dac62ceb 100644 --- a/packages/client/ui-settings-models/tests/apply.client.spec.ts +++ b/packages/client/ui-settings-models/tests/apply.client.spec.ts @@ -27,7 +27,7 @@ afterEach(() => { vi.unstubAllGlobals() }) // so browser-language detection never runs and a fresh LocaleRuntime opens on // FALLBACK_LOCALE (en); bench stages zh explicitly on the locale instead. -async function bench(isLoopback = true, mock = RemoteMock.create().load(remoteDefaultResponses), services: object = {}) { +async function bench(mock = RemoteMock.create().load(remoteDefaultResponses), services: object = {}) { onTestFinished(() => { mock.assertNoUnmatched() }) const ctx = new Context() await ctx.plugin(SlotRegistry).await() @@ -49,8 +49,6 @@ async function bench(isLoopback = true, mock = RemoteMock.create().load(remoteDe settings: mock.remote.settings, session: { initializeDefaultModel: vi.fn(async () => ({ ok: true, value: undefined })) }, }) - // The fixed Host facts the settings provider reads its persistence from. - remote.$host = { home: undefined, isLoopback } await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await() return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, remote } } @@ -242,22 +240,6 @@ describe('ui-settings-models apply', () => { expect(() => b.locale.register('settings.models', 'zh', {})).not.toThrow() expect(() => b.locale.register('settings.models', 'en', {})).not.toThrow() }) - - it('keeps remote-browser acknowledgement in process memory', async () => { - const b = await bench(false) - declare(b.slots) - await b.ctx.plugin({ inject: [...inject], apply }).await() - const entry = b.slots.entries('settings.onboarding') - .find(candidate => candidate.options.id === 'welcome-notice')! - const injected = ( - entry.inject as unknown as () => import('../src/client/WelcomeNotice.tsx').WelcomeNoticeInjected - )() - - await injected.controller.load() - expect(injected.controller.store.getSnapshot()).toEqual({ - status: 'ready', acknowledged: false, error: null, - }) - }) }) describe('pushed invalidations', () => { @@ -318,7 +300,7 @@ describe('pushed invalidations', () => { } const document = { writable: true, hasDocument: false, namespaces: [namespace] } mock.remote.settings.describe.mockResolvedValue(ok(document)) - const b = await bench(true, mock) + const b = await bench(mock) declare(b.slots) await b.ctx.plugin({ inject: [...inject], apply }).await() const entry = b.slots.entries('settings.onboarding') @@ -348,7 +330,7 @@ describe('pushed invalidations', () => { const describe = mock.remote.settings.describe describe.mockResolvedValue(ok(document)) const listProviders = vi.fn(() => Promise.resolve({ ok: true as const, value: [] })) - const b = await bench(true, mock, { listProviders }) + const b = await bench(mock, { listProviders }) declare(b.slots) await b.ctx.plugin({ inject: [...inject], apply }).await() const entry = b.slots.entries('settings.section') diff --git a/packages/client/ui-settings-models/tests/store.client.spec.ts b/packages/client/ui-settings-models/tests/store.client.spec.ts index bade6e9d63..50b3064979 100644 --- a/packages/client/ui-settings-models/tests/store.client.spec.ts +++ b/packages/client/ui-settings-models/tests/store.client.spec.ts @@ -439,24 +439,23 @@ describe('edge joins', () => { expect(state.rows[0]?.derivedCredential).toMatchObject({ configured: true }) }) - it('surfaces a settings describe failure', async () => { - const { ctx, mirror } = api({ describeSettings: () => Promise.resolve(remoteFail('settings down')) }) + it('recovers the provider directory when an initial settings failure is retried', async () => { + let settingsCall = 0 + const { ctx, mirror } = api({ + describeSettings: () => Promise.resolve(++settingsCall === 1 + ? remoteFail('settings down') + : remoteOk({ writable: true, hasDocument: false, namespaces: NAMESPACES })), + }) const store = new ModelsSettingsStore(ctx, settingsSchema, mirror) await store.load() expect(store.store.getSnapshot()).toMatchObject({ status: 'error', error: 'settings down' }) - }) - - it('reports a terminally unavailable settings mirror precisely', async () => { - const { ctx } = api() - const store = new ModelsSettingsStore( - ctx, - settingsSchema, - new SettingsDescribeMirror(ctx, 'memory'), - ) await store.load() - expect(store.store.getSnapshot()).toMatchObject({ - status: 'error', - error: 'settings are unavailable in this browser', + const state = store.store.getSnapshot() + expect(state).toMatchObject({ status: 'ready', error: null, writable: true }) + expect(state.rows.find(row => row.entry.provider === 'openai')).toMatchObject({ + configured: true, + apiKeyEnv: 'OPENAI_API_KEY', + credential: { configured: true, writable: true }, }) }) diff --git a/packages/client/ui-settings-models/tests/welcome-notice.client.spec.tsx b/packages/client/ui-settings-models/tests/welcome-notice.client.spec.tsx index 82794a042c..09cb3bb49d 100644 --- a/packages/client/ui-settings-models/tests/welcome-notice.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/welcome-notice.client.spec.tsx @@ -81,7 +81,6 @@ function mount( ctx, { namespace: WELCOME_NOTICE_SETTINGS_NAMESPACE, decode: decodeWelcomeSection }, mirror, - 'host', schemaService, ) const controller = new WelcomeNoticeStore(scope) diff --git a/packages/client/ui-settings-models/tests/welcome-store.client.spec.ts b/packages/client/ui-settings-models/tests/welcome-store.client.spec.ts index ba0e2d16ca..f03d23b912 100644 --- a/packages/client/ui-settings-models/tests/welcome-store.client.spec.ts +++ b/packages/client/ui-settings-models/tests/welcome-store.client.spec.ts @@ -3,8 +3,9 @@ import { Context } from '@deepseek-ai/cordis' import { SettingsSchemaService } from '@deepseek-ai/dsh-client-ui-settings/src/client/schema.ts' import { SettingsDescribeMirror } from '@deepseek-ai/dsh-client-ui-settings/src/client/settings-mirror.ts' import { ConfigFormController } from '@deepseek-ai/dsh-client-ui-settings/src/client/config-form.ts' -import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime' -import { decodeWelcomeSection, WelcomeNoticeStore } from '../src/client/welcome-store.ts' +import { RemoteError, TestRemote } from '@deepseek-ai/dsh-client-test-runtime' +import { apply as applySettings, inject as settingsInject } from '@deepseek-ai/dsh-client-ui-settings/client' +import { decodeWelcomeSection, WelcomeNoticeStore, type WelcomeSection } from '../src/client/welcome-store.ts' import { WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION, } from '../src/onboarding-copy.ts' @@ -26,7 +27,7 @@ function rejected(message: string) { function namespace(value: unknown = {}, revision = 0) { return { ns: WELCOME_NOTICE_SETTINGS_NAMESPACE, - schema: {}, + schema: { type: 'object', dict: { [WELCOME_NOTICE_ACK_FIELD]: { type: 'string', meta: {} } } }, value, autoGenerate: true, applies: 'live' as const, secrets: [], @@ -41,36 +42,19 @@ function acknowledgedNamespace(version: string, revision = 1) { /** The welcome store over a real mirror-derived scope and a fake wire. */ function buildWelcome( api: { describe?: ReturnType; mutate?: ReturnType }, - persistence: 'host' | 'memory' = 'host', ) { const ctx = { remote: { settings: api } } as never - const mirror = new SettingsDescribeMirror(ctx, persistence) + const mirror = new SettingsDescribeMirror(ctx) const scope = new ConfigFormController( ctx, { namespace: WELCOME_NOTICE_SETTINGS_NAMESPACE, decode: decodeWelcomeSection }, mirror, - persistence, schemaService, ) return { mirror, controller: new WelcomeNoticeStore(scope) } } describe('WelcomeNoticeStore', () => { - it('acknowledges in memory while Host settings persistence is disabled', async () => { - const describeCall = vi.fn() - const mutate = vi.fn() - const { controller } = buildWelcome({ describe: describeCall, mutate }, 'memory') - - await controller.load() - expect(controller.store.getSnapshot()).toEqual({ status: 'ready', acknowledged: false, error: null }) - await expect(controller.acknowledge()).resolves.toBe(true) - expect(controller.store.getSnapshot()).toEqual({ status: 'ready', acknowledged: true, error: null }) - await controller.load() - expect(controller.store.getSnapshot()).toEqual({ status: 'ready', acknowledged: true, error: null }) - expect(describeCall).not.toHaveBeenCalled() - expect(mutate).not.toHaveBeenCalled() - }) - it('acknowledges only the exact current copy version', async () => { for (const [version, acknowledged] of [ [undefined, false], @@ -108,6 +92,42 @@ describe('WelcomeNoticeStore', () => { expect(describeCall).toHaveBeenCalledTimes(1) }) + it('retains acknowledgement when the settings plugin is recreated', async () => { + let stored = namespace({}, 3) + const ctx = new Context() + new TestRemote(ctx, { + settings: { + describe: vi.fn(async () => ok({ writable: true, hasDocument: false, namespaces: [stored] })), + mutate: vi.fn(async () => { + stored = acknowledgedNamespace(WELCOME_NOTICE_VERSION, 4) + return ok(stored) + }), + }, + }) + const plugin = { inject: [...settingsInject], apply: applySettings } + let controller: WelcomeNoticeStore | undefined + try { + const first = ctx.plugin(plugin) + await first.await() + await ctx.configForms.describe().ensure() + controller = new WelcomeNoticeStore( + ctx.configForms.get(WELCOME_NOTICE_SETTINGS_NAMESPACE)) + await controller.load() + await expect(controller.acknowledge()).resolves.toBe(true) + controller.dispose() + await first.dispose() + await ctx.plugin(plugin).await() + await ctx.configForms.describe().ensure() + controller = new WelcomeNoticeStore( + ctx.configForms.get(WELCOME_NOTICE_SETTINGS_NAMESPACE)) + await controller.load() + expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: true }) + } finally { + controller?.dispose() + await ctx.fiber.dispose() + } + }) + it('keeps the notice pending while the settings read has not answered', async () => { const describeCall = vi.fn(() => Promise.reject(new Error('offline'))) const { mirror, controller } = buildWelcome({ describe: describeCall }) diff --git a/packages/client/ui-settings-plugin-inventory/package.json b/packages/client/ui-settings-plugin-inventory/package.json index b4f868e424..8eb5473590 100644 --- a/packages/client/ui-settings-plugin-inventory/package.json +++ b/packages/client/ui-settings-plugin-inventory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-plugin-inventory", "description": "Read-only Cordis Loader inventory tab in Web Plugins settings", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-plugins/package.json b/packages/client/ui-settings-plugins/package.json index 0f32affb06..cf9c95c7eb 100644 --- a/packages/client/ui-settings-plugins/package.json +++ b/packages/client/ui-settings-plugins/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-plugins", "description": "Built-in plugins settings section for the dsh web client: the Settings navigation entry and the tab chrome feature-owned tabs register into", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-session-log/package.json b/packages/client/ui-settings-session-log/package.json index 32fb45425d..b9bd5efedd 100644 --- a/packages/client/ui-settings-session-log/package.json +++ b/packages/client/ui-settings-session-log/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-session-log", "description": "General settings control for Session-log upload with DeepSeek API requests", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-session-log/tests/upload.client.spec.tsx b/packages/client/ui-settings-session-log/tests/upload.client.spec.tsx index 955d767892..4ddc52d85e 100644 --- a/packages/client/ui-settings-session-log/tests/upload.client.spec.tsx +++ b/packages/client/ui-settings-session-log/tests/upload.client.spec.tsx @@ -14,7 +14,7 @@ afterEach(cleanup) function fixture(writable = true) { const formState = createSnapshotStore>({ - status: 'ready', value: { enabled: true }, base: {}, user: {}, revision: 0, writable, mode: 'host', + status: 'ready', value: { enabled: true }, base: {}, user: {}, revision: 0, writable, }) const set = vi.fn(async (_field: string, enabled: unknown) => { formState.update((state) => { state.value = { enabled: enabled === true } }) diff --git a/packages/client/ui-settings-shell/package.json b/packages/client/ui-settings-shell/package.json index 017b51260c..0fd6020caf 100644 --- a/packages/client/ui-settings-shell/package.json +++ b/packages/client/ui-settings-shell/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-shell", "description": "Settings page of the shell executor on the dsh web client's Plugins page: the command timeout and the per-stream output cap of the shell namespace", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-subagent/package.json b/packages/client/ui-settings-subagent/package.json index 0899660cd8..0c1a5abe49 100644 --- a/packages/client/ui-settings-subagent/package.json +++ b/packages/client/ui-settings-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-subagent", "description": "Settings page of Subagent delegation on the dsh web client's Plugins page: recursion depth, parallel capacity, and the models agents may choose for subagents", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-web-search/package.json b/packages/client/ui-settings-web-search/package.json index 5cdf2fdfe7..db8da39f54 100644 --- a/packages/client/ui-settings-web-search/package.json +++ b/packages/client/ui-settings-web-search/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-web-search", "description": "Settings page of the DeepSeek web-search provider on the dsh web client's Plugins page: its API key, endpoint, and per-request search budget", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings/README.i18n.yaml b/packages/client/ui-settings/README.i18n.yaml index b5488bea3e..2b1ae7c2f5 100644 --- a/packages/client/ui-settings/README.i18n.yaml +++ b/packages/client/ui-settings/README.i18n.yaml @@ -18,8 +18,8 @@ en: 2417d8409aad6ca7 zh: 449eee623a2f334b /deepseek-ai-dsh-client-ui-settings/use-this-package/configuration-forms: - en: 5bda0401a6a4f158 - zh: 6778a64d3e2a7453 + en: c2b31046779bab9e + zh: ce183f67e63114b3 /deepseek-ai-dsh-client-ui-settings/use-this-package/following-served-namespaces: en: 2edd420502bebce1 zh: 6624abe01110c012 @@ -33,8 +33,8 @@ en: d756fe51d9456fb4 zh: 41de582aace7332f /deepseek-ai-dsh-client-ui-settings/understand-the-implementation/the-describe-mirror: - en: ea274a9480884a7f - zh: bbaf650e04646c04 + en: 0cd270ddcac46776 + zh: 4e03bf1d153b3f04 /deepseek-ai-dsh-client-ui-settings/understand-the-implementation/shared-entry-writes: en: 11a73ab8e9280b81 zh: f0e72a05959e3f32 @@ -51,8 +51,8 @@ en: ca75c51c89c2c9b0 zh: de54f3467ef3b9be /deepseek-ai-dsh-client-ui-settings/known-limitations-and-deferred-work: - en: d8ef5c4f3a48e05f - zh: 7c335dc8543249c6 + en: f74ad1a2cc849b42 + zh: 1320e40dcdb0b208 /deepseek-ai-dsh-client-ui-settings/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-settings/README.md b/packages/client/ui-settings/README.md index 0a7113c921..b43b91a829 100644 --- a/packages/client/ui-settings/README.md +++ b/packages/client/ui-settings/README.md @@ -29,9 +29,11 @@ Feature plugins use this package to store and edit their preferences without re- ### Configuration forms -`ctx.configForms.developerTools` owns the Coding Tools switch and the shared Web and desktop preference `ui-settings.enabled`, defaulting to `true`. Its `enabled` observable publishes accepted choices and `setEnabled` uses the same ordered settings writes. Desktop and loopback Web persist to the Host document; remote Web keeps this choice in one browser-local observable until reload without issuing Host writes. This controls presentation and HTML preview permissions, not Host authorization or Session recording. Host-backed clients keep developer features disabled until the first accepted schema-resolved value arrives; missing or failed initial responses do not enable them. Later refreshes retain the last accepted choice. +`ctx.configForms.developerTools` owns the Coding Tools switch and the shared Web and desktop preference `ui-settings.enabled`, defaulting to `true`. Its `enabled` observable publishes accepted choices and `setEnabled` uses the same ordered settings writes. Desktop and every authenticated Web browser persist to the Host document. This controls presentation and HTML preview permissions, not Host authorization or Session recording. Host-backed clients keep developer features disabled until the first accepted schema-resolved value arrives; missing or failed initial responses do not enable them. Later refreshes retain the last accepted choice. -Feature adapters use `ctx.configForms.get(entryId)` to obtain accepted values and a write queue shared by every editor of that Host entry. Snapshots contain resolved `value`, inherited `base`, raw `user`, revision, writability, and persistence mode. `set` and `unset` submit one operation; `mutate` submits one atomic operation list. Staged editors pass the revision read before editing; conflicts preserve their drafts. Unsetting removes the override and restores inheritance. +Feature adapters use `ctx.configForms.get(entryId)` to obtain accepted values and a write queue shared by every editor of that Host entry. Snapshots contain resolved `value`, inherited `base`, raw `user`, revision, and writability. `set` and `unset` submit one operation; `mutate` submits one atomic operation list. Staged editors pass the revision read before editing; conflicts preserve their drafts. Unsetting removes the override and restores inheritance. + +A configuration form persists through the Host settings service for every authenticated browser; the [authenticated-browser Host preferences note](../../../.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.md) owns that policy. `settings.describe.writable` is the authority on whether a write is accepted. ### Following served namespaces @@ -59,7 +61,7 @@ The package realizes one ownership rule: the browser keeps one shared mirror of ### The describe mirror -The Host Config of the `ui-settings` entry declares the default-on `enabled` preference. The Client plugin injects `remote` with its `settings` namespace, resolves Host persistence once from the fixed `remote.$host` facts, and owns the one `settings.describe` reader in the browser: a shared mirror refreshed on every forwarded `settings/document-updated` event and on `connection/reset` (the first connection included, closing the window where a commit lands between the eager read and the SSE subscription). Cross-namespace surfaces read it through `ctx.configForms.describe()`, a read/fold face (`getSnapshot`/`subscribe`/`ensure`, plus `acceptView` folding a write answer in). +The Host Config of the `ui-settings` entry declares the default-on `enabled` preference. The Client plugin injects `remote` with its `settings` namespace and owns the one `settings.describe` reader in the browser: a shared mirror refreshed on every forwarded `settings/document-updated` event and on `connection/reset` (the first connection included, closing the window where a commit lands between the eager read and the SSE subscription). Cross-namespace surfaces read it through `ctx.configForms.describe()`, a read/fold face (`getSnapshot`/`subscribe`/`ensure`, plus `acceptView` folding a write answer in). ### Shared entry writes @@ -101,9 +103,9 @@ None; this package neither assembles nor sends a provider request. -These limits define where the settings transport cannot reach; they are current package constraints. +These limits define the settings transport's reach and who shares a settings document; they are current package constraints. -- **Non-loopback pages get no durable settings** — this Client keeps Host persistence disabled there, so a form starts `unavailable` and never crosses the wire; form writes are inert even though Connection authentication covers the API. The shared Coding Tools preference instead provides browser-local changes. +- **Host preferences are shared across authenticated entries** — browsers accessing the same settings document share its persisted preferences, rather than retaining per-browser choices. ### Dev Note diff --git a/packages/client/ui-settings/README.zh.md b/packages/client/ui-settings/README.zh.md index 193d764637..463ec9fd15 100644 --- a/packages/client/ui-settings/README.zh.md +++ b/packages/client/ui-settings/README.zh.md @@ -29,9 +29,11 @@ kind: "package-reference" ### 配置表单 -`ctx.configForms.developerTools` 管理代码工作工具开关与 Web 和桌面端共享的偏好 `ui-settings.enabled`,默认为 `true`。其 `enabled` 可观察值发布已接受的选择,`setEnabled` 使用相同的有序设置写入。桌面端和回环 Web 将设置持久化到 Host 文档;远程 Web 将此选择保存在单个浏览器本地可观察值中,刷新后重置,不发送 Host 写入。此设置控制界面展示和 HTML 预览权限,不控制 Host 授权或 Session 记录。 使用 Host 偏好的客户端在首个经过 schema 解析并接受的值到达前保持开发者功能关闭;首次响应缺失或失败不会启用它们。后续刷新保留已接受的选择。 +`ctx.configForms.developerTools` 管理代码工作工具开关与 Web 和桌面端共享的偏好 `ui-settings.enabled`,默认为 `true`。其 `enabled` 可观察值发布已接受的选择,`setEnabled` 使用相同的有序设置写入。桌面端和每个已认证的 Web 浏览器都将设置持久化到 Host 文档。此设置控制界面展示和 HTML 预览权限,不控制 Host 授权或 Session 记录。 使用 Host 偏好的客户端在首个经过 schema 解析并接受的值到达前保持开发者功能关闭;首次响应缺失或失败不会启用它们。后续刷新保留已接受的选择。 -功能适配器使用 `ctx.configForms.get(entryId)` 获取该 Host 条目所有编辑器共享的已接受值和写入队列。快照包含解析后的 `value`、继承 `base`、原始 `user`、修订号、可写性和持久化模式。`set` 与 `unset` 提交单个操作,`mutate` 提交一个原子操作列表。暂存编辑器传入编辑前读取的修订号;冲突时保留草稿。清除操作移除覆盖并恢复继承。 +功能适配器使用 `ctx.configForms.get(entryId)` 获取该 Host 条目所有编辑器共享的已接受值和写入队列。快照包含解析后的 `value`、继承 `base`、原始 `user`、修订号和可写性。`set` 与 `unset` 提交单个操作,`mutate` 提交一个原子操作列表。暂存编辑器传入编辑前读取的修订号;冲突时保留草稿。清除操作移除覆盖并恢复继承。 + +配置表单对每个已认证浏览器都通过 Host settings 服务持久化;该策略由[已认证浏览器 Host 偏好笔记](../../../.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.zh.md)拥有。写入是否被接受由 `settings.describe.writable` 决定。 ### 跟随被服务的命名空间 @@ -59,7 +61,7 @@ kind: "package-reference" ### Describe 镜像 -`ui-settings` 条目的 Host Config 声明默认开启的 `enabled` 偏好。Client 插件注入 `remote` 及其 `settings` 命名空间,从固定的 `remote.$host` 事实一次性解析 Host 持久化模式,并持有浏览器中唯一的 `settings.describe` 读取方:一面共享镜像,在每次转发的 `settings/document-updated` 事件与 `connection/reset` 时刷新(首次连接也包含在内,关闭「提交落在急切读取与 SSE 订阅之间」的窗口)。跨命名空间表面通过 `ctx.configForms.describe()` 读它,这是一个读取/折叠面(`getSnapshot`/`subscribe`/`ensure`,另有把写应答折入的 `acceptView`)。 +`ui-settings` 条目的 Host Config 声明默认开启的 `enabled` 偏好。Client 插件注入 `remote` 及其 `settings` 命名空间,并持有浏览器中唯一的 `settings.describe` 读取方:一面共享镜像,在每次转发的 `settings/document-updated` 事件与 `connection/reset` 时刷新(首次连接也包含在内,关闭「提交落在急切读取与 SSE 订阅之间」的窗口)。跨命名空间表面通过 `ctx.configForms.describe()` 读它,这是一个读取/折叠面(`getSnapshot`/`subscribe`/`ensure`,另有把写应答折入的 `acceptView`)。 ### 共享条目写入 @@ -101,9 +103,9 @@ kind: "package-reference" -这些限制说明设置传输层够不到的地方;它们是当前包约束。 +这些限制说明设置传输层的覆盖范围以及谁共享同一份 settings 文档;它们是当前包约束。 -- **非 loopback 页面没有持久化设置**:本 Client 在那里禁用 Host 持久化,因此 表单以 `unavailable` 起步且从不跨线路;尽管 Connection 认证覆盖 API,表单写入仍在那里无效。共享的代码工作工具偏好单独提供浏览器本地变更。 +- **Host 偏好由各认证入口共享**:访问同一 settings 文档的浏览器共享其持久化偏好,而非各自保存浏览器偏好。 ### 开发备注 diff --git a/packages/client/ui-settings/package.json b/packages/client/ui-settings/package.json index e1cc80cd17..753a7e0294 100644 --- a/packages/client/ui-settings/package.json +++ b/packages/client/ui-settings/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings", "description": "Settings domain base plugin: shared configuration forms and the canonical settings slot-type contract", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings/src/client/config-form-types.ts b/packages/client/ui-settings/src/client/config-form-types.ts index 6276618562..41fc3bf738 100644 --- a/packages/client/ui-settings/src/client/config-form-types.ts +++ b/packages/client/ui-settings/src/client/config-form-types.ts @@ -8,8 +8,7 @@ import type { SettingsPathOpView } from '@deepseek-ai/dsh-api-remotes/client' export interface ConfigFormSnapshot { /** * `loading` until the first accepted section, `ready` while one stands, and - * `unavailable` when the namespace is not exposed to this client or the - * connection keeps preferences process-local (memory mode). + * `unavailable` when the namespace is not exposed to this client. */ status: 'loading' | 'ready' | 'unavailable' /** Last accepted schema-resolved section; undefined before the first acceptance. */ @@ -27,10 +26,8 @@ export interface ConfigFormSnapshot { user: unknown /** Namespace revision fencing the next write; undefined before the first Host view. */ revision: number | undefined - /** Whether the Host document accepts writes; memory mode never does. */ + /** Whether the Host document accepts writes. */ writable: boolean - /** `host` syncs with the Host document; `memory` keeps a remote browser process-local. */ - mode: 'host' | 'memory' } /** @@ -47,7 +44,7 @@ export interface ConfigForm { subscribe(listener: () => void): () => void /** * Queue one atomic namespace mutation. All operations share one revision - * fence, Host validation, persistence decision, and recovery read. Supplying + * fence, Host validation, and recovery read. Supplying * `expectedRevision` preserves an earlier read as the fence instead of using * the latest queued or mirrored revision. * @param ops - ordered field operations copied when queued. diff --git a/packages/client/ui-settings/src/client/config-form.ts b/packages/client/ui-settings/src/client/config-form.ts index cf77536a97..7e64f9f990 100644 --- a/packages/client/ui-settings/src/client/config-form.ts +++ b/packages/client/ui-settings/src/client/config-form.ts @@ -52,7 +52,7 @@ export class ConfigFormController implements ConfigForm { private tail: Promise = Promise.resolve() private writeGeneration = 0 private disposed = false - private readonly unsubscribe: (() => void) | undefined + private readonly unsubscribe: () => void /** * Revision answered by a superseded write still ahead of the mirror: the * mirror only folds the LATEST settlement in, so a queued successor takes @@ -65,29 +65,24 @@ export class ConfigFormController implements ConfigForm { * namespace carries this form's writes (reads ride the mirror). * @param spec - namespace identity and optional narrowing decoder. * @param mirror - the shared describe mirror this form derives from. - * @param persistence - client-selected Host persistence; non-loopback pages may remain process-local. * @param schema - settings-owned schema operations. */ constructor( private readonly ctx: Context, private readonly spec: ConfigFormSpec, private readonly mirror: SettingsDescribeMirror, - private readonly persistence: 'host' | 'memory', private readonly schema: SettingsSchemaService, ) { this.store = createSnapshotStore>({ - status: persistence === 'host' ? 'loading' : 'unavailable', + status: 'loading', value: undefined, base: undefined, user: undefined, revision: undefined, writable: false, - mode: persistence, }) - if (persistence === 'host') { - this.unsubscribe = mirror.subscribe(() => { this.derive() }) - this.derive() - } + this.unsubscribe = mirror.subscribe(() => { this.derive() }) + this.derive() } /** @returns the current sync snapshot (stable reference until the next change). */ @@ -167,12 +162,12 @@ export class ConfigFormController implements ConfigForm { async dispose(): Promise { this.disposed = true this.writeGeneration += 1 - this.unsubscribe?.() + this.unsubscribe() await this.tail } private enqueue(operation: () => Promise): Promise { - if (this.persistence === 'memory' || this.disposed) return Promise.resolve(false) + if (this.disposed) return Promise.resolve(false) const task = this.tail.then(async () => { if (this.disposed) return false return await operation() @@ -244,7 +239,6 @@ export class ConfigForms extends Service { readonly developerTools: DeveloperToolsPreference private readonly mirror: SettingsDescribeMirror private readonly schema: SettingsSchemaService - private readonly persistence: 'host' | 'memory' /** * The PROVIDING fiber, kept because a Service reads `ctx` as its *consumer's* * fiber: letting a shared form write through the caller's context would make @@ -254,19 +248,16 @@ export class ConfigForms extends Service { /** * @param ctx - the providing plugin's context. - * @param config - the shared describe mirror every shared form derives from, - * the settings-owned schema operations, and the Host persistence the provider - * resolved from `remote.$host`. + * @param config - the shared describe mirror every shared form derives from + * and the settings-owned schema operations. */ constructor(ctx: Context, config: { mirror: SettingsDescribeMirror schema: SettingsSchemaService - persistence: 'host' | 'memory' }) { super(ctx, 'configForms') this.mirror = config.mirror this.schema = config.schema - this.persistence = config.persistence this.owner = ctx this.developerTools = new DeveloperToolsPreference(this.get(DEVELOPER_TOOLS_NAMESPACE)) ctx.effect(() => async () => { @@ -294,7 +285,7 @@ export class ConfigForms extends Service { const existing = this.forms.get(entryId) if (existing !== undefined) return existing as ConfigFormController const form = new ConfigFormController( - this.owner, { namespace: entryId }, this.mirror, this.persistence, this.schema, + this.owner, { namespace: entryId }, this.mirror, this.schema, ) this.forms.set(entryId, form) void this.mirror.ensure() diff --git a/packages/client/ui-settings/src/client/developer-tools.ts b/packages/client/ui-settings/src/client/developer-tools.ts index d74de75956..52eb1242d0 100644 --- a/packages/client/ui-settings/src/client/developer-tools.ts +++ b/packages/client/ui-settings/src/client/developer-tools.ts @@ -1,5 +1,5 @@ /** One accepted preference drives every developer-tool consumer. */ -import { createSnapshotStore, type ObservableSnapshot } from '@deepseek-ai/dsh-client-store' +import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' import type { DeveloperToolsSettings } from '../developer-tools-settings.ts' import type { ConfigForm } from './config-form-types.ts' @@ -7,13 +7,12 @@ import type { ConfigForm } from './config-form-types.ts' export class DeveloperToolsPreference { /** Accepted enablement, observable through renderer-bound hooks. */ readonly enabled: ObservableSnapshot - private readonly local = createSnapshotStore(true) /** * @param scope - settings-owned namespace controller. */ constructor(private readonly scope: ConfigForm) { - this.enabled = scope.getSnapshot().mode === 'memory' ? this.local : { + this.enabled = { getSnapshot: () => scope.getSnapshot().value?.enabled ?? false, subscribe: (listener) => { let previous = this.enabled.getSnapshot() @@ -28,15 +27,11 @@ export class DeveloperToolsPreference { } /** - * Persist a Host choice with ordered writes, or update the shared browser-local choice. + * Persist the choice on the Host with ordered writes. * @param enabled - requested developer-tool mode. - * @returns settlement after local publication or Host acceptance; rejects after a refused write recovers. + * @returns settlement after Host acceptance; rejects after a refused write recovers. */ async setEnabled(enabled: boolean): Promise { - if (this.scope.getSnapshot().mode === 'memory') { - this.local.set(enabled) - return - } if (!await this.scope.set('enabled', enabled)) throw new Error('Developer tools preference was not saved') } } diff --git a/packages/client/ui-settings/src/client/index.ts b/packages/client/ui-settings/src/client/index.ts index 151a94d3fa..e6182781c8 100644 --- a/packages/client/ui-settings/src/client/index.ts +++ b/packages/client/ui-settings/src/client/index.ts @@ -1,7 +1,7 @@ /** Shared configuration forms and their Host describe mirror. */ import type { Context } from '@deepseek-ai/cordis' -// Type-only: the ctx.remote merge, the fixed Host facts, and the carrier's -// `connection/reset` lifecycle event, all through the assembly package. +// Type-only: the ctx.remote merge and the carrier's `connection/reset` +// lifecycle event, all through the assembly package. import type {} from '@deepseek-ai/dsh-api-remotes/client' // Type-only pair supplying `$on` and its key face without dragging a build // artifact into the Host graph (rationale beside the same pair in @@ -35,9 +35,9 @@ export const inject = ['remote', 'remote.settings'] */ export function apply(ctx: Context): void { const schema = new SettingsSchemaService(ctx) - // Every form uses the persistence mode resolved from the connected Host. - const persistence = ctx.remote.$host.isLoopback ? 'host' : 'memory' - const mirror = new SettingsDescribeMirror(ctx, persistence) + // Persistence policy: every authenticated browser persists through the Host document + // (.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.md). + const mirror = new SettingsDescribeMirror(ctx) ctx.effect(() => { const disposers = [ ctx.remote.$on('settings/document-updated', () => { void mirror.load() }), @@ -50,5 +50,5 @@ export function apply(ctx: Context): void { void mirror.ensure() return () => { for (const dispose of disposers) dispose() } }, 'ui-settings: describe mirror invalidations') - new ConfigForms(ctx, { mirror, schema, persistence }) + new ConfigForms(ctx, { mirror, schema }) } diff --git a/packages/client/ui-settings/src/client/settings-mirror.ts b/packages/client/ui-settings/src/client/settings-mirror.ts index 761a3f141c..8cdf37512d 100644 --- a/packages/client/ui-settings/src/client/settings-mirror.ts +++ b/packages/client/ui-settings/src/client/settings-mirror.ts @@ -26,11 +26,11 @@ export interface SettingsDescribeView { /** Mirror state every derived settings surface renders from. */ export interface SettingsMirrorSnapshot { /** - * `unavailable` is the terminal non-loopback state; `ready` persists across - * later failed refreshes (the held view keeps serving); `idle` means no - * answer is held and no read is running, so `ensure` will start one. + * `ready` persists across later failed refreshes (the held view keeps serving); + * `idle` means no answer is held and no read is running, so `ensure` will + * start one. */ - status: 'idle' | 'loading' | 'ready' | 'unavailable' + status: 'idle' | 'loading' | 'ready' /** The last good answer; undefined until the first success. */ view: SettingsDescribeView | undefined /** The latest refresh failure message, cleared by the next success. */ @@ -52,8 +52,8 @@ export interface SettingsDescribeFace { */ subscribe(listener: () => void): () => void /** - * Resolve once an answer is held (or the mirror is terminally unavailable), - * reading only from `idle`. + * Resolve once an answer is held, reading only from `idle`. A settled mirror + * holds either a view or the read's error. * @returns settlement of the current or newly started read, if any. */ ensure(): Promise @@ -79,14 +79,10 @@ export class SettingsDescribeMirror implements SettingsDescribeFace { /** * @param ctx - the providing plugin's context, whose `remote.settings` * namespace answers the describe read. - * @param persistence - client-selected Host persistence; non-loopback pages may remain process-local. */ - constructor( - private readonly ctx: ClientContext, - private readonly persistence: 'host' | 'memory' = 'host', - ) { + constructor(private readonly ctx: ClientContext) { this.store = createSnapshotStore({ - status: persistence === 'host' ? 'idle' : 'unavailable', + status: 'idle', view: undefined, error: null, }) @@ -112,7 +108,6 @@ export class SettingsDescribeMirror implements SettingsDescribeFace { * @returns settlement after this call's freshness is reflected. */ load(): Promise { - if (this.persistence === 'memory') return Promise.resolve() if (this.inFlight !== undefined) { this.rerun = true return this.inFlight @@ -124,13 +119,10 @@ export class SettingsDescribeMirror implements SettingsDescribeFace { } /** - * Resolve once an answer is held (or the mirror is terminally unavailable), - * reading only from `idle`. The cheap idempotent entry for surfaces that - * render on first use. + * Resolve once an answer is held, reading only from `idle`. * @returns settlement of the current or newly started read, if any. */ ensure(): Promise { - if (this.persistence === 'memory') return Promise.resolve() if (this.inFlight !== undefined) return this.inFlight if (this.getSnapshot().status === 'idle') return this.load() return Promise.resolve() diff --git a/packages/client/ui-settings/tests/config-form.client.spec.ts b/packages/client/ui-settings/tests/config-form.client.spec.ts index 3f185baf4f..e106af17ca 100644 --- a/packages/client/ui-settings/tests/config-form.client.spec.ts +++ b/packages/client/ui-settings/tests/config-form.client.spec.ts @@ -63,14 +63,14 @@ function deferred() { return { promise, resolve, reject } } -/** A host-mode mirror plus a controller derived from it, over one scripted context. */ +/** A mirror plus a controller derived from it, over one scripted context. */ function derivedScope( api: { describe?: ReturnType; mutate?: ReturnType }, spec: { namespace: string; decode?: (section: unknown) => UiTestSettings | undefined } = { namespace: 'ui-test' }, ) { const ctx = ctxWith(api) const mirror = new SettingsDescribeMirror(ctx) - const scope = new ConfigFormController(ctx, spec, mirror, 'host', settingsSchema) + const scope = new ConfigFormController(ctx, spec, mirror, settingsSchema) return { mirror, scope } } @@ -89,11 +89,11 @@ describe('ConfigFormController', () => { const describeCall = vi.fn().mockResolvedValueOnce(described({ preference: 'dark' }, 3)) const { mirror, scope } = derivedScope({ describe: describeCall }) expect(scope.getSnapshot()).toEqual({ - status: 'loading', value: undefined, revision: undefined, writable: false, mode: 'host', + status: 'loading', value: undefined, revision: undefined, writable: false, }) await mirror.load() expect(scope.getSnapshot()).toEqual({ - status: 'ready', value: { preference: 'dark' }, revision: 3, writable: true, mode: 'host', + status: 'ready', value: { preference: 'dark' }, revision: 3, writable: true, }) }) @@ -239,8 +239,8 @@ describe('ConfigFormController', () => { const mutate = vi.fn().mockResolvedValueOnce(ok(view({ preference: 'dark' }, 5))) const ctx = ctxWith({ describe: describeCall, mutate }) const mirror = new SettingsDescribeMirror(ctx) - const writer = new ConfigFormController(ctx, { namespace: 'ui-test' }, mirror, 'host', settingsSchema) - const sibling = new ConfigFormController(ctx, { namespace: 'ui-test' }, mirror, 'host', settingsSchema) + const writer = new ConfigFormController(ctx, { namespace: 'ui-test' }, mirror, settingsSchema) + const sibling = new ConfigFormController(ctx, { namespace: 'ui-test' }, mirror, settingsSchema) await mirror.load() await writer.set('preference', 'dark') expect(describeCall).toHaveBeenCalledTimes(1) @@ -424,7 +424,7 @@ describe('ConfigFormController', () => { }, } as never const scope = new ConfigFormController( - ctxWith({}), { namespace: 'ui-test' }, mirror, 'host', settingsSchema) + ctxWith({}), { namespace: 'ui-test' }, mirror, settingsSchema) expect(scope.getSnapshot()).toMatchObject({ value: { preference: 'dark' }, revision: 1 }) await scope.dispose() @@ -437,23 +437,6 @@ describe('ConfigFormController', () => { expect(scope.getSnapshot()).toMatchObject({ value: { preference: 'dark' }, revision: 1 }) }) - it('keeps a remote browser in memory mode without Host calls', async () => { - const describeCall = vi.fn() - const mutate = vi.fn() - const ctx = ctxWith({ describe: describeCall, mutate }) - const mirror = new SettingsDescribeMirror(ctx, 'memory') - const scope = new ConfigFormController( - ctx, { namespace: 'ui-test' }, mirror, 'memory', settingsSchema) - expect(scope.getSnapshot()).toEqual({ - status: 'unavailable', value: undefined, revision: undefined, writable: false, mode: 'memory', - }) - await mirror.load() - await scope.set('preference', 'dark') - await scope.dispose() - expect(describeCall).not.toHaveBeenCalled() - expect(mutate).not.toHaveBeenCalled() - }) - it('carries the composition base and the user layer into the snapshot', async () => { const layered: SettingsNamespaceView = { ...view({ preference: 'dark' }, 3), @@ -522,7 +505,7 @@ describe('ConfigForms.get', () => { let theme!: ConfigForm let locale!: ConfigForm new TestRemote(ctx, { settings: { describe: describeCall } }) - await ctx.plugin(ConfigForms, { mirror, schema: settingsSchema, persistence: 'host' }).await() + await ctx.plugin(ConfigForms, { mirror, schema: settingsSchema }).await() expect(ctx.configForms.describe()).toBe(mirror) const fiber = ctx.plugin({ inject: ['remote', 'configForms'], @@ -543,22 +526,4 @@ describe('ConfigForms.get', () => { expect(theme.getSnapshot()).toMatchObject({ revision: 1 }) }) - it('binds a remote browser in memory mode without starting a settings read', async () => { - const describeCall = vi.fn() - const mirror = new SettingsDescribeMirror(ctxWith({ describe: describeCall }), 'memory') - const ctx = new Context() - let scope!: ConfigForm - new TestRemote(ctx, { settings: { describe: describeCall } }) - await ctx.plugin(ConfigForms, { mirror, schema: settingsSchema, persistence: 'memory' }).await() - const fiber = ctx.plugin({ - inject: ['remote', 'configForms'], - apply: (plugin: Context) => { - scope = plugin.configForms.get('ui-test') - }, - }) - await fiber.await() - expect(scope.getSnapshot()).toMatchObject({ status: 'unavailable', mode: 'memory', writable: false }) - await fiber.dispose() - expect(describeCall).not.toHaveBeenCalled() - }) }) diff --git a/packages/client/ui-settings/tests/developer-tools.client.spec.ts b/packages/client/ui-settings/tests/developer-tools.client.spec.ts index 373c491dfe..77eac74052 100644 --- a/packages/client/ui-settings/tests/developer-tools.client.spec.ts +++ b/packages/client/ui-settings/tests/developer-tools.client.spec.ts @@ -63,20 +63,15 @@ describe('developer tools settings', () => { expect(ctx.configForms.developerTools.enabled.getSnapshot()).toBe(false) }) - it('shares one remote-browser preference across consumers and disposes it with the plugin', async () => { + it('shares one preference instance across consumers and disposes it with the plugin', async () => { const ctx = new Context() onTestFinished(() => ctx.fiber.dispose()) - const describeCall = vi.fn() - const remote = new TestRemote(ctx, { settings: { describe: describeCall } }) - remote.$host = { home: undefined, isLoopback: false } + const describeCall = vi.fn().mockResolvedValue({ ok: true, value: { writable: true, hasDocument: true, namespaces: [] } }) + new TestRemote(ctx, { settings: { describe: describeCall } }) const fiber = ctx.plugin({ inject, apply: clientApply }) await fiber.await() const preference = ctx.configForms.developerTools - expect(fiber.ctx.configForms.developerTools.enabled).toBe(preference.enabled) - expect(preference.enabled.getSnapshot()).toBe(true) - await preference.setEnabled(true) - expect(fiber.ctx.configForms.developerTools.enabled.getSnapshot()).toBe(true) - expect(describeCall).not.toHaveBeenCalled() + expect(fiber.ctx.configForms.developerTools).toBe(preference) await fiber.dispose() expect(ctx.get('configForms')).toBeUndefined() }) @@ -97,24 +92,6 @@ describe('developer tools settings', () => { }) }) -it('keeps remote browser choices local and publishes only changed values', async () => { - const host = stubConfigForm() - host.publish({ mode: 'memory' }) - const preference = new DeveloperToolsPreference(host.scope) - const notify = vi.fn() - const dispose = preference.enabled.subscribe(notify) - expect(preference.enabled.getSnapshot()).toBe(true) - await preference.setEnabled(false) - expect(preference.enabled.getSnapshot()).toBe(false) - await preference.setEnabled(false) - expect(notify).toHaveBeenCalledOnce() - await preference.setEnabled(true) - expect(preference.enabled.getSnapshot()).toBe(true) - expect(notify).toHaveBeenCalledTimes(2) - expect(host.set).not.toHaveBeenCalled() - dispose() -}) - it('ignores host revisions that do not change enablement', () => { const host = stubConfigForm() const preference = new DeveloperToolsPreference(host.scope) diff --git a/packages/client/ui-settings/tests/plugin.client.spec.ts b/packages/client/ui-settings/tests/plugin.client.spec.ts index 446ba9add8..3c4c0c8933 100644 --- a/packages/client/ui-settings/tests/plugin.client.spec.ts +++ b/packages/client/ui-settings/tests/plugin.client.spec.ts @@ -1,16 +1,33 @@ import { Context } from '@deepseek-ai/cordis' -import { describe, expect, it, vi } from 'vitest' +import z from '@deepseek-ai/schemastery' +import { describe, expect, it, onTestFinished, vi } from 'vitest' +import type { SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client' import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime' import { apply, inject } from '../src/client/index.ts' import { SettingsSchemaService } from '../src/client/schema.ts' import { ConfigForms } from '../src/client/config-form.ts' +const schema = z.object({ preference: z.string().required() }).toJSON() + +function described(preference: string, revision: number) { + const namespace: SettingsNamespaceView = { + ns: 'ui-test', + schema: JSON.parse(JSON.stringify(schema)) as SettingsNamespaceView['schema'], + value: { preference }, + autoGenerate: true, + applies: 'live', + secrets: [], + revision, + } + return { ok: true as const, value: { writable: true, hasDocument: true, namespaces: [namespace] } } +} + function bench() { - const describeCall = vi.fn().mockResolvedValue({ - ok: true, value: { writable: true, hasDocument: true, namespaces: [] }, - }) + const describeCall = vi.fn().mockResolvedValue(described('light', 0)) const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) const remote = new TestRemote(ctx, { settings: { describe: describeCall } }) + remote.$host = { home: undefined, isLoopback: false } return { ctx, describeCall, remote, fiber: ctx.plugin({ inject: [...inject], apply }) } } @@ -23,14 +40,24 @@ describe('settings domain base plugin', () => { await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(1) }) }) - it('refreshes the mirror on document commits and connection resets, once each', async () => { + it('refreshes remote ConfigForm values and revisions on document commits and connection resets', async () => { const { ctx, describeCall, remote, fiber } = bench() await fiber.await() - await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(1) }) - remote.emit('settings/document-updated', ['ui-test', 0]) - await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(2) }) + await ctx.configForms.describe().ensure() + const scope = ctx.configForms.get<{ preference: string }>('ui-test') + expect(scope.getSnapshot()).toMatchObject({ + status: 'ready', value: { preference: 'light' }, revision: 0, writable: true, + }) + describeCall.mockResolvedValue(described('dark', 1)) + remote.emit('settings/document-updated', ['ui-test', 1]) + await vi.waitFor(() => { + expect(scope.getSnapshot()).toMatchObject({ value: { preference: 'dark' }, revision: 1 }) + }) + describeCall.mockResolvedValue(described('system', 2)) ctx.emit('connection/reset') - await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(3) }) + await vi.waitFor(() => { + expect(scope.getSnapshot()).toMatchObject({ value: { preference: 'system' }, revision: 2 }) + }) }) it('fiber disposal retires the service and its invalidation subscriptions', async () => { diff --git a/packages/client/ui-settings/tests/settings-mirror.client.spec.ts b/packages/client/ui-settings/tests/settings-mirror.client.spec.ts index e44c3297dd..4262261399 100644 --- a/packages/client/ui-settings/tests/settings-mirror.client.spec.ts +++ b/packages/client/ui-settings/tests/settings-mirror.client.spec.ts @@ -94,15 +94,6 @@ describe('SettingsDescribeMirror', () => { expect(describeCall).toHaveBeenCalledTimes(1) }) - it('memory persistence is terminally unavailable and never touches the wire', async () => { - const describeCall = vi.fn() - const mirror = new SettingsDescribeMirror(ctxWith(describeCall), 'memory') - await mirror.ensure() - await mirror.load() - expect(mirror.getSnapshot()).toEqual({ status: 'unavailable', view: undefined, error: null }) - expect(describeCall).not.toHaveBeenCalled() - }) - it('acceptView folds one write answer into the held view without a wire read', async () => { const describeCall = vi.fn() .mockResolvedValueOnce(described([view('theme', 1), view('locale', 4)])) diff --git a/packages/client/ui-settings/tests/while-served.client.spec.ts b/packages/client/ui-settings/tests/while-served.client.spec.ts index c61286e741..eff63911f7 100644 --- a/packages/client/ui-settings/tests/while-served.client.spec.ts +++ b/packages/client/ui-settings/tests/while-served.client.spec.ts @@ -25,7 +25,7 @@ async function bench(namespaces: readonly string[]) { const ctx = new Context() new TestRemote(ctx, { settings: { describe: describeCall } }) const mirror = new SettingsDescribeMirror({ remote: { settings: { describe: describeCall } } } as never) - await ctx.plugin(ConfigForms, { mirror, schema: new SettingsSchemaService(ctx), persistence: 'host' }).await() + await ctx.plugin(ConfigForms, { mirror, schema: new SettingsSchemaService(ctx) }).await() return { ctx, mirror, describeCall } } diff --git a/packages/client/ui-shortcuts/package.json b/packages/client/ui-shortcuts/package.json index 72204436f3..516f7654a5 100644 --- a/packages/client/ui-shortcuts/package.json +++ b/packages/client/ui-shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-shortcuts", "description": "Keyboard shortcut reference, recording, and local preference editing", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-browser/package.json b/packages/client/ui-sidebar-browser/package.json index b97e65024b..5383e00f14 100644 --- a/packages/client/ui-sidebar-browser/package.json +++ b/packages/client/ui-sidebar-browser/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-browser", "description": "Sandboxed Web browser tabs for the right Sidebar", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-documentpreview/package.json b/packages/client/ui-sidebar-documentpreview/package.json index de0132b03f..d071660c88 100644 --- a/packages/client/ui-sidebar-documentpreview/package.json +++ b/packages/client/ui-sidebar-documentpreview/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-documentpreview", "description": "Extensible Sidebar previews for Office documents, spreadsheets, Markdown, code, images, PDF, HTML, and plain text", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-files/package.json b/packages/client/ui-sidebar-files/package.json index 791ed03364..bf9bc4d32f 100644 --- a/packages/client/ui-sidebar-files/package.json +++ b/packages/client/ui-sidebar-files/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-files", "description": "Workspace file tree tab type for the right Sidebar: lazy directory listing over the workspaceFiles Remote namespace, opening files into the Sidebar", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-right/package.json b/packages/client/ui-sidebar-right/package.json index 406c3322ac..a4247518af 100644 --- a/packages/client/ui-sidebar-right/package.json +++ b/packages/client/ui-sidebar-right/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-right", "description": "Right Sidebar: the docking surface's session-bound state, its panel and header expand control, and the navigation service over it", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -70,7 +70,7 @@ "picomatch": "^4.0.7", "clsx": "^2.0.0", "@deepseek-ai/dsh-util-crypto": "workspace:*", - "zod": "^4.4.3", + "zod": "4.4.3", "@deepseek-ai/dsh-client-shortcuts": "workspace:*", "@deepseek-ai/dsh-brand": "workspace:*" }, diff --git a/packages/client/ui-sidebar-terminal/package.json b/packages/client/ui-sidebar-terminal/package.json index 713c3e9c77..5a4bc2b62f 100644 --- a/packages/client/ui-sidebar-terminal/package.json +++ b/packages/client/ui-sidebar-terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-terminal", "description": "Interactive shell tabs for the right Sidebar", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar/package.json b/packages/client/ui-sidebar/package.json index 092797ec13..b93ff78675 100644 --- a/packages/client/ui-sidebar/package.json +++ b/packages/client/ui-sidebar/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar", "description": "Sidebar plugin: session multi-level tree, search, grouping, state dots", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-skill/package.json b/packages/client/ui-skill/package.json index b4e69169b7..de02a05d7a 100644 --- a/packages/client/ui-skill/package.json +++ b/packages/client/ui-skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-skill", "description": "Web skill references and the dedicated skill tool row", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-slots/package.json b/packages/client/ui-slots/package.json index 7e7e9c28fb..96aa83737a 100644 --- a/packages/client/ui-slots/package.json +++ b/packages/client/ui-slots/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-slots", "description": "Slot registry pure core: typed ordinary Slots and reusable Component Factories, derived props, Store seats, and renderer installation", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-subagent/package.json b/packages/client/ui-subagent/package.json index 77876b327d..ff0d19190b 100644 --- a/packages/client/ui-subagent/package.json +++ b/packages/client/ui-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-subagent", "description": "Subagent conversation catalog, continuation routing UI, and '@' reference source", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-theme/README.i18n.yaml b/packages/client/ui-theme/README.i18n.yaml index b485bb3eb2..d828a43c5a 100644 --- a/packages/client/ui-theme/README.i18n.yaml +++ b/packages/client/ui-theme/README.i18n.yaml @@ -9,17 +9,17 @@ en: 06c7d7624947a72a zh: ac4ca82984564825 /deepseek-ai-dsh-client-ui-theme/summary: - en: caf6bc13d14dd556 - zh: dc516b595733b697 + en: d9832b31c4cb3ca4 + zh: f2ec42bb5ae66188 /deepseek-ai-dsh-client-ui-theme/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-theme/use-this-package: - en: 219aaf8d2eadee85 - zh: f3bc1a5cfb3bfee5 + en: 5a279269e31713c1 + zh: dcd5c31fbdfd63e2 /deepseek-ai-dsh-client-ui-theme/use-this-package/appearance-and-font-size: - en: 5232d0517dfff80b - zh: 72abb7a9c4fa3ebb + en: ebf89ea31593f49c + zh: 06c98ba30ec3bb6b /deepseek-ai-dsh-client-ui-theme/use-this-package/fonts: en: b45d5b48ca799c3a zh: af65514b3759204f @@ -39,11 +39,11 @@ en: bf76e7c34a331595 zh: 5accd0260ef7c61e /deepseek-ai-dsh-client-ui-theme/understand-the-implementation/preference-persistence: - en: 7280ae0a30d26142 - zh: 5f2ba9a479fd4453 + en: b1f0b0bce5e53bae + zh: 26ded298f85834ae /deepseek-ai-dsh-client-ui-theme/further-exploration: - en: 10664fe91f2ca00b - zh: 086f8dffdc223c22 + en: 7d5b953aaeeaf591 + zh: ff4bccb0f196f2b9 /deepseek-ai-dsh-client-ui-theme/model-experience: en: 3120df665299c634 zh: 1d9f365d091d0d2a diff --git a/packages/client/ui-theme/README.md b/packages/client/ui-theme/README.md index 8f57dd4be2..1ed77be835 100644 --- a/packages/client/ui-theme/README.md +++ b/packages/client/ui-theme/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-client-ui-theme` lets Web GUI users choose `light`, `dark`, or `system`, and set the font and font size of text, code, and the terminal separately in Settings. A loopback client stores these values in the `ui-theme` settings namespace, which the local provider persists in `$DSH_HOME/cordis.patch.yml` by default. The plugin resolves `system` through `prefers-color-scheme` and publishes immutable `ThemeSnapshot`s; ui-layout applies each snapshot to the document. The package also ships the `--dsw-*` token stylesheets and injects a synchronous bootstrap so the selected palette, font size, and fonts apply before the shell loads. Third-party themes can register alias-token overrides through `ctx.theme`. +`dsh-client-ui-theme` lets Web GUI users choose `light`, `dark`, or `system`, and set the font and font size of text, code, and the terminal separately in Settings. Every authenticated browser stores these values in the `ui-theme` settings namespace, which the local provider persists in `$DSH_HOME/cordis.patch.yml` by default. The plugin resolves `system` through `prefers-color-scheme` and publishes immutable `ThemeSnapshot`s; ui-layout applies each snapshot to the document. The package also ships the `--dsw-*` token stylesheets and injects a synchronous bootstrap so the selected palette, font size, and fonts apply before the shell loads. Third-party themes can register alias-token overrides through `ctx.theme`. ## Table of Contents @@ -25,11 +25,11 @@ English | [中文](README.zh.md) ## Use this package -Users switch the color scheme, font sizes, and fonts from rows in Settings (General section); the choices persist across restarts on a loopback browser. Feature plugins consume the current snapshot through `ctx.theme` and read the `--dsw-*` tokens in CSS; they do not manage theme state themselves. +Users switch the color scheme, font sizes, and fonts from rows in Settings (General section); the choices persist across restarts in every authenticated browser. Feature plugins consume the current snapshot through `ctx.theme` and read the `--dsw-*` tokens in CSS; they do not manage theme state themselves. ### Appearance and font size -The plugin registers Appearance preference cubes and the text font-size stepper in the General section. A More font settings button after the stepper expands a region below it, collapsed on every settings open, with a font row for each of text, code, and the terminal, plus the code and terminal font-size steppers. The text stepper accepts integer values from 10 through 22 px and defaults to 14 px. It changes conversation headings and base text by the same increment, including the user bubble and composer draft; flow-row titles, summaries, and tables follow one step under the body size, while small text keeps a fixed size. The code stepper (10–16 px, default 11) sets the code-block size; inline code stays 1 px larger, and the small tool-output variant uses the same size. The terminal stepper (10–20 px, default 13) sets the sidebar terminal cell font size; changing it keeps the shell and output and refits the grid. Each accepted change writes through the Host settings API. Rapid changes serialize in gesture order with namespace revisions, and a rejected latest write reloads the durable values. Non-loopback pages keep these choices process-local. +The plugin registers Appearance preference cubes and the text font-size stepper in the General section. A More font settings button after the stepper expands a region below it, collapsed on every settings open, with a font row for each of text, code, and the terminal, plus the code and terminal font-size steppers. The text stepper accepts integer values from 10 through 22 px and defaults to 14 px. It changes conversation headings and base text by the same increment, including the user bubble and composer draft; flow-row titles, summaries, and tables follow one step under the body size, while small text keeps a fixed size. The code stepper (10–16 px, default 11) sets the code-block size; inline code stays 1 px larger, and the small tool-output variant uses the same size. The terminal stepper (10–20 px, default 13) sets the sidebar terminal cell font size; changing it keeps the shell and output and refits the grid. Each accepted change writes through the Host settings API. Rapid changes serialize in gesture order with namespace revisions, and a rejected latest write reloads the durable values. ### Fonts @@ -91,7 +91,7 @@ The `--dsw-alias-turn-trigger-*` tokens provide separate resting and hover backg ### Preference persistence -The service provides itself immediately with the schema defaults on a loopback browser, then loads the `ui-theme` namespace and writes each accepted theme, font-size, or font-family change through the Host settings API. Pushed settings changes and reconnects refetch the namespace. While its own writes are pending, the service ignores refetched sections, so an echo of an earlier write cannot revert a later change; it adopts the durable section once the writes settle. Non-loopback pages do not create that Host-backed scope. The persistence boundary is owned by the [Host-backed preferences reference](../ui-settings/README.md). +The service provides itself immediately with the schema defaults, then loads the `ui-theme` namespace and writes each accepted theme, font-size, or font-family change through the Host settings API. Pushed settings changes and reconnects refetch the namespace. While its own writes are pending, the service ignores refetched sections, so an echo of an earlier write cannot revert a later change; it adopts the durable section once the writes settle. The persistence boundary is owned by the [Host-backed preferences reference](../ui-settings/README.md). @@ -106,7 +106,7 @@ These pages cover the layout presenter, the token consumers, and the styling rul - [ui-sidebar](../ui-sidebar/README.md) — a consumer of the scrollbar rebinding contract. - [ui-conversation](../ui-conversation/README.md) — a consumer of `--dsh-scrollbar-width` for the composer seat. - [Web styling](../../../docs/web-styling.md) — the authoritative styling rules for web client components. -- [historical Host-backed preferences](../../../.agents/notes/archived/bug-fix/2026-08-06-host-backed-web-preferences.md) — the persistence boundary decision. +- [Authenticated-browser Host preferences](../../../.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.md) — the persistence policy decision. ----- diff --git a/packages/client/ui-theme/README.zh.md b/packages/client/ui-theme/README.zh.md index e35467355e..bac4c7f3a3 100644 --- a/packages/client/ui-theme/README.zh.md +++ b/packages/client/ui-theme/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -`dsh-client-ui-theme` 让 Web GUI 用户在设置中选择 `light`、`dark` 或 `system`,并分别设置正文、代码与终端的字体和字号。回环客户端把这些值存入 `ui-theme` 设置命名空间,本地提供方默认将其持久化到 `$DSH_HOME/cordis.patch.yml`。插件通过 `prefers-color-scheme` 解析 `system` 并发布不可变的 `ThemeSnapshot`;ui-layout 把每份快照应用到文档。本包还提供 `--dsw-*` token 样式表,并注入同步引导,使所选调色板、字号与字体在外壳加载前生效。第三方主题可通过 `ctx.theme` 注册别名 token 覆盖。 +`dsh-client-ui-theme` 让 Web GUI 用户在设置中选择 `light`、`dark` 或 `system`,并分别设置正文、代码与终端的字体和字号。每个已认证浏览器都把这些值存入 `ui-theme` 设置命名空间,本地提供方默认将其持久化到 `$DSH_HOME/cordis.patch.yml`。插件通过 `prefers-color-scheme` 解析 `system` 并发布不可变的 `ThemeSnapshot`;ui-layout 把每份快照应用到文档。本包还提供 `--dsw-*` token 样式表,并注入同步引导,使所选调色板、字号与字体在外壳加载前生效。第三方主题可通过 `ctx.theme` 注册别名 token 覆盖。 ## 目录 @@ -25,11 +25,11 @@ kind: "package-reference" ## 使用本包 -用户从设置(「通用」分区)的各行中切换配色方案、字号与字体;在回环浏览器上,这些选择都会跨重启持久化。功能插件通过 `ctx.theme` 消费当前快照,并在 CSS 中读取 `--dsw-*` token;它们不自行管理主题状态。 +用户从设置(「通用」分区)的各行中切换配色方案、字号与字体;在每个已认证浏览器中,这些选择都会跨重启持久化。功能插件通过 `ctx.theme` 消费当前快照,并在 CSS 中读取 `--dsw-*` token;它们不自行管理主题状态。 ### 外观与字号 -插件在「通用」分区注册外观偏好方块与正文字号步进器。步进器后的「更多字体设置」按钮在其下方展开一个区域,每次打开设置时均为折叠状态,其中包含正文、代码与终端各自的字体行,以及代码与终端的字号步进器。正文步进器接受 10 至 22 px 的整数,默认值为 14 px。它以相同增量调整会话标题与基础文本,包括用户气泡与 composer 草稿;流内行的标题、摘要与表格跟随比正文低一档的字号,小号文本保持固定字号。代码步进器(10–16 px,默认 11)设置代码块字号;行内代码保持大 1 px,工具输出的小号变体使用相同字号。终端步进器(10–20 px,默认 13)设置侧栏终端的单元格字号;修改后保留 shell 与输出,并重新适配网格。每次通过的变更都经 Host settings API 写入。连续快速变更按操作顺序携带命名空间 revision 串行写入,最新写入被拒时重新加载持久值。非 loopback 页面把这些选择都保留在进程内。 +插件在「通用」分区注册外观偏好方块与正文字号步进器。步进器后的「更多字体设置」按钮在其下方展开一个区域,每次打开设置时均为折叠状态,其中包含正文、代码与终端各自的字体行,以及代码与终端的字号步进器。正文步进器接受 10 至 22 px 的整数,默认值为 14 px。它以相同增量调整会话标题与基础文本,包括用户气泡与 composer 草稿;流内行的标题、摘要与表格跟随比正文低一档的字号,小号文本保持固定字号。代码步进器(10–16 px,默认 11)设置代码块字号;行内代码保持大 1 px,工具输出的小号变体使用相同字号。终端步进器(10–20 px,默认 13)设置侧栏终端的单元格字号;修改后保留 shell 与输出,并重新适配网格。每次通过的变更都经 Host settings API 写入。连续快速变更按操作顺序携带命名空间 revision 串行写入,最新写入被拒时重新加载持久值。 ### 字体 @@ -91,7 +91,7 @@ kind: "package-reference" ### 偏好持久化 -在 loopback 浏览器上,服务先以 schema 默认值立即提供自身,随后加载 `ui-theme` 命名空间,并把每次通过的主题、字号或字体变更经 Host settings API 写入。收到推送的设置变更时或重连后都会重新拉取该命名空间。自身写入尚未完成时,服务忽略重新拉取的配置段,避免较早写入的回显覆盖之后的修改;写入全部完成后再采用持久配置段。非 loopback 页面不会创建该 Host-backed scope。该持久化边界由 [Host 支撑的偏好参考](../ui-settings/README.zh.md) 拥有。 +服务先以 schema 默认值立即提供自身,随后加载 `ui-theme` 命名空间,并把每次通过的主题、字号或字体变更经 Host settings API 写入。收到推送的设置变更时或重连后都会重新拉取该命名空间。自身写入尚未完成时,服务忽略重新拉取的配置段,避免较早写入的回显覆盖之后的修改;写入全部完成后再采用持久配置段。该持久化边界由 [Host 支撑的偏好参考](../ui-settings/README.zh.md) 拥有。 @@ -106,7 +106,7 @@ kind: "package-reference" - [ui-sidebar](../ui-sidebar/README.zh.md)——滚动条重新绑定约定的消费方。 - [ui-conversation](../ui-conversation/README.zh.md)——为 composer 席位消费 `--dsh-scrollbar-width` 的消费方。 - [Web 样式](../../../docs/web-styling.zh.md)——Web 客户端组件的权威样式规则。 -- [历史Host 支撑的偏好](../../../.agents/notes/archived/bug-fix/2026-08-06-host-backed-web-preferences.md)——持久化边界决策。 +- [已认证浏览器 Host 偏好](../../../.agents/notes/implemented/bug-fix/2026-09-15-authenticated-browser-host-preferences.zh.md)——持久化策略决策。 ----- diff --git a/packages/client/ui-theme/package.json b/packages/client/ui-theme/package.json index 2e43211b92..b6fa339890 100644 --- a/packages/client/ui-theme/package.json +++ b/packages/client/ui-theme/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-theme", "description": "Theme plugin: Host bootstrap for the pre-plugin palette; DOM-free ThemeRuntime for light/dark/system state; --dsw-* token styles and Appearance settings row", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-theme/tests/apply.client.spec.ts b/packages/client/ui-theme/tests/apply.client.spec.ts index b49e22c548..9bb2ef9db0 100644 --- a/packages/client/ui-theme/tests/apply.client.spec.ts +++ b/packages/client/ui-theme/tests/apply.client.spec.ts @@ -169,7 +169,7 @@ describe('ui-theme apply', () => { await vi.waitFor(() => { expect(b.mutate).toHaveBeenCalledTimes(3) }) }) - it('loads Host settings at boot, refreshes its namespace, and keeps remote browsers process-local', async () => { + it('loads Host settings at boot, refreshes its namespace, and persists for a trusted remote page', async () => { const b = await bench() // The shared mirror read once at bench time; a Host-side change reaches it // through the document invalidation, exactly as production announces one. @@ -192,14 +192,15 @@ describe('ui-theme apply', () => { b.ctx.emit('connection/reset') await vi.waitFor(() => { expect(theme.getTheme().preference).toBe('dark') }) + // A non-loopback page persists through the same path as loopback. const remote = await bench(false) declareItems(remote.slots) await remote.ctx.plugin({ inject: [...inject], apply }).await() const remoteTheme = remote.ctx.get('theme') as ThemeRuntime remoteTheme.setTheme('dark') - await Promise.resolve() - expect(remote.describe).not.toHaveBeenCalled() - expect(remote.mutate).not.toHaveBeenCalled() + await vi.waitFor(() => { + expect(remote.mutate).toHaveBeenCalledWith(THEME_SETTINGS_NAMESPACE, [{ op: 'set', path: ['preference'], value: 'dark' }], 0) + }) }) it('activates before a slow settings refresh and converges when it settles', async () => { diff --git a/packages/client/ui-tool/package.json b/packages/client/ui-tool/package.json index 6058888f57..e41d049302 100644 --- a/packages/client/ui-tool/package.json +++ b/packages/client/ui-tool/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-tool", "description": "Client Tool call-tree renderer and keyed per-tool presentation slot", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-trajectory/package.json b/packages/client/ui-trajectory/package.json index f184db05d2..d847bebe73 100644 --- a/packages/client/ui-trajectory/package.json +++ b/packages/client/ui-trajectory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-trajectory", "description": "Trajectory event ledger with an interactive timing overview: pure-consumer plugin registering into the conversation ViewMap (no service)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -68,7 +68,7 @@ "@deepseek-ai/dsh-client-ui-session": "workspace:*", "@deepseek-ai/dsh-llm": "workspace:*", "@deepseek-ai/dsh-attachment": "workspace:*", - "@tanstack/react-virtual": "^3.14.10", + "@tanstack/react-virtual": "3.14.10", "diff": "^9.0.0" }, "files": [ diff --git a/packages/client/ui-trajectory/tests/client-bundle.client.spec.ts b/packages/client/ui-trajectory/tests/client-bundle.client.spec.ts index 8a6751993a..6bdac1f28f 100644 --- a/packages/client/ui-trajectory/tests/client-bundle.client.spec.ts +++ b/packages/client/ui-trajectory/tests/client-bundle.client.spec.ts @@ -50,6 +50,7 @@ describe('tsdown client artifact', () => { ['react', await import('react')], ['react/jsx-runtime', await import('react/jsx-runtime')], ['react-dom', await import('react-dom')], + ['@tanstack/react-virtual', await import('@tanstack/react-virtual')], ['@deepseek-ai/dsh-client-store', await import('@deepseek-ai/dsh-client-store')], ['@deepseek-ai/dsh-client-ui-conversation/client', await import('@deepseek-ai/dsh-client-ui-conversation/client')], ['@deepseek-ai/dsh-client-ui-primitives', await import('@deepseek-ai/dsh-client-ui-primitives')], diff --git a/packages/client/ui-user-questions/package.json b/packages/client/ui-user-questions/package.json index 58c9a968eb..9371415708 100644 --- a/packages/client/ui-user-questions/package.json +++ b/packages/client/ui-user-questions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-user-questions", "description": "Web ask_user_question composer takeover and plan-review presentation UI", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-workflow-run/package.json b/packages/client/ui-workflow-run/package.json index 79d2efd4fb..a3c5a392ef 100644 --- a/packages/client/ui-workflow-run/package.json +++ b/packages/client/ui-workflow-run/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-workflow-run", "description": "Durable workflow-run Conversation Node and nested member disclosure for dsh web", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-workspace/package.json b/packages/client/ui-workspace/package.json index f062d8392c..e5ef226767 100644 --- a/packages/client/ui-workspace/package.json +++ b/packages/client/ui-workspace/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-workspace", "description": "Workspace picker plugin: one WorkspacePicker registered into the sidebar and empty-state workspace slots", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/web/README.i18n.yaml b/packages/client/web/README.i18n.yaml index 816b541cd8..bc5ef996f5 100644 --- a/packages/client/web/README.i18n.yaml +++ b/packages/client/web/README.i18n.yaml @@ -21,11 +21,11 @@ en: f3b01aa878764e5c zh: f02eab65d441bfe0 /deepseek-ai-dsh-client-web/use-this-package/the-boot-page: - en: add354591107b80d - zh: 41136254a00d785e + en: 1757ef1631612ff5 + zh: d2d3fd0313908c12 /deepseek-ai-dsh-client-web/use-this-package/the-shared-module-table: - en: 8380cd70b861a3a8 - zh: 5a331deb7ad8fca1 + en: 33b256e108a7bd6a + zh: 3b2b1368721803b7 /deepseek-ai-dsh-client-web/use-this-package/configuration: en: 942959cb40ee31a1 zh: 9c86ac2cf6a13976 diff --git a/packages/client/web/README.md b/packages/client/web/README.md index 8fbbd89fa1..5278ad3ccb 100644 --- a/packages/client/web/README.md +++ b/packages/client/web/README.md @@ -39,9 +39,12 @@ Boot runs in two stages: the module stage adopts the parser-loaded bootstrap bat The boot page uses plain DOM and local CSS, so bundle and plugin-activation failures remain visible: it shows one spinner node whose CSS arc grows as entries activate, and reports per-entry status. The spinner and its animation phase persist until the full UI replaces the boot page. A plugin that fails import or activation is reported by name with the reason (missing service, import failure, or state) instead of a blank page. The console contains the original import error. + ### The shared module table -`PLATFORM_MODULES` (in `src/platform.ts`) names the shell-seeded shared modules — React, Cordis, and static UI libraries — and together with `PRELOADED_CLIENT_EXTERNALS` (the parser-preloaded runtime row) defines the implicit external baseline every dynamic bundle resolves against. `dsh.client.external` adds only exact non-baseline requests; see [shared modules and the module graph](../AGENTS.md#shared-modules-and-the-module-graph). +`PLATFORM_MODULES` (in `src/platform.ts`) names the shell-seeded shared modules — React, Cordis, Zod (`zod`), TanStack Virtual (`@tanstack/react-virtual`), and static UI libraries — and together with `PRELOADED_CLIENT_EXTERNALS` (the parser-preloaded runtime row) defines the implicit external baseline every dynamic bundle resolves against. `dsh.client.external` adds only exact non-baseline requests; see [shared modules and the module graph](../AGENTS.md#shared-modules-and-the-module-graph). + +Built-in and installed browser plugins in the same page receive the same module instances. The shell supplies the dependency versions declared in this package's `package.json`; the module table does not negotiate versions per plugin. External plugins that use these modules must target a host that provides the required specifiers and APIs, and keep those imports external when building their client bundle. Inlined dependencies remain private to their bundle; Host code and Workers resolve their dependencies separately. ### Configuration diff --git a/packages/client/web/README.zh.md b/packages/client/web/README.zh.md index 4e64bf2978..ccaef3e558 100644 --- a/packages/client/web/README.zh.md +++ b/packages/client/web/README.zh.md @@ -39,9 +39,12 @@ kind: "package-library" 启动页只使用原生 DOM 与本地 CSS,因此 bundle 与插件激活失败保持可见:它显示一个 spinner 节点,其 CSS 圆弧随 entry 激活而增长,并逐 entry 报告状态。spinner 及其动画相位会一直保留,直到完整 UI 替换启动页。导入或激活失败的插件会按名称报告并给出原因(缺失服务、导入失败或状态),而不是白屏。控制台包含原始导入错误。 + ### 共享模块表 -`PLATFORM_MODULES`(位于 `src/platform.ts`)列出外壳预置的共享模块——React、Cordis 与静态 UI 库——并与 `PRELOADED_CLIENT_EXTERNALS`(parser 预载的运行时行)一起定义每个动态 bundle 解析所依据的隐式 external 基座。`dsh.client.external` 只添加基座之外的精确请求;参见[共享模块与模块图](../AGENTS.md#shared-modules-and-the-module-graph)。 +`PLATFORM_MODULES`(位于 `src/platform.ts`)列出外壳预置的共享模块——React、Cordis、Zod(`zod`)、TanStack Virtual(`@tanstack/react-virtual`)与静态 UI 库——并与 `PRELOADED_CLIENT_EXTERNALS`(parser 预载的运行时行)一起定义每个动态 bundle 解析所依据的隐式 external 基座。`dsh.client.external` 只添加基座之外的精确请求;参见[共享模块与模块图](../AGENTS.md#shared-modules-and-the-module-graph)。 + +同一页面中的内置浏览器插件和已安装的浏览器插件获得相同的模块实例。外壳提供本包 `package.json` 声明的依赖版本,模块表不会为每个插件协商版本。使用这些模块的外部插件必须以提供所需模块标识符和 API 的宿主为目标,并在构建客户端 bundle 时将相应导入保留为 external。内联依赖仍归各自 bundle 私有;Host 代码与 Worker 单独解析其依赖。 ### 配置 diff --git a/packages/client/web/package.json b/packages/client/web/package.json index ceb21773d5..e3733a87e5 100644 --- a/packages/client/web/package.json +++ b/packages/client/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-web", "description": "Web boot kernel: static module table, Cordis loader, framework-free boot page, and UI-renderer handoff", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -40,7 +40,9 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^6.0.3", - "@deepseek-ai/dsh-host-webserver": "workspace:*" + "@deepseek-ai/dsh-host-webserver": "workspace:*", + "zod": "4.4.3", + "@tanstack/react-virtual": "3.14.10" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" diff --git a/packages/client/web/src/platform.ts b/packages/client/web/src/platform.ts index 2549966986..3103555215 100644 --- a/packages/client/web/src/platform.ts +++ b/packages/client/web/src/platform.ts @@ -7,6 +7,7 @@ /** The module specifiers the shell shares into the frozen module table. */ export const PLATFORM_MODULES = [ 'react', 'react/jsx-runtime', 'react-dom', 'react-dom/client', '@deepseek-ai/cordis', + 'zod', '@tanstack/react-virtual', '@deepseek-ai/dsh-client-store', '@deepseek-ai/dsh-client-ui-slots', '@deepseek-ai/dsh-client-ui-primitives', diff --git a/packages/client/web/src/seed.ts b/packages/client/web/src/seed.ts index d41f338d42..bbba6643f3 100644 --- a/packages/client/web/src/seed.ts +++ b/packages/client/web/src/seed.ts @@ -11,6 +11,8 @@ import * as ReactJsxRuntime from 'react/jsx-runtime' import * as ReactDom from 'react-dom' import * as ReactDomClient from 'react-dom/client' import * as Cordis from '@deepseek-ai/cordis' +import * as Zod from 'zod' +import * as ReactVirtual from '@tanstack/react-virtual' import * as ClientStore from '@deepseek-ai/dsh-client-store' import * as UiSlots from '@deepseek-ai/dsh-client-ui-slots' import * as UiPrimitives from '@deepseek-ai/dsh-client-ui-primitives' @@ -31,6 +33,8 @@ export function getStaticModules(): Record { 'react-dom': ReactDom, 'react-dom/client': ReactDomClient, '@deepseek-ai/cordis': Cordis, + 'zod': Zod, + '@tanstack/react-virtual': ReactVirtual, '@deepseek-ai/dsh-client-store': ClientStore, '@deepseek-ai/dsh-client-ui-slots': UiSlots, '@deepseek-ai/dsh-client-ui-primitives': UiPrimitives, diff --git a/packages/compaction/command-compact/package.json b/packages/compaction/command-compact/package.json index 3fbfb59544..9dbee619ef 100644 --- a/packages/compaction/command-compact/package.json +++ b/packages/compaction/command-compact/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-compact", "description": "Human-facing slash command for explicit session compaction", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction-basic/package.json b/packages/compaction/compaction-basic/package.json index 4eba2ada7b..ae066a2059 100644 --- a/packages/compaction/compaction-basic/package.json +++ b/packages/compaction/compaction-basic/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction-basic", "description": "Token-meter-driven compaction policy and LLM summarization backend for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction-image-offload/package.json b/packages/compaction/compaction-image-offload/package.json index 67b3dc20e7..5f205b61f5 100644 --- a/packages/compaction/compaction-image-offload/package.json +++ b/packages/compaction/compaction-image-offload/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction-image-offload", "description": "Durable image offload for image-capable routes: replace over-budget request images with placeholders and retry", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction-tool-result-pruner/package.json b/packages/compaction/compaction-tool-result-pruner/package.json index b8cd3f2062..d5947bd324 100644 --- a/packages/compaction/compaction-tool-result-pruner/package.json +++ b/packages/compaction/compaction-tool-result-pruner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction-tool-result-pruner", "description": "Replay-safe model-free head/middle/tail pruning for tool-result surface nodes", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction/package.json b/packages/compaction/compaction/package.json index 8f9c368f70..61027e194d 100644 --- a/packages/compaction/compaction/package.json +++ b/packages/compaction/compaction/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction", "description": "Abstract compaction service seam (ctx.compaction) for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/computer-use/computer-use/package.json b/packages/computer-use/computer-use/package.json index db8daf5fbe..a9e50ee220 100644 --- a/packages/computer-use/computer-use/package.json +++ b/packages/computer-use/computer-use/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-computer-use", "description": "Exclusive named computer-use provider registration", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/agent-instructions/package.json b/packages/context/agent-instructions/package.json index 16f68d3634..1f090de17d 100644 --- a/packages/context/agent-instructions/package.json +++ b/packages/context/agent-instructions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-instructions", "description": "Workspace context loader for AGENTS.md/CLAUDE.md instruction files", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/file-reference-local/package.json b/packages/context/file-reference-local/package.json index 5cb1646286..b59ef051f3 100644 --- a/packages/context/file-reference-local/package.json +++ b/packages/context/file-reference-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-file-reference-local", "description": "Local-filesystem ctx.fileReferences provider with bounded fuzzy indexes", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/file-reference/package.json b/packages/context/file-reference/package.json index 125f4f7b10..ac1d645312 100644 --- a/packages/context/file-reference/package.json +++ b/packages/context/file-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-file-reference", "description": "File-reference discovery contract and shared @file grammar", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/session-reference/package.json b/packages/context/session-reference/package.json index 9b8572ca85..cc60df45da 100644 --- a/packages/context/session-reference/package.json +++ b/packages/context/session-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-reference", "description": "Cross-session snapshot references and durable untrusted model context (ctx.sessionReferenceResolver)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -47,7 +47,7 @@ "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json index 0f778f8591..13f2be32e6 100644 --- a/packages/context/time-context/package.json +++ b/packages/context/time-context/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-time-context", "description": "Durable per-step context with the current time and elapsed time", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -31,7 +31,7 @@ "dependencies": { "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/context/tmux-context/package.json b/packages/context/tmux-context/package.json index fc3834a07d..e6c68f4554 100644 --- a/packages/context/tmux-context/package.json +++ b/packages/context/tmux-context/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tmux-context", "description": "Opt-in durable per-step context with this agent's tmux pane and window location", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -28,7 +28,7 @@ "license": "MIT", "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/core/agent-default-model/package.json b/packages/core/agent-default-model/package.json index 7acef9bb27..3adf19597d 100644 --- a/packages/core/agent-default-model/package.json +++ b/packages/core/agent-default-model/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-default-model", "description": "Default model selection shared by Agent entry points", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-loop/package.json b/packages/core/agent-loop/package.json index 1e1678594f..36f124ca94 100644 --- a/packages/core/agent-loop/package.json +++ b/packages/core/agent-loop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-loop", "description": "The concrete agent loop plugin for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -40,7 +40,7 @@ "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*", diff --git a/packages/core/agent-tool-presentation/package.json b/packages/core/agent-tool-presentation/package.json index 99aafd9731..9837b88713 100644 --- a/packages/core/agent-tool-presentation/package.json +++ b/packages/core/agent-tool-presentation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-tool-presentation", "description": "Agent-plane presentation selector: composes one agent's tools as PTC mode or native tool calls", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent/package.json b/packages/core/agent/package.json index f33481fc28..ded699a4b7 100644 --- a/packages/core/agent/package.json +++ b/packages/core/agent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent", "description": "Agent interface, registry, initiator scope, and event vocabulary for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/scope/package.json b/packages/core/scope/package.json index 29d8d56bf0..b5503b6424 100644 --- a/packages/core/scope/package.json +++ b/packages/core/scope/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-scope", "description": "Scoped-context registration primitive (scope tags, scope-filtered event dispatch) for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/session/package.json b/packages/core/session/package.json index 68fec8f3d6..b8085f30a6 100644 --- a/packages/core/session/package.json +++ b/packages/core/session/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session", "description": "Event-sourced session store for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/system-prompt/package.json b/packages/core/system-prompt/package.json index 951ec3fb95..57adb8cc95 100644 --- a/packages/core/system-prompt/package.json +++ b/packages/core/system-prompt/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-system-prompt", "description": "System prompt assembly registry for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/tools/package.json b/packages/core/tools/package.json index 1e4fa8ba01..3cc8922032 100644 --- a/packages/core/tools/package.json +++ b/packages/core/tools/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tools", "description": "Tool registry and execution pipeline for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/authorization/package.json b/packages/credentials/authorization/package.json index 8ceffd30dc..63def783d8 100644 --- a/packages/credentials/authorization/package.json +++ b/packages/credentials/authorization/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-authorization", "description": "Authorization seam (ctx.authorization): plugin-owned flows that obtain a credential through a conversation with the human", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/credentials-local/package.json b/packages/credentials/credentials-local/package.json index 93276c131e..7eafba6772 100644 --- a/packages/credentials/credentials-local/package.json +++ b/packages/credentials/credentials-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-credentials-local", "description": "File-backed credentials provider ($DSH_HOME/.env under the live process environment) for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -34,7 +34,7 @@ "@deepseek-ai/cordis": "workspace:~" }, "dependencies": { - "chokidar": "^4.0.3", + "chokidar": "5.0.0", "@deepseek-ai/schemastery": "workspace:~", "yaml": "^2.9.0" }, diff --git a/packages/credentials/credentials/package.json b/packages/credentials/credentials/package.json index e6f5d79399..e14d7f0067 100644 --- a/packages/credentials/credentials/package.json +++ b/packages/credentials/credentials/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-credentials", "description": "Abstract credential seam (ctx.credentials): settings carry references to secrets, providers own the values", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/deepseek-account-platform/package.json b/packages/credentials/deepseek-account-platform/package.json index 381954457a..c07bd45e51 100644 --- a/packages/credentials/deepseek-account-platform/package.json +++ b/packages/credentials/deepseek-account-platform/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deepseek-account-platform", "description": "Authorize DeepSeek accounts through browser PKCE", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -47,6 +47,6 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" } } diff --git a/packages/credentials/deepseek-account/package.json b/packages/credentials/deepseek-account/package.json index c1b7867842..702347596b 100644 --- a/packages/credentials/deepseek-account/package.json +++ b/packages/credentials/deepseek-account/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deepseek-account", "description": "Read account state and resolve official API credentials", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/deliverables/tool-present/package.json b/packages/deliverables/tool-present/package.json index 8bbd9e427b..f0c0e74b0f 100644 --- a/packages/deliverables/tool-present/package.json +++ b/packages/deliverables/tool-present/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-present", "description": "Explicit workspace file delivery declarations for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/deliverables/workspace-changes/package.json b/packages/deliverables/workspace-changes/package.json index 5fc5f7ae86..c82567db87 100644 --- a/packages/deliverables/workspace-changes/package.json +++ b/packages/deliverables/workspace-changes/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workspace-changes", "description": "Per-turn workspace file changes recorded from git working-tree snapshots and whole-file captures, with per-file comparisons, for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/document/office-to-pdf/package.json b/packages/document/office-to-pdf/package.json index 26cc2f353c..087d544293 100644 --- a/packages/document/office-to-pdf/package.json +++ b/packages/document/office-to-pdf/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-office-to-pdf", "description": "Shared Office-to-PDF conversion with bounded queues and caching", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -59,6 +59,6 @@ "@deepseek-ai/schemastery": "workspace:~", "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-typert-protocol": "workspace:*", - "zod": "^4.4.3" + "zod": "4.4.3" } } diff --git a/packages/experimental/agent-team-profile/package.json b/packages/experimental/agent-team-profile/package.json index 0d907efdd8..3ab45ccad5 100644 --- a/packages/experimental/agent-team-profile/package.json +++ b/packages/experimental/agent-team-profile/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team-profile", "description": "Agent Teams collaboration, tools, and Web UI in one experimental bundle", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/agent-team/package.json b/packages/experimental/agent-team/package.json index 926a547376..2dde22ee3a 100644 --- a/packages/experimental/agent-team/package.json +++ b/packages/experimental/agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team", "description": "Implicit-root Agent Teams roster, direct peer messaging, and shared task DAG", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -40,7 +40,7 @@ "dependencies": { "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/experimental/api-speech-to-text/package.json b/packages/experimental/api-speech-to-text/package.json index a44b383505..ea84596773 100644 --- a/packages/experimental/api-speech-to-text/package.json +++ b/packages/experimental/api-speech-to-text/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-api-speech-to-text", "description": "Authenticated experimental speech transcription for browser clients", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -47,7 +47,7 @@ "license": "MIT", "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/experimental/auto-review/README.i18n.yaml b/packages/experimental/auto-review/README.i18n.yaml index b94783d099..93e5fd4cd1 100644 --- a/packages/experimental/auto-review/README.i18n.yaml +++ b/packages/experimental/auto-review/README.i18n.yaml @@ -18,8 +18,8 @@ en: 8f9e85817f05aa4d zh: f60136fb39abcf29 /deepseek-ai-dsh-experimental-auto-review/use-this-package/install-into-a-profile: - en: 63f8f1baa0fb2655 - zh: 6bc64160f6d02114 + en: cf146a773e8c7010 + zh: 57f7e9be7b257fcc /deepseek-ai-dsh-experimental-auto-review/use-this-package/what-you-get: en: 556e89c0055ae9cc zh: 75afbcb9a2af6467 diff --git a/packages/experimental/auto-review/README.md b/packages/experimental/auto-review/README.md index d2ae55e7e6..779339fd48 100644 --- a/packages/experimental/auto-review/README.md +++ b/packages/experimental/auto-review/README.md @@ -35,7 +35,7 @@ From this source checkout, install the package into the Web profile through the pnpm dsh plugin --profile web add ./packages/experimental/auto-review ``` -The CLI initializes the profile when needed and appends this package's declared patch after the base and Web layers. Reconciliation activates the patch as a profile layer; a package without `dsh.bundle.patch` is only an installed dependency. Select `Auto review` with its superscript `EXP` badge in the composer or `/permission` picker and confirm the current-session risk dialog. An explicit `/permission auto` command switches directly. General settings and future-session defaults do not offer Auto. +The CLI initializes the profile when needed and appends this package's declared patch after the base and Web layers. Reconciliation activates the patch as a profile layer; a package without `dsh.bundle.patch` is only an installed dependency. Select `Auto review` with its `EXP` badge in the composer menu or `/permission` picker and confirm the current-session risk dialog. An explicit `/permission auto` command switches directly. General settings and future-session defaults do not offer Auto. Remove the layer through the same CLI: diff --git a/packages/experimental/auto-review/README.zh.md b/packages/experimental/auto-review/README.zh.md index 5f7c1d5718..15656e5ab5 100644 --- a/packages/experimental/auto-review/README.zh.md +++ b/packages/experimental/auto-review/README.zh.md @@ -35,7 +35,7 @@ kind: "package-bundle" pnpm dsh plugin --profile web add ./packages/experimental/auto-review ``` -CLI 会在需要时初始化 profile,并将本包声明的 patch 追加到 base 与 Web 层之后。Reconciliation 将 patch 激活为 profile 层;没有 `dsh.bundle.patch` 的包只是已安装依赖。在 composer 或 `/permission` 选择器中选择带右上标 `EXP` 的 `Auto review`,并确认当前会话风险对话框。显式 `/permission auto` 命令直接切换。通用设置与未来会话默认值不提供 Auto。 +CLI 会在需要时初始化 profile,并将本包声明的 patch 追加到 base 与 Web 层之后。Reconciliation 将 patch 激活为 profile 层;没有 `dsh.bundle.patch` 的包只是已安装依赖。在 composer 菜单或 `/permission` 选择器中选择带 `EXP` 标记的「自动审查」,并确认当前会话风险对话框。显式 `/permission auto` 命令直接切换。通用设置与未来会话默认值不提供 Auto。 通过同一 CLI 移除此层: diff --git a/packages/experimental/auto-review/package.json b/packages/experimental/auto-review/package.json index fa458f0726..0e224849cc 100644 --- a/packages/experimental/auto-review/package.json +++ b/packages/experimental/auto-review/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-auto-review", "description": "Per-tool LLM authorization review for the DeepSeek Harness Auto permission preset", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "icon": "./icon.svg", "repository": { "type": "git", diff --git a/packages/experimental/badge-skill-bundle/icon.svg b/packages/experimental/badge-skill-bundle/icon.svg index f93a3b591d..7bd7490b92 100644 --- a/packages/experimental/badge-skill-bundle/icon.svg +++ b/packages/experimental/badge-skill-bundle/icon.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/packages/experimental/badge-skill-bundle/package.json b/packages/experimental/badge-skill-bundle/package.json index 7853f04167..a3d065bb7e 100644 --- a/packages/experimental/badge-skill-bundle/package.json +++ b/packages/experimental/badge-skill-bundle/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-badge-skill-bundle", "description": "Offer the powered-by-DSH badge as a discoverable skill.", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/browser-use-chrome-devtools-mcp/package.json b/packages/experimental/browser-use-chrome-devtools-mcp/package.json index cee92d700c..335ad0d1ce 100644 --- a/packages/experimental/browser-use-chrome-devtools-mcp/package.json +++ b/packages/experimental/browser-use-chrome-devtools-mcp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp", "description": "Experimental per-Session Chromium browser tools through chrome-devtools-mcp", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/browser-use-playwright-mcp/package.json b/packages/experimental/browser-use-playwright-mcp/package.json index 8f006c5310..f07fa1098c 100644 --- a/packages/experimental/browser-use-playwright-mcp/package.json +++ b/packages/experimental/browser-use-playwright-mcp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-playwright-mcp", "description": "Experimental per-Session Chromium browser tools through @playwright/mcp", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/browser-use-runtime/package.json b/packages/experimental/browser-use-runtime/package.json index ebefaf7e56..24e68569e6 100644 --- a/packages/experimental/browser-use-runtime/package.json +++ b/packages/experimental/browser-use-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-runtime", "description": "Session-owned browser resource lifecycles and MCP integration for experimental providers", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/browser-use-stagehand-native/README.i18n.yaml b/packages/experimental/browser-use-stagehand-native/README.i18n.yaml index d65d51165d..e6668a0d43 100644 --- a/packages/experimental/browser-use-stagehand-native/README.i18n.yaml +++ b/packages/experimental/browser-use-stagehand-native/README.i18n.yaml @@ -24,8 +24,8 @@ en: fea3ad16a77fc478 zh: cf551a8a2d183f93 /deepseek-ai-dsh-experimental-browser-use-stagehand-native/understand-the-implementation: - en: ba13e1703d5aaca4 - zh: 91db415ed074307c + en: 123351a7c3d46f3e + zh: bf1cc2628edeb8d7 /deepseek-ai-dsh-experimental-browser-use-stagehand-native/further-exploration: en: eff8f4541eb26ca7 zh: 76a61fe9bd48df2a diff --git a/packages/experimental/browser-use-stagehand-native/README.md b/packages/experimental/browser-use-stagehand-native/README.md index 61f6c82c0a..ec1922366f 100644 --- a/packages/experimental/browser-use-stagehand-native/README.md +++ b/packages/experimental/browser-use-stagehand-native/README.md @@ -90,7 +90,7 @@ env -u NODE_USE_ENV_PROXY DSH_STAGEHAND_E2E=1 pnpm exec vitest run --config vite The host owns each launched Chromium process and its temporary profile before waiting for CDP readiness. Chromium receives the standard scrubbed child environment, preserving paths, locale, and proxy settings while excluding credential-shaped variables and DSH identity. Both modes connect the SDK inside a dedicated Worker. The Worker receives no ambient environment except the explicit source TypeScript configuration path, so its CDP connection does not inherit host proxy settings. SDK close waits for active operations. Cleanup terminates the connection Worker after the configured SDK grace; launch also kills and awaits its owned Chromium process before removing the profile. Attached external browsers remain open. Cleanup failures follow the [ownership limits](#known-limitations-and-deferred-work) below. -The [native runtime](src/native.ts) passes the explicit model configuration to Stagehand's public initialization API. Stagehand owns the model requests, response validation, and token accounting inside its browser extension. DSH records browser tool inputs and returned data, including SDK result metadata, through the existing Session log. Underlying inference request/response capture and integration into DSH Session usage accounting are deferred. +The [native runtime](src/native.ts) passes the explicit model configuration to Stagehand's public initialization API. The adapter pins Zod to the version required by Stagehand because extraction passes Zod schemas to the SDK. Stagehand owns the model requests, response validation, and token accounting inside its browser extension. DSH records browser tool inputs and returned data, including SDK result metadata, through the existing Session log. Underlying inference request/response capture and integration into DSH Session usage accounting are deferred. diff --git a/packages/experimental/browser-use-stagehand-native/README.zh.md b/packages/experimental/browser-use-stagehand-native/README.zh.md index cc64481db9..47f7e908d6 100644 --- a/packages/experimental/browser-use-stagehand-native/README.zh.md +++ b/packages/experimental/browser-use-stagehand-native/README.zh.md @@ -90,7 +90,7 @@ env -u NODE_USE_ENV_PROXY DSH_STAGEHAND_E2E=1 pnpm exec vitest run --config vite 宿主在等待 CDP 就绪前即拥有启动的 Chromium 进程及其临时配置目录。Chromium 接收标准清理后的子进程环境,保留路径、区域设置和代理配置,排除凭据形式的变量及 DSH 身份信息。两种模式都在独立 Worker 中连接 SDK。Worker 除显式的源码 TypeScript 配置路径外不接收宿主环境,因此其 CDP 连接不继承宿主代理设置。SDK 关闭会等待活动操作。清理在配置的 SDK 宽限时间后终止连接 Worker;启动模式还会终止并等待自有 Chromium 进程退出,再移除配置目录。外部连接的浏览器保持运行。清理失败遵循下方的[所有权限制](#known-limitations-and-deferred-work)。 -[原生运行时](src/native.ts)将显式模型配置传入 Stagehand 的公开初始化 API。Stagehand 在其浏览器扩展中负责模型请求、响应验证与 token 计量。DSH 通过现有 Session 日志记录浏览器工具输入和返回数据,包括 SDK 结果元数据。底层推理请求/响应捕获及其与 DSH Session 用量计量的集成均属暂缓工作。 +[原生运行时](src/native.ts)将显式模型配置传入 Stagehand 的公开初始化 API。适配器将 Zod 固定为 Stagehand 要求的版本,因为提取操作会向 SDK 传递 Zod schema。Stagehand 在其浏览器扩展中负责模型请求、响应验证与 token 计量。DSH 通过现有 Session 日志记录浏览器工具输入和返回数据,包括 SDK 结果元数据。底层推理请求/响应捕获及其与 DSH Session 用量计量的集成均属暂缓工作。 diff --git a/packages/experimental/browser-use-stagehand-native/package.json b/packages/experimental/browser-use-stagehand-native/package.json index ea9dd3142b..1b9632f111 100644 --- a/packages/experimental/browser-use-stagehand-native/package.json +++ b/packages/experimental/browser-use-stagehand-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-stagehand-native", "description": "Experimental Stagehand browser tools with separately configured native models", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -44,7 +44,7 @@ "@deepseek-ai/dsh-mcp-client": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3", + "zod": "4.4.3", "@deepseek-ai/dsh-subprocess": "workspace:*", "@puppeteer/browsers": "3.2.2" }, diff --git a/packages/experimental/claude-code-mods/package.json b/packages/experimental/claude-code-mods/package.json index 21b9d18317..13ea2530f6 100644 --- a/packages/experimental/claude-code-mods/package.json +++ b/packages/experimental/claude-code-mods/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-claude-code-mods", "description": "Experimental bridge: load Claude Code mods (hooks modules) and run their hook chains on DeepSeek Harness extension points", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/client-ui-agent-team/package.json b/packages/experimental/client-ui-agent-team/package.json index d1d727a400..73a171a53d 100644 --- a/packages/experimental/client-ui-agent-team/package.json +++ b/packages/experimental/client-ui-agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-client-ui-agent-team", "description": "Web Agent Teams roster, task board, and teammate navigation", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/client-ui-claude-code-mods/package.json b/packages/experimental/client-ui-claude-code-mods/package.json index 0fa362cc1b..64416f9042 100644 --- a/packages/experimental/client-ui-claude-code-mods/package.json +++ b/packages/experimental/client-ui-claude-code-mods/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-client-ui-claude-code-mods", "description": "Web band above the prompt for Claude Code mods: draws each session's mod tree and sends button presses back to the bridge", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/client-ui-cot-translation/package.json b/packages/experimental/client-ui-cot-translation/package.json index 90117ed048..9dcc42cc82 100644 --- a/packages/experimental/client-ui-cot-translation/package.json +++ b/packages/experimental/client-ui-cot-translation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-client-ui-cot-translation", "description": "Optional machine translation of expanded reasoning, preserving original Session text", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/client-ui-cot-translation/tests/mount.client.spec.ts b/packages/experimental/client-ui-cot-translation/tests/mount.client.spec.ts index 8731bcc974..ddd02ab7d2 100644 --- a/packages/experimental/client-ui-cot-translation/tests/mount.client.spec.ts +++ b/packages/experimental/client-ui-cot-translation/tests/mount.client.spec.ts @@ -146,10 +146,9 @@ it('loads the browser entry through its generated optional Remote contribution', }) -it.each(['loading', 'memory'] as const)('uses Host Google preferences while the settings form is %s', async (mode) => { +it('uses Host Google preferences while the settings form is loading', async () => { const preferences: CotTranslationPreferences = { provider: 'google', targetLanguage: 'ja' } const b = await fixture(false, preferences) - if (mode === 'memory') b.scope.publish({ status: 'unavailable', mode: 'memory', value: undefined }) try { const fiber = b.ctx.plugin({ inject: [...inject], apply: ctx => mountTranslation(ctx, contribution) }) await fiber diff --git a/packages/experimental/client-ui-voice-input/package.json b/packages/experimental/client-ui-voice-input/package.json index a3b6bf42b5..2c8aec199d 100644 --- a/packages/experimental/client-ui-voice-input/package.json +++ b/packages/experimental/client-ui-voice-input/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-client-ui-voice-input", "description": "Record speech and insert editable text into the conversation draft", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/computer-use-cua-driver-mcp/package.json b/packages/experimental/computer-use-cua-driver-mcp/package.json index b058a5c6b8..c03cfe0262 100644 --- a/packages/experimental/computer-use-cua-driver-mcp/package.json +++ b/packages/experimental/computer-use-cua-driver-mcp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp", "description": "Experimental computer use through an installed Cua Driver MCP executable", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/computer-use-cua-driver-native/package.json b/packages/experimental/computer-use-cua-driver-native/package.json index e9055c9bb4..f1f7ebcbd2 100644 --- a/packages/experimental/computer-use-cua-driver-native/package.json +++ b/packages/experimental/computer-use-cua-driver-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-computer-use-cua-driver-native", "description": "Experimental computer-use provider embedding the Cua Driver native npm SDK", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -38,7 +38,7 @@ "@deepseek-ai/dsh-mcp-client": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", "@trycua/cua-driver": "0.28.0", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/experimental/cot-translation-bundle/icon.svg b/packages/experimental/cot-translation-bundle/icon.svg index a0668aa95b..659d331adf 100644 --- a/packages/experimental/cot-translation-bundle/icon.svg +++ b/packages/experimental/cot-translation-bundle/icon.svg @@ -1,5 +1,16 @@ - - - - + + + + + + + + + + + + + + + diff --git a/packages/experimental/cot-translation-bundle/package.json b/packages/experimental/cot-translation-bundle/package.json index d723a94941..c0ed237a52 100644 --- a/packages/experimental/cot-translation-bundle/package.json +++ b/packages/experimental/cot-translation-bundle/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-cot-translation-bundle", "description": "Experimental machine translation of expanded reasoning", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/hook-protocol/package.json b/packages/experimental/hook-protocol/package.json index 1195c9de2c..16016ee13a 100644 --- a/packages/experimental/hook-protocol/package.json +++ b/packages/experimental/hook-protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hook-protocol", "description": "Shared Claude Code / Codex hook wire protocol: matcher engine, stdin/exit-code/stdout codec, multi-hook merge, and hook/* session events", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/hooks-claude-code/package.json b/packages/experimental/hooks-claude-code/package.json index 9799278af9..99015f344d 100644 --- a/packages/experimental/hooks-claude-code/package.json +++ b/packages/experimental/hooks-claude-code/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hooks-claude-code", "description": "Bridge plugin: run a Claude Code hooks.json / settings hook config on the DeepSeek Harness interception seams", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/hooks-codex/package.json b/packages/experimental/hooks-codex/package.json index e4eac2a937..61ddc09cbd 100644 --- a/packages/experimental/hooks-codex/package.json +++ b/packages/experimental/hooks-codex/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hooks-codex", "description": "Bridge plugin: run a Codex hooks.json hook config on the DeepSeek Harness interception seams", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/inspector-profile/icon.svg b/packages/experimental/inspector-profile/icon.svg index 5d50e0fa5a..7e1b599c02 100644 --- a/packages/experimental/inspector-profile/icon.svg +++ b/packages/experimental/inspector-profile/icon.svg @@ -1,12 +1,23 @@ - - - - + + + + + + + - - - + + + + + + + + + + + diff --git a/packages/experimental/inspector-profile/package.json b/packages/experimental/inspector-profile/package.json index 8a043b0a09..2528bcbfcd 100644 --- a/packages/experimental/inspector-profile/package.json +++ b/packages/experimental/inspector-profile/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-inspector-profile", "description": "Optional Web bundle for raw Session logs and Chat node inspection", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/inspector/icon.svg b/packages/experimental/inspector/icon.svg index 5d50e0fa5a..7e1b599c02 100644 --- a/packages/experimental/inspector/icon.svg +++ b/packages/experimental/inspector/icon.svg @@ -1,12 +1,23 @@ - - - - + + + + + + + - - - + + + + + + + + + + + diff --git a/packages/experimental/inspector/package.json b/packages/experimental/inspector/package.json index 225e54565f..4177a94c8a 100644 --- a/packages/experimental/inspector/package.json +++ b/packages/experimental/inspector/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-inspector", "description": "Experimental cross-realm CDP hub for Host debugging and Client Runtime inspection", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/ptc-runtime-python/package.json b/packages/experimental/ptc-runtime-python/package.json index 026dce8443..ef3eed920b 100644 --- a/packages/experimental/ptc-runtime-python/package.json +++ b/packages/experimental/ptc-runtime-python/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-ptc-runtime-python", "description": "CPython subprocess implementation of the DeepSeek Harness PTC execution seam", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "repository": { "type": "git", "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", diff --git a/packages/experimental/ralph-bundle/package.json b/packages/experimental/ralph-bundle/package.json index f36ebb0eec..c0bc8b9385 100644 --- a/packages/experimental/ralph-bundle/package.json +++ b/packages/experimental/ralph-bundle/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-ralph-bundle", "description": "Run explicitly requested repeated work with fresh agents.", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/session-inspector/package.json b/packages/experimental/session-inspector/package.json index 9a9b2aa39e..e9fc0837bc 100644 --- a/packages/experimental/session-inspector/package.json +++ b/packages/experimental/session-inspector/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-session-inspector", "description": "Experimental virtualized Session log and live Chat group/node inspectors", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/session-search/icon.svg b/packages/experimental/session-search/icon.svg index c15c9846f7..bab706489c 100644 --- a/packages/experimental/session-search/icon.svg +++ b/packages/experimental/session-search/icon.svg @@ -1 +1,11 @@ - + + + + + + + + + + + diff --git a/packages/experimental/session-search/package.json b/packages/experimental/session-search/package.json index e563a02406..a405f03d08 100644 --- a/packages/experimental/session-search/package.json +++ b/packages/experimental/session-search/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-session-search", "description": "Give Agents tools to search and read earlier sessions from the current workspace.", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/session-title-all-prompts-llm/package.json b/packages/experimental/session-title-all-prompts-llm/package.json index ba782a31d4..0aff79b846 100644 --- a/packages/experimental/session-title-all-prompts-llm/package.json +++ b/packages/experimental/session-title-all-prompts-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-all-prompts-llm", "description": "All-user-messages LLM provider plugin for DeepSeek Harness session titles", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/session-titles-bundle/icon.svg b/packages/experimental/session-titles-bundle/icon.svg index 1b0d19004b..5cba79b160 100644 --- a/packages/experimental/session-titles-bundle/icon.svg +++ b/packages/experimental/session-titles-bundle/icon.svg @@ -1 +1,11 @@ - + + + + + + + + + + + diff --git a/packages/experimental/session-titles-bundle/package.json b/packages/experimental/session-titles-bundle/package.json index 5232bea10c..0a7a9bc841 100644 --- a/packages/experimental/session-titles-bundle/package.json +++ b/packages/experimental/session-titles-bundle/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-session-titles-bundle", "description": "Refresh session titles as new human prompts arrive.", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/skill-badge/README.i18n.yaml b/packages/experimental/skill-badge/README.i18n.yaml index 1265a6ca82..b595cfef5e 100644 --- a/packages/experimental/skill-badge/README.i18n.yaml +++ b/packages/experimental/skill-badge/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-skill-badge/use-this-package: - en: a559a5ed8a76e342 - zh: 6558f9af8d6c9625 + en: 0da087eb29c565b0 + zh: ec86044ef1255440 /deepseek-ai-dsh-skill-badge/use-this-package/when-to-choose-it: en: 4e424f3ab2a8b5b1 zh: e38162f15f0a8354 diff --git a/packages/experimental/skill-badge/README.md b/packages/experimental/skill-badge/README.md index 25bc5dd02e..3b0e0eeef5 100644 --- a/packages/experimental/skill-badge/README.md +++ b/packages/experimental/skill-badge/README.md @@ -27,6 +27,8 @@ Agents can load the official "powered by dsh" badge skill from this bundled prov Enable the plugin to make the `dsh-badge` skill available in the session skill catalog; the model can then load it like any other skill and follow its instructions for adding a "powered by dsh" badge. +In Plugins, this component is named **DSH** and uses the DSH tag icon. Its skill is named `dsh-badge`. + ### When to choose it Choose this provider when content produced with DeepSeek Harness should carry official attribution badges, and the deployment wants the badge skill available to agents without storing it in a local skill directory. Skip it when the badge is irrelevant to the deployment. The default composition omits the plugin; installation and an explicit active row are required. diff --git a/packages/experimental/skill-badge/README.zh.md b/packages/experimental/skill-badge/README.zh.md index 7beeb177e5..9c25477422 100644 --- a/packages/experimental/skill-badge/README.zh.md +++ b/packages/experimental/skill-badge/README.zh.md @@ -27,6 +27,8 @@ agent(智能体)可以通过该内置提供方加载官方「powered by dsh 启用插件即可让 `dsh-badge` skill 出现在会话 skill 目录中;随后模型可以像加载任何其他 skill 一样加载它,并遵循其指令添加「powered by dsh」徽章。 +在插件页面中,此组件显示为 **DSH**,并使用 DSH 标签图标。它提供的 skill 名称为 `dsh-badge`。 + ### 何时选择 当用 DeepSeek Harness 生成的内容应携带官方署名徽章、且部署方希望该徽章 skill 对 agent 可用而无需存入本地 skill 目录时,选择此提供方。当徽章与部署无关时,请跳过。默认组合省略此插件;使用它需要安装包并显式插入活动行。 diff --git a/packages/experimental/skill-badge/icon.svg b/packages/experimental/skill-badge/icon.svg new file mode 100644 index 0000000000..7bd7490b92 --- /dev/null +++ b/packages/experimental/skill-badge/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/experimental/skill-badge/locale/en.json b/packages/experimental/skill-badge/locale/en.json new file mode 100644 index 0000000000..2c3c652416 --- /dev/null +++ b/packages/experimental/skill-badge/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DSH", + "description": "Provide the official powered-by-DSH badge skill." + } +} diff --git a/packages/experimental/skill-badge/locale/zh.json b/packages/experimental/skill-badge/locale/zh.json new file mode 100644 index 0000000000..79c486b476 --- /dev/null +++ b/packages/experimental/skill-badge/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DSH", + "description": "提供官方 powered-by-DSH 徽章技能。" + } +} diff --git a/packages/experimental/skill-badge/package.json b/packages/experimental/skill-badge/package.json index fdc2588323..21e93aea42 100644 --- a/packages/experimental/skill-badge/package.json +++ b/packages/experimental/skill-badge/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill-badge", "description": "Bundled dsh badge skill provider for DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -18,9 +18,13 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, - "./package.json": "./package.json" + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, + "icon": "./icon.svg", "files": [ + "icon.svg", + "locale/*.json", "lib/index.js", "assets", "lib/types/**/*.d.ts" diff --git a/packages/experimental/skill-badge/tests/skill-badge.spec.ts b/packages/experimental/skill-badge/tests/skill-badge.spec.ts index f66e1bb9dc..f788164c4d 100644 --- a/packages/experimental/skill-badge/tests/skill-badge.spec.ts +++ b/packages/experimental/skill-badge/tests/skill-badge.spec.ts @@ -29,6 +29,19 @@ describe('dsh-skill-badge', () => { expect(await ctx.skills.list()).toEqual([]) }) + it('presents the component as DSH with the bundle tag artwork in both locales', async () => { + expect(JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'))).toMatchObject({ + icon: './icon.svg', + exports: { './locale/*.json': './locale/*.json' }, + }) + for (const language of ['en', 'zh']) { + expect(JSON.parse(await readFile(new URL(`../locale/${language}.json`, import.meta.url), 'utf8'))) + .toMatchObject({ meta: { title: 'DSH' } }) + } + expect(await readFile(new URL('../icon.svg', import.meta.url), 'utf8')) + .toBe(await readFile(new URL('../../badge-skill-bundle/icon.svg', import.meta.url), 'utf8')) + }) + it('ships the official 726×120 PNG unchanged', async () => { const image = await readFile(new URL('../assets/dsh-badge.png', import.meta.url)) expect(image.readUInt32BE(16)).toBe(726) diff --git a/packages/experimental/speech-to-text-sensevoice/package.json b/packages/experimental/speech-to-text-sensevoice/package.json index 5cbfb277a7..f1217e5475 100644 --- a/packages/experimental/speech-to-text-sensevoice/package.json +++ b/packages/experimental/speech-to-text-sensevoice/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-speech-to-text-sensevoice", "description": "Local SenseVoice ONNX transcription with a managed sherpa-onnx process", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -37,7 +37,7 @@ "dependencies": { "@deepseek-ai/schemastery": "workspace:~", "@deepseek-ai/dsh-timeout": "workspace:*", - "zod": "^4.4.3", + "zod": "4.4.3", "sherpa-onnx-node": "1.13.8" }, "peerDependencies": { diff --git a/packages/experimental/speech-to-text/package.json b/packages/experimental/speech-to-text/package.json index 135486e5c5..c119558c2e 100644 --- a/packages/experimental/speech-to-text/package.json +++ b/packages/experimental/speech-to-text/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-speech-to-text", "description": "Experimental speech recognition with independently selectable providers", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/terminal-bundle/package.json b/packages/experimental/terminal-bundle/package.json index 18d5e3822b..4f09c47dfc 100644 --- a/packages/experimental/terminal-bundle/package.json +++ b/packages/experimental/terminal-bundle/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-terminal-bundle", "description": "Keep shell processes alive across tool calls.", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/tool-agent-team/package.json b/packages/experimental/tool-agent-team/package.json index 95222ce232..d00265adeb 100644 --- a/packages/experimental/tool-agent-team/package.json +++ b/packages/experimental/tool-agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-tool-agent-team", "description": "Scoped model-facing Agent Teams tools over ctx.agentTeams", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/tool-ralph/package.json b/packages/experimental/tool-ralph/package.json index 3458cc12db..fca05d68cc 100644 --- a/packages/experimental/tool-ralph/package.json +++ b/packages/experimental/tool-ralph/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-ralph", "description": "Model-facing fresh-agent Ralph loop over the workflow and subagent seams", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/tool-session-query/package.json b/packages/experimental/tool-session-query/package.json index c2708f81bc..32f3903015 100644 --- a/packages/experimental/tool-session-query/package.json +++ b/packages/experimental/tool-session-query/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-session-query", "description": "Workspace-authorized model-facing session history search, trace, and event read tools", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/tool-terminal/package.json b/packages/experimental/tool-terminal/package.json index c3140c8ea5..11ee72a485 100644 --- a/packages/experimental/tool-terminal/package.json +++ b/packages/experimental/tool-terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-terminal", "description": "Six model-facing persistent PTY tools with owner isolation and generic background-job integration", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/tool-worktree/icon.svg b/packages/experimental/tool-worktree/icon.svg index 69cc03bf5e..b8e63a6b81 100644 --- a/packages/experimental/tool-worktree/icon.svg +++ b/packages/experimental/tool-worktree/icon.svg @@ -1,6 +1,18 @@ - - - - - + + + + + + + + + + + + + + + + + diff --git a/packages/experimental/tool-worktree/package.json b/packages/experimental/tool-worktree/package.json index 80c110c98d..b670ecf50c 100644 --- a/packages/experimental/tool-worktree/package.json +++ b/packages/experimental/tool-worktree/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-tool-worktree", "description": "Model tool that creates and enters a new Git worktree", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/translator/package.json b/packages/experimental/translator/package.json index 0111d23db6..31f2c3da09 100644 --- a/packages/experimental/translator/package.json +++ b/packages/experimental/translator/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-translator", "description": "Machine translation with reusable Session results", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/voice-input-bundle/package.json b/packages/experimental/voice-input-bundle/package.json index 846db038ad..d7935dc51e 100644 --- a/packages/experimental/voice-input-bundle/package.json +++ b/packages/experimental/voice-input-bundle/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-voice-input-bundle", "description": "Experimental voice input with local SenseVoice; downloads its runtime on first use", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/webhook-github/package.json b/packages/experimental/webhook-github/package.json index f1f5c2c14a..710d0de493 100644 --- a/packages/experimental/webhook-github/package.json +++ b/packages/experimental/webhook-github/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-webhook-github", "description": "Signed GitHub HTTP webhook adapter for the DeepSeek Harness webhook runtime", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/webhook/package.json b/packages/experimental/webhook/package.json index 3674193d07..742e04231b 100644 --- a/packages/experimental/webhook/package.json +++ b/packages/experimental/webhook/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-webhook", "description": "Fire-and-forget webhook rule runtime that creates Workspace-backed DeepSeek Harness Sessions", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/webworker-packer/package.json b/packages/experimental/webworker-packer/package.json index 27220fa0b3..ca5ac4da53 100644 --- a/packages/experimental/webworker-packer/package.json +++ b/packages/experimental/webworker-packer/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-webworker-packer", "description": "Build-time packer for the browser runtime's base VFS image and ordered data-overlay archives", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/webworker-runtime/package.json b/packages/experimental/webworker-runtime/package.json index 2fd84363f7..8605766f06 100644 --- a/packages/experimental/webworker-runtime/package.json +++ b/packages/experimental/webworker-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-webworker-runtime", "description": "Browser-only harness runtime: in-memory VFS, module transform and loader, postMessage tunnel, and the dedicated Web Worker assembly, with the Node-compatibility layer that lets the host tree run unchanged", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -64,7 +64,7 @@ "@deepseek-ai/node-addon-system": "workspace:~", "@types/picomatch": "^4.0.3", "@types/readable-stream": "^4.0.24", - "chokidar": "^5.0.0" + "chokidar": "5.0.0" }, "files": [ "lib/index.js", diff --git a/packages/experimental/webworker-runtime/tests/node/chokidar.spec.ts b/packages/experimental/webworker-runtime/tests/node/chokidar.spec.ts index 9d78737a09..389cd6ebab 100644 --- a/packages/experimental/webworker-runtime/tests/node/chokidar.spec.ts +++ b/packages/experimental/webworker-runtime/tests/node/chokidar.spec.ts @@ -23,10 +23,10 @@ interface ChokidarFixture { const CHOKIDAR_FIXTURES: readonly ChokidarFixture[] = [ { - label: 'Chokidar 4 from credentials', + label: 'Chokidar 5 from credentials', consumerManifest: 'packages/credentials/credentials-local/package.json', - chokidarFiles: ['package.json', 'esm/package.json', 'esm/index.js', 'esm/handler.js'], - readdirpFiles: ['package.json', 'esm/package.json', 'esm/index.js'], + chokidarFiles: ['package.json', 'index.js', 'handler.js'], + readdirpFiles: ['package.json', 'index.js'], }, { label: 'Chokidar 5 from skill-filesystem', diff --git a/packages/experimental/worktree/package.json b/packages/experimental/worktree/package.json index 493ddce15b..637bdf5e28 100644 --- a/packages/experimental/worktree/package.json +++ b/packages/experimental/worktree/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-worktree", "description": "Creates a named Git worktree and changes the calling Session working directory", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/cordis-client-runner/package.json b/packages/extensions/cordis-client-runner/package.json index 57f0f6e8b0..44b4f09d32 100644 --- a/packages/extensions/cordis-client-runner/package.json +++ b/packages/extensions/cordis-client-runner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cordis-client-runner", "description": "Browser half of dynamic dual-half plugin packages: event subscription, closure evaluation, guard facade, and loader entries", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/cordis-host-runner/package.json b/packages/extensions/cordis-host-runner/package.json index 88e037d1f7..6bf679baad 100644 --- a/packages/extensions/cordis-host-runner/package.json +++ b/packages/extensions/cordis-host-runner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cordis-host-runner", "description": "Dynamic package definition registry, host-half sandbox lifecycle, and invoke handler table for model-mounted dual-half packages", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -46,7 +46,7 @@ "dependencies": { "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/extensions/tool-cordis/package.json b/packages/extensions/tool-cordis/package.json index a9be3331b5..117afcd2a5 100644 --- a/packages/extensions/tool-cordis/package.json +++ b/packages/extensions/tool-cordis/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-cordis", "description": "Read-only runtime API inspection for Harness plugin development", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/ui-cordis/package.json b/packages/extensions/ui-cordis/package.json index 419b88a221..0ab70fc393 100644 --- a/packages/extensions/ui-cordis/package.json +++ b/packages/extensions/ui-cordis/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-cordis", "description": "Cordis dynamic-plugin definition card: the keyed cordis_define tool row with its run/stop switch", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/feedback/command-feedback/package.json b/packages/feedback/command-feedback/package.json index 6886dd91c1..4272fcc315 100644 --- a/packages/feedback/command-feedback/package.json +++ b/packages/feedback/command-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-feedback", "description": "Log-only session feedback: the record event, the sessionFeedback Host Remote, and the human-facing slash command", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -51,7 +51,7 @@ "@deepseek-ai/dsh-typert-protocol": "workspace:*" }, "dependencies": { - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis-plugin-include": "workspace:~", diff --git a/packages/feedback/message-feedback/package.json b/packages/feedback/message-feedback/package.json index 2fbbf44e5f..f904b55a15 100644 --- a/packages/feedback/message-feedback/package.json +++ b/packages/feedback/message-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-message-feedback", "description": "Canonical Session-log ratings and notes for finalized assistant messages", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -54,7 +54,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis-plugin-include": "workspace:~", diff --git a/packages/fs/fs-local/package.json b/packages/fs/fs-local/package.json index b61ad778c2..708ca9d9bc 100644 --- a/packages/fs/fs-local/package.json +++ b/packages/fs/fs-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-local", "description": "Local-filesystem implementation of the DeepSeek Harness filesystem seam (ctx.fs)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -31,7 +31,7 @@ "@deepseek-ai/cordis": "workspace:~" }, "dependencies": { - "chokidar": "^4.0.3", + "chokidar": "5.0.0", "koffi": "3.1.1", "@deepseek-ai/schemastery": "workspace:~" }, diff --git a/packages/fs/fs-observation-policy/package.json b/packages/fs/fs-observation-policy/package.json index 45b7115610..58b8e19fd5 100644 --- a/packages/fs/fs-observation-policy/package.json +++ b/packages/fs/fs-observation-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-observation-policy", "description": "File-context policy plugin for the DeepSeek Harness — observed-state, read-before-edit, and version-guarded write/edit added over the ctx.fs provider seam through the fs/* event gate (no service API)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs-sandbox/package.json b/packages/fs/fs-sandbox/package.json index 8147cfab14..844758f7a9 100644 --- a/packages/fs/fs-sandbox/package.json +++ b/packages/fs/fs-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-sandbox", "description": "Sandbox-enforcing implementation of the DeepSeek Harness filesystem seam: fences write/edit by the per-call sandbox mode (read-only denies mutation, workspace-write contains it to the workspace + temp roots) while reads pass through", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs/package.json b/packages/fs/fs/package.json index 7c64b4d90d..d235fdcfa6 100644 --- a/packages/fs/fs/package.json +++ b/packages/fs/fs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs", "description": "Abstract filesystem capability seam (ctx.fs) for the DeepSeek Harness — vocabulary types, the FileSystem service (text IO + optional version-guarded atomic mutations), and the fs/* policy event vocabulary", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-fs-search/package.json b/packages/fs/tool-fs-search/package.json index 406fa179dd..5885144af6 100644 --- a/packages/fs/tool-fs-search/package.json +++ b/packages/fs/tool-fs-search/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-fs-search", "description": "Model-facing filesystem discovery tools (glob, grep) backed by the packaged ripgrep binary (@vscode/ripgrep)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-fs/package.json b/packages/fs/tool-fs/package.json index 87afa35ff6..a902203342 100644 --- a/packages/fs/tool-fs/package.json +++ b/packages/fs/tool-fs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-fs", "description": "Model-facing filesystem tools (read, write, edit) over the DeepSeek Harness filesystem seam (ctx.fs)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-str-replace-editor/package.json b/packages/fs/tool-str-replace-editor/package.json index 7f5f2ac3cf..78a80f9c6e 100644 --- a/packages/fs/tool-str-replace-editor/package.json +++ b/packages/fs/tool-str-replace-editor/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-str-replace-editor", "description": "Model-facing view, create, literal replace, and line insert tool over the Harness filesystem service", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/goal/command-goal/package.json b/packages/goal/command-goal/package.json index 4692291dba..a52384d662 100644 --- a/packages/goal/command-goal/package.json +++ b/packages/goal/command-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-goal", "description": "Human-facing slash command for persisted same-session goals", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/goal/goal-round-driver/package.json b/packages/goal/goal-round-driver/package.json index 268b6526ae..8c0a98720e 100644 --- a/packages/goal/goal-round-driver/package.json +++ b/packages/goal/goal-round-driver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-goal-round-driver", "description": "Race-fenced same-session goal-round driver", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/goal/goal/package.json b/packages/goal/goal/package.json index c2b217de5c..8c99a4be08 100644 --- a/packages/goal/goal/package.json +++ b/packages/goal/goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-goal", "description": "Event-sourced same-session goal state and lifecycle service for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -59,7 +59,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/goal/tool-goal/package.json b/packages/goal/tool-goal/package.json index 9d38fbf653..2bef2b4948 100644 --- a/packages/goal/tool-goal/package.json +++ b/packages/goal/tool-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-goal", "description": "Model-facing same-session goal tools with execution-time authority checks", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/guard/repeat-tool-reminder/package.json b/packages/guard/repeat-tool-reminder/package.json index 7d2fc8082b..1ecdab5fe5 100644 --- a/packages/guard/repeat-tool-reminder/package.json +++ b/packages/guard/repeat-tool-reminder/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-repeat-tool-reminder", "description": "Repeat-tool-call guard plugin: advisory reminders when an agent loops on identical tool calls", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/guard/timeout-policy/package.json b/packages/guard/timeout-policy/package.json index c716bdf1e7..59aa4ae6fd 100644 --- a/packages/guard/timeout-policy/package.json +++ b/packages/guard/timeout-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-call-timeout-policy", "description": "Tool-call timeout policy: a tools/execute wrapper that arms a per-tool deadline on exec.signal and returns TOOL_TIMEOUT when it wins", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-auto/package.json b/packages/host/directory-picker-auto/package.json index 412cdbd853..583de44661 100644 --- a/packages/host/directory-picker-auto/package.json +++ b/packages/host/directory-picker-auto/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-auto", "description": "Adaptive chooser of the directory-picker seam: resolves the host situation at boot and mounts the native or browse backend for the DeepSeek Harness web GUI host", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-browse/package.json b/packages/host/directory-picker-browse/package.json index bda4a30ea5..2d7afd4db9 100644 --- a/packages/host/directory-picker-browse/package.json +++ b/packages/host/directory-picker-browse/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-browse", "description": "In-app browsing backend of the directory-picker seam (listing/creation primitives over the host filesystem)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-native/package.json b/packages/host/directory-picker-native/package.json index a129f21a2e..e88530126b 100644 --- a/packages/host/directory-picker-native/package.json +++ b/packages/host/directory-picker-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-native", "description": "Native-OS-chooser backend of the directory-picker seam for the DeepSeek Harness web GUI host", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker/package.json b/packages/host/directory-picker/package.json index ddb6145f1f..e0d3d570c9 100644 --- a/packages/host/directory-picker/package.json +++ b/packages/host/directory-picker/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker", "description": "Abstract workspace-directory picking seam (ctx.directoryPicker) for the DeepSeek Harness web GUI host", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/frontend-static/package.json b/packages/host/frontend-static/package.json index d6faa0b18d..341352a2ed 100644 --- a/packages/host/frontend-static/package.json +++ b/packages/host/frontend-static/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-frontend-static", "description": "SPA dist server for the Web shell: owns the webserver fallback seat, serving explicit index entries and static assets with traversal rejection and 404 misses", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/open-in-app/package.json b/packages/host/open-in-app/package.json index 25149ca12e..b2a6bd03f5 100644 --- a/packages/host/open-in-app/package.json +++ b/packages/host/open-in-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-open-in-app", "description": "Host half of open-in-app: resolved application catalog, icons, and the launch endpoint as three webServer routes", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/plugin-inventory/package.json b/packages/host/plugin-inventory/package.json index 7a09c38637..abf51934ab 100644 --- a/packages/host/plugin-inventory/package.json +++ b/packages/host/plugin-inventory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-plugin-inventory", "description": "Read-only Remote projection of current Cordis Loader plugin state", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -44,7 +44,7 @@ ], "license": "MIT", "dependencies": { - "zod": "^4.4.3" + "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/cordis-plugin-loader": "workspace:~", diff --git a/packages/host/product-telemetry-otel/package.json b/packages/host/product-telemetry-otel/package.json index b027a678c9..5a6a98f5db 100644 --- a/packages/host/product-telemetry-otel/package.json +++ b/packages/host/product-telemetry-otel/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-product-telemetry-otel", "description": "Explicit product usage events exported through OpenTelemetry HTTP logs", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -28,9 +28,9 @@ "license": "MIT", "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "@opentelemetry/api-logs": "^0.222.0", - "@opentelemetry/otlp-exporter-base": "^0.222.0", - "@opentelemetry/sdk-logs": "^0.222.0" + "@opentelemetry/api-logs": "0.222.0", + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/sdk-logs": "0.222.0" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/host/webserver/package.json b/packages/host/webserver/package.json index 23fd5f843d..445ca07fda 100644 --- a/packages/host/webserver/package.json +++ b/packages/host/webserver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-webserver", "description": "Web route-registration plugin: HTTP and upgrade routes, index transform taps, and static dist fallback; knows no harness concepts", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/identity/anonymous-user-id/package.json b/packages/identity/anonymous-user-id/package.json index 315f77de69..579f125951 100644 --- a/packages/identity/anonymous-user-id/package.json +++ b/packages/identity/anonymous-user-id/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-anonymous-user-id", "description": "Shared anonymous user identity for DeepSeek Harness telemetry and feedback correlation", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/commands/package.json b/packages/interaction/commands/package.json index cabd389d23..3ccc8638f5 100644 --- a/packages/interaction/commands/package.json +++ b/packages/interaction/commands/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-commands", "description": "Plugin-owned human command registry for DeepSeek Harness UIs", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -59,7 +59,7 @@ }, "dependencies": { "@deepseek-ai/dsh-util-crypto": "workspace:*", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/interaction/permission-presets/package.json b/packages/interaction/permission-presets/package.json index b387c7f877..c0744dd1fc 100644 --- a/packages/interaction/permission-presets/package.json +++ b/packages/interaction/permission-presets/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-permission-presets", "description": "User-facing permission presets (ctx.permissionPresets) for the DeepSeek Harness: one product-level Permissions select bundling the sandbox-mode and approval-policy knobs, written through to their own session events", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -60,7 +60,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/dsh-shell": "workspace:*", diff --git a/packages/interaction/tool-ask-user/package.json b/packages/interaction/tool-ask-user/package.json index 92b36a324f..b2f84f0d83 100644 --- a/packages/interaction/tool-ask-user/package.json +++ b/packages/interaction/tool-ask-user/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-ask-user", "description": "Model-facing ask_user_question tool over the ctx.userQuestions seam", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/user-approval/package.json b/packages/interaction/user-approval/package.json index 3eec2d2a52..f68a6bc7fa 100644 --- a/packages/interaction/user-approval/package.json +++ b/packages/interaction/user-approval/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-user-approval", "description": "User-approval seam (ctx.approval) for the DeepSeek Harness: one-shot permission decisions dispatched to composed answerers over the approval/request waterfall, fail-closed by default", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/user-questions/package.json b/packages/interaction/user-questions/package.json index 6f245a1c18..595a60fedc 100644 --- a/packages/interaction/user-questions/package.json +++ b/packages/interaction/user-questions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-user-questions", "description": "Abstract user-questions seam (ctx.userQuestions) for asking the human during agent runs", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -64,7 +64,7 @@ "@deepseek-ai/dsh-tools": "workspace:*" }, "dependencies": { - "zod": "^4.4.3", + "zod": "4.4.3", "@deepseek-ai/schemastery": "workspace:~" } } diff --git a/packages/jobs/jobs-local/package.json b/packages/jobs/jobs-local/package.json index 17c6bade1a..62b9e4fdc1 100644 --- a/packages/jobs/jobs-local/package.json +++ b/packages/jobs/jobs-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-jobs-local", "description": "Process-local implementation of the DeepSeek Harness background job registry seam", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/jobs/package.json b/packages/jobs/jobs/package.json index 40e47524e8..116be0f826 100644 --- a/packages/jobs/jobs/package.json +++ b/packages/jobs/jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-jobs", "description": "Background job registry (ctx.jobs) for the DeepSeek Harness — shared ids, owner isolation, polling, cancellation, and completion listeners for long-running tool work", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/tool-jobs/package.json b/packages/jobs/tool-jobs/package.json index 7f696f2d42..5830da7705 100644 --- a/packages/jobs/tool-jobs/package.json +++ b/packages/jobs/tool-jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-jobs", "description": "Model-facing background job control tools (job_output, job_list, job_kill) over the ctx.jobs registry", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/deepseek-llm-api-extensions/package.json b/packages/llm/deepseek-llm-api-extensions/package.json index a90d7f749b..a09355d960 100644 --- a/packages/llm/deepseek-llm-api-extensions/package.json +++ b/packages/llm/deepseek-llm-api-extensions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deepseek-llm-api-extensions", "description": "Additive request-field registry for the official DeepSeek LLM API adapter", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-deepseek-account/package.json b/packages/llm/llm-deepseek-account/package.json index 39938a7ebb..922cb5ba56 100644 --- a/packages/llm/llm-deepseek-account/package.json +++ b/packages/llm/llm-deepseek-account/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-deepseek-account", "description": "DeepSeek account provider authentication and discovery", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-deepseek-api-key/package.json b/packages/llm/llm-deepseek-api-key/package.json index cf53d0831a..4100c95663 100644 --- a/packages/llm/llm-deepseek-api-key/package.json +++ b/packages/llm/llm-deepseek-api-key/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-deepseek-api-key", "description": "DeepSeek api-key provider authentication and discovery", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-deepseek/package.json b/packages/llm/llm-deepseek/package.json index 204e755f09..7aa840ccb7 100644 --- a/packages/llm/llm-deepseek/package.json +++ b/packages/llm/llm-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-deepseek", "description": "DeepSeek Messages adapter", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-pi-ai/package.json b/packages/llm/llm-pi-ai/package.json index ba4e7dd712..426cdee5cc 100644 --- a/packages/llm/llm-pi-ai/package.json +++ b/packages/llm/llm-pi-ai/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-pi-ai", "description": "pi-ai-backed DeepSeek adapter for the DeepSeek Harness LLM seam (design-verification twin of dsh-llm-deepseek)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-retry/package.json b/packages/llm/llm-retry/package.json index faf14468bb..034b41d48e 100644 --- a/packages/llm/llm-retry/package.json +++ b/packages/llm/llm-retry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-retry", "description": "Provider-routed LLM request retry policy for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -41,7 +41,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/dsh-brand": "workspace:*", diff --git a/packages/llm/llm/package.json b/packages/llm/llm/package.json index e510ddbd25..1009deef35 100644 --- a/packages/llm/llm/package.json +++ b/packages/llm/llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm", "description": "Provider-neutral LLM service interface for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -61,7 +61,7 @@ "dependencies": { "@deepseek-ai/dsh-util-crypto": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3", + "zod": "4.4.3", "@deepseek-ai/dsh-timeout": "workspace:*", "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", diff --git a/packages/llm/plugin-package-inventory-deepseek/package.json b/packages/llm/plugin-package-inventory-deepseek/package.json index 3ab8965d38..249be41494 100644 --- a/packages/llm/plugin-package-inventory-deepseek/package.json +++ b/packages/llm/plugin-package-inventory-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plugin-package-inventory-deepseek", "description": "Active Loader-backed plugin package inventory for official DeepSeek LLM API requests", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/token-meter/package.json b/packages/llm/token-meter/package.json index b7323e5680..e0f83a29e7 100644 --- a/packages/llm/token-meter/package.json +++ b/packages/llm/token-meter/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-token-meter", "description": "Replay-aware token measurement service (ctx.tokenMeter) for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -47,7 +47,7 @@ "dependencies": { "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/lsp/lsp-stdio/package.json b/packages/lsp/lsp-stdio/package.json index c63e5b1765..9482ebd77b 100644 --- a/packages/lsp/lsp-stdio/package.json +++ b/packages/lsp/lsp-stdio/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-lsp-stdio", "description": "Generic stdio language-server provider for the DeepSeek Harness LSP capability seam (ctx.lsp) — spawns configured servers, translates JSON-RPC, and serves transient-open goToDefinition/findReferences/goToImplementation/hover queries in the host filesystem namespace", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/lsp/package.json b/packages/lsp/lsp/package.json index 274cbd11ec..c435aed359 100644 --- a/packages/lsp/lsp/package.json +++ b/packages/lsp/lsp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-lsp", "description": "Abstract LSP capability seam (ctx.lsp) for the DeepSeek Harness — language-server provider registry keyed by branded id and extension mapping, order-independent per-query selection, normalized definition/references/implementation/hover requests and results, and the LspError taxonomy", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/tool-lsp/package.json b/packages/lsp/tool-lsp/package.json index 0f21928f28..8cb859f251 100644 --- a/packages/lsp/tool-lsp/package.json +++ b/packages/lsp/tool-lsp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-lsp", "description": "Model-facing lsp tool over the DeepSeek Harness LSP capability seam (ctx.lsp) \u2014 one read-only tool with goToDefinition/findReferences/goToImplementation/hover operations, one-based UTF-16 cursor coordinates, bounded location rendering, and hover normalization", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/mcp/mcp-client/package.json b/packages/mcp/mcp-client/package.json index dedce66e52..0def023e1b 100644 --- a/packages/mcp/mcp-client/package.json +++ b/packages/mcp/mcp-client/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-mcp-client", "description": "MCP client bridge: connects to MCP servers and registers their tools on ctx.tools", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -56,7 +56,7 @@ "@deepseek-ai/dsh-http-proxy": "workspace:*", "@modelcontextprotocol/server": "2.2.0", "@modelcontextprotocol/node": "2.0.0", - "zod": "^4.4.3", + "zod": "4.4.3", "@deepseek-ai/dsh-mcp-resources": "workspace:*", "@deepseek-ai/dsh-system-prompt": "workspace:*" }, diff --git a/packages/mcp/mcp-resources/package.json b/packages/mcp/mcp-resources/package.json index 2a6b1d1817..447375bc32 100644 --- a/packages/mcp/mcp-resources/package.json +++ b/packages/mcp/mcp-resources/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-mcp-resources", "description": "Scoped MCP resource discovery and reading through shared model tools", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/plan/plan-mode/package.json b/packages/plan/plan-mode/package.json index d6b6bc8f4b..db1ba9197d 100644 --- a/packages/plan/plan-mode/package.json +++ b/packages/plan/plan-mode/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plan-mode", "description": "Logged per-agent plan mode with deployment guidance, a direct slash command, and a user-reviewed exit", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -53,7 +53,7 @@ }, "dependencies": { "@deepseek-ai/dsh-brand": "workspace:*", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/preset/agent-preset-registry/package.json b/packages/preset/agent-preset-registry/package.json index 783644adf6..bd30b49128 100644 --- a/packages/preset/agent-preset-registry/package.json +++ b/packages/preset/agent-preset-registry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-preset-registry", "description": "Declarative Agent preset registry and profile-backed editing", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -63,7 +63,7 @@ "dependencies": { "@deepseek-ai/dsh-app-boot": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3", + "zod": "4.4.3", "js-yaml": "^4.3.2" }, "devDependencies": { diff --git a/packages/preset/agent-preset/package.json b/packages/preset/agent-preset/package.json index a54ace97aa..e480861bee 100644 --- a/packages/preset/agent-preset/package.json +++ b/packages/preset/agent-preset/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-preset", "description": "Declare an Agent capability composition in Cordis YAML", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/preset/persona/package.json b/packages/preset/persona/package.json index 2ac606cbf2..0cc3dc968e 100644 --- a/packages/preset/persona/package.json +++ b/packages/preset/persona/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-persona", "description": "Composition-authored deployment persona section for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/ptc-runtime/ptc-runtime-node/package.json b/packages/ptc-runtime/ptc-runtime-node/package.json index 931eb8cef3..c3b6fb2338 100644 --- a/packages/ptc-runtime/ptc-runtime-node/package.json +++ b/packages/ptc-runtime/ptc-runtime-node/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-ptc-runtime-node", "description": "Sandboxed Node process implementation of the DeepSeek Harness PTC execution capability", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/ptc-runtime/ptc-runtime/package.json b/packages/ptc-runtime/ptc-runtime/package.json index 7550079c00..bf2b414e4a 100644 --- a/packages/ptc-runtime/ptc-runtime/package.json +++ b/packages/ptc-runtime/ptc-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-ptc-runtime", "description": "Abstract PTC execution seam (ctx.ptcRuntime) for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-local/package.json b/packages/sandbox/sandbox-local/package.json index dafad4ebe0..cc6c077df8 100644 --- a/packages/sandbox/sandbox-local/package.json +++ b/packages/sandbox/sandbox-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-local", "description": "Local process-sandbox backends for the DeepSeek Harness sandbox seam: bwrap, the npm-distributed landlock-run launcher, macOS Seatbelt, or the Windows ACL restricted-token runner — functionally probed, fail-closed", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-policy/package.json b/packages/sandbox/sandbox-policy/package.json index a6432636df..3dc7f0be2c 100644 --- a/packages/sandbox/sandbox-policy/package.json +++ b/packages/sandbox/sandbox-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-policy", "description": "Per-call sandbox policy resolver and current model context: deployment fallbacks plus each session's mode and workspace root, shared by every enforcing capability family", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -36,7 +36,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/sandbox/sandbox-windows-acl/package.json b/packages/sandbox/sandbox-windows-acl/package.json index 873054d5b0..89f2354f6e 100644 --- a/packages/sandbox/sandbox-windows-acl/package.json +++ b/packages/sandbox/sandbox-windows-acl/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-windows-acl", "description": "Windows ACL write-restriction sandbox backend (restricted-token spawn with capability-SID write allowlist) for the DeepSeek Harness sandbox seam", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox/package.json b/packages/sandbox/sandbox/package.json index eb456ae513..907eb5b350 100644 --- a/packages/sandbox/sandbox/package.json +++ b/packages/sandbox/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox", "description": "Abstract process-sandbox seam (ctx.sandbox) for the DeepSeek Harness: same-world confinement vocabulary and the SandboxProvider contract", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/schedule/schedule/package.json b/packages/schedule/schedule/package.json index 7ea453734a..3ae7fe54dd 100644 --- a/packages/schedule/schedule/package.json +++ b/packages/schedule/schedule/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-schedule", "description": "Host-wide durable reminders with shared management and original-Session delivery", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -63,7 +63,7 @@ "dependencies": { "@deepseek-ai/schemastery": "workspace:~", "@js-temporal/polyfill": "0.5.1", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis-plugin-loader": "workspace:~", diff --git a/packages/schedule/tool-schedule/package.json b/packages/schedule/tool-schedule/package.json index 4e932cfd7a..ded10ff0af 100644 --- a/packages/schedule/tool-schedule/package.json +++ b/packages/schedule/tool-schedule/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-schedule", "description": "Model-facing reminder management tools (schedule_create, schedule_list, schedule_update, schedule_delete) over the Host ctx.schedule service", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/client/package.json b/packages/sdk/client/package.json index 0ebd31e34c..bd99c6b052 100644 --- a/packages/sdk/client/package.json +++ b/packages/sdk/client/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-client", "description": "TypeScript client SDK for driving a DeepSeek Harness runtime subprocess over stdio JSON-RPC: the DeepSeekHarness high-level turns API and the lower-level HarnessClient", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/protocol/package.json b/packages/sdk/protocol/package.json index 4c147d6e6a..26f4cc2791 100644 --- a/packages/sdk/protocol/package.json +++ b/packages/sdk/protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-protocol", "description": "Shared wire protocol for the DeepSeek Harness SDK runtime: the newline-delimited JSON-RPC stdio transport and the named request, result, and notification types spoken between the runtime server and SDK clients", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/server/package.json b/packages/sdk/server/package.json index 7d2ed5c36c..b3cffc809c 100644 --- a/packages/sdk/server/package.json +++ b/packages/sdk/server/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-jsonrpc-server", "description": "Stdio JSON-RPC server plugin for out-of-process DeepSeek Harness SDK clients", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-log-export/package.json b/packages/session-query/session-log-export/package.json index a57a18900e..7e076e802f 100644 --- a/packages/session-query/session-log-export/package.json +++ b/packages/session-query/session-log-export/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-log-export", "description": "Web Session-log export command and shared download dialog", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-query-sqlite/package.json b/packages/session-query/session-query-sqlite/package.json index 158957833b..9c2d1232e5 100644 --- a/packages/session-query/session-query-sqlite/package.json +++ b/packages/session-query/session-query-sqlite/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-query-sqlite", "description": "Concrete ctx.sessionQuery backend with SQLite FTS5 search", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-query/package.json b/packages/session-query/session-query/package.json index 67fdf0490e..accc95e121 100644 --- a/packages/session-query/session-query/package.json +++ b/packages/session-query/session-query/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-query", "description": "Combined session query service contract with concrete reads, traces, and filters", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-checkpoint-policy/package.json b/packages/session/session-checkpoint-policy/package.json index 9b6d62374d..d451481aa1 100644 --- a/packages/session/session-checkpoint-policy/package.json +++ b/packages/session/session-checkpoint-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-checkpoint-policy", "description": "Semantic session durability checkpoints before model requests and tool side effects", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-catalog/package.json b/packages/session/session-format-catalog/package.json index 36e8ab8df0..0e2ff78c97 100644 --- a/packages/session/session-format-catalog/package.json +++ b/packages/session/session-format-catalog/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-catalog", "description": "Build-static first-party Session format codec and migration catalog", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v0-to-v1/package.json b/packages/session/session-format-v0-to-v1/package.json index e04844f76b..d071b2edec 100644 --- a/packages/session/session-format-v0-to-v1/package.json +++ b/packages/session/session-format-v0-to-v1/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v0-to-v1", "description": "Frozen released-v0 Session codec and identity migration to v1", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v1-to-v2/package.json b/packages/session/session-format-v1-to-v2/package.json index b65773bf7f..b2ebbad9d2 100644 --- a/packages/session/session-format-v1-to-v2/package.json +++ b/packages/session/session-format-v1-to-v2/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v1-to-v2", "description": "Frozen released-v1 Session codec and assistant-stream migration to v2", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v2-to-v3/package.json b/packages/session/session-format-v2-to-v3/package.json index c9fd5bb969..bdae0f2f05 100644 --- a/packages/session/session-format-v2-to-v3/package.json +++ b/packages/session/session-format-v2-to-v3/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v2-to-v3", "description": "Streaming system-prompt, canonical-envelope and PTC migration into V3", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v3-to-v4/package.json b/packages/session/session-format-v3-to-v4/package.json index 41704c96df..6d0bba0985 100644 --- a/packages/session/session-format-v3-to-v4/package.json +++ b/packages/session/session-format-v3-to-v4/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v3-to-v4", "description": "Streaming tool-role migration and delivery validation from Session V3 to V4", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format/package.json b/packages/session/session-format/package.json index bfffee8f17..6b328b29c6 100644 --- a/packages/session/session-format/package.json +++ b/packages/session/session-format/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format", "description": "Streaming adjacent Session format migration machinery", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-log-deepseek/package.json b/packages/session/session-log-deepseek/package.json index 55d7e89235..4e48f512fb 100644 --- a/packages/session/session-log-deepseek/package.json +++ b/packages/session/session-log-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-log-deepseek", "description": "Incremental lossless session-log request extension for the official DeepSeek LLM API", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-persistence-jsonl/package.json b/packages/session/session-persistence-jsonl/package.json index 8104843888..22af65bb47 100644 --- a/packages/session/session-persistence-jsonl/package.json +++ b/packages/session/session-persistence-jsonl/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-persistence-jsonl", "description": "JSONL durable session persistence backend for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-persistence/package.json b/packages/session/session-persistence/package.json index 6b9647575c..79140f7de7 100644 --- a/packages/session/session-persistence/package.json +++ b/packages/session/session-persistence/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-persistence", "description": "Abstract durable session persistence seam (ctx.sessionPersistence) for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-projection-cache/package.json b/packages/session/session-projection-cache/package.json index 712a587078..5eb3c8fca6 100644 --- a/packages/session/session-projection-cache/package.json +++ b/packages/session/session-projection-cache/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-projection-cache", "description": "Persisted projection cache (ctx.sessionProjectionCache): durable per-session checkpoint records on the session_projcache storage domain (per-record layout), throttled write-behind, and the cached listing read", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -29,7 +29,7 @@ "dependencies": { "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/session/session-projection/package.json b/packages/session/session-projection/package.json index 92bc24db4d..e222ea670c 100644 --- a/packages/session/session-projection/package.json +++ b/packages/session/session-projection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-projection", "description": "Session-projection seam: the merge-extensible projection type table, the provider contract, and the ctx.sessionProjections registry serving whole current values of log-derived per-session state", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -32,7 +32,7 @@ ], "license": "MIT", "dependencies": { - "zod": "^4.4.3" + "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/dsh-session": "workspace:*", diff --git a/packages/session/session-stats/package.json b/packages/session/session-stats/package.json index a6b9228458..b6f18a1e27 100644 --- a/packages/session/session-stats/package.json +++ b/packages/session/session-stats/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-stats", "description": "Whole-log conversation counts and wall times projection (sessionStats) for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -42,7 +42,7 @@ "@deepseek-ai/cordis": "workspace:~" }, "dependencies": { - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis-plugin-include": "workspace:~", diff --git a/packages/session/session-telemetry-otel/package.json b/packages/session/session-telemetry-otel/package.json index 879c434958..0cf56f83df 100644 --- a/packages/session/session-telemetry-otel/package.json +++ b/packages/session/session-telemetry-otel/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-telemetry-otel", "description": "Feedback-authorized Session logs over byte-bounded OpenTelemetry HTTP requests", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -27,7 +27,7 @@ ], "license": "MIT", "dependencies": { - "@opentelemetry/api-logs": "^0.222.0", + "@opentelemetry/api-logs": "0.222.0", "@deepseek-ai/schemastery": "workspace:~" }, "peerDependencies": { diff --git a/packages/session/session-telemetry/package.json b/packages/session/session-telemetry/package.json index 079101c947..b9355115ed 100644 --- a/packages/session/session-telemetry/package.json +++ b/packages/session/session-telemetry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-telemetry", "description": "SessionTelemetryBackend seam for the DeepSeek Harness: session-event capture, projection, redaction, and handoff to a reporting backend", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-first-prompt-llm/package.json b/packages/session/session-title-first-prompt-llm/package.json index 47e6263fe4..8b8e32ed38 100644 --- a/packages/session/session-title-first-prompt-llm/package.json +++ b/packages/session/session-title-first-prompt-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-first-prompt-llm", "description": "First-message LLM provider plugin for DeepSeek Harness session titles", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-llm/package.json b/packages/session/session-title-llm/package.json index e9c50790c4..b04589837a 100644 --- a/packages/session/session-title-llm/package.json +++ b/packages/session/session-title-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-llm", "description": "Shared LLM generation policy for DeepSeek Harness session-title providers", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title/package.json b/packages/session/session-title/package.json index 245844c59e..d6dc02897e 100644 --- a/packages/session/session-title/package.json +++ b/packages/session/session-title/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title", "description": "Log-backed session title service and provider registry for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -46,7 +46,7 @@ "dependencies": { "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/session/session-turn-outline/package.json b/packages/session/session-turn-outline/package.json index 6afd2081ae..83cbd0b0b2 100644 --- a/packages/session/session-turn-outline/package.json +++ b/packages/session/session-turn-outline/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-turn-outline", "description": "Whole-log turn outline projection (turnOutline) for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -42,7 +42,7 @@ "@deepseek-ai/dsh-session-projection": "workspace:*" }, "dependencies": { - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/session/tool-working-directory/package.json b/packages/session/tool-working-directory/package.json index dc30eec34c..32a8966f09 100644 --- a/packages/session/tool-working-directory/package.json +++ b/packages/session/tool-working-directory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-working-directory", "description": "Read and change the active Session working directory", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/working-directory/package.json b/packages/session/working-directory/package.json index 3a0f00f37d..a45d0601a3 100644 --- a/packages/session/working-directory/package.json +++ b/packages/session/working-directory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-working-directory", "description": "Session working directories with durable changes and model context", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/settings/settings/package.json b/packages/settings/settings/package.json index 5b9e7f78de..0cc53944b8 100644 --- a/packages/settings/settings/package.json +++ b/packages/settings/settings/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-settings", "description": "Abstract user-settings seam (ctx.settings) for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/bash-local/package.json b/packages/shell/bash-local/package.json index 18682d30ef..04a57aea2d 100644 --- a/packages/shell/bash-local/package.json +++ b/packages/shell/bash-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-bash-local", "description": "Local-subprocess implementation of the DeepSeek Harness bash executor seam", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/bash-sandbox/package.json b/packages/shell/bash-sandbox/package.json index 85f41305e3..fbacd24f17 100644 --- a/packages/shell/bash-sandbox/package.json +++ b/packages/shell/bash-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-bash-sandbox", "description": "Sandbox-consuming implementation of the DeepSeek Harness bash executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/pwsh-local/package.json b/packages/shell/pwsh-local/package.json index 57c81122f6..2e084dd662 100644 --- a/packages/shell/pwsh-local/package.json +++ b/packages/shell/pwsh-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-pwsh-local", "description": "Local PowerShell implementation of the DeepSeek Harness bash executor seam", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/pwsh-sandbox/package.json b/packages/shell/pwsh-sandbox/package.json index 82bdf0c17d..6973ac34b5 100644 --- a/packages/shell/pwsh-sandbox/package.json +++ b/packages/shell/pwsh-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-pwsh-sandbox", "description": "Sandbox-consuming implementation of the DeepSeek Harness PowerShell executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/shell-env/package.json b/packages/shell/shell-env/package.json index 2cfa07bf5c..6fdb329411 100644 --- a/packages/shell/shell-env/package.json +++ b/packages/shell/shell-env/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-shell-env", "description": "Tool-independent managed DSH_* shell environment registry", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/shell/package.json b/packages/shell/shell/package.json index 176aaae77e..1d4577a95b 100644 --- a/packages/shell/shell/package.json +++ b/packages/shell/shell/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-shell", "description": "Abstract bash executor seam (ctx.shell) for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-bash-persistent/package.json b/packages/shell/tool-bash-persistent/package.json index 9fc096e693..585aa89797 100644 --- a/packages/shell/tool-bash-persistent/package.json +++ b/packages/shell/tool-bash-persistent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-bash-persistent", "description": "Model-facing owner-scoped persistent Bash tool backed by the Harness PTY service", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-bash/package.json b/packages/shell/tool-bash/package.json index 5f51b174c3..e080a55a26 100644 --- a/packages/shell/tool-bash/package.json +++ b/packages/shell/tool-bash/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-bash", "description": "Model-facing bash tool with optional generic background-job and sandbox-escalation support", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-pwsh-persistent/package.json b/packages/shell/tool-pwsh-persistent/package.json index 0564de02c6..fb71d87933 100644 --- a/packages/shell/tool-pwsh-persistent/package.json +++ b/packages/shell/tool-pwsh-persistent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-pwsh-persistent", "description": "Model-facing owner-scoped persistent PowerShell tool backed by the Harness PTY service", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-pwsh/package.json b/packages/shell/tool-pwsh/package.json index 3128beaada..382154dc19 100644 --- a/packages/shell/tool-pwsh/package.json +++ b/packages/shell/tool-pwsh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-pwsh", "description": "Model-facing pwsh tool over the bash executor seam", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill-filesystem/package.json b/packages/skill/skill-filesystem/package.json index 310428186a..fde17358c5 100644 --- a/packages/skill/skill-filesystem/package.json +++ b/packages/skill/skill-filesystem/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill-filesystem", "description": "Local filesystem skill provider for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -33,7 +33,7 @@ "@deepseek-ai/cordis": "workspace:~" }, "dependencies": { - "chokidar": "^5.0.0", + "chokidar": "5.0.0", "@deepseek-ai/schemastery": "workspace:~", "yaml": "^2.4.2" }, diff --git a/packages/skill/skill-office/package.json b/packages/skill/skill-office/package.json index 52e30831d8..c14ba723b9 100644 --- a/packages/skill/skill-office/package.json +++ b/packages/skill/skill-office/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill-office", "description": "Bundled Word, PowerPoint, and Excel workflows and structural checks", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill/package.json b/packages/skill/skill/package.json index f8cd3704e4..bec408df5d 100644 --- a/packages/skill/skill/package.json +++ b/packages/skill/skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill", "description": "Agent skill provider registry for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/tool-skill/package.json b/packages/skill/tool-skill/package.json index 202e6eb998..c62384056a 100644 --- a/packages/skill/tool-skill/package.json +++ b/packages/skill/tool-skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-skill", "description": "Model-facing skill loading tool for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/tool-workspace-dependencies/package.json b/packages/skill/tool-workspace-dependencies/package.json index c304f1e001..56df5403e8 100644 --- a/packages/skill/tool-workspace-dependencies/package.json +++ b/packages/skill/tool-workspace-dependencies/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-workspace-dependencies", "description": "The load_workspace_dependencies tool: absolute paths into a bundled Python, Node.js, and pnpm payload", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill-local/package.json b/packages/spill/spill-local/package.json index fc1537ba5c..52070e4be8 100644 --- a/packages/spill/spill-local/package.json +++ b/packages/spill/spill-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill-local", "description": "Local-filesystem implementation of the DeepSeek Harness spill storage seam (private session-scoped files)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill-policy/package.json b/packages/spill/spill-policy/package.json index bb903da012..d5e9499048 100644 --- a/packages/spill/spill-policy/package.json +++ b/packages/spill/spill-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill-policy", "description": "Token-budgeted tool-result retention with recoverable text and image paths", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -68,7 +68,7 @@ "@deepseek-ai/dsh-ptc-runtime": "workspace:*", "@deepseek-ai/dsh-attachment-local": "workspace:*", "@deepseek-ai/dsh-tool-fs": "workspace:*", - "sharp": "^0.35.5", + "sharp": "0.35.5", "@deepseek-ai/dsh-agent-loop-testkit": "workspace:*" } } diff --git a/packages/spill/spill/package.json b/packages/spill/spill/package.json index c8923dae7d..d6e920e14d 100644 --- a/packages/spill/spill/package.json +++ b/packages/spill/spill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill", "description": "Abstract spill storage seam (ctx.spillStore) for the DeepSeek Harness — save oversized tool text and return a retrieval locator", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/ssh/fs-ssh/package.json b/packages/ssh/fs-ssh/package.json index 85ced49f07..9b44f31413 100644 --- a/packages/ssh/fs-ssh/package.json +++ b/packages/ssh/fs-ssh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-ssh", "description": "Filesystem provider over the shared POSIX SSH helper", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -35,7 +35,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/ssh/sandbox-ssh/package.json b/packages/ssh/sandbox-ssh/package.json index ffd4826b2b..f23e1c7da0 100644 --- a/packages/ssh/sandbox-ssh/package.json +++ b/packages/ssh/sandbox-ssh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-ssh", "description": "Remote POSIX sandbox argv provider over the shared SSH helper", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -33,7 +33,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/ssh/ssh-helper-runtime/package.json b/packages/ssh/ssh-helper-runtime/package.json index 397403466e..71d4874d3b 100644 --- a/packages/ssh/ssh-helper-runtime/package.json +++ b/packages/ssh/ssh-helper-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-ssh-helper-runtime", "description": "Private executable carrier for the POSIX SSH helper and managed workers", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "private": true, "type": "module", "main": "lib/index.js", diff --git a/packages/ssh/ssh/package.json b/packages/ssh/ssh/package.json index da4a65f40a..868acf1341 100644 --- a/packages/ssh/ssh/package.json +++ b/packages/ssh/ssh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-ssh", "description": "Shared OpenSSH connection and versioned POSIX remote helper", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -60,7 +60,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/ssh/subprocess-ssh/package.json b/packages/ssh/subprocess-ssh/package.json index c30f8755fb..7ac81535b9 100644 --- a/packages/ssh/subprocess-ssh/package.json +++ b/packages/ssh/subprocess-ssh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess-ssh", "description": "Subprocess and terminal provider over the shared POSIX SSH helper", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -34,7 +34,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/storage/storage-domain/package.json b/packages/storage/storage-domain/package.json index 1d585399c1..b04ff22e42 100644 --- a/packages/storage/storage-domain/package.json +++ b/packages/storage/storage-domain/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-domain", "description": "Domain data form (ctx.storage.domain): schema-validated, event-emitting KV domains over storage backends for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -32,7 +32,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/dsh-storage": "workspace:*", diff --git a/packages/storage/storage-json/package.json b/packages/storage/storage-json/package.json index a1cff23fbe..46df6bab7e 100644 --- a/packages/storage/storage-json/package.json +++ b/packages/storage/storage-json/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-json", "description": "JSON file KV storage backend for the DeepSeek Harness storage hub", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage-sqlite/package.json b/packages/storage/storage-sqlite/package.json index 198b865be1..e21395f864 100644 --- a/packages/storage/storage-sqlite/package.json +++ b/packages/storage/storage-sqlite/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-sqlite", "description": "SQLite storage backend (kv facet) for the DeepSeek Harness storage hub", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage/package.json b/packages/storage/storage/package.json index e7f05faf0d..4f871fc0ad 100644 --- a/packages/storage/storage/package.json +++ b/packages/storage/storage/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage", "description": "Storage hub (ctx.storage): named backend registry plus mounted data-form facilities for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-acp/package.json b/packages/subagent/subagent-acp/package.json index e6814533a2..f43904d4b1 100644 --- a/packages/subagent/subagent-acp/package.json +++ b/packages/subagent/subagent-acp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-acp", "description": "Out-of-process ACP subagent backend: drives a child agent in a spawned subprocess over the Agent Client Protocol", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-claude-code/package.json b/packages/subagent/subagent-claude-code/package.json index 4103fd3054..29e5278691 100644 --- a/packages/subagent/subagent-claude-code/package.json +++ b/packages/subagent/subagent-claude-code/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-claude-code", "description": "One-shot Claude Code subagent provider over the official Agent SDK", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -50,7 +50,7 @@ "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", "@modelcontextprotocol/sdk": "^1.31.0", - "zod": "^4.4.3", + "zod": "4.4.3", "@deepseek-ai/dsh-tool-subagent": "workspace:*" }, "devDependencies": { diff --git a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts index 9f9bc27d81..cbb516b28a 100644 --- a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts +++ b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts @@ -360,7 +360,7 @@ describe('task admission and package contracts', () => { '@modelcontextprotocol/sdk', '^1.31.0', ) - expect(manifest.dependencies).toHaveProperty('zod', '^4.4.3') + expect(manifest.dependencies).toHaveProperty('zod', '4.4.3') expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-codex') const sdkRoot = dirname(fileURLToPath( diff --git a/packages/subagent/subagent-codex/package.json b/packages/subagent/subagent-codex/package.json index 1d3daa2d58..264b369cc8 100644 --- a/packages/subagent/subagent-codex/package.json +++ b/packages/subagent/subagent-codex/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-codex", "description": "One-shot Codex subagent provider over the official app-server protocol", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-dsh-sdk/package.json b/packages/subagent/subagent-dsh-sdk/package.json index 432bc66908..bfc61b6b97 100644 --- a/packages/subagent/subagent-dsh-sdk/package.json +++ b/packages/subagent/subagent-dsh-sdk/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-dsh-sdk", "description": "Out-of-process SDK subagent backend: drives a child DeepSeek Harness runtime subprocess over stdio JSON-RPC through the TypeScript SDK client", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-fork-in-process/package.json b/packages/subagent/subagent-fork-in-process/package.json index 9213bcaf4e..b0d444096a 100644 --- a/packages/subagent/subagent-fork-in-process/package.json +++ b/packages/subagent/subagent-fork-in-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-fork-in-process", "description": "In-process fork subagent backend: runs a child agent seeded with a prefix of the parent's log", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-spawn-in-process/package.json b/packages/subagent/subagent-spawn-in-process/package.json index d05dfe5fea..28d65cd8c0 100644 --- a/packages/subagent/subagent-spawn-in-process/package.json +++ b/packages/subagent/subagent-spawn-in-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-spawn-in-process", "description": "In-process spawn subagent backend: runs a fresh child agent on ctx.agents", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent/package.json b/packages/subagent/subagent/package.json index f96d04b84b..678886de4b 100644 --- a/packages/subagent/subagent/package.json +++ b/packages/subagent/subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent", "description": "Abstract subagent seam (ctx.subagents): named-provider registry for delegating to child agents", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -52,7 +52,7 @@ "@deepseek-ai/dsh-chunked-list": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/subagent/tool-subagent-control/package.json b/packages/subagent/tool-subagent-control/package.json index 736d54e0a9..d609b330cb 100644 --- a/packages/subagent/tool-subagent-control/package.json +++ b/packages/subagent/tool-subagent-control/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-subagent-control", "description": "Globally named send_message, interrupt_agent, and list_agents tools over ctx.subagents continuations", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index f6c8100036..e9aa173b5b 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-subagent", "description": "Model-facing subagent delegation tool over the ctx.subagents seam", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -44,7 +44,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis-plugin-loader": "workspace:~", diff --git a/packages/subprocess/subprocess-local/package.json b/packages/subprocess/subprocess-local/package.json index 75a4bb5c82..2e6029c0d7 100644 --- a/packages/subprocess/subprocess-local/package.json +++ b/packages/subprocess/subprocess-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess-local", "description": "Local-subprocess implementation of the DeepSeek Harness subprocess seam", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/subprocess/package.json b/packages/subprocess/subprocess/package.json index 80d36d5586..8e14e50936 100644 --- a/packages/subprocess/subprocess/package.json +++ b/packages/subprocess/subprocess/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess", "description": "Subprocess seam (ctx.subprocess) for the DeepSeek Harness \u2014 managed process groups, bounded spill-backed output, and escalated kills behind one abstract service", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/win32-process/package.json b/packages/subprocess/win32-process/package.json index 2caabeb749..ec8649f5b0 100644 --- a/packages/subprocess/win32-process/package.json +++ b/packages/subprocess/win32-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-win32-process", "description": "Shared low-level Win32 process, stdio, and Job Object primitives", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/telemetry/otel/README.i18n.yaml b/packages/telemetry/otel/README.i18n.yaml index 92d25cbcbb..fb3da80c49 100644 --- a/packages/telemetry/otel/README.i18n.yaml +++ b/packages/telemetry/otel/README.i18n.yaml @@ -18,8 +18,8 @@ en: f141a9d909c555d8 zh: e4f5b690ccb1337c /deepseek-ai-dsh-otel/understand-the-implementation: - en: db3709ac9594b0de - zh: 8e5c84967d1eafbb + en: 56298bd5a3be2cdb + zh: 4dff467a0cb30a38 /deepseek-ai-dsh-otel/further-exploration: en: ee17b991de098cc0 zh: f90257e411921f2b diff --git a/packages/telemetry/otel/README.md b/packages/telemetry/otel/README.md index d70e961cc2..e49ced6468 100644 --- a/packages/telemetry/otel/README.md +++ b/packages/telemetry/otel/README.md @@ -36,6 +36,8 @@ Headers are explicit. The service does not add a channel header or inherit ambie `src/index.ts` registers the service; `event-log.ts` owns ordinary SDK batching; `session-log.ts` owns byte/count scheduling; `transport.ts` supplies the Session SDK JSON HTTP delegate and owns the shared exporter metrics recorder, which receives no meter provider and records nothing; `event-transport.ts` uses Got for cancellable ordinary-event HTTP requests and retry waits with SDK serialization and export accounting. No global OTel provider is installed; the accepted exporter options exclude the upstream `selfObsMeterProvider`, and the Session processor accepts only the batching fields it implements, so exporter and Session-processor self-observability metrics stay unrecorded. The Session processor measures each record once, groups conservative sizes, and waits for the SDK concurrency queue to clear after every callback before sending another request. +OpenTelemetry SDK dependencies use exact versions from one coordinated release across this service and its two adapters. + Composition tests exercise independent channels and service removal; adapter tests cover dependent-fiber cleanup and feedback authorization. diff --git a/packages/telemetry/otel/README.zh.md b/packages/telemetry/otel/README.zh.md index 200e747a7c..7dd081deb5 100644 --- a/packages/telemetry/otel/README.zh.md +++ b/packages/telemetry/otel/README.zh.md @@ -36,6 +36,8 @@ Header 显式提供。服务不会附加 channel header,也不继承环境中 `src/index.ts` 注册服务;`event-log.ts` 负责普通事件的 SDK 聚合;`session-log.ts` 负责字节和条数调度;`transport.ts` 提供 Session 的 SDK JSON HTTP delegate,并持有共享的 exporter 指标记录器(不接收 meter provider,不记录任何指标);`event-transport.ts` 使用 Got 实现可取消的普通事件 HTTP 请求和重试等待,序列化与导出计数仍由 SDK 负责。不安装全局 OTel provider;接受的 exporter 选项排除上游的 `selfObsMeterProvider`,Session processor 也只接受它实现的聚合字段,因此 exporter 和 Session processor 的自观测指标都不会被记录。Session processor 对每条记录只计量一次,按保守大小组包,并在每次回调后等待 SDK 并发队列清理完成才发送下一请求。 +本服务及其两个适配器的 OpenTelemetry SDK 依赖使用同一配套版本系列的精确版本。 + 组合测试覆盖独立通道和服务移除;适配器测试覆盖依赖 fiber 清理与反馈授权。 diff --git a/packages/telemetry/otel/package.json b/packages/telemetry/otel/package.json index e5fd5a6ad6..a725c0dd39 100644 --- a/packages/telemetry/otel/package.json +++ b/packages/telemetry/otel/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-otel", "description": "Cordis service for independent ordinary-event and byte-bounded Session-log OTLP channels", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -28,12 +28,12 @@ "license": "MIT", "dependencies": { "@opentelemetry/api": "^1.9.1", - "@opentelemetry/api-logs": "^0.222.0", - "@opentelemetry/otlp-exporter-base": "^0.222.0", - "@opentelemetry/resources": "^2.11.0", - "@opentelemetry/sdk-logs": "^0.222.0", + "@opentelemetry/api-logs": "0.222.0", + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-logs": "0.222.0", "@opentelemetry/core": "2.11.0", - "@opentelemetry/otlp-transformer": "^0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0", "got": "^14.6.6" }, "peerDependencies": { diff --git a/packages/terminal/terminal-bash/package.json b/packages/terminal/terminal-bash/package.json index 8bef012d2d..e1bef2814c 100644 --- a/packages/terminal/terminal-bash/package.json +++ b/packages/terminal/terminal-bash/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-terminal-bash", "description": "Persistent shell PTY backend over the DeepSeek Harness subprocess terminal primitive", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/terminal/terminal/package.json b/packages/terminal/terminal/package.json index 1540d795c1..8654dd43e5 100644 --- a/packages/terminal/terminal/package.json +++ b/packages/terminal/terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-terminal", "description": "Persistent PTY session seam for the DeepSeek Harness — owner-scoped ids, backend registry, interactive sends, reads, signals, and awaited cleanup", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/agent-loop-testkit/package.json b/packages/test-support/agent-loop-testkit/package.json index 557acfef7f..8299e120eb 100644 --- a/packages/test-support/agent-loop-testkit/package.json +++ b/packages/test-support/agent-loop-testkit/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-loop-testkit", "description": "Prerequisite mounting, production AgentLoop drivers, and Inbox stubs for tests", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/client-runtime/package.json b/packages/test-support/client-runtime/package.json index 1b9912ddac..a8b2050af4 100644 --- a/packages/test-support/client-runtime/package.json +++ b/packages/test-support/client-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-test-runtime", "description": "Browser test runtimes: a jsdom slot bench with test-owned Session and Workspace doubles, and a whole-client tier that boots the web roster through the production bootClient over an endpoint-named Remote mock", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/client-runtime/src/config-form.ts b/packages/test-support/client-runtime/src/config-form.ts index df1f6eea01..97dadd2f91 100644 --- a/packages/test-support/client-runtime/src/config-form.ts +++ b/packages/test-support/client-runtime/src/config-form.ts @@ -25,14 +25,14 @@ export interface StubConfigForm { } /** - * Build an in-memory settings scope for service specs: starts in the host - * loading state, records writes, and lets the test publish Host acceptances. + * Build an in-memory settings scope for service specs: starts in the loading + * state, records writes, and lets the test publish Host acceptances. * @returns the stub handle. */ export function stubConfigForm(): StubConfigForm { let snapshot: ConfigFormSnapshot = { status: 'loading', value: undefined, base: undefined, user: undefined, - revision: undefined, writable: false, mode: 'host', + revision: undefined, writable: false, } const listeners = new Set<() => void>() const set = vi.fn(() => Promise.resolve(true)) diff --git a/packages/test-support/llm-mock-server/package.json b/packages/test-support/llm-mock-server/package.json index 98c7c17c55..b0aca2ae6a 100644 --- a/packages/test-support/llm-mock-server/package.json +++ b/packages/test-support/llm-mock-server/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-mock-server", "description": "Scriptable Messages HTTP/SSE fault server for LLM recovery tests", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/llm-replay/package.json b/packages/test-support/llm-replay/package.json index f7a38026f8..f32a383be5 100644 --- a/packages/test-support/llm-replay/package.json +++ b/packages/test-support/llm-replay/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-replay", "description": "Replay LLM plugin: short-circuits llm/stream with model chunks reconstructed from a recorded session JSONL (keyless snapshot tests)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/loader-smoke/package.json b/packages/test-support/loader-smoke/package.json index 0470324adf..fe71fae91a 100644 --- a/packages/test-support/loader-smoke/package.json +++ b/packages/test-support/loader-smoke/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-loader-smoke", "description": "Shared subprocess and direct-agent harness for keyless real-Loader example smoke tests", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/remote-mock/package.json b/packages/test-support/remote-mock/package.json index 38dd89e3d7..bef2af3926 100644 --- a/packages/test-support/remote-mock/package.json +++ b/packages/test-support/remote-mock/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-remote-mock", "description": "Endpoint-named mock for Typert Remote traffic: unary answers and stream scripts per /, live stream control, a log, and the Connection carrier face whole-client specs install", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/session-snapshot/package.json b/packages/test-support/session-snapshot/package.json index 51a0b989a2..71fe0be2c8 100644 --- a/packages/test-support/session-snapshot/package.json +++ b/packages/test-support/session-snapshot/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-snapshot", "description": "Session-log snapshot core with an ACP protocol adapter, expected-output normalization, and fixture invariants", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/todo/tool-todo/package.json b/packages/todo/tool-todo/package.json index a140cd0a49..834d7e3fa1 100644 --- a/packages/todo/tool-todo/package.json +++ b/packages/todo/tool-todo/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-todo", "description": "Model-facing todo_write tool over the DeepSeek Harness event-sourced session log", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -33,7 +33,7 @@ "license": "MIT", "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/typert/generator/package.json b/packages/typert/generator/package.json index 95810252a7..73d22fd9a4 100644 --- a/packages/typert/generator/package.json +++ b/packages/typert/generator/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-generator", "description": "TypeScript project analyzer and model-driven Typert artifact generator", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -42,6 +42,6 @@ "@deepseek-ai/dsh-tool-cordis": "workspace:*", "@deepseek-ai/dsh-typert-registry": "workspace:*", "@deepseek-ai/cordis": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" } } diff --git a/packages/typert/loader/package.json b/packages/typert/loader/package.json index d66d99a679..23e9137a93 100644 --- a/packages/typert/loader/package.json +++ b/packages/typert/loader/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-loader", "description": "Loader integration for generated Typert package contributions", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -39,6 +39,6 @@ "@deepseek-ai/cordis-plugin-loader": "workspace:~", "@deepseek-ai/dsh-typert-registry": "workspace:*", "@deepseek-ai/cordis": "workspace:~", - "zod": "^4.4.3" + "zod": "4.4.3" } } diff --git a/packages/typert/protocol/package.json b/packages/typert/protocol/package.json index 7bc01940aa..4227729aad 100644 --- a/packages/typert/protocol/package.json +++ b/packages/typert/protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-protocol", "description": "Compiler-independent Remote metadata and Typert provider protocols", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/typert/registry/package.json b/packages/typert/registry/package.json index 2e37402492..8db9a4cacd 100644 --- a/packages/typert/registry/package.json +++ b/packages/typert/registry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-registry", "description": "Runtime registry for generated package reflection and Zod schemas", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -48,7 +48,7 @@ ], "license": "MIT", "dependencies": { - "zod": "^4.4.3" + "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" diff --git a/packages/util/atomic-write/package.json b/packages/util/atomic-write/package.json index 2455bc3780..9c29758f16 100644 --- a/packages/util/atomic-write/package.json +++ b/packages/util/atomic-write/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-atomic-write", "description": "Zero-dependency atomic file replacement: exclusive-create random-suffix temp + rename carrying the caller-stated permissions (writeFileAtomic)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/brand/package.json b/packages/util/brand/package.json index 1e58f76b74..d855fef5a4 100644 --- a/packages/util/brand/package.json +++ b/packages/util/brand/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-brand", "description": "Stateless branded primitive types for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/chunked-list/package.json b/packages/util/chunked-list/package.json index 955057de01..631ef1f226 100644 --- a/packages/util/chunked-list/package.json +++ b/packages/util/chunked-list/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-chunked-list", "description": "Persistent append-only chunked lists with bounded copying and JSON checkpoint validation", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -33,6 +33,6 @@ "@deepseek-ai/cordis": "workspace:~" }, "dependencies": { - "zod": "^4.4.3" + "zod": "4.4.3" } } diff --git a/packages/util/code-language/package.json b/packages/util/code-language/package.json index ddf14d11fa..8bd71f6484 100644 --- a/packages/util/code-language/package.json +++ b/packages/util/code-language/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-code-language", "description": "Single file-extension to syntax-highlighting language table shared by Client code surfaces and the Host read card", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/crypto/package.json b/packages/util/crypto/package.json index 2caeb9b5fc..16a09c33fb 100644 --- a/packages/util/crypto/package.json +++ b/packages/util/crypto/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-crypto", "description": "Zero-dependency browser-safe UUID and byte-encoding helpers", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/deque/package.json b/packages/util/deque/package.json index 634a2f9025..63a02d1de9 100644 --- a/packages/util/deque/package.json +++ b/packages/util/deque/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deque", "description": "Zero-dependency circular deque with amortized constant-time end operations and bounded vacant storage", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/home-paths/package.json b/packages/util/home-paths/package.json index 93068e5a61..7d34a37112 100644 --- a/packages/util/home-paths/package.json +++ b/packages/util/home-paths/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-home-paths", "description": "Shared filesystem path helpers for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/http-proxy/package.json b/packages/util/http-proxy/package.json index d3b2b59257..8c5f473cda 100644 --- a/packages/util/http-proxy/package.json +++ b/packages/util/http-proxy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-http-proxy", "description": "Process-wide outbound HTTP proxy policy for DeepSeek Harness: resolve it from the launch environment and install it as undici's global dispatcher", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -30,7 +30,7 @@ "@deepseek-ai/cordis": "workspace:~" }, "dependencies": { - "undici": "^8.10.2" + "undici": "8.11.2" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~" diff --git a/packages/util/launch-environment/package.json b/packages/util/launch-environment/package.json index c7dc136326..9c1b6e5c32 100644 --- a/packages/util/launch-environment/package.json +++ b/packages/util/launch-environment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-launch-environment", "description": "Immutable DeepSeek Harness launch environment that records which layer supplied each value", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/lazy-require/package.json b/packages/util/lazy-require/package.json index 5cff0efb9e..49518d0d18 100644 --- a/packages/util/lazy-require/package.json +++ b/packages/util/lazy-require/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-lazy-require", "description": "Caller-relative, success-cached lazy loading for CommonJS-compatible Host dependencies", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/native-command/package.json b/packages/util/native-command/package.json index 09ef7d1ae9..55d4133cb9 100644 --- a/packages/util/native-command/package.json +++ b/packages/util/native-command/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-native-command", "description": "Host-native command and path-opening utilities with shell-free execution, cancellation, desktop detection, and WSL handoff", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/output-retention/package.json b/packages/util/output-retention/package.json index 9f0ab57695..431d86d274 100644 --- a/packages/util/output-retention/package.json +++ b/packages/util/output-retention/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-output-retention", "description": "Zero-dependency bounded-retention primitive: ItemRetainer/TextRetainer + neutral notice helpers (what did we keep, what did we omit)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/package-manifest/package.json b/packages/util/package-manifest/package.json index c85511b79c..1bd51132dc 100644 --- a/packages/util/package-manifest/package.json +++ b/packages/util/package-manifest/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-package-manifest", "description": "Shared type declarations for package.json.dsh configuration fields", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/time/package.json b/packages/util/time/package.json index bc27b6d943..c4daadc0f0 100644 --- a/packages/util/time/package.json +++ b/packages/util/time/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-time", "description": "Zero-dependency time vocabulary shared by wire boundaries: canonicalClientTimeZone (IANA zone validation and canonicalization only, no formatting)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/timeout/package.json b/packages/util/timeout/package.json index a1f6e1c835..f3450e821d 100644 --- a/packages/util/timeout/package.json +++ b/packages/util/timeout/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-timeout", "description": "Zero-dependency timeout/deadline primitive: clampTimeout, deadline, timeoutOf, TimeoutReason (timing + classification only, no termination)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/values/package.json b/packages/util/values/package.json index 65a13b177f..f417428688 100644 --- a/packages/util/values/package.json +++ b/packages/util/values/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-values", "description": "Duplicate-install-safe value primitives for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/workspace-path/package.json b/packages/util/workspace-path/package.json index 008798f662..d606942e73 100644 --- a/packages/util/workspace-path/package.json +++ b/packages/util/workspace-path/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-workspace-path", "description": "Browser-safe Workspace path and display helpers", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/tool-web/package.json b/packages/web/tool-web/package.json index 4065557b42..86637c2d8e 100644 --- a/packages/web/tool-web/package.json +++ b/packages/web/tool-web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-web", "description": "Model-facing web tools (web_search, web_fetch) over the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-fetch-http/package.json b/packages/web/web-fetch-http/package.json index 066df024e9..d59f357822 100644 --- a/packages/web/web-fetch-http/package.json +++ b/packages/web/web-fetch-http/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-fetch-http", "description": "Anonymous public HTTP(S) fetch provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -35,7 +35,7 @@ "dependencies": { "@deepseek-ai/schemastery": "workspace:~", "ipaddr.js": "^2.5.0", - "undici": "^8.10.2" + "undici": "8.11.2" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/web/web-search-deepseek/package.json b/packages/web/web-search-deepseek/package.json index 1be9919401..93bcdd1331 100644 --- a/packages/web/web-search-deepseek/package.json +++ b/packages/web/web-search-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-deepseek", "description": "DeepSeek-backed search provider (native web_search via the Anthropic-compatible API) for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-exa/package.json b/packages/web/web-search-exa/package.json index 7db65adb01..5dd577d515 100644 --- a/packages/web/web-search-exa/package.json +++ b/packages/web/web-search-exa/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-exa", "description": "Exa-backed search provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-perplexity/package.json b/packages/web/web-search-perplexity/package.json index 8e4b78b771..58b1069713 100644 --- a/packages/web/web-search-perplexity/package.json +++ b/packages/web/web-search-perplexity/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-perplexity", "description": "Perplexity-backed search provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web/package.json b/packages/web/web/package.json index 10fbd3a665..d5ae491fed 100644 --- a/packages/web/web/package.json +++ b/packages/web/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web", "description": "Abstract web access capability seam (ctx.web) for the DeepSeek Harness — search/fetch provider registry, registration-order-independent selection, request/result vocabulary, and the WebError taxonomy", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/tool-workflow/package.json b/packages/workflow/tool-workflow/package.json index 09d223f289..bbc01e6dfb 100644 --- a/packages/workflow/tool-workflow/package.json +++ b/packages/workflow/tool-workflow/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-workflow", "description": "Model-facing workflow tool: run a JavaScript orchestration script over ctx.workflowEngine", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/workflow-ptc/package.json b/packages/workflow/workflow-ptc/package.json index 6e970275c6..5f1e1cb5d6 100644 --- a/packages/workflow/workflow-ptc/package.json +++ b/packages/workflow/workflow-ptc/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workflow-ptc", "description": "Workflow orchestration in the shared sandboxed Node PTC runtime", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/workflow/package.json b/packages/workflow/workflow/package.json index db9af7f3e0..7fa0783353 100644 --- a/packages/workflow/workflow/package.json +++ b/packages/workflow/workflow/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workflow", "description": "Workflow capability seam: ctx.workflowEngine service, run vocabulary, and workflow/* events", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workspace/workspace/package.json b/packages/workspace/workspace/package.json index e8ed63de35..640d7155af 100644 --- a/packages/workspace/workspace/package.json +++ b/packages/workspace/workspace/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workspace", "description": "Workspace entity registry (ctx.workspaceRegistry): durable workspace records with validated session attachment over the domain data form for the DeepSeek Harness", - "version": "0.2.1-alpha.1", + "version": "0.2.1-alpha.2", "publishConfig": { "access": "public" }, @@ -41,7 +41,7 @@ }, "dependencies": { "@deepseek-ai/dsh-brand": "workspace:*", - "zod": "^4.4.3" + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3a3d56aa5..f7e6215387 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,6 +118,9 @@ importers: '@types/semver': specifier: ^7.8.0 version: 7.8.0 + '@types/papaparse': + specifier: ^5.5.2 + version: 5.5.2 '@types/spdx-expression-parse': specifier: ^4.0.0 version: 4.0.0 @@ -181,6 +184,9 @@ importers: oxlint-tsgolint: specifier: 7.0.2001 version: 7.0.2001 + papaparse: + specifier: 5.5.3 + version: 5.5.3 pnpm: specifier: 11.28.5 version: 11.28.5 @@ -745,7 +751,7 @@ importers: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) sharp: - specifier: ^0.35.5 + specifier: 0.35.5 version: 0.35.5(@types/node@22.20.1) typescript: specifier: ^6.0.3 @@ -799,7 +805,7 @@ importers: specifier: ^0.1.5 version: 0.1.5 koffi: - specifier: ^3.1.0 + specifier: 3.1.1 version: 3.1.1 devDependencies: '@deepseek-ai/cordis-plugin-include': @@ -1160,7 +1166,7 @@ importers: packages/api/account-controller: dependencies: zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -1222,7 +1228,7 @@ importers: specifier: ^8.18.1 version: 8.18.1 zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 packages/api/job-controller: @@ -1234,7 +1240,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -1383,7 +1389,7 @@ importers: specifier: workspace:* version: link:../../interaction/user-questions zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 packages/api/session-controller: @@ -1401,7 +1407,7 @@ importers: specifier: ^3.0.2 version: 3.0.2 zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -1543,7 +1549,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -1589,7 +1595,7 @@ importers: specifier: ^6.0.0 version: 6.0.0 zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -1650,7 +1656,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -1699,7 +1705,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -1754,7 +1760,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery sharp: - specifier: ^0.35.5 + specifier: 0.35.5 version: 0.35.5(@types/node@22.20.1) devDependencies: '@deepseek-ai/cordis': @@ -1883,8 +1889,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery chokidar: - specifier: ^4.0.3 - version: 4.0.3 + specifier: 5.0.0 + version: 5.0.0 node-addon-require-builtin: specifier: ^0.1.9 version: 0.1.9 @@ -1944,7 +1950,7 @@ importers: specifier: ^2.9.0 version: 2.9.0 zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -2932,7 +2938,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -2969,7 +2975,7 @@ importers: specifier: workspace:* version: link:../../typert/protocol zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -3089,7 +3095,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -3494,7 +3500,7 @@ importers: specifier: workspace:* version: link:../../util/workspace-path '@tanstack/react-virtual': - specifier: ^3.14.10 + specifier: 3.14.10 version: 3.14.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/react': specifier: ~18.3.1 @@ -3711,7 +3717,7 @@ importers: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 packages/client/ui-deliverables: @@ -4465,7 +4471,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -5746,7 +5752,7 @@ importers: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 packages/client/ui-sidebar-terminal: @@ -6179,7 +6185,7 @@ importers: specifier: workspace:* version: link:../../util/values '@tanstack/react-virtual': - specifier: ^3.14.10 + specifier: 3.14.10 version: 3.14.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/react': specifier: ~18.3.1 @@ -6442,6 +6448,9 @@ importers: '@deepseek-ai/dsh-host-webserver': specifier: workspace:* version: link:../../host/webserver + '@tanstack/react-virtual': + specifier: 3.14.10 + version: 3.14.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/react': specifier: ~18.3.1 version: 18.3.31 @@ -6457,6 +6466,9 @@ importers: typescript: specifier: ^6.0.3 version: 6.0.3 + zod: + specifier: 4.4.3 + version: 4.4.3 packages/compaction/command-compact: devDependencies: @@ -6763,7 +6775,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -6842,7 +6854,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -6897,7 +6909,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -6992,7 +7004,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -7187,8 +7199,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery chokidar: - specifier: ^4.0.3 - version: 4.0.3 + specifier: 5.0.0 + version: 5.0.0 yaml: specifier: ^2.9.0 version: 2.9.0 @@ -7230,7 +7242,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -7368,7 +7380,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -7399,7 +7411,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -7470,7 +7482,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -7773,7 +7785,7 @@ importers: specifier: 3.2.2 version: 3.2.2(yauzl@3.4.0) zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -8233,7 +8245,7 @@ importers: specifier: 0.28.0 version: 0.28.0 zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -8737,7 +8749,7 @@ importers: specifier: 1.13.8 version: 1.13.8 zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -9350,7 +9362,7 @@ importers: specifier: ^4.0.24 version: 4.0.24 chokidar: - specifier: ^5.0.0 + specifier: 5.0.0 version: 5.0.0 packages/experimental/worktree: @@ -9444,7 +9456,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -9559,7 +9571,7 @@ importers: packages/feedback/command-feedback: dependencies: zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -9599,7 +9611,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -9654,8 +9666,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery chokidar: - specifier: ^4.0.3 - version: 4.0.3 + specifier: 5.0.0 + version: 5.0.0 koffi: specifier: 3.1.1 version: 3.1.1 @@ -9922,7 +9934,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -10215,7 +10227,7 @@ importers: packages/host/plugin-inventory: dependencies: zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -10246,13 +10258,13 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery '@opentelemetry/api-logs': - specifier: ^0.222.0 + specifier: 0.222.0 version: 0.222.0 '@opentelemetry/otlp-exporter-base': - specifier: ^0.222.0 + specifier: 0.222.0 version: 0.222.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-logs': - specifier: ^0.222.0 + specifier: 0.222.0 version: 0.222.0(@opentelemetry/api@1.9.1) devDependencies: '@deepseek-ai/cordis': @@ -10317,7 +10329,7 @@ importers: specifier: workspace:* version: link:../../util/crypto zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -10351,7 +10363,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -10444,7 +10456,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -10591,7 +10603,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -10766,7 +10778,7 @@ importers: version: link:../../../vendor/schemastery '@earendil-works/pi-ai': specifier: ^1.0.2 - version: 1.0.2(patch_hash=1cc211bc358d30cb58bd975f39c03e296f8a277c3de845514d12f9ae77ebab5f)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) + version: 1.0.2(patch_hash=1cc211bc358d30cb58bd975f39c03e296f8a277c3de845514d12f9ae77ebab5f)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3) devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -10814,7 +10826,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -10918,7 +10930,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -11108,7 +11120,7 @@ importers: specifier: ^2026.8.31 version: 2026.8.31(supports-color@9.4.0)(zod@4.4.3) zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 packages/mcp/mcp-resources: @@ -11145,7 +11157,7 @@ importers: specifier: workspace:* version: link:../../util/brand zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -11231,7 +11243,7 @@ importers: specifier: ^4.3.2 version: 4.3.2 zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -11419,7 +11431,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -11490,7 +11502,7 @@ importers: specifier: 0.5.1 version: 0.5.1 zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -12066,7 +12078,7 @@ importers: packages/session/session-projection: dependencies: zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -12085,7 +12097,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -12110,7 +12122,7 @@ importers: packages/session/session-stats: dependencies: zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -12150,7 +12162,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery '@opentelemetry/api-logs': - specifier: ^0.222.0 + specifier: 0.222.0 version: 0.222.0 devDependencies: '@deepseek-ai/cordis': @@ -12214,7 +12226,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -12316,7 +12328,7 @@ importers: packages/session/session-turn-outline: dependencies: zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -12900,7 +12912,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery chokidar: - specifier: ^5.0.0 + specifier: 5.0.0 version: 5.0.0 yaml: specifier: ^2.4.2 @@ -13128,7 +13140,7 @@ importers: specifier: workspace:* version: link:../../core/tools sharp: - specifier: ^0.35.5 + specifier: 0.35.5 version: 0.35.5(@types/node@22.20.1) packages/ssh/fs-ssh: @@ -13137,7 +13149,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -13162,7 +13174,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -13181,7 +13193,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -13354,7 +13366,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -13382,7 +13394,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -13433,7 +13445,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -13597,7 +13609,7 @@ importers: specifier: ^1.31.0 version: 1.31.0(supports-color@9.4.0)(zod@4.4.3) zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -13887,7 +13899,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -14041,22 +14053,22 @@ importers: specifier: ^1.9.1 version: 1.9.1 '@opentelemetry/api-logs': - specifier: ^0.222.0 + specifier: 0.222.0 version: 0.222.0 '@opentelemetry/core': specifier: 2.11.0 version: 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/otlp-exporter-base': - specifier: ^0.222.0 + specifier: 0.222.0 version: 0.222.0(@opentelemetry/api@1.9.1) '@opentelemetry/otlp-transformer': - specifier: ^0.222.0 + specifier: 0.222.0 version: 0.222.0(@opentelemetry/api@1.9.1) '@opentelemetry/resources': - specifier: ^2.11.0 + specifier: 2.11.0 version: 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-logs': - specifier: ^0.222.0 + specifier: 0.222.0 version: 0.222.0(@opentelemetry/api@1.9.1) got: specifier: ^14.6.6 @@ -14438,7 +14450,7 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -14497,7 +14509,7 @@ importers: specifier: workspace:* version: link:../registry zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 packages/typert/loader: @@ -14519,7 +14531,7 @@ importers: specifier: workspace:* version: link:../registry zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 packages/typert/protocol: @@ -14535,7 +14547,7 @@ importers: packages/typert/registry: dependencies: zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -14560,7 +14572,7 @@ importers: packages/util/chunked-list: dependencies: zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -14594,8 +14606,8 @@ importers: packages/util/http-proxy: dependencies: undici: - specifier: ^8.10.2 - version: 8.10.2 + specifier: 8.11.2 + version: 8.11.2 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -14738,8 +14750,8 @@ importers: specifier: ^2.5.0 version: 2.5.0 undici: - specifier: ^8.10.2 - version: 8.10.2 + specifier: 8.11.2 + version: 8.11.2 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -14987,7 +14999,7 @@ importers: specifier: workspace:* version: link:../../util/brand zod: - specifier: ^4.4.3 + specifier: 4.4.3 version: 4.4.3 devDependencies: '@deepseek-ai/cordis': @@ -15635,6 +15647,10 @@ importers: packages: + papaparse@5.5.3: + resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} + + '@agentclientprotocol/sdk@1.4.0': resolution: {integrity: sha512-/eufudw+aFY1LKLolT6yFE6UMmYRl7fMJ/DEONSIyR6wI3slHWITBsANRGqXEY8FRzqUxwh7QEaGiZHcJPVThg==} peerDependencies: @@ -22688,8 +22704,8 @@ packages: resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} engines: {node: '>=20.18.1'} - undici@8.10.2: - resolution: {integrity: sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==} + undici@8.11.2: + resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==} engines: {node: '>=22.19.0'} unicode-properties@1.4.1: @@ -23217,6 +23233,9 @@ packages: snapshots: + papaparse@5.5.3: {} + + '@agentclientprotocol/sdk@1.4.0(zod@4.4.3)': dependencies: zod: 4.4.3 @@ -23851,7 +23870,7 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@earendil-works/pi-ai@1.0.2(patch_hash=1cc211bc358d30cb58bd975f39c03e296f8a277c3de845514d12f9ae77ebab5f)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3)': + '@earendil-works/pi-ai@1.0.2(patch_hash=1cc211bc358d30cb58bd975f39c03e296f8a277c3de845514d12f9ae77ebab5f)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.129.0(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1127.0 @@ -23860,7 +23879,7 @@ snapshots: '@smithy/node-http-handler': 4.12.1 http-proxy-agent: 9.1.0(supports-color@9.4.0) https-proxy-agent: 9.1.0(supports-color@9.4.0) - openai: 7.19.0(@aws-sdk/credential-provider-node@3.972.84)(@smithy/signature-v4@5.7.4)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) + openai: 7.19.0(@aws-sdk/credential-provider-node@3.972.84)(@smithy/signature-v4@5.7.4)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3) partial-json: 0.1.7 typebox: 1.3.27 transitivePeerDependencies: @@ -29331,11 +29350,11 @@ snapshots: powershell-utils: 0.2.1 wsl-utils: 1.0.0 - openai@7.19.0(@aws-sdk/credential-provider-node@3.972.84)(@smithy/signature-v4@5.7.4)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3): + openai@7.19.0(@aws-sdk/credential-provider-node@3.972.84)(@smithy/signature-v4@5.7.4)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3): optionalDependencies: '@aws-sdk/credential-provider-node': 3.972.84 '@smithy/signature-v4': 5.7.4 - undici: 8.10.2 + undici: 8.11.2 ws: 8.21.3 zod: 4.4.3 @@ -30503,7 +30522,7 @@ snapshots: undici@7.29.1: {} - undici@8.10.2: {} + undici@8.11.2: {} unicode-properties@1.4.1: dependencies: diff --git a/python/sdk-runtime/README.i18n.yaml b/python/sdk-runtime/README.i18n.yaml index 766f49137b..2397bcb14a 100644 --- a/python/sdk-runtime/README.i18n.yaml +++ b/python/sdk-runtime/README.i18n.yaml @@ -6,14 +6,14 @@ en: 1ea1d8e6d2c14b02 zh: 8e0b9903158667a1 /deepseek-harness-runtime-bin/installed-commands-and-artifacts: - en: af4e7919327e1991 - zh: b4114823b4f1a3fa + en: b35529d69ef29f15 + zh: 4e3d9e42955cd010 /deepseek-harness-runtime-bin/python-module-api: - en: a88b7574e0e00a28 - zh: 3fd4fe8c0b338e79 + en: 78a200e328d96637 + zh: f3b7a0b69b7a39e0 /deepseek-harness-runtime-bin/packaged-profile-resolution: - en: 641c647efc49c568 - zh: a29371de9fbbebfd + en: f1123b8835b57bbe + zh: 05af612c33c5d855 /deepseek-harness-runtime-bin/build-and-distribution: - en: 437ab8ba3bbe7ed6 - zh: 3f321a73e472220b + en: 01b6d838deae9c7b + zh: 88b09ab5f1ae14ca diff --git a/python/sdk-runtime/README.md b/python/sdk-runtime/README.md index 8c82991c02..4f7fe4dd0f 100644 --- a/python/sdk-runtime/README.md +++ b/python/sdk-runtime/README.md @@ -10,11 +10,22 @@ The wheel installs a `dsh` console command and the `deepseek_harness_runtime` Py Production executables are named `deepseek-harness-sdk-runtime--` under the module's `runtime/` directory; Windows uses the `.exe` suffix. Linux and macOS wheels include a target-native `-rg` sidecar, Windows includes `-rg.exe`, and macOS also includes `-spawn-helper` for `node-pty`. Published targets are Linux x64, Linux arm64, macOS arm64, macOS x64, and Windows x64. The wheel tag and payload must match exactly; no Windows arm64 wheel is published. -Each target also requires `-office/`, where the stem excludes `.exe`. This directory contains the complete installed Office packages and their dependencies, preserving engine resources, manifests, licenses, source inventories, and helper permissions. Copy this directory together with the executable. A missing target engine fails the sidecar build with its npm package name and target platform/architecture. +The wheel carries only the executable, native execution helpers, and `-/` with `downloads.json` and the lightweight `office-skills/` tree. CPython, authoring libraries, standalone Node, pnpm, and the complete Office sidecar are optional downloads. Importing Python modules, starting SDK profiles, and processing Office files never download resources. -Each wheel also includes `-/primary-runtime/` (CPython, locked Office Python libraries, standalone Node, and pnpm) and sibling `office-skills/` (three default workflows and their shared checker). These are ordinary relocatable files, not bytes embedded in the executable. The shared builder selects target-native archives for all five wheel targets and executes its smoke on the native build host. Packaging and installed-runtime lookup reject missing resources, wrong-platform metadata, and lost Python or Node executable permissions. The short platform directory avoids repeating the executable name in Python DLL paths on Windows. +Download either resource explicitly before using it: -The packaged bootstrap supplies `DSH_BUNDLED_PRIMARY_RUNTIME` as a carrier default. The `sdk` profile uses it when `DSH_PRIMARY_RUNTIME` is unset; an explicit path overrides it, and an empty string disables the query and Office provider. External payloads retain the `primary-runtime/` plus sibling `office-skills/` layout. Loaded skills expose the bundled Node and adjacent Office CLI as absolute paths; custom Python-only payloads must patch `skill-office.config.cli: false` or supply `skill-office.config.node`. The SDK reads Python in place without copying it into `DSH_HOME`. Source and dev-only Node carriers have no bundled default; they use an explicit `DSH_PRIMARY_RUNTIME`. +```py +from deepseek_harness_runtime import download_office, download_primary_runtime + +office = download_office() +primary_runtime = download_primary_runtime() +``` + +`download_office()` downloads the locked npm Kit, target engine, and complete dependency tree, plus standalone Node for the Kit CLI. `download_primary_runtime()` independently downloads CPython, locked Python libraries, Node, pnpm, and Office skills. Both return absolute resource directories and reuse completed caches. The authoring download validates its target, interpreters, library directory and skills before returning. The existing local `installPrimaryRuntime()` operation still copies an already available payload; it does not download. The shared builder applies the [NumPy/pandas test-file exclusions](../../apps/desktop/README.md#bundled-workspace-dependencies). + +`DSH_RESOURCE_CACHE` selects an absolute cache root, defaulting to `~/.cache/deepseek-harness/resources`. Resource identities include the release's locked inputs. Downloads verify archive hashes and package identities, preserve executable permissions, and publish completed directories atomically. Failed downloads can be retried. `DSH_RESOURCE_DOWNLOAD_TIMEOUT_MS` bounds each archive download, including its body; the default is `300000`, and accepted values are integers from `1` to `2147483647`. Downloads follow the Harness proxy environment policy. Installed wheel files are never modified. Release versions have separate cache directories; unused versions require manual removal. Downloads use the locked upstream URLs without a registry or mirror override. + +On subsequent launches the packaged bootstrap exposes downloaded resources to the SDK profile. `DSH_PRIMARY_RUNTIME` overrides the cached authoring environment; an empty value disables its query. `DSH_OFFICE_SIDECAR` can select an absolute sidecar directory with its standalone Node. The Office checker is included in the wheel; users may provide their own Python environment and document libraries without downloading the authoring environment. Office skills are available when either resource is selected; authoring-only deployments expose the skills with the Kit CLI disabled. External authoring payloads retain the `primary-runtime/` plus sibling `office-skills/` layout. The SDK reads resources in place without copying them into `DSH_HOME`. The `sdk-minimal` profile does not mount these providers. Skill selection is independent of delivery: project, custom-directory, and user filesystem skills override same-name bundled skills. An SDK patch can disable only the Office provider while retaining the Python query: @@ -25,7 +36,7 @@ Skill selection is independent of delivery: project, custom-directory, and user To replace the three Office workflows and shared checker as a set, patch `skill-office.config.assetRoot` to another absolute resource directory. Use the filesystem skill provider for arbitrary skill collections. Configuration patches apply at process startup; changing skills does not require rebuilding the runtime wheel or Python environment. -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. +Repository builds also materialize a dev-only `runtime/node/` carrier. It runs `node runtime/node/runtime-bootstrap.mjs` 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 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`. @@ -34,6 +45,9 @@ Both carriers execute the same `dsh` grammar and shipped profiles, including the - `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. +- `download_office() -> Path` explicitly downloads or reuses the Office sidecar and its standalone Node. +- `download_primary_runtime() -> Path` explicitly downloads or reuses the authoring environment. +- `resolve_office_launch_args() -> tuple[str, str]` locates the installed Node and Kit CLI without downloading; missing resources raise `FileNotFoundError` with the download instruction. - `main()` implements the installed `dsh` console command and rejects an absent or blank `DSH_HOME`. On Windows it waits for the bundled process with inherited standard streams and forwards its exit status; on POSIX it replaces the Python process. Unsupported platforms and missing executables or sidecars raise `FileNotFoundError` with the build and installation routes. Unknown runtime modes raise `ValueError`. @@ -42,7 +56,7 @@ Unsupported platforms and missing executables or sidecars raise `FileNotFoundErr `dsh` initializes shipped profiles under the explicit home, composes their bundle patches, and loads bundled plugins from the executable's virtual filesystem. Runtime resolution uses an in-memory generation instead of disk symlinks or proxy packages. Fallback imports use recorded declaring-package paths, including paths inside the executable's virtual filesystem, so built-in rows and external plugin peers share the bundled Cordis/module instance. Native shared libraries and Windows ConPTY addons are packaged with native addons, while ripgrep and the macOS PTY helper remain executable sidecars. -The Python bootstrap resolves the Office kit from its adjacent directory so native helpers and URL Workers use real filesystem paths. The kit owns engine selection and validation; the Python bootstrap adds no runtime download or compilation. The Office smoke also retrieves CLI paths from the loaded skill and executes capabilities and DOCX conversion with an empty PATH. +Python and the packaged bootstrap share the resource layout: `//office/`, `node/bin/node[.exe]`, and `node_modules/@deepseek-ai/libreoffice-kit/lib/cli.js`. Automatic Office cache discovery requires the `complete` marker to match the manifest identity; an explicit `DSH_OFFICE_SIDECAR` requires the Node and CLI files. The Python bootstrap resolves the Office kit from its explicitly downloaded or selected sidecar so native helpers and URL Workers use real filesystem paths. The same resource preparation implementation serves repository builds and explicit Python downloads. The kit owns document operations; the Office smoke retrieves CLI paths from the loaded skill and executes capabilities and DOCX conversion with an empty PATH. External profile management uses `dsh plugin --profile ...`. That command requires `pnpm` on `PATH`; ordinary SDK/profile execution does not. @@ -50,6 +64,6 @@ External profile management uses `dsh plugin --profile ...`. That command Production deployment permits unused workspace patches for packages outside the runtime closure; patches for included packages must still apply successfully. This exception is confined to the deploy command; repository installation still rejects unused patches. -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. Deployment disables workspace hoisting so unrelated source packages stay out of the payload. Staging shares root workspace packages with nested consumers of the same pnpm dependency identity, preserving singleton services while retaining different peer resolutions. `scripts/build-python-release.py` stages release-shaped wheels at the root repository version and pins `deepseek-harness-sdk` to the exact runtime version. +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. Only the target platform’s PTY prebuilds are packaged. Package README, CHANGELOG, HISTORY Markdown, source maps, and TypeScript declarations outside runtime assets are omitted. Deployment disables workspace hoisting, and packaging excludes checkout dependencies and workspace source trees outside the deployment so dependency scans cannot add development packages to the payload. Staging shares root workspace packages with nested consumers of the same pnpm dependency identity, preserving singleton services while retaining different peer resolutions. `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 the installed distribution and executable identities, then exercises default and customized SDK profiles, external plugins, MCP, native tools, direct JSON-RPC, committed snapshots, and the real provider on trusted runs. Its Office scenario relocates the complete target payload and converts DOCX with the required platform engine: the target’s declared native engine, or WASM when no native engine is declared. 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). +The installed-wheel smoke creates a clean virtual environment outside the checkout, proves the installed distribution and executable identities, then exercises default and customized SDK profiles, external plugins, MCP, native tools, direct JSON-RPC, committed snapshots, and the real provider on trusted runs. Its Office scenario relocates the lightweight target payload, explicitly downloads the sidecar, and converts DOCX with the required platform engine: the target’s declared native engine, or WASM when no native engine is declared. 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 27405eaf74..6a85b1b25e 100644 --- a/python/sdk-runtime/README.zh.md +++ b/python/sdk-runtime/README.zh.md @@ -10,11 +10,22 @@ wheel 包会安装 `dsh` 控制台命令和 `deepseek_harness_runtime` Python 生产可执行程序位于模块的 `runtime/` 目录,命名为 `deepseek-harness-sdk-runtime--`;Windows 使用 `.exe` 后缀。Linux 与 macOS wheel 包含目标平台原生的 `-rg` 伴随程序,Windows 包含 `-rg.exe`,macOS 还包含 `node-pty` 使用的 `-spawn-helper`。已发布目标是 Linux x64、Linux arm64、macOS arm64、macOS x64 与 Windows x64。wheel 包标签必须与载荷严格匹配;不发布 Windows arm64 wheel 包。 -每个目标还要求 `-office/`,其中 stem 不含 `.exe`。该目录包含完整的已安装 Office 包及其依赖,保留引擎资源、清单、许可证、源码清单与辅助程序权限。复制可执行文件时必须一并复制此目录。缺少目标引擎会使 sidecar 构建失败,错误会指出其 npm 包名与目标平台/架构。 +wheel 仅携带可执行程序、原生执行辅助程序,以及包含 `downloads.json` 和轻量 `office-skills/` 目录的 `-/`。CPython、创作库、独立 Node、pnpm 和完整 Office sidecar 均为可选下载资源。导入 Python 模块、启动 SDK profile 和处理 Office 文件都不会下载资源。 -每个 wheel 还包含 `-/primary-runtime/`(CPython、锁定版本的 Office Python 库、独立 Node 和 pnpm)及同级 `office-skills/`(三个默认工作流与共用检查脚本)。它们是可重定位的普通文件,不嵌入可执行文件。共享构建器为全部五个 wheel 目标选择原生归档,并在对应构建主机上执行冒烟检查。打包与已安装运行时定位会拒绝缺失资源、平台不符的元数据及 Python 或 Node 执行权限丢失。 较短的平台目录避免在 Windows Python DLL 路径中重复可执行文件名称。 +使用前显式下载所需的任一种资源: -打包启动器通过 `DSH_BUNDLED_PRIMARY_RUNTIME` 提供载体默认路径。`sdk` profile 在未设置 `DSH_PRIMARY_RUNTIME` 时使用该路径;显式路径覆盖它,空字符串禁用查询与 Office 提供方。外部 payload 保持 `primary-runtime/` 与同级 `office-skills/` 布局。加载后的技能给出内置 Node 与伴随 Office CLI 的绝对路径;自定义的纯 Python payload 必须配置 `skill-office.config.cli: false` 或提供 `skill-office.config.node`。SDK 原位读取 Python,不复制到 `DSH_HOME`。源码与仅供开发的 Node 载体没有随包默认路径,需显式设置 `DSH_PRIMARY_RUNTIME`。 +```py +from deepseek_harness_runtime import download_office, download_primary_runtime + +office = download_office() +primary_runtime = download_primary_runtime() +``` + +`download_office()` 下载锁定版本的 npm Kit、目标引擎及完整依赖树,并准备运行 Kit CLI 的独立 Node。`download_primary_runtime()` 独立下载 CPython、锁定的 Python 库、Node、pnpm 和 Office skills。两者返回绝对资源目录,并复用完整缓存。创作下载会在返回前校验目标平台、解释器、库目录与 skills。现有本地 `installPrimaryRuntime()` 操作仍只复制已有载荷,不执行下载。 共享构建器采用 [NumPy/pandas 测试文件裁剪规则](../../apps/desktop/README.zh.md#bundled-workspace-dependencies)。 + +`DSH_RESOURCE_CACHE` 指定绝对缓存根目录,默认为 `~/.cache/deepseek-harness/resources`。资源身份包含发布版本锁定的输入。下载会校验归档哈希和包身份、保留执行权限,并原子发布完整目录。下载失败后可重试。`DSH_RESOURCE_DOWNLOAD_TIMEOUT_MS` 限制每个归档的下载耗时,包含响应内容;默认值为 `300000`,接受 `1` 到 `2147483647` 的整数。下载遵循 Harness 代理环境策略。已安装的 wheel 文件不会被修改。不同发布版本使用独立缓存目录;不再使用的版本需手动删除。下载使用锁定的上游 URL,不提供 registry 或镜像覆盖。 + +后续启动时,打包 bootstrap 将已下载资源提供给 SDK profile。`DSH_PRIMARY_RUNTIME` 覆盖缓存中的创作环境;空值禁用其查询。`DSH_OFFICE_SIDECAR` 可选择包含独立 Node 的绝对 sidecar 目录。Office checker 随 wheel 提供;用户可自行提供 Python 环境和文档库,无需下载创作环境。选择任一种资源即可使用 Office skills;仅有创作环境时,skills 中的 Kit CLI 被禁用。外部创作载荷保留 `primary-runtime/` 及同级 `office-skills/` 布局。SDK 原位读取资源,不复制到 `DSH_HOME`。`sdk-minimal` profile 不挂载这些提供方。 选择 skills 与交付资源相互独立:项目、自定义目录和用户文件系统 skills 优先于同名随包 skills。SDK patch 可以仅禁用 Office 提供方,同时保留 Python 查询: @@ -25,7 +36,7 @@ wheel 包会安装 `dsh` 控制台命令和 `deepseek_harness_runtime` Python 要成套替换三个 Office 工作流与共用检查脚本,可将 `skill-office.config.assetRoot` 配置为另一个绝对资源目录。任意 skill 集合通过文件系统 skill 提供方加载。配置 patch 在进程启动时生效;切换 skills 无需重建 runtime wheel 或 Python 环境。 -仓库构建还会物化仅限开发的 `runtime/node/` 载体。它在系统 Node 22.19 或更高版本上运行 `node runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。系统不会自动选择它,而且 wheel 包与 sdist 均不包含它。 +仓库构建还会物化仅限开发的 `runtime/node/` 载体。它在系统 Node 22.19 或更高版本上运行 `node runtime/node/runtime-bootstrap.mjs`。系统不会自动选择它,而且 wheel 包与 sdist 均不包含它。 两种载体执行相同的 `dsh` 语法与随附 profile,包括独立的 `sdk-minimal` 配置树,以及包含前端产物的完整 `web` profile。私有 `dsh-python-runtime-closure` manifest(元数据清单)定义打包依赖闭包;不存在 Python 专用 Node 应用或检入的默认 `cordis.yml`。 @@ -34,6 +45,9 @@ wheel 包会安装 `dsh` 控制台命令和 `deepseek_harness_runtime` Python - `bundled_package_dir() -> Path` 返回已安装模块数据根目录,并校验发布元数据。 - `bundled_runtime_path() -> Path` 返回当前平台可执行程序,并校验必需伴随文件。 - `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]` 默认返回可执行程序 argv。显式 `mode="node"` 或 `DSH_RUNTIME_MODE=node` 会选择仅限仓库使用的 Node 载体。 +- `download_office() -> Path` 显式下载或复用 Office sidecar 及其独立 Node。 +- `download_primary_runtime() -> Path` 显式下载或复用创作环境。 +- `resolve_office_launch_args() -> tuple[str, str]` 定位已安装的 Node 和 Kit CLI,不执行下载;资源缺失时抛出含下载指令的 `FileNotFoundError`。 - `main()` 实现已安装的 `dsh` 控制台命令,并拒绝缺失或空白的 `DSH_HOME`。在 Windows 上,它让打包进程继承标准流,等待其结束并转发退出状态;在 POSIX 上,它替换 Python 进程。 不支持的平台以及缺失的可执行程序或伴随文件会抛出 `FileNotFoundError`,并指出构建与安装路径。未知运行时模式会抛出 `ValueError`。 @@ -42,7 +56,7 @@ wheel 包会安装 `dsh` 控制台命令和 `deepseek_harness_runtime` Python `dsh` 在显式指定的主目录下初始化随附 profile、组合其 bundle patch,并从可执行程序的虚拟文件系统加载内置插件。运行时解析使用内存中的 generation,不创建磁盘符号链接或代理包。fallback 导入使用记录的声明包路径,包括可执行程序虚拟文件系统内的路径,因此内置配置项与外部插件 peer 共享内置的 Cordis/模块实例。原生共享库与 Windows ConPTY addon 会同其他原生 addon 一起打包;ripgrep 与 macOS PTY helper 仍是可执行伴随程序。 -Python bootstrap 从相邻目录解析 Office kit,让原生辅助程序与 URL Worker 使用真实文件系统路径。kit 负责引擎选择与校验;Python bootstrap 不增加运行时下载或编译。Office smoke 还从已加载技能获取 CLI 路径,并在空 PATH 下执行 capabilities 和 DOCX 转换。 +Python 与打包的 bootstrap 共享资源布局:`//office/`、`node/bin/node[.exe]` 与 `node_modules/@deepseek-ai/libreoffice-kit/lib/cli.js`。自动发现 Office 缓存要求 `complete` 标记匹配清单 identity;显式 `DSH_OFFICE_SIDECAR` 要求 Node 与 CLI 文件存在。Python bootstrap 从显式下载或选择的 sidecar 解析 Office kit,让原生辅助程序与 URL Worker 使用真实文件系统路径。仓库构建与显式 Python 下载共用资源准备实现。kit 负责文档操作;Office smoke 从已加载技能获取 CLI 路径,并在空 PATH 下执行 capabilities 和 DOCX 转换。 外部 profile 管理使用 `dsh plugin --profile ...`。该命令要求 `PATH` 中存在 `pnpm`;普通 SDK/profile 运行不需要它。 @@ -50,6 +64,6 @@ Python bootstrap 从相邻目录解析 Office kit,让原生辅助程序与 URL 生产部署允许工作区中不属于运行时闭包的补丁保持未使用;闭包内包的补丁仍必须成功应用。此例外仅用于部署命令,仓库安装仍拒绝未使用的补丁。 -在仓库根目录运行 `pnpm exec tsx scripts/build-exe-for-python-sdk.ts`,会校验闭包、构建包、部署无符号链接的文件树、打包所选目标,并把可执行程序及伴随文件同步到本模块。部署会关闭工作区提升,避免无关源码包进入载荷。暂存步骤让使用相同 pnpm 依赖标识的嵌套消费者共享根目录的工作区包,保留服务单例,同时保留不同的对等依赖解析。`scripts/build-python-release.py` 按仓库根版本暂存发布形态的 wheel 包,并将 `deepseek-harness-sdk` 固定到完全相同的运行时版本。 +在仓库根目录运行 `pnpm exec tsx scripts/build-exe-for-python-sdk.ts`,会校验闭包、构建包、部署无符号链接的文件树、打包所选目标,并把可执行程序及伴随文件同步到本模块。打包时只保留目标平台的 PTY 预构建文件,并排除运行时资源目录之外的包内 README、CHANGELOG、HISTORY Markdown 文档、source map 和 TypeScript 声明。部署会关闭工作区提升,打包时还会排除部署目录之外的检出依赖和工作区源码树,防止依赖扫描把开发包加入载荷。暂存步骤让使用相同 pnpm 依赖标识的嵌套消费者共享根目录的工作区包,保留服务单例,同时保留不同的对等依赖解析。`scripts/build-python-release.py` 按仓库根版本暂存发布形态的 wheel 包,并将 `deepseek-harness-sdk` 固定到完全相同的运行时版本。 -已安装 wheel 包冒烟测试会在检出目录外创建干净的虚拟环境,验证分发物与可执行程序的来源,然后覆盖默认及自定义 SDK profile、外部插件、MCP、原生工具、直接 JSON-RPC、检入快照,以及可信运行中的真实提供方。Office 场景会迁移完整的目标载荷目录,并使用所需的平台引擎转换 DOCX:使用目标已声明的原生引擎,未声明原生引擎时使用 WASM。另见 [Python 贡献者工作流](../development.zh.md) 与 [installed-wheel 测试决策](../../.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)。 +已安装 wheel 包冒烟测试会在检出目录外创建干净的虚拟环境,验证分发物与可执行程序的来源,然后覆盖默认及自定义 SDK profile、外部插件、MCP、原生工具、直接 JSON-RPC、检入快照,以及可信运行中的真实提供方。Office 场景会迁移轻量目标载荷、显式下载 sidecar,并使用所需的平台引擎转换 DOCX:使用目标已声明的原生引擎,未声明原生引擎时使用 WASM。另见 [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-runtime/hatch_build.py b/python/sdk-runtime/hatch_build.py index f1357bff52..eaee407bce 100644 --- a/python/sdk-runtime/hatch_build.py +++ b/python/sdk-runtime/hatch_build.py @@ -86,8 +86,6 @@ class RuntimeBuildHook(BuildHookInterface): ) if "-macos-" in expected_executable: expected_files.append(f"{expected_executable}-spawn-helper") - office = runtime_dir / f"{expected_executable.removesuffix('.exe')}-office" - expected_files.append(office.name) resources = runtime_dir / target expected_files.append(resources.name) expected_files.sort() @@ -98,20 +96,9 @@ class RuntimeBuildHook(BuildHookInterface): ) for executable in runtime_files: if executable == resources: - validate = runpy.run_path(str(runtime_dir.parent / "_resources.py"))["validate_resources"] + validate = runpy.run_path(str(runtime_dir.parent / "_resources.py"))["validate_downloads"] validate(resources, target) continue - if executable == office: - adapter = office / "node_modules/@deepseek-ai/libreoffice-kit/package.json" - if not adapter.is_file(): - raise RuntimeError(f"runtime Office dependency is missing: {adapter}") - native = target.replace("win-", "win32-").replace("macos-", "darwin-") - declared = json.loads(adapter.read_text(encoding="utf-8")).get("optionalDependencies", {}) - engine = native if f"@deepseek-ai/libreoffice-kit-{native}" in declared else "wasm" - required = office / "node_modules" / f"@deepseek-ai/libreoffice-kit-{engine}/prebuilds.json" - if not required.is_file(): - raise RuntimeError(f"runtime Office dependency is missing: {required}") - continue if not executable.is_file(): raise RuntimeError(f"runtime executable is not a file: {executable}") if platform_tag != "win_amd64" and executable.stat().st_mode & stat.S_IXUSR == 0: diff --git a/python/sdk-runtime/runtime-bootstrap.mjs b/python/sdk-runtime/runtime-bootstrap.mjs index 52836d746e..d860ccafca 100644 --- a/python/sdk-runtime/runtime-bootstrap.mjs +++ b/python/sdk-runtime/runtime-bootstrap.mjs @@ -1,20 +1,78 @@ #!/usr/bin/env node /** Private entry owned by the Python single-file runtime packaging. */ +import { existsSync } from 'node:fs' +import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises' +import { homedir } from 'node:os' import { registerHooks } from 'node:module' -import { dirname, join } from 'node:path' +import { dirname, isAbsolute, join } from 'node:path' import { isSea } from 'node:sea' import { fileURLToPath, pathToFileURL } from 'node:url' -if (isSea()) { - // Office spawns executable helpers and URL workers; its complete package tree must be real files. - const parentURL = pathToFileURL(`${process.execPath.replace(/\.exe$/i, '')}-office/package.json`).href +const platform = process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'win' : process.platform +const resourceDirectory = join(isSea() ? dirname(process.execPath) : dirname(dirname(fileURLToPath(import.meta.url))), `${platform}-${process.arch}`) +let downloads +let cache +if (existsSync(join(resourceDirectory, 'downloads.json'))) { + downloads = JSON.parse(await readFile(join(resourceDirectory, 'downloads.json'), 'utf8')) + const root = process.env.DSH_RESOURCE_CACHE ?? join(homedir(), '.cache', 'deepseek-harness', 'resources') + if (!isAbsolute(root)) throw new Error('DSH_RESOURCE_CACHE must be an absolute directory') + if (!/^[a-f0-9]{64}$/u.test(downloads.identity)) throw new Error('runtime download manifest has an invalid cache identity') + cache = join(root, downloads.identity) +} + +/** Publish a complete immutable resource directory; overlapping downloads may share the winner. */ +async function downloadResource(kind) { + if (downloads === undefined) throw new Error('runtime download manifest is missing; rebuild or reinstall the runtime wheel') + const destination = join(cache, kind) + if (!existsSync(join(destination, 'complete'))) { + await mkdir(cache, { recursive: true }) + const staging = await mkdtemp(join(cache, `.${kind}-`)) + let disposeProxy + try { + const { installProxyFromEnvironment } = await import('@deepseek-ai/dsh-http-proxy') + disposeProxy = await installProxyFromEnvironment({ get(name) { + const value = process.env[name] + return value === undefined ? undefined : { value } + } }, message => console.error(message)) + const { downloadNodeRuntime, preparePrimaryRuntime } = await import('./primary-runtime.mjs') + const archives = join(cache, 'archives') + if (kind === 'office') { + const { downloadOfficeSidecar } = await import('./office-sidecar.mjs') + await downloadNodeRuntime(downloads.target, join(staging, 'node'), archives) + await downloadOfficeSidecar(downloads.office, staging, archives) + } else { + await preparePrimaryRuntime({ target: downloads.target, output: staging, cache: archives, + version: downloads.version, pnpmArchive: downloads.pnpm, skillSource: join(resourceDirectory, 'office-skills') }) + } + await writeFile(join(staging, 'complete'), downloads.identity) + try { await rename(staging, destination) } catch (error) { + if (!['EEXIST', 'ENOTEMPTY', 'EPERM'].includes(error.code) + || await readFile(join(destination, 'complete'), 'utf8') !== downloads.identity) throw error + } + } finally { + try { await disposeProxy?.() } finally { await rm(staging, { recursive: true, force: true }) } + } + } + if (await readFile(join(destination, 'complete'), 'utf8') !== downloads.identity) throw new Error(`runtime cache identity mismatch: ${destination}`) + return kind === 'primary' ? join(destination, 'primary-runtime') : destination +} + +const office = process.env.DSH_OFFICE_SIDECAR ?? (cache !== undefined && existsSync(join(cache, 'office', 'complete')) ? join(cache, 'office') : undefined) +if (office !== undefined) { + if (!isAbsolute(office)) throw new Error('DSH_OFFICE_SIDECAR must be an absolute directory') + const parentURL = pathToFileURL(join(office, 'package.json')).href registerHooks({ resolve(specifier, context, nextResolve) { - const office = specifier === '@deepseek-ai/libreoffice-kit' || specifier === '@deepseek-ai/libreoffice-kit/package.json' - return nextResolve(specifier, office ? { ...context, parentURL } : context) + const kit = specifier === '@deepseek-ai/libreoffice-kit' || specifier === '@deepseek-ai/libreoffice-kit/package.json' + return nextResolve(specifier, kit ? { ...context, parentURL } : context) }, }) + process.env.DSH_BUNDLED_OFFICE_NODE = join(office, 'node', 'bin', process.platform === 'win32' ? 'node.exe' : 'node') + process.env.DSH_BUNDLED_OFFICE_CLI = join(office, 'node_modules', '@deepseek-ai', 'libreoffice-kit', 'lib', 'cli.js') +} else if (isSea()) { + process.env.DSH_BUNDLED_OFFICE_CLI = '' } +if (downloads !== undefined) process.env.DSH_BUNDLED_OFFICE_SKILLS = join(resourceDirectory, 'office-skills') const selectorName = 'DSH_SUBPROCESS_RUNNER' const selection = process.env[selectorName] @@ -22,17 +80,21 @@ const aclRunner = process.platform === 'win32' ? fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-sandbox-windows-acl/runner')) : undefined -if (aclRunner !== undefined && process.argv[2] === aclRunner) { +const resource = process.env.DSH_RUNTIME_DOWNLOAD +if (resource !== undefined) { + Reflect.deleteProperty(process.env, 'DSH_RUNTIME_DOWNLOAD') + if (resource !== 'office' && resource !== 'primary') throw new Error('DSH_RUNTIME_DOWNLOAD must select office or primary') + const path = await downloadResource(resource) + console.log(JSON.stringify(path)) +} else if (aclRunner !== undefined && process.argv[2] === aclRunner) { process.argv.splice(1, 1) await import('@deepseek-ai/dsh-sandbox-windows-acl/runner') } else if (process.env.DSH_PTC_RUNTIME_NODE === '1') { Reflect.deleteProperty(process.env, 'DSH_PTC_RUNTIME_NODE') await import('@deepseek-ai/dsh-ptc-runtime-node/process') } else if (selection === undefined) { - if (isSea()) { - // Carrier default stays separate so process/home environment and profile patches can override it. - const platform = process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'win' : process.platform - process.env.DSH_BUNDLED_PRIMARY_RUNTIME = join(dirname(process.execPath), `${platform}-${process.arch}`, 'primary-runtime') + if (cache !== undefined && existsSync(join(cache, 'primary', 'complete'))) { + process.env.DSH_BUNDLED_PRIMARY_RUNTIME = join(cache, 'primary', 'primary-runtime') } const { runCli } = await import('@deepseek-ai/dsh/lib/bin.js') await runCli() diff --git a/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py b/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py index 174d629160..bd853f73b1 100644 --- a/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py +++ b/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py @@ -10,7 +10,7 @@ Two runtime carriers coexist under ``runtime/``, both injected by the repo's 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/lib/bin.js`` on a + runtime/node/runtime-bootstrap.mjs`` on a system Node >= 22.19. It is the current checkout's source build, never selected automatically, and excluded from wheel/sdist distributions. @@ -28,7 +28,7 @@ import shutil import subprocess import sys from pathlib import Path -from ._resources import validate_resources +from ._resources import validate_downloads, validate_resources, office_launch_args PACKAGE_METADATA_FILENAME = "deepseek-harness-runtime.json" @@ -59,10 +59,9 @@ def bundled_runtime_path() -> Path: """Absolute path of the bundled single-file runtime executable for the current platform. Raises FileNotFoundError when the platform is unsupported, the executable - has not been placed into this package, the ripgrep or Office sidecar is - missing, the authoring resources are incomplete, or the required macOS - spawn helper is missing. Invalid authoring metadata or Python permissions - raise ValueError. Missing executable/Office messages name + has not been placed into this package, the ripgrep sidecar or download + manifest is missing, or the required macOS spawn helper is missing. + Invalid download metadata raises ValueError. Missing executable messages name the acquisition routes (acquisition strategy is deliberately separate from this lookup interface, so an on-demand download can replace it without touching callers). @@ -92,22 +91,7 @@ def bundled_runtime_path() -> Path: f"deepseek-harness-runtime-bin is missing the node-pty spawn helper at {helper}. " + _EXE_ACQUISITION_HINT ) - office = path.with_name(f"{path.name.removesuffix('.exe')}-office") - adapter = office / "node_modules/@deepseek-ai/libreoffice-kit/package.json" - if not adapter.is_file(): - raise FileNotFoundError( - f"deepseek-harness-runtime-bin is missing the Office sidecar at {office}. " - + _EXE_ACQUISITION_HINT - ) - native = tag.replace("win-", "win32-").replace("macos-", "darwin-") - declared = json.loads(adapter.read_text(encoding="utf-8")).get("optionalDependencies", {}) - engine = native if f"@deepseek-ai/libreoffice-kit-{native}" in declared else "wasm" - if not (office / "node_modules" / f"@deepseek-ai/libreoffice-kit-{engine}/prebuilds.json").is_file(): - raise FileNotFoundError( - f"deepseek-harness-runtime-bin is missing the Office sidecar engine {engine} at {office}. " - + _EXE_ACQUISITION_HINT - ) - validate_resources(path.with_name(tag), tag) + validate_downloads(path.with_name(tag), tag) return path @@ -152,14 +136,7 @@ def _current_platform_tag() -> str: def _node_launch_args() -> tuple[str, str]: node_root = bundled_package_dir() / "runtime" / "node" - bin_js = ( - node_root - / "node_modules" - / "@deepseek-ai" - / "dsh" - / "lib" - / "bin.js" - ) + bin_js = node_root / "runtime-bootstrap.mjs" if not bin_js.is_file(): raise FileNotFoundError( f"the dev-only node runtime closure is missing at {node_root} " @@ -176,6 +153,46 @@ def _node_launch_args() -> tuple[str, str]: return (node, str(bin_js)) +def _download(resource: str) -> Path: + result = subprocess.run(resolve_bundled_launch_args(), capture_output=True, encoding="utf-8", + env={**os.environ, "DSH_RUNTIME_DOWNLOAD": resource}) + if result.returncode: + raise RuntimeError(result.stderr.strip() or f"resource download failed with exit code {result.returncode}") + path = json.loads(result.stdout) + if not isinstance(path, str) or not Path(path).is_absolute(): + raise ValueError("runtime returned an invalid resource path") + return Path(path) + + +def download_office() -> Path: + """Download the locked npm Office sidecar and standalone Node into the resource cache. + + Return its absolute directory. Reuse completed downloads; failures leave no + published partial installation. DSH_RESOURCE_CACHE selects the cache root. + This explicit operation requires network access on a cache miss, not a + Harness home, agent, or model. Other SDK operations never trigger downloads. + """ + return _download("office") + + +def download_primary_runtime() -> Path: + """Download locked CPython, libraries, Node, pnpm and skills into the resource cache. + + Return the absolute primary-runtime directory. This is independent of the + Office engine download and does not copy resources into a Harness home. + Completed downloads are reused; DSH_RESOURCE_CACHE selects the cache root. + """ + path = _download("primary") + validate_resources(path.parent, _current_platform_tag()) + return path + + +def resolve_office_launch_args() -> tuple[str, str]: + """Locate the explicitly downloaded Office CLI and its standalone Node; never download.""" + tag = _current_platform_tag() + return office_launch_args(bundled_runtime_path().with_name(tag), tag) + + def main() -> None: """Launch the CLI with explicit DSH_HOME; wait on Windows, replace the process on POSIX.""" if not os.environ.get("DSH_HOME", "").strip(): @@ -197,6 +214,9 @@ __all__ = [ "RUNTIME_MODE_ENV_VAR", "bundled_package_dir", "bundled_runtime_path", + "download_office", + "download_primary_runtime", + "resolve_office_launch_args", "main", "resolve_bundled_launch_args", ] diff --git a/python/sdk-runtime/src/deepseek_harness_runtime/_resources.py b/python/sdk-runtime/src/deepseek_harness_runtime/_resources.py index e0f2026cb4..ae547ac26c 100644 --- a/python/sdk-runtime/src/deepseek_harness_runtime/_resources.py +++ b/python/sdk-runtime/src/deepseek_harness_runtime/_resources.py @@ -1,8 +1,12 @@ -"""Validate the authoring resources in staged, installed, or archived runtime wheels.""" +"""Validate wheel download manifests, downloaded resources, and Office process launches.""" from __future__ import annotations import json +import os +import signal +import subprocess +from collections.abc import Sequence import re import stat import zipfile @@ -41,3 +45,77 @@ def validate_resources(root: Path | zipfile.Path, target: str) -> None: if isinstance(binary, zipfile.Path) else binary.stat().st_mode) if mode & stat.S_IXUSR == 0: raise ValueError(f"runtime interpreter lost its executable bit: {binary}") + + +def validate_downloads(root: Path | zipfile.Path, target: str) -> dict: + """Validate the lightweight download manifest carried by one platform wheel.""" + path = root / "downloads.json" + value = json.loads(path.read_text(encoding="utf-8")) + if (not isinstance(value, dict) or value.get("target") != target.replace("macos-", "mac-") + or not isinstance(value.get("identity"), str) + or not re.fullmatch(r"[a-f0-9]{64}", value["identity"]) + or not isinstance(value.get("office"), list) or not value["office"] + or not isinstance(value.get("pnpm"), dict)): + raise ValueError(f"invalid runtime download manifest: {path}") + for file in ["scripts/check_office.py", *(f"office-{kind}/SKILL.md" for kind in ("docx", "pptx", "xlsx"))]: + if not (root / "office-skills" / file).is_file(): + raise FileNotFoundError(f"runtime Office skill is missing: {root / 'office-skills' / file}") + return value + + +def office_launch_args(root: Path, target: str) -> tuple[str, str]: + """Require an explicitly installed sidecar, including its independent Node interpreter.""" + metadata = validate_downloads(root, target) + cache = Path(os.environ.get("DSH_RESOURCE_CACHE", str(Path.home() / ".cache/deepseek-harness/resources"))) + if not cache.is_absolute(): + raise ValueError("DSH_RESOURCE_CACHE must be an absolute directory") + office = Path(os.environ.get("DSH_OFFICE_SIDECAR", str(cache / metadata["identity"] / "office"))) + if not office.is_absolute(): + raise ValueError("DSH_OFFICE_SIDECAR must be an absolute directory") + if "DSH_OFFICE_SIDECAR" not in os.environ: + complete = office / "complete" + if not complete.is_file(): + raise FileNotFoundError(f"Office download is incomplete: {office}. Call deepseek_harness_runtime.download_office() first.") + if complete.read_text(encoding="utf-8") != metadata["identity"]: + raise ValueError(f"Office cache identity mismatch: {office}") + node = office / "node/bin" / ("node.exe" if target == "win-x64" else "node") + cli = office / "node_modules/@deepseek-ai/libreoffice-kit/lib/cli.js" + for path in (node, cli): + if not path.is_file(): + raise FileNotFoundError(f"Office resource is missing: {path}. Call deepseek_harness_runtime.download_office() first.") + return str(node), str(cli) + + +def run_office_process(arguments: Sequence[str]) -> subprocess.CompletedProcess[bytes]: + """Run Office with captured streams; a second interrupt kills its process tree.""" + taskkill = os.path.join(os.environ.get("SystemRoot", ""), "System32", "taskkill.exe") + if os.name == "nt" and not os.path.isabs(taskkill): + raise ValueError("Office operations on Windows require an absolute SystemRoot environment value.") + with subprocess.Popen( + arguments, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + start_new_session=os.name != "nt", + ) as process: + try: + stdout, stderr = process.communicate() + except KeyboardInterrupt: + # Windows console Ctrl+C already reaches children in the same console. + if os.name != "nt": + process.send_signal(signal.SIGINT) + try: + process.communicate() + except KeyboardInterrupt: + if os.name == "nt": + subprocess.run([taskkill, "/PID", str(process.pid), "/T", "/F"], + stdin=subprocess.DEVNULL, capture_output=True, check=False) + else: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError as error: + # The CLI and its children can finish before the second interrupt. + pass + process.communicate() + raise + return subprocess.CompletedProcess(arguments, process.returncode, stdout, stderr) diff --git a/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index 8ac185aaf9..0d2460d5ed 100644 --- a/python/sdk/README.i18n.yaml +++ b/python/sdk/README.i18n.yaml @@ -5,6 +5,9 @@ /deepseek-harness-python-sdk: en: 43f7de064985e731 zh: 9ad59629660b1f13 +/deepseek-harness-python-sdk/optional-office-operations: + en: ddf4dabea526cef1 + zh: 6e31cab10d2f3ed8 /deepseek-harness-python-sdk/start-a-runtime: en: 9a069c1a7cbb0779 zh: 7f625a802cd1e9ea diff --git a/python/sdk/README.md b/python/sdk/README.md index e186699fd4..78226d3d2e 100644 --- a/python/sdk/README.md +++ b/python/sdk/README.md @@ -8,6 +8,24 @@ Python subprocess SDK for driving DeepSeek Harness over newline-delimited JSON-R python -m pip install deepseek-harness-sdk ``` +## Optional Office operations + +Download the Office runtime explicitly, then call the local API without an agent, Session, model request, API key, or Harness home: + +```py +from deepseek_harness_runtime import download_office +from deepseek_harness.office import convert, recalculate, render_images + +download_office() +pdf = convert("report.docx", "report.pdf") +workbook = recalculate("forecast.xlsx", "calculated.xlsx") +preview = render_images("report.docx", "report-preview", pages=[1]) +``` + +The output file or image directory must not exist. Successful outputs belong to the caller; Kit removes failed outputs. `OfficeOptions` supplies operation limits and font preferences. Results expose typed paths, missing fonts, and image metadata. `OfficeError` retains CLI failure codes; malformed results raise `SdkProtocolError`. Recalculation does not verify business logic or Excel fidelity. Ctrl+C waits for CLI cleanup; a second interrupt forces termination and may leave partial output. Windows requires an absolute `SystemRoot` and a shared console for Ctrl+C delivery. + +The authoring environment is independently optional through `deepseek_harness_runtime.download_primary_runtime()`. See the [runtime reference](../sdk-runtime/README.md) for cache selection, resource activation, and download behavior. SDK startup and Office operations never acquire missing resources automatically. + ## Start a runtime 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. diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index 9176c7d17b..c288da9be1 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -8,6 +8,24 @@ python -m pip install deepseek-harness-sdk ``` +## 可选 Office 操作 + +显式下载 Office 运行时后,即可调用本地 API,无需 agent、Session、模型请求、API key 或 Harness home: + +```py +from deepseek_harness_runtime import download_office +from deepseek_harness.office import convert, recalculate, render_images + +download_office() +pdf = convert("report.docx", "report.pdf") +workbook = recalculate("forecast.xlsx", "calculated.xlsx") +preview = render_images("report.docx", "report-preview", pages=[1]) +``` + +输出文件或图片目录必须不存在。调用方拥有成功输出;Kit 删除失败输出。`OfficeOptions` 提供操作限制和字体偏好。结果包含类型化路径、缺失字体和图片元数据。`OfficeError` 保留 CLI 失败代码;无效结果抛出 `SdkProtocolError`。重算不验证业务逻辑或 Excel 兼容性。Ctrl+C 会等待 CLI 清理;再次中断会强制终止,可能留下未完成输出。Windows 要求绝对 `SystemRoot` 路径,并通过共享控制台传递 Ctrl+C。 + +创作环境可通过 `deepseek_harness_runtime.download_primary_runtime()` 独立选择。缓存选择、资源启用与下载行为见[运行时参考](../sdk-runtime/README.zh.md)。SDK 启动和 Office 操作都不会自动获取缺失资源。 + ## 启动运行时 Python SDK 没有独立的应用入口。它以 `--profile sdk` 启动内置的 `dsh` CLI(命令行界面);所选 profile 负责 JSON-RPC 服务器、agent(智能体)组合、凭据、持久化、工具和关闭流程。 diff --git a/python/sdk/src/deepseek_harness/office.py b/python/sdk/src/deepseek_harness/office.py new file mode 100644 index 0000000000..9e2dab604d --- /dev/null +++ b/python/sdk/src/deepseek_harness/office.py @@ -0,0 +1,219 @@ +"""Local Office operations through the runtime wheel's dsoffice CLI. + +Calls are synchronous and do not create agents, sessions, or model requests. +The CLI owns conversion deadlines, engine termination, and failed-output cleanup. +Console Ctrl+C waits for CLI cleanup before raising KeyboardInterrupt; a second +interrupt forces termination and can leave partial output. On Windows, callers +must share the CLI's console for Ctrl+C delivery. +""" + +from __future__ import annotations + +import json +import os +from collections.abc import Sequence +from dataclasses import dataclass, fields +from pathlib import Path +from typing import Literal, TypeVar + +import deepseek_harness_runtime +from deepseek_harness_runtime._resources import run_office_process +from pydantic import BaseModel, ConfigDict, Field, ValidationError + +from .errors import HarnessError, SdkProtocolError + + +class OfficeError(HarnessError): + """A failed Office operation, including its CLI error code and exit status.""" + + def __init__(self, code: str, message: str, exit_code: int) -> None: + super().__init__(message) + self.code = code + self.exit_code = exit_code + + +class _OfficeModel(BaseModel): + model_config = ConfigDict(strict=True, frozen=True) + + +class ConversionResult(_OfficeModel): + """Saved conversion or recalculation output and missing declared fonts.""" + + backend: Literal["native", "wasm"] + output_path: Path = Field(alias="outputPath") + missing_fonts: list[str] = Field(alias="missingFonts") + + +class ImageRectangle(_OfficeModel): + """Source rectangle in CSS pixels at 96 DPI, before raster scaling.""" + + x: float + y: float + width: float + height: float + + +class RenderedImage(_OfficeModel): + """One saved PNG and its source page, worksheet, or worksheet fragment.""" + + index: int + path: Path + width: int + height: int + rectangle: ImageRectangle + byte_length: int = Field(alias="byteLength") + page: int | None = None + sheet: str | None = None + range: str | None = None + rtl: bool | None = None + + +class RenderImagesResult(_OfficeModel): + """Image manifest for one saved input; page_count includes unselected pages.""" + + schema_version: Literal[1] = Field(alias="schemaVersion") + backend: Literal["native", "wasm"] + raster_engine: Literal["libreoffice", "pdfium"] = Field(alias="rasterEngine") + source: Literal["saved"] + input_path: Path = Field(alias="inputPath") + source_sha256: str = Field(alias="sourceSha256") + dpi: float + page_count: int = Field(alias="pageCount") + images: list[RenderedImage] + missing_fonts: list[str] = Field(alias="missingFonts") + + +@dataclass(frozen=True, slots=True) +class OfficeOptions: + """Kit limits and fonts; omitted values retain the installed kit defaults. + + timeout_ms bounds the engine operation, including font work and output, + after explicit resource download. The call waits for engine exit and cleanup. + Font paths may be relative to cwd; fallback groups replace the kit defaults. + """ + + timeout_ms: int | None = None + max_input_bytes: int | None = None + max_output_bytes: int | None = None + max_image_resolution: int | None = None + max_archive_entries: int | None = None + max_uncompressed_bytes: int | None = None + max_font_files: int | None = None + max_font_file_bytes: int | None = None + max_loaded_font_bytes: int | None = None + font_directories: Sequence[str | os.PathLike[str]] | None = None + initial_font_families: Sequence[str] | None = None + font_fallbacks: Sequence[Sequence[str]] | None = None + + def _arguments(self) -> list[str]: + arguments: list[str] = [] + for field in fields(self): + value = getattr(self, field.name) + if value is None: + continue + if field.name == "font_directories": + for path in value: + arguments.extend(("--font-directory", os.fspath(path))) + elif field.name == "initial_font_families": + for family in value: + arguments.extend(("--initial-font-family", family)) + elif field.name == "font_fallbacks": + arguments.extend(("--font-fallbacks", json.dumps(value))) + else: + arguments.extend(("--" + field.name.replace("_", "-"), str(value))) + return arguments + + +class _CliFailure(_OfficeModel): + code: str + error: str + + +_Result = TypeVar("_Result", bound=_OfficeModel) + + +def _run(arguments: list[str], options: OfficeOptions | None, result_type: type[_Result]) -> _Result: + process = run_office_process( + [*deepseek_harness_runtime.resolve_office_launch_args(), *arguments, + *(options._arguments() if options is not None else [])], + ) + stdout, stderr = process.stdout, process.stderr + if process.returncode != 0: + try: + failure = _CliFailure.model_validate_json(stderr) + except ValidationError: + raise OfficeError("failed", stderr.decode("utf-8", errors="replace").strip() + or "Office CLI exited without a diagnostic.", + process.returncode) from None + raise OfficeError(failure.code, failure.error, process.returncode) + try: + return result_type.model_validate_json(stdout) + except ValidationError as error: + raise SdkProtocolError("Office CLI returned an invalid result.") from error + + +def render_images( + input_path: str | os.PathLike[str], + output_dir: str | os.PathLike[str], + *, + pages: Literal["all"] | Sequence[int] | None = None, + sheet: str | None = None, + range: str | None = None, + dpi: int | None = None, + max_pages: int | None = None, + max_pixels: int | None = None, + max_dimension: int | None = None, + options: OfficeOptions | None = None, +) -> RenderImagesResult: + """Render Office or PDF to PNG files and manifest.json in a fresh directory. + + Pages are one-based document pages or slides. Worksheets use an exact + sheet name and optional A1 range instead. Paths may be relative to cwd. + The caller owns successful output; failed batches remove the new directory. + Raises OfficeError for CLI failures and SdkProtocolError for invalid JSON. + """ + arguments = ["render", "--input", os.fspath(input_path), "--output-dir", os.fspath(output_dir)] + selections = { + "pages": pages if isinstance(pages, str) or pages is None else ",".join(map(str, pages)), + "sheet": sheet, "range": range, "dpi": dpi, "max-pages": max_pages, + "max-pixels": max_pixels, "max-dimension": max_dimension, + } + for name, value in selections.items(): + if value is not None: + arguments.extend(("--" + name, str(value))) + return _run(arguments, options, RenderImagesResult) + + +def convert( + input_path: str | os.PathLike[str], + output_path: str | os.PathLike[str], + *, + sheet: str | None = None, + options: OfficeOptions | None = None, +) -> ConversionResult: + """Export Office to the format named by a fresh output file's extension. + + Paths may be relative to cwd. CSV export requires an exact sheet name for + multi-sheet input. Existing output is rejected; failed output is removed. + Raises OfficeError for CLI failures and SdkProtocolError for invalid JSON. + """ + arguments = ["convert", "--input", os.fspath(input_path), "--output", os.fspath(output_path)] + if sheet is not None: + arguments.extend(("--sheet", sheet)) + return _run(arguments, options, ConversionResult) + + +def recalculate( + input_path: str | os.PathLike[str], + output_path: str | os.PathLike[str], + *, + options: OfficeOptions | None = None, +) -> ConversionResult: + """Refresh workbook formula caches into a distinct, fresh XLSX or ODS file. + + Input accepts XLS, XLSX, or ODS. Paths may be relative to cwd. Formulas + survive, but recalculation does not verify business logic or Excel fidelity. + Raises OfficeError for CLI failures and SdkProtocolError for invalid JSON. + """ + return _run(["recalculate", "--input", os.fspath(input_path), "--output", os.fspath(output_path)], + options, ConversionResult) diff --git a/python/sdk/tests/fixtures/office_cli.py b/python/sdk/tests/fixtures/office_cli.py new file mode 100644 index 0000000000..51e999a907 --- /dev/null +++ b/python/sdk/tests/fixtures/office_cli.py @@ -0,0 +1,48 @@ +"""Office JSON subprocess peer; engine behavior belongs to the real kit smoke.""" + +import json +import sys +from pathlib import Path + +receipt = Path(sys.argv[1]) +arguments = sys.argv[2:] +receipt.write_text(json.dumps(arguments), encoding="utf-8") +command = arguments[0] +values = dict(zip(arguments[1::2], arguments[2::2])) +source = Path(values["--input"]) +if source.name == "error.docx": + print(json.dumps({"code": "invalid-document", "error": "Document cannot be loaded."}), file=sys.stderr) + sys.exit(1) +if source.name == "crash.docx": + print("engine process unavailable", file=sys.stderr) + sys.exit(7) +if source.name == "invalid.docx": + print(json.dumps({"backend": "native", "outputPath": 3, "missingFonts": []})) + sys.exit(0) +if source.name == "invalid-bytes.docx": + sys.stdout.buffer.write(b"\xff") + sys.exit(0) +if source.name == "timeout.docx": + output = Path(values["--output"]) + output.write_text("partial output", encoding="utf-8") + output.unlink() + print(json.dumps({"code": "timeout", "error": "Conversion timed out."}), file=sys.stderr) + sys.exit(1) +if command == "render": + output = Path(values["--output-dir"]) + output.mkdir() + image = output / "page-1.png" + image.write_bytes(b"png fixture") + result = { + "schemaVersion": 1, "backend": "native", "rasterEngine": "libreoffice", "source": "saved", + "inputPath": str(source.resolve()), "sourceSha256": "abc123", "dpi": 144, "pageCount": 3, + "images": [{"index": 0, "page": 1, "path": str(image.resolve()), "width": 64, "height": 48, + "rectangle": {"x": 0, "y": 0, "width": 32, "height": 24}, "byteLength": 11}], + "missingFonts": ["Fixture Sans"], + } + (output / "manifest.json").write_text(json.dumps(result), encoding="utf-8") +else: + output = Path(values["--output"]) + output.write_text("converted fixture", encoding="utf-8") + result = {"backend": "wasm", "outputPath": str(output.resolve()), "missingFonts": []} +print(json.dumps(result)) diff --git a/python/sdk/tests/test_office.py b/python/sdk/tests/test_office.py new file mode 100644 index 0000000000..e4f5039436 --- /dev/null +++ b/python/sdk/tests/test_office.py @@ -0,0 +1,101 @@ +"""Exercise Office argv and JSON through an actual subprocess without a model.""" + +from __future__ import annotations + +import json +import sys +from collections.abc import Callable +from pathlib import Path + +import deepseek_harness_runtime +import pytest + +from deepseek_harness.errors import SdkProtocolError +from deepseek_harness.office import ConversionResult, OfficeError, OfficeOptions, convert, recalculate, render_images + + +@pytest.fixture +def office_cli(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + receipt = tmp_path / "arguments.json" + script = Path(__file__).parent / "fixtures" / "office_cli.py" + monkeypatch.setattr(deepseek_harness_runtime, "resolve_office_launch_args", + lambda: (sys.executable, str(script), str(receipt))) + return receipt + + +@pytest.mark.parametrize("operation", [convert, recalculate]) +def test_saved_output_and_literal_path_arguments( + operation: Callable[..., ConversionResult], office_cli: Path, tmp_path: Path, +) -> None: + source = tmp_path / "工作簿 $name with spaces.xlsx" + output = tmp_path / "saved workbook.xlsx" + result = operation(source, output) + assert result.output_path == output + assert output.read_text() == "converted fixture" + assert result.backend == "wasm" + assert result.missing_fonts == [] + assert json.loads(office_cli.read_text()) == [operation.__name__, "--input", str(source), "--output", str(output)] + + +def test_render_images_reads_the_saved_manifest(office_cli: Path, tmp_path: Path) -> None: + output = tmp_path / "preview" + result = render_images(tmp_path / "deck.pptx", output, pages=[1, 3], dpi=144, max_pages=10, + max_pixels=1_000_000, max_dimension=4096) + assert result.page_count == 3 + assert result.images[0].path.read_bytes() == b"png fixture" + assert result.images[0].rectangle.width == 32 + assert result.images[0].byte_length == 11 + assert result.missing_fonts == ["Fixture Sans"] + assert result.input_path == tmp_path / "deck.pptx" + assert json.loads(office_cli.read_text())[5:] == [ + "--pages", "1,3", "--dpi", "144", "--max-pages", "10", "--max-pixels", "1000000", "--max-dimension", "4096", + ] + + +def test_worksheet_selection_and_csv_sheet_keep_exact_names(office_cli: Path, tmp_path: Path) -> None: + sheet = "收入 $Values" + render_images("workbook.xlsx", tmp_path / "preview", sheet=sheet, range="A1:E9") + assert json.loads(office_cli.read_text())[5:] == ["--sheet", sheet, "--range", "A1:E9"] + convert("workbook.xlsx", tmp_path / "data.csv", sheet=sheet) + assert json.loads(office_cli.read_text())[5:] == ["--sheet", sheet] + + +def test_limits_and_fonts_reach_the_cli(office_cli: Path, tmp_path: Path) -> None: + convert("report.docx", tmp_path / "report.pdf", options=OfficeOptions( + timeout_ms=4567, max_input_bytes=1024, max_output_bytes=2048, max_image_resolution=72, + max_archive_entries=10, max_uncompressed_bytes=4096, max_font_files=20, + max_font_file_bytes=8192, max_loaded_font_bytes=16384, + font_directories=[tmp_path / "Fonts One", tmp_path / "Fonts Two"], + initial_font_families=["Fixture Sans", "中文字体"], font_fallbacks=[["Preferred", "Fallback"]], + )) + assert json.loads(office_cli.read_text())[5:] == [ + "--timeout-ms", "4567", "--max-input-bytes", "1024", "--max-output-bytes", "2048", + "--max-image-resolution", "72", "--max-archive-entries", "10", "--max-uncompressed-bytes", "4096", + "--max-font-files", "20", "--max-font-file-bytes", "8192", "--max-loaded-font-bytes", "16384", + "--font-directory", str(tmp_path / "Fonts One"), "--font-directory", str(tmp_path / "Fonts Two"), + "--initial-font-family", "Fixture Sans", "--initial-font-family", "中文字体", + "--font-fallbacks", '[["Preferred", "Fallback"]]', + ] + + +@pytest.mark.parametrize(("filename", "code", "exit_code", "message"), [ + ("error.docx", "invalid-document", 1, "Document cannot be loaded"), + ("crash.docx", "failed", 7, "engine process unavailable"), + ("timeout.docx", "timeout", 1, "Conversion timed out"), +]) +def test_cli_failures_preserve_diagnostics_after_exit( + office_cli: Path, tmp_path: Path, filename: str, code: str, exit_code: int, message: str, +) -> None: + output = tmp_path / "result.pdf" + with pytest.raises(OfficeError, match=message) as failure: + convert(filename, output, options=OfficeOptions(timeout_ms=100)) + assert failure.value.code == code + assert failure.value.exit_code == exit_code + assert not output.exists() + assert json.loads(office_cli.read_text())[-2:] == ["--timeout-ms", "100"] + + +@pytest.mark.parametrize("filename", ["invalid.docx", "invalid-bytes.docx"]) +def test_invalid_cli_result_fails_at_the_json_reader(office_cli: Path, tmp_path: Path, filename: str) -> None: + with pytest.raises(SdkProtocolError, match="Office CLI returned an invalid result"): + convert(filename, tmp_path / "result.pdf") diff --git a/python/sdk/tests/test_office_interrupts.py b/python/sdk/tests/test_office_interrupts.py new file mode 100644 index 0000000000..2cd4dbd612 --- /dev/null +++ b/python/sdk/tests/test_office_interrupts.py @@ -0,0 +1,106 @@ +"""Office interruption terminates the owned process tree before returning to callers.""" +from __future__ import annotations + +import ntpath +import os +import signal +import subprocess +import sys +import time +from pathlib import Path +from types import SimpleNamespace + +import pytest +from deepseek_harness_runtime import _resources + + +def wait_for_file(path: Path, process: subprocess.Popen, timeout: float = 30) -> None: + deadline = time.monotonic() + timeout + while not path.is_file(): + if process.poll() is not None: + raise AssertionError(f"fixture exited before {path.name}: {process.returncode}") + if time.monotonic() >= deadline: + raise TimeoutError(f"fixture never produced {path}") + time.sleep(0.01) + + +@pytest.mark.skipif(os.name == "nt", reason="POSIX process groups and SIGINT delivery") +def test_second_interrupt_waits_for_the_entire_office_process_tree(tmp_path: Path) -> None: + fixture = tmp_path / "office.py" + ready, interrupted, pidfile = (tmp_path / name for name in ("ready", "interrupted", "pid")) + fixture.write_text("""import json,os,signal,subprocess,sys +from pathlib import Path +root=Path(sys.argv[1]) +(root/'pid').write_text(str(os.getpid())) +signal.signal(signal.SIGINT,lambda *_:(root/'interrupted').touch()) +child=subprocess.Popen([sys.executable,'-c', + "import signal,sys; from pathlib import Path; signal.signal(signal.SIGINT,signal.SIG_IGN); Path(sys.argv[1]).touch(); signal.pause()",str(root/'child-ready')]) +(root/'ready').touch() +child.wait() +""", encoding="utf-8") + code = ("import sys; from deepseek_harness_runtime._resources import run_office_process; " + f"args=[sys.executable,{str(fixture)!r},{str(tmp_path)!r}]\n" + "try: run_office_process(args)\n" + "except KeyboardInterrupt: print('tree terminated',flush=True); sys.exit(73)\n") + process = subprocess.Popen([sys.executable, "-c", code], stdout=subprocess.PIPE, stderr=subprocess.PIPE, + start_new_session=True, env={**os.environ, + "PYTHONPATH": str(Path(_resources.__file__).resolve().parents[1])}) + try: + wait_for_file(ready, process) + wait_for_file(tmp_path / "child-ready", process) + os.kill(process.pid, signal.SIGINT) + wait_for_file(interrupted, process) + os.kill(process.pid, signal.SIGINT) + # The grandchild inherits the captured streams; communicate can finish only after it closes them. + stdout, stderr = process.communicate(timeout=30) + assert process.returncode == 73, stderr + assert stdout == b"tree terminated\n" + finally: + if pidfile.is_file(): + try: + os.killpg(int(pidfile.read_text()), signal.SIGKILL) + except ProcessLookupError as error: + # The successful interruption path already terminated this group. + pass + if process.poll() is None: + process.kill() + process.communicate(timeout=30) + + +def test_windows_second_interrupt_uses_absolute_taskkill_and_waits(monkeypatch: pytest.MonkeyPatch) -> None: + calls = [] + + class Process: + pid = 73 + returncode = -9 + attempts = 0 + + def __enter__(self): + return self + + def __exit__(self, *_): + pass + + def communicate(self): + self.attempts += 1 + if self.attempts < 3: + raise KeyboardInterrupt + return b"", b"" + + process = Process() + + def popen(arguments, **options): + assert options == {"stdin": subprocess.DEVNULL, "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, "start_new_session": False} + return process + + monkeypatch.setattr(_resources, "os", SimpleNamespace(name="nt", path=ntpath, environ={"SystemRoot": r"C:\Windows"})) + monkeypatch.setattr(_resources, "subprocess", SimpleNamespace( + Popen=popen, DEVNULL=subprocess.DEVNULL, PIPE=subprocess.PIPE, + run=lambda arguments, **options: calls.append((arguments, options)), + )) + with pytest.raises(KeyboardInterrupt): + _resources.run_office_process(["node.exe", "cli.js"]) + assert process.attempts == 3 + assert calls == [([r"C:\Windows\System32\taskkill.exe", "/PID", "73", "/T", "/F"], + {"stdin": subprocess.DEVNULL, "capture_output": True, "check": False})] diff --git a/python/sdk/tests/test_release_version.py b/python/sdk/tests/test_release_version.py index b6ddb572e3..77824c1f72 100644 --- a/python/sdk/tests/test_release_version.py +++ b/python/sdk/tests/test_release_version.py @@ -145,7 +145,7 @@ def test_stage_runtime_copies_platform_payload( spawn_helper.write_bytes(b"helper") spawn_helper.chmod(0o755) expected[spawn_helper.name] = b"helper" - office = executable.parent / build_python_release.office_sidecar_name(executable.name) + office = executable.parent / f"{executable.name.removesuffix('.exe')}-office" office_asset = office / "node_modules" / "@deepseek-ai" / "libreoffice-kit-wasm" / "assets" / "soffice.data" office_asset.parent.mkdir(parents=True) office_asset.write_bytes(b"office data") @@ -163,7 +163,7 @@ def test_stage_runtime_copies_platform_payload( for path in runtime_dir.glob("deepseek-harness-sdk-runtime-*") if path.is_file() } == expected - assert (runtime_dir / office.name / office_asset.relative_to(office)).read_bytes() == b"office data" + assert not (runtime_dir / office.name).exists() assert (runtime_dir / resources.name / resource.relative_to(resources)).read_text() == "checker" pyproject = (destination / "pyproject.toml").read_text() assert 'license = "MIT"' in pyproject @@ -191,43 +191,9 @@ def test_stage_runtime_rejects_a_noncanonical_executable_name(tmp_path: Path) -> ) -@pytest.mark.parametrize("platform_tag,selected", [ - ("manylinux_2_28_x86_64", "wasm"), ("macosx_14_0_arm64", "darwin-arm64"), - ("macosx_14_0_x86_64", "darwin-x64"), ("win_amd64", "win32-x64"), - ("manylinux_2_28_x86_64", "linux-x64"), ("macosx_14_0_arm64", "wasm"), -]) -@pytest.mark.parametrize("invalid", [None, "asset", "missing-engine", "foreign-engine", "helper-mode"]) -def test_office_wheel_requires_only_target_engine( - tmp_path: Path, platform_tag: str, selected: str, invalid: str | None, -) -> None: - root = "runtime-office/node_modules" - wheel = tmp_path / "office.zip" - native = selected != "wasm" - if invalid == "helper-mode" and (not native or platform_tag == "win_amd64"): - pytest.skip("executable mode applies to POSIX native helpers") - engine = ({"kind": "native", "executable": "bin/helper"} if native else - {"kind": "wasm", "loader": "loader.cjs", "wasm": "engine.wasm", "data": "engine.data", "metadata": "fonts.json"}) - with zipfile.ZipFile(wheel, "w") as archive: - archive.writestr(f"{root}/@deepseek-ai/libreoffice-kit/package.json", json.dumps({ - "optionalDependencies": {f"@deepseek-ai/libreoffice-kit-{selected}": "0.0.1"}, - })) - base = f"{root}/@deepseek-ai/libreoffice-kit-{selected}" - if invalid != "missing-engine": - archive.writestr(f"{base}/prebuilds.json", json.dumps({"engine": engine})) - for field in (("executable",) if native else ("loader", "wasm", "data", "metadata")): - if invalid == "asset" and field == ("executable" if native else "data"): - continue - asset = zipfile.ZipInfo(f"{base}/{engine[field]}") - asset.external_attr = (0o644 if invalid == "helper-mode" else 0o755) << 16 - archive.writestr(asset, b"payload") - if invalid == "foreign-engine": - foreign = "wasm" if native else "darwin-arm64" - archive.writestr(f"{root}/@deepseek-ai/libreoffice-kit-{foreign}/prebuilds.json", "{}") - with zipfile.ZipFile(wheel) as archive: - if invalid is None: - build_python_release.verify_office_payload(archive, root, platform_tag) - else: - message = {"asset": "asset is missing", "missing-engine": "dependency is missing", - "foreign-engine": "Unexpected Office engine", "helper-mode": "executable bit"}[invalid] - with pytest.raises(RuntimeError, match=message): - build_python_release.verify_office_payload(archive, root, platform_tag) +def test_wheel_verification_rejects_oversized_artifacts(tmp_path: Path) -> None: + wheel = tmp_path / "oversized.whl" + with wheel.open("wb") as output: + output.truncate(100_000_001) + with pytest.raises(RuntimeError, match="PyPI single-file limit"): + build_python_release.verify_wheel(wheel, "runtime", "1.2.3", ("win_amd64", "runtime.exe")) diff --git a/python/sdk/tests/test_runtime_resolution.py b/python/sdk/tests/test_runtime_resolution.py index 6cf7edf742..e1e094c583 100644 --- a/python/sdk/tests/test_runtime_resolution.py +++ b/python/sdk/tests/test_runtime_resolution.py @@ -5,6 +5,7 @@ from __future__ import annotations import json import os import subprocess +import shutil import sys import zipfile from pathlib import Path @@ -36,6 +37,11 @@ def _resource_sidecars(executable: Path, native_targets: tuple[str, ...] = ("dar f"@deepseek-ai/libreoffice-kit-{target}": "0.0.1" for target in (*native_targets, "wasm") }}), encoding="utf-8") resources = executable.with_name(tag) + resources.mkdir(parents=True, exist_ok=True) + (resources / "downloads.json").write_text(json.dumps({ + "target": tag.replace("macos-", "mac-"), "identity": "a" * 64, + "office": [{"name": "fixture"}], "pnpm": {}, + })) manifest = resources / "primary-runtime/runtime.json" manifest.parent.mkdir(parents=True) manifest.write_text(json.dumps({"platform": native.rsplit("-", 1)[0], "arch": tag.rsplit("-", 1)[1], @@ -195,19 +201,20 @@ def test_runtime_requires_complete_resource_sidecars( monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path) monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64") - with pytest.raises(FileNotFoundError, match="Office sidecar"): + with pytest.raises(FileNotFoundError, match="downloads.json"): runtime.bundled_runtime_path() office = _resource_sidecars(executable) assert runtime.bundled_runtime_path() == executable (office / "node_modules/@deepseek-ai/libreoffice-kit-wasm/prebuilds.json").unlink() - with pytest.raises(FileNotFoundError, match="Office sidecar"): - runtime.bundled_runtime_path() + assert runtime.bundled_runtime_path() == executable + shutil.rmtree(executable.with_name("linux-x64") / "primary-runtime") + assert runtime.bundled_runtime_path() == executable 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 = tmp_path / "runtime" / "node" / "runtime-bootstrap.mjs" bin_js.parent.mkdir(parents=True) bin_js.touch() monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path) @@ -303,7 +310,7 @@ def test_windows_console_branch_preserves_real_child_io_and_completion(tmp_path: ("macos-arm64", ("darwin-arm64",)), ("macos-x64", ("darwin-x64",)), ("win-x64", ("win32-x64",)), ("linux-x64", ("linux-x64",)), ("macos-arm64", ()), ]) -def test_runtime_requires_its_platform_office_engine( +def test_runtime_starts_independently_of_office_engine( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, target: str, native_targets: tuple[str, ...], ) -> None: extension = ".exe" if target.startswith("win-") else "" @@ -322,5 +329,102 @@ def test_runtime_requires_its_platform_office_engine( foreign = office / "node_modules/@deepseek-ai" / ("libreoffice-kit-darwin-arm64" if engine.parent.name == "libreoffice-kit-wasm" else "libreoffice-kit-wasm") / "prebuilds.json" foreign.parent.mkdir(parents=True, exist_ok=True) foreign.write_text("{}") - with pytest.raises(FileNotFoundError, match="Office sidecar"): - runtime.bundled_runtime_path() + assert runtime.bundled_runtime_path() == executable + + +@pytest.mark.parametrize("function,resource", [ + (runtime.download_office, "office"), + (runtime.download_primary_runtime, "primary"), +]) +def test_explicit_download_uses_runtime_without_a_harness_home( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, function, resource: str, +) -> None: + root = tmp_path / "中文缓存" + root.mkdir() + _resource_sidecars(root / "deepseek-harness-sdk-runtime-linux-x64") + monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64") + script = tmp_path / "download.py" + destination = root / "linux-x64/primary-runtime" if resource == "primary" else root + script.write_text("import json,os,sys\n" + f"assert os.environ['DSH_RUNTIME_DOWNLOAD'] == {resource!r}\n" + + f"sys.stdout.buffer.write(json.dumps({str(destination)!r}, ensure_ascii=False).encode('utf-8'))\n", encoding="utf-8") + monkeypatch.delenv("DSH_HOME", raising=False) + monkeypatch.setattr(runtime, "resolve_bundled_launch_args", lambda: (sys.executable, str(script))) + assert function() == destination + script.write_text("import sys\nsys.stderr.buffer.write('下载失败'.encode('utf-8'))\nsys.exit(2)\n", encoding="utf-8") + with pytest.raises(RuntimeError, match="下载失败"): + function() + + +@pytest.mark.skipif(os.name == "nt", reason="LC_ALL=C is a POSIX locale fixture") +def test_download_decodes_utf8_in_an_ascii_locale(tmp_path: Path) -> None: + script = tmp_path / "download.py" + destination = tmp_path / "中文缓存" + script.write_text("import sys\nsys.stdout.buffer.write(" + json.dumps(str(destination), ensure_ascii=False).encode('utf-8').__repr__() + ")\n", encoding="utf-8") + code = ("import sys,locale,deepseek_harness_runtime as r; " + "assert locale.getpreferredencoding(False).lower() not in ('utf-8','utf8'); " + f"r.resolve_bundled_launch_args=lambda:(sys.executable,{str(script)!a}); " + f"assert str(r.download_office())=={str(destination)!a}") + result = subprocess.run([sys.executable, "-c", code], capture_output=True, encoding="utf-8", env={ + **os.environ, "LC_ALL": "C", "PYTHONUTF8": "0", "PYTHONCOERCECLOCALE": "0", + "PYTHONPATH": str(Path(runtime.__file__).resolve().parents[1]), + }, timeout=30) + assert result.returncode == 0, result.stderr + + +@pytest.mark.parametrize("missing", ["primary-runtime/runtime.json", "primary-runtime/dependencies/python/bin/python3", + "primary-runtime/dependencies/node/bin/node", "office-skills/scripts/check_office.py"]) +def test_primary_download_rejects_an_incomplete_payload(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, missing: str) -> None: + _resource_sidecars(tmp_path / "deepseek-harness-sdk-runtime-linux-x64") + root = tmp_path / "linux-x64" + (root / missing).unlink() + monkeypatch.setattr(runtime, "_download", lambda resource: root / "primary-runtime") + monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64") + with pytest.raises(FileNotFoundError): + runtime.download_primary_runtime() + + +def test_office_resolution_requires_explicit_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + from deepseek_harness_runtime._resources import office_launch_args + + executable = tmp_path / "deepseek-harness-sdk-runtime-linux-x64" + _resource_sidecars(executable) + cache = tmp_path / "cache" + monkeypatch.setenv("DSH_RESOURCE_CACHE", str(cache)) + monkeypatch.delenv("DSH_OFFICE_SIDECAR", raising=False) + with pytest.raises(FileNotFoundError, match=r"download_office\(\)"): + office_launch_args(tmp_path / "linux-x64", "linux-x64") + office = cache / ("a" * 64) / "office" + node = office / "node/bin/node" + cli = office / "node_modules/@deepseek-ai/libreoffice-kit/lib/cli.js" + for path in (node, cli): + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() + with pytest.raises(FileNotFoundError, match="incomplete"): + office_launch_args(tmp_path / "linux-x64", "linux-x64") + (office / "complete").write_text("wrong", encoding="utf-8") + with pytest.raises(ValueError, match="identity mismatch"): + office_launch_args(tmp_path / "linux-x64", "linux-x64") + (office / "complete").write_text("a" * 64, encoding="utf-8") + assert office_launch_args(tmp_path / "linux-x64", "linux-x64") == (str(node), str(cli)) + (office / "complete").unlink() + monkeypatch.setenv("DSH_OFFICE_SIDECAR", str(office)) + assert office_launch_args(tmp_path / "linux-x64", "linux-x64") == (str(node), str(cli)) + + +@pytest.mark.parametrize("invalid", ["target", "identity", "office", "pnpm", "skills"]) +def test_download_manifest_rejects_invalid_wheel_resources(tmp_path: Path, invalid: str) -> None: + from deepseek_harness_runtime._resources import validate_downloads + + _resource_sidecars(tmp_path / "deepseek-harness-sdk-runtime-linux-x64") + root = tmp_path / "linux-x64" + path = root / "downloads.json" + manifest = json.loads(path.read_text(encoding="utf-8")) + if invalid == "skills": + (root / "office-skills/scripts/check_office.py").unlink() + with pytest.raises(FileNotFoundError, match="Office skill"): + validate_downloads(root, "linux-x64") + else: + manifest[invalid] = {"target": "win-x64", "identity": "z" * 64, "office": [], "pnpm": []}[invalid] + path.write_text(json.dumps(manifest), encoding="utf-8") + with pytest.raises(ValueError, match="invalid runtime download manifest"): + validate_downloads(root, "linux-x64") diff --git a/scripts/build-exe-for-python-sdk-office.spec.ts b/scripts/build-exe-for-python-sdk-office.spec.ts index d664a0ffbc..64c61687ee 100644 --- a/scripts/build-exe-for-python-sdk-office.spec.ts +++ b/scripts/build-exe-for-python-sdk-office.spec.ts @@ -1,8 +1,12 @@ -import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' -import { dirname, join } from 'node:path' +import { join } from 'node:path' import { afterEach, expect, it } from 'vitest' -import { copyOfficeSidecar } from './build-exe-for-python-sdk-office.ts' +import { createHash } from 'node:crypto' +import { c as tar } from 'tar' +import { downloadOfficeSidecar, runtimeNpmArchive } from './build-exe-for-python-sdk-office.ts' + +import { officePackageDirectories } from './libreoffice-packages.mjs' const temporaryDirectories: string[] = [] @@ -24,33 +28,8 @@ async function fixture() { return { root, staging, destination, packageAt } } -it('copies package-owned data, licenses, helper permissions, and nested dependencies', async () => { - const { staging, destination, packageAt } = await fixture() - const entry = await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1' }, dependencies: { decoder: '1' } }) - const engine = await packageAt('@deepseek-ai/libreoffice-kit-wasm') - const decoder = await packageAt('decoder', { dependencies: { codec: '1' } }, entry) - await packageAt('codec', {}, decoder) - await packageAt('codec', { version: '2.0.0' }) - const assets = ['assets/soffice.data', 'prebuilds.json', 'licenses/LICENSE', 'bin/helper'] - for (const name of assets) { - const path = join(engine, name) - await mkdir(dirname(path), { recursive: true }) - await writeFile(path, name) - } - await chmod(join(engine, 'bin/helper'), 0o755) - - const packages = await copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }) - - expect(packages).toHaveLength(4) - expect(await readFile(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit-wasm/assets/soffice.data'), 'utf8')).toBe('assets/soffice.data') - expect(await readFile(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit-wasm/licenses/LICENSE'), 'utf8')).toBe('licenses/LICENSE') - expect(await readFile(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit/node_modules/decoder/node_modules/codec/package.json'), 'utf8')).toContain('codec') - await expect(stat(join(destination, 'node_modules/codec'))).rejects.toMatchObject({ code: 'ENOENT' }) - if (process.platform !== 'win32') expect((await stat(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit-wasm/bin/helper'))).mode & 0o111).toBe(0o111) -}) - -it('copies installed target optionals and leaves other platforms and absent optionals out', async () => { - const { staging, destination, packageAt } = await fixture() +it('locates installed target optionals and leaves other platforms and absent optionals out', async () => { + const { staging, packageAt } = await fixture() await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { native: '1', foreign: '1', absent: '1' }, }) @@ -58,84 +37,112 @@ it('copies installed target optionals and leaves other platforms and absent opti await packageAt('native', { os: ['linux'], cpu: ['x64'] }) await packageAt('foreign', { os: ['darwin'], cpu: ['arm64'] }) - const packages = await copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }) + const packages = await officePackageDirectories(staging, { platform: 'linux', arch: 'x64' }) - expect(packages.map(path => path.replaceAll('\\', '/'))).toEqual([ + expect(packages.map(path => path.slice(staging.length + 1).replaceAll('\\', '/'))).toEqual([ 'node_modules/@deepseek-ai/libreoffice-kit', 'node_modules/@deepseek-ai/libreoffice-kit-wasm', 'node_modules/native', ]) }) -it('rejects a missing required dependency before producing a sidecar', async () => { - const { staging, destination, packageAt } = await fixture() +it('rejects a missing required dependency in the deployed closure', async () => { + const { staging, packageAt } = await fixture() await packageAt('@deepseek-ai/libreoffice-kit', { dependencies: { 'dsh-missing-office-fixture': '1' } }) - await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' })) + await expect(officePackageDirectories(staging, { platform: 'linux', arch: 'x64' })) .rejects.toThrow('dsh-missing-office-fixture required by @deepseek-ai/libreoffice-kit is missing') - await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' }) }) it('rejects an incomplete installed optional instead of omitting it', async () => { - const { staging, destination, packageAt } = await fixture() + const { staging, packageAt } = await fixture() await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { broken: '1' } }) await mkdir(join(staging, 'node_modules/broken'), { recursive: true }) - await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' })) + await expect(officePackageDirectories(staging, { platform: 'linux', arch: 'x64' })) .rejects.toMatchObject({ code: 'ENOENT' }) }) it('rejects an ancestor dependency outside the deployed closure', async () => { - const { root, staging, destination, packageAt } = await fixture() + const { root, staging, packageAt } = await fixture() await packageAt('@deepseek-ai/libreoffice-kit', { dependencies: { 'ancestor-office-fixture': '1' } }) await packageAt('ancestor-office-fixture', {}, root) - await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' })) + await expect(officePackageDirectories(staging, { platform: 'linux', arch: 'x64' })) .rejects.toThrow('outside the deployed closure') }) -it('requires the declared WASM engine inside the deployed closure', async () => { - const { root, staging, destination, packageAt } = await fixture() - await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1' } }) +it.each([true, false])('requires a staged WASM engine when declared=%s even when an ancestor has one', async (declared) => { + const { root, staging, packageAt } = await fixture() + await packageAt('@deepseek-ai/libreoffice-kit', declared ? { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1' } } : {}) await packageAt('@deepseek-ai/libreoffice-kit-wasm', {}, root) - await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' })) + await expect(officePackageDirectories(staging, { platform: 'linux', arch: 'x64' })) .rejects.toThrow('Office engine @deepseek-ai/libreoffice-kit-wasm required for linux/x64 is missing.') - await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' }) }) it.each([ ['darwin', 'arm64', 'darwin-arm64'], ['darwin', 'x64', 'darwin-x64'], ['win32', 'arm64', 'win32-arm64'], ['win32', 'x64', 'win32-x64'], ['linux', 'x64', 'linux-x64'], ['linux', 'arm64', 'wasm'], ['freebsd', 'x64', 'wasm'], -])('copies only the %s/%s engine even when other engines are staged', async (platform, arch, selected) => { - const { staging, destination, packageAt } = await fixture() +])('locates only the %s/%s engine even when other engines are staged', async (platform, arch, selected) => { + const { staging, packageAt } = await fixture() const targets = ['darwin-arm64', 'darwin-x64', 'win32-arm64', 'win32-x64', 'linux-x64', 'wasm'] const names = targets.map(target => `@deepseek-ai/libreoffice-kit-${target}`) await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: Object.fromEntries(names.map(name => [name, '0.0.1'])) }) for (const name of names) await packageAt(name) const expected = `@deepseek-ai/libreoffice-kit-${selected}` - const packages = await copyOfficeSidecar(staging, destination, { platform, arch }) - expect(packages.map(path => path.replaceAll('\\', '/'))).toEqual([ + const packages = await officePackageDirectories(staging, { platform, arch }) + expect(packages.map(path => path.slice(staging.length + 1).replaceAll('\\', '/'))).toEqual([ 'node_modules/@deepseek-ai/libreoffice-kit', `node_modules/${expected}`, ]) }) it.each(['darwin', 'win32', 'linux'])('%s requires its native package even when WASM is staged', async (platform) => { - const { staging, destination, packageAt } = await fixture() + const { staging, packageAt } = await fixture() await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1', [`@deepseek-ai/libreoffice-kit-${platform}-arm64`]: '0.0.1', } }) await packageAt('@deepseek-ai/libreoffice-kit-wasm') - await expect(copyOfficeSidecar(staging, destination, { platform, arch: 'arm64' })) + await expect(officePackageDirectories(staging, { platform, arch: 'arm64' })) .rejects.toThrow(`Office engine @deepseek-ai/libreoffice-kit-${platform}-arm64 required for ${platform}/arm64 is missing.`) - await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' }) }) -it('requires a staged Linux WASM engine even when an ancestor has one', async () => { - const { root, staging, destination, packageAt } = await fixture() - await packageAt('@deepseek-ai/libreoffice-kit') - await packageAt('@deepseek-ai/libreoffice-kit-wasm', {}, root) - await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' })) - .rejects.toThrow('Office engine @deepseek-ai/libreoffice-kit-wasm required for linux/x64 is missing.') - await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' }) +it('downloads pinned npm packages into the sidecar without resolving or executing package scripts', async () => { + const { root, destination } = await fixture() + const source = join(root, 'package') + await mkdir(source) + await writeFile(join(source, 'package.json'), JSON.stringify({ name: 'fixture', version: '1.0.0', scripts: { install: 'must-not-run' } })) + await writeFile(join(source, 'worker.js'), 'worker') + const archive = join(root, 'package.tgz') + await tar({ file: archive, cwd: root, gzip: true }, ['package']) + const bytes = await readFile(archive) + const checksum = createHash('sha512').update(bytes).digest() + const cache = join(root, 'archives') + await mkdir(cache) + await writeFile(join(cache, checksum.toString('hex')), bytes) + const artifact = { name: 'fixture', version: '1.0.0', directory: 'node_modules/fixture', + url: 'https://unused.invalid/fixture.tgz', integrity: `sha512-${checksum.toString('base64')}` } + await downloadOfficeSidecar([artifact], destination, cache) + expect(await readFile(join(destination, 'node_modules/fixture/worker.js'), 'utf8')).toBe('worker') + await expect(downloadOfficeSidecar([{ ...artifact, directory: 'node_modules/../../outside' }], destination, cache)) + .rejects.toThrow('invalid package directory') + await expect(downloadOfficeSidecar([{ ...artifact, version: '2.0.0' }], destination, cache)) + .rejects.toThrow('package identity mismatch') + await writeFile(join(cache, checksum.toString('hex')), 'corrupted archive') + await expect(downloadOfficeSidecar([artifact], destination, cache)).rejects.toThrow('checksum mismatch') +}) + + +it('takes npm integrity from the workspace lock without querying registry metadata', async () => { + const { root } = await fixture() + const lockfile = join(root, 'pnpm-lock.yaml') + const integrity = `sha512-${Buffer.alloc(64, 7).toString('base64')}` + await writeFile(lockfile, JSON.stringify({ packages: { '@scope/fixture@1.2.3': { resolution: { integrity } } } })) + expect(await runtimeNpmArchive('@scope/fixture', '1.2.3', lockfile)).toEqual({ + name: '@scope/fixture', version: '1.2.3', integrity, + url: 'https://registry.npmjs.org/@scope/fixture/-/fixture-1.2.3.tgz', + }) + await expect(runtimeNpmArchive('@scope/fixture', '1.2.4', lockfile)).rejects.toThrow('missing SHA-512 integrity') + await writeFile(lockfile, JSON.stringify({ packages: { '@scope/fixture@1.2.3': { resolution: { integrity: 'sha256-invalid' } } } })) + await expect(runtimeNpmArchive('@scope/fixture', '1.2.3', lockfile)).rejects.toThrow('missing SHA-512 integrity') }) diff --git a/scripts/build-exe-for-python-sdk-office.ts b/scripts/build-exe-for-python-sdk-office.ts index bc482c9c1f..f1ffe4bd63 100644 --- a/scripts/build-exe-for-python-sdk-office.ts +++ b/scripts/build-exe-for-python-sdk-office.ts @@ -1,6 +1,8 @@ /** Keep LibreOffice workers, prebuilt engines, and their dependencies on the real filesystem. */ -import { cp, mkdir, rm } from 'node:fs/promises' -import { join, relative, sep } from 'node:path' +import { readFile } from 'node:fs/promises' +import { isAbsolute, join, relative, sep } from 'node:path' +import { downloadRuntimeNpmPackage, type RuntimeNpmArchive } from './primary-runtime/prepare.ts' +import { load as parseYaml } from 'js-yaml' import { officePackageDirectories } from './libreoffice-packages.mjs' /** pkg applies these exclusions to dependency `files` as well as root asset globs. */ @@ -9,29 +11,53 @@ export const OFFICE_ASSET_IGNORES = [ '**/node_modules/@deepseek-ai/libreoffice-kit-*/**', ] +/** One npm package at its existing sidecar dependency location. */ +export interface OfficePackageArchive extends RuntimeNpmArchive { + /** Relative package directory inside the sidecar. */ + readonly directory: string +} + /** - * Copy the installed Office dependency tree without changing package contents or executable modes. - * Harness sidecars require the kit's declared target native engine, or WASM for other targets. - * Missing target engines fail with their package name; missing required dependencies or paths outside the deployed closure also fail. - * @param staging - Symlink-free deployed Node closure. - * @param destination - Target-specific Office directory beside the executable; replaced when present. - * @param target - Node platform and CPU of the executable. - * @returns Relative package directories included in the sidecar. + * Resolve one installed npm version against its reviewed workspace lock integrity. + * @param name - Published package name. + * @param version - Exact installed version. + * @param lockfile - Workspace lock recording the exact published archive. + * @returns Locked npm tarball identity; rejects versions without SHA-512 integrity. */ -export async function copyOfficeSidecar( - staging: string, - destination: string, - target: { platform: string; arch: string }, -): Promise { +export async function runtimeNpmArchive(name: string, version: string, lockfile = join(import.meta.dirname, '../pnpm-lock.yaml')): Promise { + const lock = parseYaml(await readFile(lockfile, 'utf8')) as { packages?: Record } + const integrity = lock.packages?.[`${name}@${version}`]?.resolution?.integrity + if (typeof integrity !== 'string' || !/^sha512-[A-Za-z0-9+/]+={0,2}$/u.test(integrity)) { + throw new Error(`runtime npm lock: missing SHA-512 integrity for ${name}@${version}`) + } + return { name, version, url: `https://registry.npmjs.org/${name}/-/${name.split('/').at(-1)}-${version}.tgz`, integrity } +} + +/** + * Lock the installed Office closure to published npm tarballs, preserving nested dependency locations. + * @param staging - Complete installed Node closure. + * @param target - Sidecar platform and CPU. + * @returns Download records for the selected engine and all dependencies. + */ +export async function officeSidecarArchives(staging: string, target: { platform: string; arch: string }): Promise { const directories = await officePackageDirectories(staging, target) - await rm(destination, { recursive: true, force: true }) - await mkdir(destination, { recursive: true }) - for (const source of directories) { - const nestedModules = join(source, 'node_modules') - await cp(source, join(destination, relative(staging, source)), { - recursive: true, - filter: path => path !== nestedModules && !path.startsWith(nestedModules + sep), - }) + return Promise.all(directories.map(async (directory) => { + const manifest = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8')) as { name: string; version: string } + return { ...await runtimeNpmArchive(manifest.name, manifest.version), directory: relative(staging, directory).split(sep).join('/') } + })) +} + +/** + * Download the complete pinned Office sidecar without npm or package lifecycle scripts. + * @param packages - Build-locked npm packages and dependency locations. + * @param destination - Empty sidecar directory. + * @param cache - Verified archive cache. + * @returns Resolves after all packages are unpacked and checked. + */ +export async function downloadOfficeSidecar(packages: readonly OfficePackageArchive[], destination: string, cache: string): Promise { + for (const artifact of packages) { + if (isAbsolute(artifact.directory) || artifact.directory.split(/[\\/]/u).some(part => part === '..') + || !artifact.directory.startsWith('node_modules/')) throw new Error(`Office download: invalid package directory ${artifact.directory}`) + await downloadRuntimeNpmPackage(artifact, join(destination, artifact.directory), cache) } - return directories.map(directory => relative(staging, directory)) } diff --git a/scripts/build-exe-for-python-sdk.spec.ts b/scripts/build-exe-for-python-sdk.spec.ts index 3e02d7e9b1..0ce2206f1e 100644 --- a/scripts/build-exe-for-python-sdk.spec.ts +++ b/scripts/build-exe-for-python-sdk.spec.ts @@ -175,7 +175,8 @@ describe('Python runtime executable builder CLI', () => { expect(result.status).toBe(0) expect(result.stdout).toContain('exec pkg') expect(result.stdout).toContain('--sea --targets node24-macos-x64') - expect(result.stdout).toContain('prepare Python and Office skills for mac-x64') + expect(result.stdout).toContain('keep only node-pty prebuilds darwin-x64') + expect(result.stdout).toContain('lock optional resource downloads and copy Office skills') expect(result.stdout).toContain(join(root, 'dist-exe', 'macos-x64')) }) diff --git a/scripts/build-exe-for-python-sdk.ts b/scripts/build-exe-for-python-sdk.ts index 1ac823d28e..d61453fa76 100644 --- a/scripts/build-exe-for-python-sdk.ts +++ b/scripts/build-exe-for-python-sdk.ts @@ -6,17 +6,20 @@ * runtime imports that pkg cannot discover statically. */ +import { createHash } from 'node:crypto' +import { build as bundle } from 'tsdown' import { spawn } from 'node:child_process' import { pnpmInvocation, restoreLegacyHoists } from './executable-packaging.ts' import { existsSync, statSync } from 'node:fs' -import { chmod, copyFile, cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' +import { chmod, copyFile, cp, glob, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises' import { basename, dirname, join, resolve, sep } from 'node:path' import { parseArgs } from 'node:util' +import { pathToFileURL } from 'node:url' import { resolveLinuxNodePtyAddon, resolveWindowsNodePtyAddons } from './executable-native-pty.ts' -import { copyOfficeSidecar, OFFICE_ASSET_IGNORES } from './build-exe-for-python-sdk-office.ts' +import { officeSidecarArchives, runtimeNpmArchive, OFFICE_ASSET_IGNORES } from './build-exe-for-python-sdk-office.ts' import { deduplicateStagedWorkspacePackages, materializeStagedLinks } from './build-exe-for-python-sdk-staging.ts' -import { preparePrimaryRuntime, smokePrimaryRuntime, type PrimaryRuntimeTarget } from './primary-runtime/prepare.ts' +import { prepareOfficeSkillAssets } from './primary-runtime/prepare.ts' +import primaryLock from './primary-runtime/lock.json' with { type: 'json' } const root = resolve(import.meta.dirname, '..') @@ -45,6 +48,7 @@ const DEPLOY_ONLY_DOCS = ['README.md', 'README.zh.md', 'README.i18n.yaml'] */ const ASSET_GLOBS = [ 'package.json', + '*.mjs', 'node_modules/**/*.js', 'node_modules/**/*.cjs', 'node_modules/**/*.mjs', @@ -301,6 +305,9 @@ class SingleExeBuild { for (const name of DEPLOY_ONLY_DOCS) console.log(`build-exe-for-python-sdk: [dry-run] rm -f ${join(this.staging, name)}`) } else { await Promise.all(DEPLOY_ONLY_DOCS.map(name => rm(join(this.staging, name), { force: true }))) + for await (const file of glob('node_modules/**/{README*.md,CHANGELOG*.md,HISTORY*.md,*.map,*.d.ts,*.d.mts,*.d.cts}', { + cwd: this.staging, exclude: ['node_modules/**/assets/**'], + })) await rm(join(this.staging, file), { force: true }) } } @@ -331,7 +338,14 @@ class SingleExeBuild { /** Add the executable entry and pkg assets to the staged manifest. */ async injectPkgConfig(): Promise { - const patch = { bin: ENTRY_BIN, pkg: { assets: ASSET_GLOBS, ignore: OFFICE_ASSET_IGNORES } } + const sourceModules = join(root, 'node_modules').replaceAll('\\', '/') + const patch = { bin: ENTRY_BIN, pkg: { assets: ASSET_GLOBS, ignore: [ + ...OFFICE_ASSET_IGNORES, + `${sourceModules}/**`, `${sourceModules}/.pnpm/**`, + ...['packages', 'apps', 'vendor', 'native', 'scripts'].map(directory => `${join(root, directory).replaceAll('\\', '/')}/**`), + `${root.replaceAll('\\', '/')}/tsconfig*.json`, + '**/*.map', '**/*.d.ts', '**/*.d.mts', '**/*.d.cts', + ] } } const manifestPath = join(this.staging, 'package.json') if (this.cli.dryRun) { console.log(`build-exe-for-python-sdk: [dry-run] patch ${manifestPath} with ${JSON.stringify(patch)}`) @@ -343,8 +357,28 @@ class SingleExeBuild { if (!existsSync(join(this.staging, ENTRY_BIN))) { throw new Error(`build-exe-for-python-sdk: staged bootstrap ${join(this.staging, ENTRY_BIN)} is missing.`) } + const helpers = [ + { name: 'primary-runtime', source: 'scripts/primary-runtime/prepare.ts', exports: 'downloadNodeRuntime, preparePrimaryRuntime' }, + { name: 'office-sidecar', source: 'scripts/build-exe-for-python-sdk-office.ts', exports: 'downloadOfficeSidecar' }, + ] + const entries: Record = {} + for (const helper of helpers) { + const entry = join(this.staging, `.${helper.name}-entry.ts`) + entries[helper.name] = entry + await writeFile(entry, `export { ${helper.exports} } from ${JSON.stringify(join(root, helper.source).replaceAll('\\', '/'))}\n`) + } + try { + await bundle({ config: false, tsconfig: false, inputOptions: { tsconfig: false }, target: 'es2024', entry: entries, + outDir: this.staging, clean: false, format: 'esm', platform: 'node', dts: false, + deps: { neverBundle: [/^@deepseek-ai\//u], alwaysBundle: [/.*/u] }, shims: true, outExtensions: () => ({ js: '.mjs' }), + define: { 'import.meta.main': 'false' } }) + } finally { + for (const entry of Object.values(entries)) await rm(entry, { force: true }) + } const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as Record await writeFile(manifestPath, `${JSON.stringify({ ...manifest, ...patch }, null, 2)}\n`) + await this.run('resource helper imports', process.execPath, ['--input-type=module', '--eval', + ['primary-runtime.mjs', 'office-sidecar.mjs'].map(file => `await import(${JSON.stringify(pathToFileURL(join(this.staging, file)).href)})`).join('\n')]) console.log(`build-exe-for-python-sdk: injected pkg config into ${manifestPath}`) } @@ -357,6 +391,7 @@ class SingleExeBuild { const productBase = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`) const product = target.platform === 'win' ? `${productBase}.exe` : productBase await this.prepareNativePty(target) + const platform = target.platform === 'macos' ? 'darwin' : target.platform === 'win' ? 'win32' : target.platform if (!this.cli.dryRun) await mkdir(this.outDir, { recursive: true }) await this.runPnpm(`pkg ${target.spec}`, [ 'exec', @@ -371,26 +406,22 @@ class SingleExeBuild { if (!this.cli.dryRun && !existsSync(product)) { throw new Error(`build-exe-for-python-sdk: product ${product} is missing after the pkg run; inspect ${this.outDir}.`) } - const office = `${productBase}-office` - if (this.cli.dryRun) { - console.log(`build-exe-for-python-sdk: [dry-run] copy Office dependency closure from ${this.staging} to ${office}`) - } else { - const platform = target.platform === 'macos' ? 'darwin' : target.platform === 'win' ? 'win32' : target.platform - const packages = await copyOfficeSidecar(this.staging, office, { platform, arch: target.arch }) - console.log(`build-exe-for-python-sdk: copied ${packages.length} Office packages to ${office}`) - } const ripgrep = await this.copyRipgrepSidecar(target, product) const resources = join(this.outDir, `${target.platform}-${target.arch}`) - const runtimeTarget = `${target.platform === 'macos' ? 'mac' : target.platform}-${target.arch}` as PrimaryRuntimeTarget if (this.cli.dryRun) { - console.log(`build-exe-for-python-sdk: [dry-run] prepare Python and Office skills for ${runtimeTarget} in ${resources}`) + console.log(`build-exe-for-python-sdk: [dry-run] lock optional resource downloads and copy Office skills to ${resources}`) } else { - const { version } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as { version: string } - await preparePrimaryRuntime({ target: runtimeTarget, output: resources, - cache: join(tmpdir(), 'dsh-primary-runtime-downloads'), version }) - smokePrimaryRuntime(join(resources, 'primary-runtime')) + await rm(resources, { recursive: true, force: true }) + await mkdir(resources, { recursive: true }) + const { version, packageManager } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as { version: string; packageManager: string } + const office = await officeSidecarArchives(this.staging, { platform, arch: target.arch }) + const pnpm = await runtimeNpmArchive('pnpm', packageManager.replace('pnpm@', '')) + const inputs = { version, target: `${target.platform === 'macos' ? 'mac' : target.platform}-${target.arch}`, office, pnpm } + const identity = createHash('sha256').update(JSON.stringify({ ...inputs, primaryLock })).digest('hex') + await writeFile(join(resources, 'downloads.json'), JSON.stringify({ ...inputs, identity }, undefined, 2) + '\n') + await prepareOfficeSkillAssets(join(this.staging, 'node_modules/@deepseek-ai/dsh-skill-office/assets'), join(resources, 'office-skills')) } - if (target.platform !== 'macos') return [product, ripgrep, office, resources] + if (target.platform !== 'macos') return [product, ripgrep, resources] const spawnHelper = `${product}-spawn-helper` const source = join(this.staging, 'node_modules', 'node-pty', 'prebuilds', `darwin-${target.arch}`, 'spawn-helper') if (this.cli.dryRun) { @@ -399,7 +430,7 @@ class SingleExeBuild { await copyFile(source, spawnHelper) await chmod(spawnHelper, 0o755) } - return [product, ripgrep, spawnHelper, office, resources] + return [product, ripgrep, spawnHelper, resources] } /** Copy the target ripgrep binary beside the executable so Node can spawn it outside pkg's virtual filesystem. */ @@ -446,6 +477,17 @@ class SingleExeBuild { 'node_modules', 'node-pty', ) + const platform = target.platform === 'macos' ? 'darwin' : target.platform === 'win' ? 'win32' : target.platform + const selected = `${platform}-${target.arch}` + const prebuilds = join(this.staging, 'node_modules/node-pty/prebuilds') + if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] keep only node-pty prebuilds ${selected}`) + else { + const source = join(packageDirectory, 'prebuilds', selected) + if (existsSync(source)) await cp(source, join(prebuilds, selected), { recursive: true }) + for (const entry of await readdir(prebuilds, { withFileTypes: true })) { + if (entry.isDirectory() && entry.name !== selected) await rm(join(prebuilds, entry.name), { recursive: true, force: true }) + } + } if (target.platform === 'win') { if (target.arch !== 'x64') { throw new Error('build-exe-for-python-sdk: Windows supports x64 only.') diff --git a/scripts/build-python-release.py b/scripts/build-python-release.py index 2918feb380..82c9f9833a 100644 --- a/scripts/build-python-release.py +++ b/scripts/build-python-release.py @@ -56,11 +56,6 @@ def runtime_filenames(executable_name: str) -> tuple[str, ...]: return (*names, f"{executable_name}-spawn-helper") if "-macos-" in executable_name else names -def office_sidecar_name(executable_name: str) -> str: - """Return the complete Office dependency directory for one executable.""" - return f"{executable_name.removesuffix('.exe')}-office" - - def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--package", choices=("sdk", "runtime"), required=True) @@ -229,54 +224,18 @@ def stage_runtime(destination: Path, version: str, executable: Path, executable_ source_directory = executable.parent for filename in runtime_filenames(executable_name): shutil.copy2(source_directory / filename, runtime_dir / filename) - office = office_sidecar_name(executable_name) - shutil.copytree(source_directory / office, runtime_dir / office) resources = executable_name.removeprefix("deepseek-harness-sdk-runtime-").removesuffix(".exe") shutil.copytree(source_directory / resources, runtime_dir / resources) -def verify_office_payload(archive: zipfile.ZipFile, office_modules: str, platform_tag: str) -> None: - """Check packaged engine assets and native helper executable permissions.""" - names = set(archive.namelist()) - adapter = f"{office_modules}/@deepseek-ai/libreoffice-kit/package.json" - if adapter not in names: - raise RuntimeError("Office dependency is missing: libreoffice-kit") - engines = f"{office_modules}/@deepseek-ai" - target = next(name for name, value in PLATFORMS.items() if value[0] == platform_tag) - native_target = target.replace("win-", "win32-").replace("macos-", "darwin-") - declared = json.loads(archive.read(adapter)).get("optionalDependencies", {}) - selected = native_target if f"@deepseek-ai/libreoffice-kit-{native_target}" in declared else "wasm" - for required in (f"libreoffice-kit-{selected}/prebuilds.json",): - if f"{engines}/{required}" not in names: - raise RuntimeError(f"Office dependency is missing: {required}") - manifests = ( - name for name in names - if name.startswith(f"{engines}/libreoffice-kit-") - and name.endswith("/prebuilds.json") - and name.count("/") == engines.count("/") + 2 - ) - for manifest_path in manifests: - if manifest_path != f"{engines}/libreoffice-kit-{selected}/prebuilds.json": - raise RuntimeError(f"Unexpected Office engine for {platform_tag}: {manifest_path}") - manifest = json.loads(archive.read(manifest_path)) - engine = manifest["engine"] - native = engine["kind"] == "native" - fields = ("executable",) if native else ("loader", "wasm", "data", "metadata") - for field in fields: - asset = f"{manifest_path.rsplit('/', 1)[0]}/{engine[field]}" - if asset not in names: - raise RuntimeError(f"Office engine asset is missing: {asset}") - mode = archive.getinfo(asset).external_attr >> 16 - if native and platform_tag != "win_amd64" and mode & stat.S_IXUSR == 0: - raise RuntimeError(f"Office helper lost its executable bit: {asset}") - - def verify_wheel( wheel: Path, package: str, version: str, platform: tuple[str, str] | None, ) -> None: + if wheel.stat().st_size > 100_000_000: + raise RuntimeError(f"{wheel} is {wheel.stat().st_size} bytes and exceeds the PyPI single-file limit of 100000000 bytes") expected_tag = "py3-none-any" if platform is None else f"py3-none-{platform[0]}" expected_distribution = SDK_DISTRIBUTION if package == "sdk" else RUNTIME_DISTRIBUTION dist_info = f"{expected_distribution.replace('-', '_')}-{version}.dist-info" @@ -308,15 +267,12 @@ def verify_wheel( ] if package == "runtime": assert platform is not None - office = office_sidecar_name(platform[1]) resources = platform[1].removeprefix("deepseek-harness-sdk-runtime-").removesuffix(".exe") - expected_files = sorted((*runtime_filenames(platform[1]), office, resources)) + expected_files = sorted((*runtime_filenames(platform[1]), resources)) found_files = sorted({name.split("/runtime/", 1)[1].split("/", 1)[0] for name in runtime_payload}) if found_files != expected_files: raise RuntimeError(f"{wheel} runtime payload must be {expected_files}, found {found_files}") - office_modules = f"deepseek_harness_runtime/runtime/{office}/node_modules" - verify_office_payload(archive, office_modules, platform[0]) - validate = runpy.run_path(str(ROOT / "python/sdk-runtime/src/deepseek_harness_runtime/_resources.py"))["validate_resources"] + validate = runpy.run_path(str(ROOT / "python/sdk-runtime/src/deepseek_harness_runtime/_resources.py"))["validate_downloads"] target = next(name for name, value in PLATFORMS.items() if value == platform) validate(zipfile.Path(archive, f"deepseek_harness_runtime/runtime/{resources}/"), target) for runtime_file in runtime_payload: diff --git a/scripts/client-bundle-purity.spec.ts b/scripts/client-bundle-purity.spec.ts index b172dab5f4..20fba8edbc 100644 --- a/scripts/client-bundle-purity.spec.ts +++ b/scripts/client-bundle-purity.spec.ts @@ -184,10 +184,12 @@ describe('client bundle purity gate', () => { neverBundle: (specifier: string) => boolean } - expect(requesting.neverBundle('react')).toBe(true) - expect(requesting.neverBundle('zod')).toBe(false) - expect(plain.neverBundle('react')).toBe(true) - expect(plain.neverBundle('@deepseek-ai/dsh-client-store')).toBe(true) + for (const bundle of [requesting, plain]) { + for (const specifier of ['react', 'zod', '@tanstack/react-virtual', '@deepseek-ai/dsh-client-store']) { + expect(bundle.neverBundle(specifier)).toBe(true) + } + expect(bundle.neverBundle('zod/v3')).toBe(false) + } }) }) diff --git a/scripts/coverage-file-times.spec.ts b/scripts/coverage-file-times.spec.ts index be412f49a0..6f3174af2d 100644 --- a/scripts/coverage-file-times.spec.ts +++ b/scripts/coverage-file-times.spec.ts @@ -17,7 +17,7 @@ async function temporaryRoot(): Promise { } describe('coverage file timing report', () => { - it('includes import and hook costs once without serializing coverage or test objects', async () => { + it('preserves duplicate file records and costs without serializing coverage or test objects', async () => { const root = await temporaryRoot() const reporter = new CoverageFileTimesReporter() reporter.onInit({ config: { root, outputFile: { fileTimes: 'times.json' } } }) @@ -38,10 +38,13 @@ describe('coverage file timing report', () => { // timing report must read only their paths and diagnostics. toJSON: () => { throw new Error('test module was serialized') }, } - await reporter.onTestRunEnd([module]) + await reporter.onTestRunEnd([module, module]) const output = await readFile(join(root, 'times.json'), 'utf8') - expect(JSON.parse(output)).toEqual({ 'packages/a/tests/x.spec.ts': 315 }) + expect(JSON.parse(output)).toEqual([ + { file: 'packages/a/tests/x.spec.ts', duration: 315 }, + { file: 'packages/a/tests/x.spec.ts', duration: 315 }, + ]) expect(output.endsWith('\n')).toBe(true) expect(output).not.toContain('coverageMap') expect(output).not.toContain('assertionResults') @@ -52,7 +55,7 @@ describe('coverage file timing report', () => { const reporter = new CoverageFileTimesReporter() reporter.onInit({ config: { root, outputFile: { fileTimes: 'times.json' } } }) await reporter.onTestRunEnd([]) - expect(await readFile(join(root, 'times.json'), 'utf8')).toBe('{}\n') + expect(await readFile(join(root, 'times.json'), 'utf8')).toBe('[]\n') }) const missingOutputs: ResolvedConfig['outputFile'][] = ['report.json', {}, { fileTimes: '' }] diff --git a/scripts/coverage-file-times.ts b/scripts/coverage-file-times.ts index 7d9646636e..4b765f7097 100644 --- a/scripts/coverage-file-times.ts +++ b/scripts/coverage-file-times.ts @@ -3,7 +3,7 @@ import { writeFile } from 'node:fs/promises' import { relative, resolve, sep } from 'node:path' import type { Reporter, ResolvedConfig, TestModule } from 'vitest/node' -/** Vitest reporter that writes repository-relative file durations to `outputFile.fileTimes`. */ +/** Vitest reporter that writes repository-relative file and duration records to `outputFile.fileTimes`. */ export default class CoverageFileTimesReporter implements Reporter { private root = '' private outputFile = '' @@ -28,16 +28,16 @@ export default class CoverageFileTimesReporter implements Reporter { * @returns after the compact timing report has been written. */ public async onTestRunEnd(testModules: readonly Pick[]): Promise { - const durations: Record = {} - for (const module of testModules) { + const records = testModules.map((module) => { const diagnostic = module.diagnostic() const file = relative(this.root, module.moduleId).split(sep).join('/') - durations[file] = diagnostic.environmentSetupDuration + const duration = diagnostic.environmentSetupDuration + diagnostic.prepareDuration + diagnostic.setupDuration + diagnostic.collectDuration + diagnostic.duration - } - await writeFile(this.outputFile, `${JSON.stringify(durations)}\n`, 'utf8') + return { file, duration } + }) + await writeFile(this.outputFile, `${JSON.stringify(records)}\n`, 'utf8') } } diff --git a/scripts/coverage-partitions.spec.ts b/scripts/coverage-partitions.spec.ts index 99bf586f81..2e5ebdb705 100644 --- a/scripts/coverage-partitions.spec.ts +++ b/scripts/coverage-partitions.spec.ts @@ -15,7 +15,7 @@ import { forwardedCoverageArgs, parseCoveragePartitionCount, parseCoverageTestTimeout, - parseListOutput, + parseFileInventory, readFileDurations, writeFileDurations, type CoverageCommand, @@ -40,6 +40,11 @@ async function writeBlob(command: CoverageCommand): Promise { if (command.blobPath === undefined) return await mkdir(dirname(command.blobPath), { recursive: true }) await writeFile(command.blobPath, '{}') + const configArgument = command.args.find(argument => argument.startsWith('--config='))! + const source = await readFile(join(command.cwd, configArgument.slice('--config='.length)), 'utf8') + const files = [...source.matchAll(/"([^"\n]+\.spec\.tsx?)"/g)].map(match => match[1]!) + const reportArgument = command.args.find(argument => argument.startsWith('--outputFile.fileTimes='))! + await writeFile(join(command.cwd, reportArgument.slice('--outputFile.fileTimes='.length)), JSON.stringify(files.map(file => ({ file, duration: 0 })))) } async function temporaryRoot(): Promise { @@ -196,24 +201,36 @@ describe('weighted partition assignment', () => { }) describe('coverage file inventory', () => { - it('parses vitest list --filesOnly output, keeps project ownership, and drops exempt suites', async () => { + it('keeps JSON file paths and project ownership while dropping exempt suites', async () => { const root = await temporaryRoot() const exemptDir = join(root, 'packages/experimental/webworker-runtime/tests') await mkdir(exemptDir, { recursive: true }) await writeFile(join(exemptDir, 'transform-corpus.spec.ts'), '') - const output = [ - '[thread-safe] packages/a/tests/a.spec.ts', - '[process-bound] packages/b/tests/b.spec.ts', - '[thread-safe] packages/experimental/webworker-runtime/tests/transform-corpus.spec.ts', - 'not a test line', - ].join('\n') - const inventory = parseListOutput(output, root) + const output = JSON.stringify([ + { projectName: 'thread-safe', file: join(root, 'packages/a/tests/a.spec.ts') }, + { projectName: 'process-bound', file: join(root, 'packages/b/tests/with spaces.spec.ts') }, + { projectName: 'thread-safe', file: join(exemptDir, 'transform-corpus.spec.ts') }, + ]) + const inventory = parseFileInventory(output, root) expect(inventory.files).toEqual([ 'packages/a/tests/a.spec.ts', - 'packages/b/tests/b.spec.ts', + 'packages/b/tests/with spaces.spec.ts', ]) expect(inventory.projectOf.get('packages/a/tests/a.spec.ts')).toBe('thread-safe') - expect(inventory.projectOf.get('packages/b/tests/b.spec.ts')).toBe('process-bound') + expect(inventory.projectOf.get('packages/b/tests/with spaces.spec.ts')).toBe('process-bound') + }) + + it.each(['[', '{}', '[null]', '[{}]', '[{"file":42}]', '[{"file":"a.spec.ts","projectName":"unknown"}]'])('rejects malformed inventories instead of dropping entries: %s', async (output) => { + const root = await temporaryRoot() + expect(() => parseFileInventory(output, root)).toThrow() + }) + + it('rejects duplicate file ownership', async () => { + const root = await temporaryRoot() + const file = join(root, 'a.spec.ts') + expect(() => parseFileInventory(JSON.stringify([ + { file, projectName: 'thread-safe' }, { file, projectName: 'process-bound' }, + ]), root)).toThrow('more than once') }) it('removes every Typert package from instrumented files and project assignments', async () => { @@ -230,8 +247,8 @@ describe('coverage file inventory', () => { await writeFile(join(root, file), '') } const retained = 'packages/api/gateway/tests/rpc.spec.ts' - const inventory = parseListOutput( - [...typertFiles, retained].map(file => `[thread-safe] ${file}`).join('\n'), + const inventory = parseFileInventory( + JSON.stringify([...typertFiles, retained].map(file => ({ projectName: 'thread-safe', file: join(root, file) }))), root, ) expect(inventory.files).toEqual([retained]) @@ -272,19 +289,21 @@ describe('coverage file inventory', () => { it('extracts per-file durations from compact partition timing reports', async () => { const root = await temporaryRoot() const report = join(root, 'partition-1.times.json') - await writeFile(report, JSON.stringify({ - 'packages/a/tests/x.spec.ts': 500, - 'packages/a/tests/y.spec.ts': 0, - 'packages/a/tests/invalid.spec.ts': -1, - 'packages/a/tests/malformed.spec.ts': 'unknown', - })) + await writeFile(report, JSON.stringify([ + { file: 'packages/a/tests/x.spec.ts', duration: 500 }, + { file: 'packages/a/tests/y.spec.ts', duration: 0 }, + { file: 'packages/a/tests/invalid.spec.ts', duration: -1 }, + { file: 'packages/a/tests/malformed.spec.ts', duration: 'unknown' }, + null, + { duration: 10 }, + ])) const durations = collectPartitionDurations([report]) expect(durations.get('packages/a/tests/x.spec.ts')).toBe(500) expect(durations.get('packages/a/tests/y.spec.ts')).toBe(0) expect(durations.size).toBe(2) }) - it.each(['null', '[]', '42', '"invalid"', '{'])('ignores corrupt timing metadata %s', async (contents) => { + it.each(['null', '{}', '42', '"invalid"', '{'])('ignores corrupt timing metadata %s', async (contents) => { const root = await temporaryRoot() const report = join(root, 'partition.times.json') await writeFile(report, contents) @@ -442,6 +461,31 @@ describe('coverage location canonicalization', () => { }) describe('coverage partition coordinator', () => { + it.each(['omitted', 'unexpected', 'duplicate', 'malformed', 'unnamed', 'missing-report'])('refuses %s test results even when every partition exits successfully', async (failure) => { + const root = await temporaryRoot() + const runCommand = vi.fn(async (command: CoverageCommand) => { + await writeBlob(command) + if (command.label === 'partition 1/2') { + const reportArgument = command.args.find(argument => argument.startsWith('--outputFile.fileTimes='))! + const reportPath = join(root, reportArgument.slice('--outputFile.fileTimes='.length)) + let report = JSON.parse(await readFile(reportPath, 'utf8')) as Array<{ file: string; duration: number }> + if (failure === 'omitted') report = [] + if (failure === 'unexpected') report.push({ file: 'unassigned.spec.ts', duration: 0 }) + if (failure === 'duplicate') report.push(report[0]!) + await writeFile(reportPath, failure === 'malformed' ? '{}' : JSON.stringify(report)) + if (failure === 'unnamed') await writeFile(reportPath, '[{}]') + if (failure === 'missing-report') await rm(reportPath) + } + return passed + }) + const coordinator = new CoveragePartitionCoordinator({ + root, partitions: 2, pnpmEntrypoint: '/pnpm.cjs', files: ['a.spec.ts', 'b.spec.ts'], runCommand, + }) + await expect(coordinator.run()).rejects.toThrow() + expect(runCommand).toHaveBeenCalledTimes(2) + await expect(access(join(root, 'coverage/.partitioned/vitest-partition-1.config.ts'))).resolves.toBeUndefined() + }) + const weightedFiles = ['a.spec.ts', 'b.spec.ts', 'c.spec.ts'] const weightedDurations = new Map([ ['a.spec.ts', 100], @@ -466,6 +510,7 @@ describe('coverage partition coordinator', () => { }) await expect(coordinator.run()).resolves.toBe(0) + await expect(access(join(root, 'coverage/.partitioned'))).rejects.toMatchObject({ code: 'ENOENT' }) // The coordinator passes a root-relative argument and runs every child with // the repository root as its working directory, so the argument must name diff --git a/scripts/coverage-partitions.ts b/scripts/coverage-partitions.ts index 5489861c85..b0cccc5338 100644 --- a/scripts/coverage-partitions.ts +++ b/scripts/coverage-partitions.ts @@ -5,7 +5,7 @@ */ import { spawn } from 'node:child_process' import { globSync, readFileSync, writeFileSync } from 'node:fs' -import { lstat, mkdir, readdir, rm, unlink, writeFile } from 'node:fs/promises' +import { lstat, mkdir, readdir, readFile, rm, unlink, writeFile } from 'node:fs/promises' import { join, relative, sep } from 'node:path' import type { TestUserConfig } from 'vitest/config' import { coverageExemptHeavySuites } from './coverage-exempt.ts' @@ -165,19 +165,27 @@ export interface InstrumentedInventory { } /** - * Parse `vitest list --filesOnly` output into the instrumented inventory: - * one `[pool] path` line per file, deduplicated, minus the exempt heavy - * suites that `vitest list` itself does not exclude. + * Parse Vitest's JSON file inventory, excluding uninstrumented heavy suites. + * @param output - JSON written by `vitest list --filesOnly --json=`. + * @param root - repository root used to make file paths relative. + * @returns sorted test files and their owning Vitest projects. + * @throws when a record is malformed or a file has multiple project assignments. */ -export function parseListOutput(output: string, root: string): InstrumentedInventory { +export function parseFileInventory(output: string, root: string): InstrumentedInventory { + const entries: unknown = JSON.parse(output) + if (!Array.isArray(entries)) throw new Error('coverage inventory must be a JSON array') const files = new Set() const projectOf = new Map() - for (const line of output.split(/\r?\n/)) { - const match = /^\[([^\]]+)\]\s+(\S+\.spec\.(?:ts|tsx))$/.exec(line) - if (match !== null && match[1] !== undefined && match[2] !== undefined) { - files.add(match[2]) - projectOf.set(match[2], match[1]) + for (const value of entries) { + const entry: unknown = value + if (entry === null || typeof entry !== 'object' || !('file' in entry) || typeof entry.file !== 'string' + || !('projectName' in entry) || (entry.projectName !== 'thread-safe' && entry.projectName !== 'process-bound')) { + throw new Error('coverage inventory requires a file and its thread-safe or process-bound project') } + const file = relative(root, entry.file).split(sep).join('/') + if (files.has(file)) throw new Error(`coverage inventory lists ${file} more than once`) + files.add(file) + projectOf.set(file, entry.projectName) } for (const suite of coverageExemptHeavySuites) { for (const file of globSync(suite.exclude, { cwd: root })) { @@ -201,26 +209,24 @@ export function parseListOutput(output: string, root: string): InstrumentedInven async function collectInstrumentedFiles( root: string, pnpmEntrypoint: string, + inventoryPath: string, filters: readonly string[] = [], ): Promise { - const invocation = pnpmInvocation(['exec', 'vitest', 'list', '--filesOnly', ...filters], { npm_execpath: pnpmEntrypoint }) - const output = await runListCommand(invocation.command, invocation.args, root) - return parseListOutput(output, root) + const invocation = pnpmInvocation(['exec', 'vitest', 'list', '--filesOnly', `--json=${inventoryPath}`, ...filters], { npm_execpath: pnpmEntrypoint }) + await runListCommand(invocation.command, invocation.args, root) + return parseFileInventory(await readFile(inventoryPath, 'utf8'), root) } -/** Run `vitest list` and return its stdout, or throw with exit code and stderr. */ -function runListCommand(command: string, args: string[], root: string): Promise { +/** Wait for `vitest list` to write its JSON file, or throw with exit code and stderr. */ +function runListCommand(command: string, args: string[], root: string): Promise { return new Promise((resolveList, rejectList) => { - const child = spawn(command, args, { cwd: root, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] }) - let output = '' + const child = spawn(command, args, { cwd: root, env: process.env, stdio: ['ignore', 'ignore', 'pipe'] }) let errorOutput = '' - child.stdout.setEncoding('utf8') child.stderr.setEncoding('utf8') - child.stdout.on('data', (chunk: string) => { output += chunk }) child.stderr.on('data', (chunk: string) => { errorOutput += chunk }) child.once('error', (error: Error) => { rejectList(error) }) child.once('close', (code) => { - if (code === 0) resolveList(output) + if (code === 0) resolveList() else rejectList(new Error(`vitest list exited with ${String(code ?? 'signal')}${errorOutput === '' ? '' : `: ${errorOutput.trim().slice(0, 300)}`}`)) }) }) @@ -301,7 +307,7 @@ export function writeFileDurations( /** * Extract per-file durations from compact timing reports, one per partition. - * @param reportFiles - reports mapping repository-relative file paths to milliseconds. + * @param reportFiles - reports listing repository-relative file paths and milliseconds. * @returns finite, non-negative file costs from readable reports. */ export function collectPartitionDurations(reportFiles: readonly string[]): Map { @@ -313,10 +319,13 @@ export function collectPartitionDurations(reportFiles: readonly string[]): Map= 0) { + durations.set(record.file, record.duration) + } } } return durations @@ -479,13 +488,15 @@ export class CoveragePartitionCoordinator { } /** - * Run every partition before one merged threshold check. + * Run every partition and verify its test-file inventory before one merged threshold check. + * Failed runs retain inventories, partition configs, and test reports in coverage/.partitioned. * @returns zero only when every partition and the merge command succeed. */ public async run(): Promise { await removeOwnedTree(join(this.root, 'coverage')) await mkdir(this.blobsRoot, { recursive: true }) + let succeeded = false try { const assignments = await this.assignFiles() this.assertNonEmptyAssignments(assignments) @@ -506,13 +517,16 @@ export class CoveragePartitionCoordinator { // run, but the completed partitions' timings are still worth keeping. this.persistDurations(this.partitions) await this.assertCompleteBlobSet(commands) + await this.assertCompleteTestFiles(assignments) const mergeCommand = this.mergeCommand() console.log(`coverage-partitions: start ${mergeCommand.label}`) const mergeResult = await this.runCommand(mergeCommand) - return results.some(commandFailed) || commandFailed(mergeResult) ? 1 : 0 + succeeded = !results.some(commandFailed) && !commandFailed(mergeResult) + return succeeded ? 0 : 1 } finally { - await removeOwnedTree(this.temporaryRoot) + if (succeeded) await removeOwnedTree(this.temporaryRoot) + else console.error(`coverage-partitions: diagnostics retained at ${this.relativePath(this.temporaryRoot)}`) } } @@ -559,7 +573,7 @@ export class CoveragePartitionCoordinator { // values (`--testTimeout 5000`) must never be mistaken for filters. const separator = this.vitestArgs.indexOf('--') const filters = separator >= 0 ? this.vitestArgs.slice(separator + 1) : [] - const inventory = await collectInstrumentedFiles(this.root, this.pnpmEntrypoint, filters) + const inventory = await collectInstrumentedFiles(this.root, this.pnpmEntrypoint, join(this.temporaryRoot, 'inventory.json'), filters) files = inventory.files this.projectOf = inventory.projectOf weights = readFileDurations(this.root) @@ -651,6 +665,30 @@ export class CoveragePartitionCoordinator { throw new Error(`coverage partitions produced ${JSON.stringify(actual)}; expected ${JSON.stringify(expected)}.`) } } + + /** Require each compact timing report to account for every assigned file exactly once. */ + private async assertCompleteTestFiles(assignments: readonly (readonly string[])[]): Promise { + await Promise.all(assignments.map(async (expected, index) => { + const path = join(this.temporaryRoot, `partition-${index + 1}.times.json`) + const report: unknown = JSON.parse(await readFile(path, 'utf8')) + if (!Array.isArray(report)) { + throw new Error(`coverage partition report ${path} must contain file timing records`) + } + const actual = report.map((result: unknown) => { + if (result === null || typeof result !== 'object' || !('file' in result) || typeof result.file !== 'string') { + throw new Error(`coverage partition report ${path} contains a test result without a filename`) + } + return result.file + }) + const observed = new Set(actual) + const assigned = new Set(expected) + const missing = expected.filter(file => !observed.has(file)) + const unexpected = actual.filter(file => !assigned.has(file)) + if (missing.length > 0 || unexpected.length > 0 || observed.size !== actual.length) { + throw new Error(`coverage partition ${index + 1}: missing files ${JSON.stringify(missing)}; unexpected files ${JSON.stringify(unexpected)}; duplicate results ${actual.length - observed.size}`) + } + })) + } } /** Spawn one pnpm-backed command without a platform shell. */ diff --git a/scripts/fixtures/python-sdk-office.mjs b/scripts/fixtures/python-sdk-office.mjs index 1f11e2cdbf..1b11a089f4 100644 --- a/scripts/fixtures/python-sdk-office.mjs +++ b/scripts/fixtures/python-sdk-office.mjs @@ -15,7 +15,7 @@ export async function apply(ctx, config) { if (json === undefined) throw new Error('Office skill did not supply CLI paths') const { libreofficeKit: { node, cli } } = JSON.parse(json) const options = { env: { ...process.env, PATH: '' }, timeout: 120_000 } - const capabilities = await promisify(execFile)(node, [cli, 'capabilities'], options) + const capabilities = await promisify(execFile)(node, [cli, 'capabilities', '--json'], options) await promisify(execFile)(node, [cli, 'convert', '--input', config.input, '--output', config.output + '.cli.pdf'], options) const pdf = await readFile(config.output + '.cli.pdf') if (pdf.subarray(0, 5).toString() !== '%PDF-') throw new Error('Skill CLI did not create a PDF') diff --git a/scripts/no-unknown-casts.baseline.json b/scripts/no-unknown-casts.baseline.json index a4d48030dc..5018e4dea9 100644 --- a/scripts/no-unknown-casts.baseline.json +++ b/scripts/no-unknown-casts.baseline.json @@ -625,7 +625,7 @@ }, "packages/client/ui-settings-models/tests/apply.client.spec.ts": { "0464c472dfc74994388bf63a6b3c110df506ce44fe47a5040e93c67a5eb3b4ae": 1, - "04984a2ce1486250236e638e0197da0a1a840855ed3975c59130a85358c7e5fe": 5, + "04984a2ce1486250236e638e0197da0a1a840855ed3975c59130a85358c7e5fe": 4, "5c8c41ba658c0576bea6c34b1226c27e7b0328c03c9eea0d6c920f940c9d8f5b": 1 }, "packages/client/ui-settings-models/tests/components.client.spec.tsx": { diff --git a/scripts/primary-runtime/prepare.spec.ts b/scripts/primary-runtime/prepare.spec.ts index 6100bc2b5b..11b5b7a6a8 100644 --- a/scripts/primary-runtime/prepare.spec.ts +++ b/scripts/primary-runtime/prepare.spec.ts @@ -1,6 +1,6 @@ import { readFileSync } from 'node:fs' import { createHash } from 'node:crypto' -import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { basename, join } from 'node:path' import { zipSync } from 'fflate' @@ -61,7 +61,11 @@ it('extracts a hash-verified cached library without a Python installer or networ const hash = createHash('sha256').update(libraryWheel).digest('hex') const archive = join(root, hash) await writeFile(archive, libraryWheel) + const original = await stat(archive) expect(await downloadPrimaryRuntimeAsset('https://unused.invalid/library.whl', hash, root)).toBe(archive) + const reused = await stat(archive) + expect(reused.ino).toBe(original.ino) + expect(reused.mtimeMs).toBe(original.mtimeMs) await unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages')) expect(await readFile(join(root, 'site-packages/sample.py'), 'utf8')).toBe('sample = 42\n') await writeFile(archive, 'corrupt archive') diff --git a/scripts/primary-runtime/prepare.ts b/scripts/primary-runtime/prepare.ts index a156a7564a..33735052bc 100644 --- a/scripts/primary-runtime/prepare.ts +++ b/scripts/primary-runtime/prepare.ts @@ -1,8 +1,8 @@ /** Prepare pinned, relocatable script interpreters without installing into the build host. */ import { execFileSync } from 'node:child_process' -import { createHash } from 'node:crypto' -import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { createHash, randomUUID } from 'node:crypto' +import { cpSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs' import { cp } from 'node:fs/promises' import { createRequire } from 'node:module' import { tmpdir } from 'node:os' @@ -10,30 +10,101 @@ import { dirname, join, resolve } from 'node:path' import { parseArgs } from 'node:util' import extractZip from 'extract-zip' import { x as extractTar } from 'tar' -import { parsePrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../../packages/skill/tool-workspace-dependencies/src/index.ts' +import { parsePrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '@deepseek-ai/dsh-tool-workspace-dependencies' import lock from './lock.json' with { type: 'json' } +import { prunePrimaryRuntimePythonTests } from './prune-python-tests.ts' /** * Download or reuse an archive only when its bytes match the release lock. * @param url - Locked archive URL. - * @param sha256 - Expected SHA-256 digest. + * @param sha256 - Expected hexadecimal digest for the selected algorithm. * @param cache - Download cache directory. + * @param algorithm - Digest algorithm; npm archives use SHA-512. * @returns Verified local archive path. */ -export async function downloadPrimaryRuntimeAsset(url: string, sha256: string, cache: string): Promise { +export async function downloadPrimaryRuntimeAsset(url: string, sha256: string, cache: string, algorithm: 'sha256' | 'sha512' = 'sha256'): Promise { + mkdirSync(cache, { recursive: true }) const destination = join(cache, sha256) let bytes: Buffer + let cached = true try { bytes = readFileSync(destination) } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error - const response = await fetch(url) + cached = false + const timeout = Number(process.env.DSH_RESOURCE_DOWNLOAD_TIMEOUT_MS ?? '300000') + if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 2_147_483_647) throw new Error('DSH_RESOURCE_DOWNLOAD_TIMEOUT_MS must be an integer from 1 to 2147483647') + const response = await fetch(url, { signal: AbortSignal.timeout(timeout) }) if (!response.ok) throw new Error(`primary runtime download: ${String(response.status)} ${url}`) bytes = Buffer.from(await response.arrayBuffer()) } - if (createHash('sha256').update(bytes).digest('hex') !== sha256) throw new Error(`primary runtime download: checksum mismatch for ${url}`) - writeFileSync(destination, bytes) + if (createHash(algorithm).update(bytes).digest('hex') !== sha256) { + rmSync(destination, { force: true }) + throw new Error(`primary runtime download: checksum mismatch for ${url}`) + } + if (cached) return destination + const temporary = join(cache, `.${randomUUID()}`) + try { + writeFileSync(temporary, bytes) + renameSync(temporary, destination) + } finally { + rmSync(temporary, { force: true }) + } return destination } +/** Pinned npm tarball, without dependency resolution or lifecycle scripts. */ +export interface RuntimeNpmArchive { + /** Exact published package name. */ + readonly name: string + /** Exact published version. */ + readonly version: string + /** Registry tarball URL. */ + readonly url: string + /** npm SHA-512 integrity. */ + readonly integrity: string +} + +/** + * Download and unpack one locked npm package without running its scripts. + * @param artifact - Published tarball identity and integrity. + * @param destination - Empty package directory. + * @param cache - Verified archive cache. + * @returns Resolves after package identity validation. + */ +export async function downloadRuntimeNpmPackage(artifact: RuntimeNpmArchive, destination: string, cache: string): Promise { + if (!/^sha512-[A-Za-z0-9+/]+={0,2}$/u.test(artifact.integrity)) throw new Error(`runtime download: invalid integrity for ${artifact.name}`) + const checksum = Buffer.from(artifact.integrity.slice(7), 'base64').toString('hex') + const archive = await downloadPrimaryRuntimeAsset(artifact.url, checksum, cache, 'sha512') + mkdirSync(destination, { recursive: true }) + await extractTar({ file: archive, cwd: destination, strip: 1 }) + const installed = JSON.parse(readFileSync(join(destination, 'package.json'), 'utf8')) as { name?: string; version?: string } + if (installed.name !== artifact.name || installed.version !== artifact.version) throw new Error(`runtime download: package identity mismatch for ${artifact.name}`) +} + +/** + * Prepare the locked standalone Node independently of Python and pnpm. + * @param target - Target interpreter platform. + * @param destination - Directory receiving bin/node and LICENSE. + * @param cache - Verified archive cache. + * @returns Resolves after extracting the interpreter. + */ +export async function downloadNodeRuntime(target: PrimaryRuntimeTarget, destination: string, cache: string): Promise { + const artifact = lock.targets[target] + const filename = `node-v${lock.nodeVersion}-${artifact.nodeArchive}` + const archive = await downloadPrimaryRuntimeAsset(`https://nodejs.org/dist/v${lock.nodeVersion}/${filename}`, artifact.nodeSha256, cache) + const staging = mkdtempSync(join(tmpdir(), 'dsh-node-')) + try { + if (target === 'win-x64') await extractZip(archive, { dir: staging }) + else await extractTar({ file: archive, cwd: staging }) + const source = join(staging, filename.replace(/\.(?:zip|tar\.gz)$/u, '')) + mkdirSync(join(destination, 'bin'), { recursive: true }) + cpSync(join(source, ...(target === 'win-x64' ? ['node.exe'] : ['bin', 'node'])), + join(destination, 'bin', target === 'win-x64' ? 'node.exe' : 'node')) + cpSync(join(source, 'LICENSE'), join(destination, 'LICENSE')) + } finally { + rmSync(staging, { recursive: true, force: true }) + } +} + async function pythonArchive(target: keyof typeof lock.targets, cache: string): Promise { const artifact = lock.targets[target] const filename = `cpython-${lock.pythonVersion}+${lock.pythonRelease}-${artifact.pythonTarget}-install_only_stripped.tar.gz` @@ -54,7 +125,7 @@ export function primaryRuntimePayloadDigest( // Identity preserves key order within the selected target, wheel records and distribution map, plus wheel-entry order. // Bump format when extraction or assembly changes payload bytes without changing locked inputs. return createHash('sha256').update(JSON.stringify({ - format: 4, target, pythonVersion, pythonRelease, nodeVersion: pnpmVersion === undefined ? undefined : nodeVersion, + format: 6, target, pythonVersion, pythonRelease, nodeVersion: pnpmVersion === undefined ? undefined : nodeVersion, artifact: runtimeLock.targets[target], wheels, pythonPackages, pnpm: pnpmVersion, })).digest('hex') } @@ -91,7 +162,7 @@ export async function prepareOfficeSkillAssets(source: string, destination: stri /** A locked interpreter and wheel target. */ export type PrimaryRuntimeTarget = keyof typeof lock.targets -/** Build-only inputs shared by Desktop and SDK carriers. */ +/** Resource preparation inputs shared by builds and explicit SDK downloads. */ export interface PreparePrimaryRuntimeOptions { /** Target whose archives and wheels are downloaded. */ readonly target: PrimaryRuntimeTarget @@ -101,6 +172,10 @@ export interface PreparePrimaryRuntimeOptions { readonly cache: string /** Carrier release recorded in the legacy desktopVersion manifest field. */ readonly version: string + /** Locked npm pnpm tarball for installed carriers; build hosts otherwise copy their installed pnpm. */ + readonly pnpmArchive?: RuntimeNpmArchive + /** Ordinary filesystem Office assets; build hosts otherwise resolve the workspace package. */ + readonly skillSource?: string /** Omit Node.js and pnpm for carriers providing only Python. */ readonly pythonOnly?: boolean } @@ -113,7 +188,6 @@ export interface PreparePrimaryRuntimeOptions { export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOptions): Promise { const { target } = options const paths = { runtime: resolve(options.output), downloads: resolve(options.cache) } - const artifact = lock.targets[target] mkdirSync(paths.runtime, { recursive: true }) mkdirSync(paths.downloads, { recursive: true }) const staging = mkdtempSync(join(tmpdir(), 'dsh-primary-')) @@ -123,23 +197,18 @@ export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOption mkdirSync(dependencies, { recursive: true }) let pnpmVersion: string | undefined if (!options.pythonOnly) { - const nodeFilename = `node-v${lock.nodeVersion}-${artifact.nodeArchive}` - const nodeArchive = await downloadPrimaryRuntimeAsset(`https://nodejs.org/dist/v${lock.nodeVersion}/${nodeFilename}`, artifact.nodeSha256, paths.downloads) - const unpackedNode = join(staging, 'node') - mkdirSync(unpackedNode) - if (target === 'win-x64') await extractZip(nodeArchive, { dir: unpackedNode }) - else await extractTar({ file: nodeArchive, cwd: unpackedNode }) - const nodeSource = join(unpackedNode, nodeFilename.replace(/\.(?:zip|tar\.gz)$/u, '')) - mkdirSync(join(dependencies, 'node', 'bin'), { recursive: true }) - mkdirSync(join(dependencies, 'node', 'node_modules')) - writeFileSync(join(dependencies, 'node', 'node_modules', 'README.txt'), 'Reserved for bundled Node packages. pnpm uses its default installation directories.\n') - cpSync(join(nodeSource, ...(target === 'win-x64' ? ['node.exe'] : ['bin', 'node'])), - join(dependencies, 'node', 'bin', target === 'win-x64' ? 'node.exe' : 'node')) - cpSync(join(nodeSource, 'LICENSE'), join(dependencies, 'node', 'LICENSE')) - const require = createRequire(import.meta.url) - const pnpmManifest = require.resolve('pnpm') - pnpmVersion = (JSON.parse(readFileSync(pnpmManifest, 'utf8')) as { version: string }).version - await cp(dirname(pnpmManifest), join(dependencies, 'pnpm'), { recursive: true, dereference: true }) + const node = join(dependencies, 'node') + await downloadNodeRuntime(target, node, paths.downloads) + mkdirSync(join(node, 'node_modules')) + writeFileSync(join(node, 'node_modules', 'README.txt'), 'Reserved for bundled Node packages. pnpm uses its default installation directories.\n') + if (options.pnpmArchive !== undefined) { + pnpmVersion = options.pnpmArchive.version + await downloadRuntimeNpmPackage(options.pnpmArchive, join(dependencies, 'pnpm'), paths.downloads) + } else { + const pnpmManifest = createRequire(import.meta.url).resolve('pnpm') + pnpmVersion = (JSON.parse(readFileSync(pnpmManifest, 'utf8')) as { version: string }).version + await cp(dirname(pnpmManifest), join(dependencies, 'pnpm'), { recursive: true, dereference: true }) + } } await extractTar({ file: await pythonArchive(target, paths.downloads), cwd: dependencies }) const manifest: PrimaryRuntimeManifest = { @@ -152,9 +221,10 @@ export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOption pythonPackages: lock.pythonPackages, } const entries = workspaceDependencyPaths(output, manifest) - for (const wheel of [...artifact.wheels, ...lock.wheels]) { + for (const wheel of [...lock.targets[target].wheels, ...lock.wheels]) { await unpackPrimaryRuntimeWheel(await downloadPrimaryRuntimeAsset(wheel.url, wheel.sha256, paths.downloads), entries.pythonPackages) } + prunePrimaryRuntimePythonTests(entries.pythonPackages) writeFileSync(join(output, 'runtime.json'), `${JSON.stringify(manifest, undefined, 2)}\n`) const destination = join(paths.runtime, 'primary-runtime') rmSync(destination, { recursive: true, force: true }) @@ -162,8 +232,8 @@ export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOption } finally { rmSync(staging, { recursive: true, force: true }) } - const require = createRequire(import.meta.url) - await prepareOfficeSkillAssets(join(dirname(require.resolve('@deepseek-ai/dsh-skill-office/package.json')), 'assets'), + const assets = options.skillSource ?? join(dirname(createRequire(import.meta.url).resolve('@deepseek-ai/dsh-skill-office/package.json')), 'assets') + await prepareOfficeSkillAssets(assets, join(paths.runtime, 'office-skills')) } diff --git a/scripts/primary-runtime/prune-python-tests.spec.ts b/scripts/primary-runtime/prune-python-tests.spec.ts new file mode 100644 index 0000000000..25b59f3e3c --- /dev/null +++ b/scripts/primary-runtime/prune-python-tests.spec.ts @@ -0,0 +1,85 @@ +import { createHash } from 'node:crypto' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { zipSync } from 'fflate' +import { expect, it } from 'vitest' +import lock from './lock.json' with { type: 'json' } +import { unpackPrimaryRuntimeWheel } from './prepare.ts' +import { prunePrimaryRuntimePythonTests } from './prune-python-tests.ts' + +const records = { + numpy: `numpy-${lock.pythonPackages.numpy}.dist-info/RECORD`, + pandas: `pandas-${lock.pythonPackages.pandas}.dist-info/RECORD`, +} + +it('removes only NumPy/pandas test directories and keeps retained wheel files and RECORD hashes', async () => { + const root = await mkdtemp(join(tmpdir(), 'primary-python-pruning-')) + try { + const removed = [ + 'numpy/tests/test_numpy.py', 'numpy/_core/tests/test_core.py', 'numpy/_core/tests/data/values,small.csv', + 'numpy/testing/tests/test_utils.py', 'pandas/tests/io/test_excel.py', 'pandas/tests/io/data/workbook.xlsx', + ] + const retained = [ + 'numpy/__init__.py', 'numpy/_core/_multiarray_tests.so', 'numpy/_core/_multiarray_tests.pyd', + 'numpy/testing/__init__.py', 'numpy/_core/tests.py', 'numpy/data/values,small.csv', + 'pandas/__init__.py', 'pandas/testing.py', 'pandas/_testing/__init__.py', 'pandas/_libs/testing.pyd', + `numpy-${lock.pythonPackages.numpy}.dist-info/licenses/LICENSE.txt`, + `pandas-${lock.pythonPackages.pandas}.dist-info/LICENSE`, + 'other/tests/test_other.py', 'numpy_extra/tests/test_extra.py', + ] + const files = Object.fromEntries([...removed, ...retained].map(path => [path, Buffer.from(`contents of ${path}\n`)])) + // Quoted paths exercise RECORD's CSV encoding even when a filename contains a comma. + const row = (path: string): string => `"${path}",sha256=${createHash('sha256').update(files[path]!).digest('base64url')},${files[path]!.length}\n` + for (const [name, record] of Object.entries(records)) { + const paths = [...removed, ...retained].filter(path => path.startsWith(`${name}/`) || path.startsWith(`${name}-`)) + files[record] = Buffer.from(`${paths.map(row).join('')}${record},,\n`) + } + const archive = join(root, 'libraries.whl') + const sitePackages = join(root, 'site-packages') + const wheel = Buffer.from(zipSync(files)) + await writeFile(archive, wheel) + await unpackPrimaryRuntimeWheel(archive, sitePackages) + await mkdir(join(sitePackages, 'numpy/empty/tests'), { recursive: true }) + + prunePrimaryRuntimePythonTests(sitePackages) + + for (const path of [...removed, 'numpy/tests', 'numpy/empty/tests', 'pandas/tests']) { + await expect(readFile(join(sitePackages, path))).rejects.toMatchObject({ code: 'ENOENT' }) + } + for (const path of retained) expect(await readFile(join(sitePackages, path))).toEqual(files[path]) + for (const [name, record] of Object.entries(records)) { + const lines = (await readFile(join(sitePackages, record), 'utf8')).trimEnd().split('\n') + const expected = retained.filter(path => path.startsWith(`${name}/`) || path.startsWith(`${name}-`)) + expect(lines).toHaveLength(expected.length + 1) + for (const path of expected) { + const expectedRow = row(path).trimEnd() + expect(lines).toContain(path.includes(',') ? expectedRow : expectedRow.replace(`"${path}"`, path)) + } + expect(lines).toContain(`${record},,`) + } + expect(await readFile(archive)).toEqual(wheel) + const before = await Promise.all(Object.values(records).map(record => readFile(join(sitePackages, record)))) + prunePrimaryRuntimePythonTests(sitePackages) + expect(await Promise.all(Object.values(records).map(record => readFile(join(sitePackages, record))))).toEqual(before) + } finally { + await rm(root, { recursive: true, force: true }) + } +}) + +it.each(['"unterminated,,\n', 'numpy/tests/test_numpy.py,\n', '', ',,\n'])( + 'rejects malformed installed-file records before deleting that distribution\'s tests: %j', async (record) => { + const root = await mkdtemp(join(tmpdir(), 'primary-python-record-')) + try { + await mkdir(join(root, `numpy-${lock.pythonPackages.numpy}.dist-info`)) + await writeFile(join(root, records.numpy), record) + await mkdir(join(root, 'numpy/tests'), { recursive: true }) + const testFile = join(root, 'numpy/tests/test_numpy.py') + await writeFile(testFile, 'test fixture\n') + expect(() => { prunePrimaryRuntimePythonTests(root) }).toThrow('invalid wheel RECORD') + expect(await readFile(testFile, 'utf8')).toBe('test fixture\n') + } finally { + await rm(root, { recursive: true, force: true }) + } + }, +) diff --git a/scripts/primary-runtime/prune-python-tests.ts b/scripts/primary-runtime/prune-python-tests.ts new file mode 100644 index 0000000000..a953ca9afb --- /dev/null +++ b/scripts/primary-runtime/prune-python-tests.ts @@ -0,0 +1,41 @@ +/** Remove upstream Python test suites from the staged primary runtime. */ + +import { readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import Papa from 'papaparse' +import lock from './lock.json' with { type: 'json' } + +function removeTestDirectories(root: string, directory: string): string[] { + const removed: string[] = [] + for (const entry of readdirSync(join(root, directory), { withFileTypes: true })) { + if (!entry.isDirectory()) continue + const path = `${directory}/${entry.name}` + if (entry.name === 'tests') { + rmSync(join(root, path), { recursive: true }) + removed.push(path) + } else { + removed.push(...removeTestDirectories(root, path)) + } + } + return removed +} + +/** + * Prune NumPy/pandas tests and their RECORD rows, preserving testing helpers and native extensions. + * @param sitePackages - Staged site-packages directory containing the locked wheels. + * @throws If a locked distribution's RECORD is missing or malformed. + */ +export function prunePrimaryRuntimePythonTests(sitePackages: string): void { + for (const name of ['numpy', 'pandas'] as const) { + const record = join(sitePackages, `${name}-${lock.pythonPackages[name]}.dist-info`, 'RECORD') + const { data, errors } = Papa.parse(readFileSync(record, 'utf8'), { delimiter: ',', skipEmptyLines: true }) + if (errors.length > 0 || data.length === 0 + || !data.every((row): row is [string, string, string] => row.length === 3 && Boolean(row[0]))) { + throw new Error(`primary runtime: invalid wheel RECORD: ${record}`) + } + const removed = removeTestDirectories(sitePackages, name) + if (removed.length === 0) continue + const retained = data.filter(([path]) => !removed.some(directory => path.startsWith(`${directory}/`))) + writeFileSync(record, `${Papa.unparse(retained, { newline: '\n' })}\n`) + } +} diff --git a/scripts/primary-runtime/smoke.py b/scripts/primary-runtime/smoke.py index 232d67e108..96e9b6c32b 100644 --- a/scripts/primary-runtime/smoke.py +++ b/scripts/primary-runtime/smoke.py @@ -1,6 +1,8 @@ """Exercise the relocated Office payload with its own isolated interpreter.""" +import csv import importlib.metadata +import io import json from pathlib import Path import re @@ -9,6 +11,8 @@ import sys import tempfile import numpy +# NumPy testing helpers require this native extension even without upstream test directories. +import numpy._core._multiarray_tests import pandas from docx import Document from docx.shared import Inches as DocxInches @@ -38,6 +42,16 @@ def main(): assert actual == expected, (name, actual, expected) assert numpy.arange(4).sum() == 6 assert pandas.DataFrame({"n": [1, 2]}).n.sum() == 3 + numpy.testing.assert_allclose(numpy.arange(4), [0, 1, 2, 3]) + pandas.testing.assert_frame_equal(pandas.DataFrame({"n": [1, 2]}), pandas.DataFrame({"n": [1, 2]})) + for library in (numpy, pandas): + root = Path(library.__file__).parent + assert not any(path.is_dir() for path in root.rglob("tests")), root + distribution = importlib.metadata.distribution(library.__name__) + record = distribution.read_text("RECORD") + assert record, library.__name__ + for file, _hash, _size in csv.reader(io.StringIO(record)): + assert distribution.locate_file(file).is_file(), file with tempfile.TemporaryDirectory(prefix="dsh-office-smoke-") as directory: root = Path(directory) diff --git a/scripts/python-runtime-download.spec.ts b/scripts/python-runtime-download.spec.ts new file mode 100644 index 0000000000..7d3b1bcfa4 --- /dev/null +++ b/scripts/python-runtime-download.spec.ts @@ -0,0 +1,135 @@ +/** Explicit resource acquisition through the Python carrier's private dispatcher. */ +import { spawn } from 'node:child_process' +import { mkdir, mkdtemp, readFile, readdir, rm, writeFile, copyFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { expect, it } from 'vitest' + +async function fixture() { + const root = await mkdtemp(join(tmpdir(), 'dsh-resource-download-')) + const bootstrap = join(root, 'node', 'runtime-bootstrap.mjs') + const platform = process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'win' : process.platform + const resources = join(root, `${platform}-${process.arch}`) + const cache = join(root, 'cache') + async function file(path: string, content: string) { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, content) + } + await file(join(resources, 'downloads.json'), JSON.stringify({ identity: 'a'.repeat(64), office: [], pnpm: {} })) + await mkdir(dirname(bootstrap), { recursive: true }) + await copyFile(new URL('../python/sdk-runtime/runtime-bootstrap.mjs', import.meta.url), bootstrap) + await file(join(root, 'node/node_modules/@deepseek-ai/dsh-http-proxy/package.json'), '{"type":"module","exports":"./index.mjs"}') + await file(join(root, 'node/node_modules/@deepseek-ai/dsh-http-proxy/index.mjs'), 'export async function installProxyFromEnvironment(env) { if (typeof env.get !== "function") throw new Error("missing environment lookup"); return async () => {} }') + await file(join(root, 'node/node_modules/@deepseek-ai/dsh-sandbox-windows-acl/package.json'), '{"type":"module","exports":{"./runner":"./runner.mjs"}}') + await file(join(root, 'node/node_modules/@deepseek-ai/dsh-sandbox-windows-acl/runner.mjs'), '') + await file(join(root, 'node/node_modules/@deepseek-ai/dsh/package.json'), '{"type":"module"}') + await file(join(root, 'node/node_modules/@deepseek-ai/dsh/lib/bin.js'), 'export async function runCli() { if (process.argv[2]) { console.log(JSON.stringify({ argv: process.argv.slice(2) })); return } console.log(JSON.stringify({ primary: process.env.DSH_BUNDLED_PRIMARY_RUNTIME, office: process.env.DSH_BUNDLED_OFFICE_CLI })) }') + await file(join(root, 'node/primary-runtime.mjs'), ` + import { mkdir, writeFile } from 'node:fs/promises'; + import { join } from 'node:path'; + export async function downloadNodeRuntime(target, destination) { + if (process.env.DSH_RUNTIME_DOWNLOAD !== undefined) throw new Error("private selector leaked"); + await mkdir(join(destination, 'bin'), { recursive: true }); + await writeFile(join(destination, 'bin', process.platform === 'win32' ? 'node.exe' : 'node'), 'node'); + } + export async function preparePrimaryRuntime({ output }) { + if (process.env.DSH_RUNTIME_DOWNLOAD !== undefined) throw new Error("private selector leaked"); + await mkdir(join(output, 'primary-runtime'), { recursive: true }); + await writeFile(join(output, 'primary-runtime/runtime.json'), '{}'); + } + `) + await file(join(root, 'node/office-sidecar.mjs'), ` + import { mkdir, writeFile } from 'node:fs/promises'; + import { join } from 'node:path'; + export async function downloadOfficeSidecar(packages, destination) { + const directory = join(destination, 'node_modules/@deepseek-ai/libreoffice-kit/lib'); + await mkdir(directory, { recursive: true }); + await writeFile(join(directory, 'cli.js'), 'cli'); + if (process.env.DSH_FIXTURE_FAIL) throw new Error('fixture download failed'); + if (process.env.DSH_FIXTURE_BARRIER) { + process.stderr.write('ready\\n'); + await new Promise(resolve => process.stdin.once('data', resolve)); + process.stdin.pause(); + } + } + `) + function run(resource?: string, extra: NodeJS.ProcessEnv = {}, args: string[] = []) { + const child = spawn(process.execPath, [bootstrap, ...args], { + env: { ...process.env, DSH_RESOURCE_CACHE: cache, DSH_RUNTIME_DOWNLOAD: resource, DSH_BUNDLED_PRIMARY_RUNTIME: undefined, + DSH_OFFICE_SIDECAR: undefined, DSH_BUNDLED_OFFICE_CLI: undefined, ...extra }, + stdio: ['pipe', 'pipe', 'pipe'], + }) + let stdout = '', stderr = '' + child.stdout.on('data', (data) => { stdout += String(data) }) + let reportReady: (() => void) | undefined + let failReady: ((error: Error) => void) | undefined + const ready = extra.DSH_FIXTURE_BARRIER === undefined ? Promise.resolve() + : new Promise((resolve, reject) => { reportReady = resolve; failReady = reject }) + child.stderr.on('data', (data) => { stderr += String(data); if (stderr.includes('ready\n')) reportReady?.() }) + const done = new Promise<{ code: number | null; stdout: string; stderr: string }>((resolve, reject) => { + child.once('error', reject) + child.once('close', (code) => { failReady?.(new Error(`download exited before readiness: ${stderr}`)); resolve({ code, stdout, stderr }) }) + }) + return { child, ready, done } + } + return { root, cache, run } +} + +it('starts without downloading, independently downloads either resource, and reuses completed caches', async () => { + const { root, cache, run } = await fixture() + try { + expect(await run().done).toMatchObject({ code: 0, stdout: '{}\n' }) + await expect(readdir(cache)).rejects.toMatchObject({ code: 'ENOENT' }) + const primary = await run('primary').done + expect(primary.code, primary.stderr).toBe(0) + expect(JSON.parse(primary.stdout)).toBe(join(cache, 'a'.repeat(64), 'primary', 'primary-runtime')) + await expect(readdir(join(cache, 'a'.repeat(64), 'office'))).rejects.toMatchObject({ code: 'ENOENT' }) + const office = await run('office').done + expect(office.code, office.stderr).toBe(0) + const location = JSON.parse(office.stdout) as string + expect(await readFile(join(location, 'node_modules/@deepseek-ai/libreoffice-kit/lib/cli.js'), 'utf8')).toBe('cli') + expect(await run('office', { DSH_FIXTURE_FAIL: '1' }).done).toMatchObject({ code: 0, stdout: office.stdout }) + } finally { await rm(root, { recursive: true, force: true }) } +}) + +it('cleans failed staging and allows an explicit retry', async () => { + const { root, cache, run } = await fixture() + try { + const failed = await run('office', { DSH_FIXTURE_FAIL: '1' }).done + expect(failed.code).toBe(1) + expect(failed.stderr).toContain('fixture download failed') + expect(await readdir(join(cache, 'a'.repeat(64)))).toEqual([]) + expect((await run('office').done).code).toBe(0) + } finally { await rm(root, { recursive: true, force: true }) } +}) + +it('publishes one complete cache when two processes download concurrently', async () => { + const { root, cache, run } = await fixture() + const jobs = [run('office', { DSH_FIXTURE_BARRIER: '1' }), run('office', { DSH_FIXTURE_BARRIER: '1' })] + try { + await Promise.all(jobs.map(job => job.ready)) + for (const job of jobs) job.child.stdin.end('continue') + const results = await Promise.all(jobs.map(job => job.done)) + for (const result of results) expect(result.code, result.stderr).toBe(0) + expect(results[0]!.stdout).toBe(results[1]!.stdout) + expect(await readdir(join(cache, 'a'.repeat(64)))).toEqual(['office']) + } finally { + for (const job of jobs) if (job.child.exitCode === null && job.child.signalCode === null) job.child.kill() + await Promise.allSettled(jobs.map(job => job.done)) + await rm(root, { recursive: true, force: true }) + } +}) + + +it('leaves ordinary CLI arguments to the CLI and rejects an invalid private download selector', async () => { + const { root, cache, run } = await fixture() + try { + expect(await run(undefined, {}, ['--download-office']).done).toMatchObject({ + code: 0, stdout: JSON.stringify({ argv: ['--download-office'] }) + '\n', + }) + const rejected = await run('invalid').done + expect(rejected.code).toBe(1) + expect(rejected.stderr).toContain('DSH_RUNTIME_DOWNLOAD must select office or primary') + await expect(readdir(cache)).rejects.toMatchObject({ code: 'ENOENT' }) + } finally { await rm(root, { recursive: true, force: true }) } +}) diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index d79a0a3f88..417edb67d2 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -884,10 +884,18 @@ def main() -> None: def smoke_sdk_authoring(base_url: str, executable: Path, update_snapshots: bool) -> None: """Query the bundled Python and switch skills without replacing that environment.""" + + with tempfile.TemporaryDirectory(prefix="dsh-sdk-authoring-cache-") as cache_directory: + _smoke_sdk_authoring_cached(base_url, executable, update_snapshots, Path(cache_directory)) + + +def _smoke_sdk_authoring_cached(base_url: str, executable: Path, update_snapshots: bool, cache: Path) -> None: from deepseek_harness import DeepSeekHarness - resources = executable.with_name(executable.name.removeprefix("deepseek-harness-sdk-runtime-").removesuffix(".exe")) - manifest = json.loads((resources / "primary-runtime/runtime.json").read_text()) + cache_environment = {**os.environ, "DSH_RESOURCE_CACHE": str(cache), "DSH_RUNTIME_DOWNLOAD": "primary"} + primary = Path(json.loads(subprocess.check_output([str(executable)], encoding="utf-8", env=cache_environment))) + resources = primary.parent + manifest = json.loads((primary / "runtime.json").read_text()) for mode in ("default", "replacement", "disabled"): with tempfile.TemporaryDirectory(prefix="dsh-sdk-authoring-") as temporary: root = Path(temporary).resolve() @@ -903,7 +911,7 @@ def smoke_sdk_authoring(base_url: str, executable: Path, update_snapshots: bool) provider="deepseek-official", model="smoke-model", cwd=str(root), dsh_bin=str(executable), dsh_home=str(home), patches=(str(patch),), api_key="sk-keyless-smoke", base_url=base_url, - env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1"}, + env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1", "DSH_RESOURCE_CACHE": str(cache)}, request_timeout_seconds=60, ) as harness: result = harness.run(AUTHORING_PROMPT, session_id="authoring") @@ -958,7 +966,9 @@ def smoke_sdk_office(executable: Path) -> None: shutil.copytree(source, destination) else: shutil.copy2(source, destination) - office = root / f"{stem}-office" + cache_environment = {**os.environ, "DSH_RESOURCE_CACHE": str(root / "cache")} + office = Path(json.loads(subprocess.check_output([str(relocated)], encoding="utf-8", env={**cache_environment, "DSH_RUNTIME_DOWNLOAD": "office"}))) + assert not (office.parent / "primary").exists(), "Office download unexpectedly prepared the authoring environment" adapter = office / "node_modules/@deepseek-ai/libreoffice-kit/package.json" native = stem.removeprefix("deepseek-harness-sdk-runtime-").replace("win-", "win32-").replace("macos-", "darwin-") declared = json.loads(adapter.read_text(encoding="utf-8")).get("optionalDependencies", {}) @@ -997,7 +1007,7 @@ def smoke_sdk_office(executable: Path) -> None: patches=(str(patch),), api_key="sk-keyless-smoke", base_url="http://127.0.0.1:9", - env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1"}, + env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1", "DSH_RESOURCE_CACHE": str(root / "cache")}, # The startup plugin awaits a converter with a 120-second deadline before JSON-RPC is ready. initialize_timeout_seconds=180, request_timeout_seconds=180, @@ -1007,10 +1017,32 @@ def smoke_sdk_office(executable: Path) -> None: if result["backend"] != expected_backend: raise AssertionError(f"Office conversion did not use {expected_backend}: {result}") if not result["moduleUrl"].startswith(office.as_uri() + "/"): - raise AssertionError(f"Office module was not loaded from the relocated wheel: {result}") + raise AssertionError(f"Office module was not loaded from the downloaded sidecar: {result}") pdf = output.read_bytes() if len(pdf) < 100 or not pdf.startswith(b"%PDF-"): raise AssertionError(f"Office conversion produced an invalid PDF at {output}") + workbook = root / "input.xlsx" + with zipfile.ZipFile(workbook, "w", compression=zipfile.ZIP_DEFLATED) as archive: + archive.writestr("[Content_Types].xml", '') + archive.writestr("_rels/.rels", '') + archive.writestr("xl/workbook.xml", '') + archive.writestr("xl/_rels/workbook.xml.rels", '') + archive.writestr("xl/worksheets/sheet1.xml", '1+20') + subprocess.run([sys.executable, "-c", """ +import sys, zipfile +from pathlib import Path +from xml.etree import ElementTree +from deepseek_harness.office import convert, render_images, recalculate +root = Path(sys.argv[1]) +assert convert(root / 'document.docx', root / 'python.pdf').backend == sys.argv[2] +rendered = render_images(root / 'python.pdf', root / 'images', pages=[1]) +assert rendered.images and rendered.images[0].path.read_bytes().startswith(b'\\x89PNG') +recalculate(root / 'input.xlsx', root / 'calculated.xlsx') +with zipfile.ZipFile(root / 'calculated.xlsx') as archive: + sheet = ElementTree.fromstring(archive.read('xl/worksheets/sheet1.xml')) + assert sheet.find('.//{*}c[@r="A1"]/{*}v').text == '3' +""", str(root), expected_backend], env={name: value for name, value in cache_environment.items() if name != "DSH_OFFICE_SIDECAR"}, check=True, timeout=180) + assert not (office.parent / "primary").exists(), "Python Office API unexpectedly downloaded authoring resources" print(f"smoke-python-runtime: relocated Office {result['backend']} DOCX produced {len(pdf)} PDF bytes") @@ -1336,6 +1368,8 @@ def smoke_sdk_dynamic_tools(base_url: str, executable: Path, update_snapshots: b "DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1", "HOME": str(user_home), "USERPROFILE": str(user_home), "DSH_AGENTS_HOME": str(user_home / ".agents"), + # Pin the tool baseline independently of optional resources in the host cache. + "DSH_PRIMARY_RUNTIME": "", }, api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60, ) as harness: @@ -2307,11 +2341,17 @@ def build_dynamic_tools_snapshot_files( assert changes == [[], [{"type": "tool_addition", "tool": {"type": "tool_reference", "name": "snapshot_ping"}}], []], changes assert requests[1]["messages"][:len(requests[0]["messages"])] == requests[0]["messages"], "tool addition changed earlier messages" replacements = [(str(cwd), "{{cwd}}"), (result.session_id, "{{session}}")] + header_references = {header["seq"]: f"{{{{header:{index + 1}}}}}" for index, header in enumerate(headers)} + snapshot_events = [ + {**event, "data": {**event["data"], "headerSeq": header_references[event["data"]["headerSeq"]]}} + if event["type"] == "developer/message" and "headerSeq" in event["data"] else event + for event in events if event["type"] in {"request/header", "request/context", "developer/message"} + ] evidence = { "finalResponse": result.final_response, "declarations": declarations, "requestToolChanges": changes, - "events": [event for event in events if event["type"] in {"request/header", "request/context", "developer/message"}], + "events": snapshot_events, "toolCalls": calls, "pingResult": ping, } diff --git a/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json b/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json index 7a8a75aa85..d93f0c33c1 100644 --- a/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json +++ b/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json @@ -56,7 +56,6 @@ "job_list", "job_output", "list_agents", - "load_workspace_dependencies", "read", "read_image", "send_message", @@ -87,7 +86,7 @@ }, { "type": "request/header", - "seq": 20, + "seq": 19, "time": 0, "data": { "header": { @@ -113,7 +112,6 @@ "job_list", "job_output", "list_agents", - "load_workspace_dependencies", "read", "read_image", "send_message", @@ -135,7 +133,7 @@ }, { "type": "developer/message", - "seq": 21, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -153,13 +151,13 @@ "role": "developer", "id": "{{messageId}}" }, - "headerSeq": 20 + "headerSeq": "{{header:2}}" }, "surfaceOp": "append" }, { "type": "request/header", - "seq": 28, + "seq": 26, "time": 0, "data": { "header": { @@ -185,7 +183,6 @@ "job_list", "job_output", "list_agents", - "load_workspace_dependencies", "read", "read_image", "send_message", @@ -206,7 +203,7 @@ }, { "type": "developer/message", - "seq": 29, + "seq": 27, "time": 0, "data": { "turn": 1, diff --git a/scripts/verify-package-meta.spec.ts b/scripts/verify-package-meta.spec.ts index 1aaea96429..3227c18c04 100644 --- a/scripts/verify-package-meta.spec.ts +++ b/scripts/verify-package-meta.spec.ts @@ -1,11 +1,13 @@ /** Plugin metadata checks use private source-only packages and real resource exports. */ -import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { afterEach, beforeEach, expect, it } from 'vitest' import { packageMetaProblems } from './verify-package-meta.ts' +const validSvg = '' + let root: string let dir: string beforeEach(() => { @@ -26,7 +28,7 @@ function json(path: string, contents: unknown): void { function manifest(fields: object = {}): void { json('package.json', { - name: '@test/plugin', + name: '@deepseek-ai/dsh-test-plugin', description: 'Package description', type: 'module', exports: { '.': './lib/index.js', './package.json': './package.json', './locale/*.json': './locale/*.json' }, @@ -46,14 +48,89 @@ it('does not require locale resources for exported package name and description' it.each(['legacy.svg', './legacy.svg'])('accepts a manifest-only icon %s', (icon) => { manifest({ icon, files: ['legacy.svg'] }) - file('legacy.svg', 'legacy') + file('legacy.svg', validSvg) expect(packageMetaProblems(root)).toEqual([]) }) +it.each(['manifest', 'export'])('accepts SVG geometry and local references selected by %s', (selection) => { + manifest({ + ...selection === 'manifest' ? { icon: './icon.svg' } : {}, + exports: { './package.json': './package.json', './icon': './icon.svg' }, files: ['icon.svg'], + }) + file('icon.svg', validSvg.replace('', ''), 'well-formed XML'], + ['wrong root', validSvg.replaceAll('svg', 'g'), 'svg root'], + ['wrong namespace', validSvg.replace('http://www.w3.org/2000/svg', 'urn:other'), 'SVG namespace'], + ['toolbar canvas', validSvg.replace('0 0 36 36', '0 0 16 16'), 'viewBox="0 0 36 36"'], + ['toolbar width', validSvg.replace('width="36"', 'width="16"'), 'width="36"'], + ['toolbar height', validSvg.replace('height="36"', 'height="16"'), 'height="36"'], + ['missing definition', validSvg.replace('url(#ink)', 'url(#missing)'), 'must name a local id'], + ['external definition', validSvg.replace('url(#ink)', 'url(https://example.test/ink.svg#ink)'), 'must name a local id'], + ['external href', validSvg.replace(']+\/>/u, ''), 'must contain geometry'], + ...['script', 'style', 'image', 'foreignObject', 'text', 'animate', 'animateTransform', 'animateMotion', 'set'] + .map(tag => [tag, validSvg.replace('', `<${tag} />`), 'self-contained geometry']), +])('rejects SVG %s through metadata verification', (_name, source, diagnostic) => { + manifest({ icon: './icon.svg', files: ['icon.svg'] }) + file('icon.svg', source) + const problems = packageMetaProblems(root) + expect(problems).toHaveLength(1) + expect(problems[0]).toContain(`${join(dir, 'icon.svg')}:`) + expect(problems[0]).toContain(diagnostic) +}) + +it('validates the selected ./icon SVG fallback without a manifest icon', () => { + manifest({ exports: { './package.json': './package.json', './icon': './icon.svg' }, files: ['icon.svg'] }) + file('icon.svg', validSvg.replace('width="36"', 'width="16"')) + expect(packageMetaProblems(root)).toEqual([`${realpathSync(join(dir, 'icon.svg'))}: SVG icon must use width="36" height="36" viewBox="0 0 36 36"`]) +}) + +it('validates the manifest icon instead of the lower-priority SVG export', () => { + manifest({ icon: './icon.svg', exports: { './package.json': './package.json', './icon': './fallback.svg' }, files: ['icon.svg', 'fallback.svg'] }) + file('icon.svg', validSvg) + file('fallback.svg', 'not XML') + expect(packageMetaProblems(root)).toEqual([]) +}) + +it.each(['png', 'jpg', 'jpeg', 'webp'].flatMap(extension => ['manifest', 'export'].map(selection => ({ extension, selection }))))( + 'keeps $extension bytes opaque for $selection icons', ({ extension, selection }) => { + manifest({ + ...selection === 'manifest' ? { icon: `./icon.${extension}` } : {}, + exports: { './package.json': './package.json', './icon': `./icon.${extension}` }, files: [`icon.${extension}`], + }) + file(`icon.${extension}`, 'opaque raster bytes') + expect(packageMetaProblems(root)).toEqual([]) + }, +) + +it.each(['@local/demo', '@deepseek-ai/other'])('keeps nonofficial %s SVGs outside official artwork conventions', (name) => { + manifest({ name, icon: './icon.svg', files: ['icon.svg'] }) + file('icon.svg', '') + expect(packageMetaProblems(root)).toEqual([]) +}) + +it('checks official artwork in named nested packages', () => { + manifest({ exports: { '.': './entry.js' }, files: ['nested'] }) + json('nested/package.json', { + name: '@deepseek-ai/dsh-nested', exports: { './icon': './icon.svg' }, files: ['icon.svg'], + }) + file('nested/icon.svg', validSvg.replace('width="36"', 'width="16"')) + expect(packageMetaProblems(root)).toEqual([ + `${realpathSync(join(dir, 'nested/icon.svg'))}: SVG icon must use width="36" height="36" viewBox="0 0 36 36"`, + ]) +}) + it('reads the manifest image and still rejects an unresolvable lower-priority export', () => { manifest({ icon: './legacy.svg', files: ['legacy.svg'], exports: { './package.json': './package.json', './icon': '../invalid.svg' } }) - file('legacy.svg', 'legacy') + file('legacy.svg', validSvg) expect(packageMetaProblems(root)).toEqual([`${join(dir, 'package.json')}: exports["./icon"] must resolve to a file`]) }) @@ -62,7 +139,7 @@ it.each([['display'], ['display/manifest.json'], ['display/logo.svg']]) manifest({ files, exports: { './package.json': './display/manifest.json' } }) json('display/manifest.json', { icon: './logo.svg' }) - file('display/logo.svg', 'legacy') + file('display/logo.svg', validSvg) const problems = packageMetaProblems(root).join('\n') if (files.includes('display')) expect(problems).toBe('') else expect(problems).toContain(`files must include ${files.includes('display/manifest.json') ? 'display/logo.svg' : 'display/manifest.json'}`) @@ -71,15 +148,15 @@ it.each([['display'], ['display/manifest.json'], ['display/logo.svg']]) it('requires the root manifest icon declaration to remain accessible', () => { manifest({ icon: './legacy.svg', files: ['legacy.svg'], exports: { './icon': './legacy.svg' } }) - file('legacy.svg', 'legacy') + file('legacy.svg', validSvg) expect(packageMetaProblems(root).join('\n')).toContain('exports must expose its icon declaration') }) it.each([null, false, '', './missing.svg'])('diagnoses legacy icon %j despite a valid fallback export', (icon) => { manifest({ icon, files: ['fallback.svg'], exports: { './package.json': './package.json', './icon': './fallback.svg' } }) - file('fallback.svg', 'fallback') - expect(packageMetaProblems(root).join('\n')).toContain('Plugin metadata for @test/plugin:') + file('fallback.svg', validSvg) + expect(packageMetaProblems(root).join('\n')).toContain('Plugin metadata for @deepseek-ai/dsh-test-plugin:') }) it.each([undefined, ['art'], ['art/*.webp'], ['./art/icon.webp']])('accepts an exported icon without other resources: %j', (files) => { @@ -96,7 +173,7 @@ it.each([['lib'], ['art', '!art/icon.webp']])('rejects an exported icon omitted it.each([false, 1, '', './icon.gif', '/tmp/icon.svg', './missing.png', '../outside.svg'])('validates exported icon targets: %j', (icon) => { manifest({ exports: { './icon': icon } }) - expect(packageMetaProblems(root).join('\n')).toMatch(/Plugin metadata for @test\/plugin:|exports\["\.\/icon"\] must resolve to a file/u) + expect(packageMetaProblems(root).join('\n')).toMatch(/Plugin metadata for @deepseek-ai\/dsh-test-plugin:|exports\["\.\/icon"\] must resolve to a file/u) }) it.each([['assets'], ['lib']])('checks publication of the Node-selected conditional icon: %j', (...files) => { @@ -116,8 +193,8 @@ it('ignores package.json exported at a subpath plugin address', () => { it.each([['search.svg'], ['lib']])('reads and publishes a subpath icon export: %j', (...files) => { manifest({ icon: './root.svg', exports: { './package.json': './package.json', './search': './search.js', './search/icon': './search.svg' }, files: ['root.svg', ...files] }) - file('root.svg', 'root') - file('search.svg', 'search') + file('root.svg', validSvg) + file('search.svg', validSvg) const problems = packageMetaProblems(root) if (files.includes('search.svg')) expect(problems).toEqual([]) else expect(problems).toEqual([`${join(dir, 'package.json')}: files must include search.svg`]) @@ -165,8 +242,8 @@ it.each([ files: ['locale', 'icon.svg'], ...fields, }) json('examples/nested/locale/en.json', { meta: { title } }) - file('examples/nested/icon.svg', '') - file('examples/nested/fallback.svg', '') + file('examples/nested/icon.svg', validSvg) + file('examples/nested/fallback.svg', validSvg) const problems = packageMetaProblems(root) if (expected === undefined) expect(problems).toEqual([]) else expect(problems.join('\n')).toContain(expected) @@ -255,7 +332,7 @@ it('does not read built locale JSON while checking a source icon declaration', ( icon: './icon.svg', files: ['icon.svg'], exports: { './package.json': './package.json', './locale/en.json': './lib/locale/en.json' }, }) - file('icon.svg', '') + file('icon.svg', validSvg) file('lib/locale/en.json', '{') json('locale/en.json', { meta: { title: 'Plugin' } }) const problems = packageMetaProblems(root).join('\n') @@ -270,7 +347,7 @@ it.each(['lib', 'tests'])('rejects an icon declaration when its only locale is e }) file(`${directory}/locale/en.json`, '{}') const problems = packageMetaProblems(root).join('\n') - expect(problems).toContain('@test/plugin: icon metadata requires locale resources to resolve to source JSON') + expect(problems).toContain('@deepseek-ai/dsh-test-plugin: icon metadata requires locale resources to resolve to source JSON') expect(problems).not.toContain('Plugin metadata for') }) @@ -594,7 +671,7 @@ it('reports metadata failures from more than one workspace package', () => { json('locale/en.json', { meta: { title: 'Second' } }) json('locale/zh.json', { meta: { description: '' } }) const problems = packageMetaProblems(root).join('\n') - expect(problems).toContain('@test/plugin') + expect(problems).toContain('@deepseek-ai/dsh-test-plugin') expect(problems).toContain('@test/second') }) @@ -603,6 +680,6 @@ it.each([[], ['locale']])('rejects a display manifest in build output without re if (locales.length > 0) json('locale/en.json', { meta: { title: 'Plugin' } }) file('lib/display.json', '{') const problems = packageMetaProblems(root).join('\n') - expect(problems).toContain('@test/plugin/package.json: display manifest must resolve to source JSON') + expect(problems).toContain('@deepseek-ai/dsh-test-plugin/package.json: display manifest must resolve to source JSON') expect(problems).not.toContain('Plugin metadata for') }) diff --git a/scripts/verify-package-meta.ts b/scripts/verify-package-meta.ts index a7fdd34f1a..0d0188530e 100644 --- a/scripts/verify-package-meta.ts +++ b/scripts/verify-package-meta.ts @@ -1,8 +1,9 @@ -/** Validate source locale metadata, Node resource exports, and package publication selections. */ +/** Validate source locale metadata, official SVG artwork, Node resource exports, and publication selections. */ import { globSync, readFileSync, realpathSync } from 'node:fs' import { basename, dirname, join, matchesGlob, relative, resolve } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' +import { JSDOM } from 'jsdom' import { readPluginMeta, resolvePluginResource } from '../packages/boot/app-boot/src/package-meta.ts' interface Manifest { @@ -72,7 +73,46 @@ function published(file: string, files: string[]): boolean { && !files.some(pattern => pattern.startsWith('!') && covered(pattern.slice(1))) } -function packageProblems(manifestPath: string): string[] { +/** Structural artwork conventions only; not an SVG sanitizer or a rendered-pixel check. */ +function svgArtworkProblems(icon: string, parser: DOMParser): string[] { + const prefix = 'data:image/svg+xml;base64,' + if (!icon.startsWith(prefix)) return [] + const document = parser.parseFromString(Buffer.from(icon.slice(prefix.length), 'base64').toString('utf8'), 'image/svg+xml') + if (document.querySelector('parsererror') !== null) return ['SVG icon must contain well-formed XML'] + const svg = document.documentElement + if (svg.localName !== 'svg' || svg.namespaceURI !== 'http://www.w3.org/2000/svg') { + return ['SVG icon must have an svg root in the SVG namespace'] + } + const problems: string[] = [] + if (svg.getAttribute('viewBox') !== '0 0 36 36' || svg.getAttribute('width') !== '36' || svg.getAttribute('height') !== '36') { + problems.push('SVG icon must use width="36" height="36" viewBox="0 0 36 36"') + } + if (svg.querySelector('path, rect, circle, ellipse, line, polyline, polygon') === null) { + problems.push('SVG icon must contain geometry') + } + if (svg.querySelector('script, style, image, foreignObject, text, animate, animateTransform, animateMotion, set') !== null) { + problems.push('SVG icon must use self-contained geometry without scripts, styles, images, foreignObject, text, or animation') + } + const elements = [svg, ...svg.querySelectorAll('*')] + const ids = elements.flatMap(element => element.hasAttribute('id') ? [element.id] : []) + const uniqueIds = new Set(ids) + if (uniqueIds.size !== ids.length) problems.push('SVG icon must use unique ids') + for (const element of elements) { + for (const attribute of element.attributes) { + if (/^on/iu.test(attribute.name)) problems.push(`SVG icon must not use event handler ${attribute.name}`) + const references = [...attribute.value.matchAll(/url\(([^)]+)\)/gu)].map(match => (match[1] ?? '').replace(/['"]/gu, '').trim()) + if (attribute.localName === 'href') references.push(attribute.value) + for (const reference of references) { + if (!reference.startsWith('#') || !uniqueIds.has(reference.slice(1))) { + problems.push(`SVG icon reference ${reference} must name a local id`) + } + } + } + } + return problems +} + +function packageProblems(manifestPath: string, parser: DOMParser): string[] { const problems: string[] = [] const dir = realpathSync(dirname(manifestPath)) const pkg = JSON.parse(readFileSync(manifestPath, 'utf8')) as Manifest @@ -152,6 +192,9 @@ function packageProblems(manifestPath: string): string[] { : displayManifest !== undefined && typeof iconDocument?.icon === 'string' ? resolve(dirname(displayManifest), iconDocument.icon) : lookup(`${specifier}/icon`) + if (pkg.name.startsWith('@deepseek-ai/dsh-') && meta?.icon !== undefined) { + problems.push(...svgArtworkProblems(meta.icon, parser).map(problem => `${iconTarget ?? specifier}: ${problem}`)) + } for (const target of [displayManifest, iconTarget]) { if (target === undefined) continue const file = relative(dir, target).replaceAll('\\', '/') @@ -209,18 +252,26 @@ function packageProblems(manifestPath: string): string[] { })) continue problems.push(`${manifestPath}: exports must expose ${document.file} as a plugin locale resource with an en.json discovery baseline`) } - return problems.concat(children.flatMap(child => packageProblems(join(dir, child.file)))) + return problems.concat(children.flatMap(child => packageProblems(join(dir, child.file), parser))) } /** * Check workspace and named nested package metadata without evaluating plugin entries or reading built output. + * Selected SVG icons in @deepseek-ai/dsh-* packages follow official artwork conventions. + * Other packages and supported raster icons keep their metadata and publication checks. * @param root - repository or fixture root. - * @returns diagnostics for an empty package corpus, invalid metadata, inaccessible resources, or omitted publication files. + * @returns diagnostics for an empty package corpus, invalid metadata or SVG artwork, inaccessible resources, or omitted publication files. */ export function packageMetaProblems(root: string): string[] { const manifests = globSync('packages/*/*/package.json', { cwd: root }).map(path => path.replaceAll('\\', '/')) if (manifests.length === 0) return [`${root}: no workspace package manifests found`] - return manifests.flatMap(path => packageProblems(join(root, path))) + const { window } = new JSDOM() + try { + const parser = new window.DOMParser() + return manifests.flatMap(path => packageProblems(join(root, path), parser)) + } finally { + window.close() + } } if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { @@ -229,6 +280,6 @@ if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath( console.error(problems.join('\n')) process.exitCode = 1 } else { - console.log('verify-package-meta: plugin locale metadata, resource exports, and publication files are valid.') + console.log('verify-package-meta: plugin locale metadata, SVG artwork, resource exports, and publication files are valid.') } } diff --git a/snapshots/web/auto-review-approval/ui.expected.md b/snapshots/web/auto-review-approval/ui.expected.md index 71cc3eb2b2..4d29eebaea 100644 --- a/snapshots/web/auto-review-approval/ui.expected.md +++ b/snapshots/web/auto-review-approval/ui.expected.md @@ -18,9 +18,7 @@ - 'button "Tool call Error: Auto review of tool \"mystery\" failed; its body was not executed: auto-review: reviewer ended with error UNKNOWN: provider unavailable"' - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" -- 'button "Access mode, current: Auto review EXP"': - - text: Auto review - - superscript: EXP +- 'button "Access mode, current: Auto review"': Auto review - button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] - button "1 turns 2 steps" @@ -49,9 +47,7 @@ - button "Inspect" - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" -- 'button "Access mode, current: Auto review EXP"': - - text: Auto review - - superscript: EXP +- 'button "Access mode, current: Auto review"': Auto review - button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] - button "1 turns 2 steps" diff --git a/snapshots/web/auto-review-denial/ui.expected.md b/snapshots/web/auto-review-denial/ui.expected.md index d3172476e3..45a660ffb2 100644 --- a/snapshots/web/auto-review-denial/ui.expected.md +++ b/snapshots/web/auto-review-denial/ui.expected.md @@ -19,9 +19,7 @@ - button "Bash Rejected by Auto review" - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" -- 'button "Access mode, current: Auto review EXP"': - - text: Auto review - - superscript: EXP +- 'button "Access mode, current: Auto review"': Auto review - button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] - button "1 turns 2 steps" @@ -51,9 +49,7 @@ - button "Inspect" - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" -- 'button "Access mode, current: Auto review EXP"': - - text: Auto review - - superscript: EXP +- 'button "Access mode, current: Auto review"': Auto review - button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] - button "1 turns 2 steps" @@ -110,9 +106,7 @@ - tabpanel "Result": "AutoReviewDeniedError: AUTO_REVIEW_DENIED Error: Auto review rejected tool \"bash\"; its body was not executed" - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" -- 'button "Access mode, current: Auto review EXP"': - - text: Auto review - - superscript: EXP +- 'button "Access mode, current: Auto review"': Auto review - button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] - button "1 turns 2 steps" diff --git a/tsconfig.host.json b/tsconfig.host.json index af38cdb341..fda1ea2a30 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -99,7 +99,7 @@ "apps/web/tests/desktop-onboarding.e2e.ts", "apps/web/tests/bonus-notice.e2e.ts", "apps/web/tests/onboarding-usable-provider.e2e.ts", - "apps/web/tests/remote-welcome.e2e.ts", + "apps/web/tests/remote-settings.e2e.ts", "apps/web/tests/public-mount.e2e.ts", "apps/web/tests/workspace-new-session-folding.e2e.ts", "apps/web/tests/workspace-management.e2e.ts",