fix(sdk): use prepared pnpm for approved plugin management

This commit is contained in:
fz
2026-10-27 23:30:15 +08:00
parent c0f40e469d
commit 1c9490b410
19 changed files with 332 additions and 29 deletions
+4 -4
View File
@@ -15,8 +15,8 @@
en: 4e53164159b5ce41
zh: 89582dc0294d0776
/deepseek-ai-dsh-sdk-app/use-this-package:
en: a8f01c2dd55d7e6c
zh: b29c9c13f77043ad
en: 63aa871622cea3af
zh: 8f00a69640caadc5
/deepseek-ai-dsh-sdk-app/model-experience:
en: 215e7ba838619b7b
zh: a311da3843709f90
@@ -33,8 +33,8 @@
en: b1169743449b2272
zh: 98d465d802bf55c1
/deepseek-ai-dsh-sdk-app/known-limitations-and-deferred-work:
en: 4a0b06b6560f82ba
zh: 1701545ece16a274
en: d1d4567b7d2b9d8d
zh: c1618617d1562d8b
/deepseek-ai-dsh-sdk-app/known-limitations-and-deferred-work/dev-note:
en: b9b4e27513ebba78
zh: 51b123ac60ff28c6
+2 -2
View File
@@ -35,7 +35,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 model can use [`plugin_manager`](../../boot/plugin-manager/README.md) in native and PTC modes. Every action, including listing, requires full access or approval for that call. The default workspace-write/ask policy needs a client approval responder: enable `approvalRequests` and handle `approval/request` through `harness.client.onRequest()`, or supply an approver in the composition. Without one, the operation is refused as unavailable. Package installation/removal also needs `pnpm` on `PATH` or an explicit `plugin-manager.pnpmCommand`; the Python wheel does not supply it automatically. Saved profile changes report `restart-required` with `no-hmr`; the current runtime and its sessions retain their tools until the caller closes and starts the runtime again. Set `tool-plugin-manager.disabled: true` through a profile patch to hide this tool. The standalone `sdk-minimal` profile does not include it.
The model can use [`plugin_manager`](../../boot/plugin-manager/README.md) in native and PTC modes. Every action, including listing, requires full access or approval for that call. The default workspace-write/ask policy needs a client approval responder: enable `approvalRequests` and handle `approval/request` through `harness.client.onRequest()`, or supply an approver in the composition. Without one, the operation is refused as unavailable. For Python package installation/removal, explicitly prepare Node and pnpm with `download_primary_runtime()` before starting the runtime; the packaged launcher then supplies them to plugin management without requiring system pnpm. Without a selected payload, package operations need `pnpm` on `PATH` or an explicit `plugin-manager.pnpmCommand`. See the [interactive approval example](../../../python/sdk/examples/manage_plugins.py). Saved profile changes report `restart-required` with `no-hmr`; the current runtime and its sessions retain their tools until the caller closes and starts the runtime again. Set `tool-plugin-manager.disabled: true` through a profile patch to hide this tool. The standalone `sdk-minimal` profile does not include it.
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.
@@ -68,7 +68,7 @@ Enabling `plugin_manager` changes the tool-schema prefix from earlier SDK versio
- **Schedules share the runtime lifetime** — delivery needs a running process and its persistent Session storage.
- **A profile can omit the SDK server** — a custom profile selected by the TypeScript client must retain this bundle or another `dsh-sdk-jsonrpc-server` row; client initialization fails when no peer answers.
- **User plugins can violate stdout purity** — profile and per-launch patches are trusted application composition. The shipped bundle writes no non-protocol stdout, but it cannot contain an arbitrary inserted plugin.
- **Plugin changes share a profile** — saved changes affect `<DSH_HOME>/profiles/sdk` and other embeddings using that home on their next launch; use separate `dshHome` directories for independent compositions. Package operations fail without an available pnpm executable; downloaded authoring resources are not automatically wired into the plugin manager.
- **Plugin changes share a profile** — saved changes affect `<DSH_HOME>/profiles/sdk` and other embeddings using that home on their next launch; use separate `dshHome` directories for independent compositions. The Python launcher uses Node/pnpm from its selected downloaded runtime; without that payload or an external pnpm, package operations fail.
- **Configuration changes require restart** — the `sdk-app` bundle disables HMR in YAML so one stdio connection never observes a replacement server or Agent dependency.
+2 -2
View File
@@ -35,7 +35,7 @@ kind: "package-bundle"
SDK 使用 base 默认提供的 `read`、`write` 和 `edit`。要添加 `str_replace_editor`,请使用 [base 配置指南](../base/README.zh.md#use-this-package)中的显式插入 patch。独立的 `sdk-minimal` profile 自行决定其工具选择。
模型可以在 native 和 PTC 模式下使用 [`plugin_manager`](../../boot/plugin-manager/README.zh.md)。每个操作(包括列举)都要求完整权限或本次调用的批准。默认 workspace-write/ask 策略需要客户端审批应答方:启用 `approvalRequests`,并通过 `harness.client.onRequest()` 处理 `approval/request`,或在组合中提供审批器。没有应答方时,操作因 unavailable 被拒绝。安装/移除包还要求 `PATH` 中有 `pnpm`,或显式配置 `plugin-manager.pnpmCommand`;Python wheel 不会自动提供它。保存的 profile 变更返回 `restart-required` 和 `no-hmr`;当前运行时及其会话保留原有工具,直到调用方关闭并重新启动运行时。通过 profile patch 设置 `tool-plugin-manager.disabled: true` 可隐藏此工具。独立的 `sdk-minimal` profile 不包含它。
模型可以在 native 和 PTC 模式下使用 [`plugin_manager`](../../boot/plugin-manager/README.zh.md)。每个操作(包括列举)都要求完整权限或本次调用的批准。默认 workspace-write/ask 策略需要客户端审批应答方:启用 `approvalRequests`,并通过 `harness.client.onRequest()` 处理 `approval/request`,或在组合中提供审批器。没有应答方时,操作因 unavailable 被拒绝。Python 安装/移除包时,先显式调用 `download_primary_runtime()` 准备 Node 和 pnpm,再启动运行时;打包启动器会将它们提供给插件管理,无需系统 pnpm。未选择载荷时,包操作仍要求 `PATH` 中有 `pnpm`,或显式配置 `plugin-manager.pnpmCommand`。可运行的接入方式见[交互审批示例](../../../python/sdk/examples/manage_plugins.py)。保存的 profile 变更返回 `restart-required` 和 `no-hmr`;当前运行时及其会话保留原有工具,直到调用方关闭并重新启动运行时。通过 profile patch 设置 `tool-plugin-manager.disabled: true` 可隐藏此工具。独立的 `sdk-minimal` profile 不包含它。
Python 运行时向本 profile 提供显式下载的创作资源和 Office sidecar。`DSH_PRIMARY_RUNTIME` 覆盖创作载荷;空值禁用其查询。Office skills 可独立使用任一种资源。Profile patch 可独立于 `workspace-dependencies` 禁用 `skill-office` 或替换其 `assetRoot`;同名文件系统 skills 优先。下载与配置见[运行时包](../../../python/sdk-runtime/README.zh.md)。
@@ -68,7 +68,7 @@ profile 继承 base 系统提示词,包括 `You are an AI agent powered by Dee
- **定时任务依赖运行时生命周期**:投递需要运行中的进程及其持久化 Session 存储。
- **profile 可能省略 SDK server**:TypeScript client 选择的自定义 profile 必须保留本组合包或另一个 `dsh-sdk-jsonrpc-server` 配置项;没有 peer 响应时,client 初始化会失败。
- **用户插件可以破坏 stdout 纯净性**:profile 与逐次启动 patch 属于受信任应用组合。随附组合包不会向 stdout 写入非协议内容,但无法约束任意插入插件。
- **插件变更共享 profile** — 保存的变更影响 `<DSH_HOME>/profiles/sdk`,以及使用同一 home 的其他嵌入方的下次启动;独立组合应使用不同的 `dshHome` 目录。没有可用 pnpm 可执行文件时,包操作会失败;下载的创作资源不会自动接入插件管理器。
- **插件变更共享 profile** — 保存的变更影响 `<DSH_HOME>/profiles/sdk`,以及使用同一 home 的其他嵌入方的下次启动;独立组合应使用不同的 `dshHome` 目录。Python 启动器使用所选下载运行时中的 Node/pnpm;没有该载荷或外部 pnpm 时,包操作会失败。
- **配置变化需要重启**:`sdk-app` 组合包在 YAML 中禁用 HMR,因此一个 stdio 连接不会观察到 server 或 agent 依赖被替换。
+2 -2
View File
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-sdk-client/use-this-package:
en: 6c37c21a2e53376b
zh: e707d3e81c1242bd
en: e4a67206e52d524e
zh: db85694ac394a8ea
/deepseek-ai-dsh-sdk-client/use-this-package/running-agent-turns-with-deepseekharness:
en: 01c58dddd7e62de2
zh: 6dbd46e244f3f135
+24
View File
@@ -29,6 +29,30 @@ Use this client when TypeScript code must drive a complete Harness runtime from
For client-owned approvals, call `harness.client.start()`, install `harness.client.onRequest(handler)`, then call `harness.start()` with `approvalRequests: true` in the Harness options. The handler receives `approval/request` parameters and returns `{ outcome: "allowed-once" | "rejected" | "cancelled" }`; a rejected handler returns a JSON-RPC error and the runtime fails that approval closed. A replacement client needs its own handler; a closed client cannot restart. Native user questions keep their `onUserQuestion` callback. Enabling approvals without the additional-request handler returns an unknown-request error and fails approvals closed as `unavailable`. Without the opt-in, existing approval answerers remain in control. See the [wire protocol](../protocol/README.md) for request fields and cancellation.
For example, this terminal embedding asks for each one-operation grant. It does not change the Session's permission mode. Node deployments need an available pnpm for package operations; the [Python runtime](../../../python/sdk-runtime/README.md) can supply its explicitly downloaded Node/pnpm instead.
```ts
import { createInterface } from 'node:readline/promises'
import { stdin, stdout } from 'node:process'
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
const terminal = createInterface({ input: stdin, output: stdout })
try {
await using harness = new DeepSeekHarness({
dshHome: '/absolute/path/to/isolated-dsh-home', approvalRequests: true,
})
harness.client.start()
harness.client.onRequest(async (method, params) => {
if (method !== 'approval/request') throw new Error(`Unsupported request: ${method}`)
const answer = await terminal.question(`${JSON.stringify(params)}\nAllow once? [y/N] `)
return { outcome: answer.trim().toLowerCase() === 'y' ? 'allowed-once' : 'rejected' }
})
console.log((await harness.run('List the installed plugins.')).finalResponse)
} finally {
terminal.close()
}
```
`await harness.resumeSession(savedId)` restores a persisted session from the configured `dshHome` and returns a `HarnessSession` ready for `run()`. Save the original session id and reuse its home and workspace. Restoration preserves conversation history and its directory, repairs an interrupted final turn, and sends no new input. Missing, unreadable, incompatible, or occupied sessions reject without creating a replacement. `session(id)` remains a lazy handle for an in-process session; it does not request persisted restoration. Shell, browser, and other external processes are not restored by this API.
`HarnessSession.getWorkingDirectory()` reads the effective directory; `setWorkingDirectory(path)` changes it and returns the validated absolute path. Either method creates an unknown Session without starting a model turn. Relative paths use the Session's current directory. Origin metadata, permissions, and existing processes remain unchanged. `HarnessClient` exposes the same methods with a leading `sessionId` argument.
+24
View File
@@ -29,6 +29,30 @@ kind: "package-library"
若由客户端处理审批,先调用 `harness.client.start()`,安装 `harness.client.onRequest(handler)`,再调用 `harness.start()`,并在 Harness 选项中设置 `approvalRequests: true`。处理器接收 `approval/request` 参数,返回 `{ outcome: "allowed-once" | "rejected" | "cancelled" }`;处理器拒绝时返回 JSON-RPC 错误,运行时拒绝放行该审批。替换客户端时需要为新实例安装处理器;已关闭的客户端不能重启。原生问答仍由 `onUserQuestion` 回调处理。启用审批但未安装附加请求处理器时,服务端请求收到未知请求错误,审批以 `unavailable` 拒绝放行。未启用时保留现有审批应答链。请求字段与取消规则见[协议](../protocol/README.zh.md)。
下面的终端接入示例会逐项询问一次性授权,不改变会话的权限模式。Node 部署的包操作需要可用的 pnpm;[Python 运行时](../../../python/sdk-runtime/README.zh.md) 可以提供显式下载的 Node/pnpm。
```ts
import { createInterface } from 'node:readline/promises'
import { stdin, stdout } from 'node:process'
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
const terminal = createInterface({ input: stdin, output: stdout })
try {
await using harness = new DeepSeekHarness({
dshHome: '/absolute/path/to/isolated-dsh-home', approvalRequests: true,
})
harness.client.start()
harness.client.onRequest(async (method, params) => {
if (method !== 'approval/request') throw new Error(`Unsupported request: ${method}`)
const answer = await terminal.question(`${JSON.stringify(params)}\nAllow once? [y/N] `)
return { outcome: answer.trim().toLowerCase() === 'y' ? 'allowed-once' : 'rejected' }
})
console.log((await harness.run('List the installed plugins.')).finalResponse)
} finally {
terminal.close()
}
```
`await harness.resumeSession(savedId)` 从配置的 `dshHome` 恢复持久化会话,并返回可调用 `run()` 的 `HarnessSession`。请保存原会话 id,并沿用其 home 和工作区。恢复会保留对话历史及其目录、修复最后一个被中断的轮次,且不发送新输入。会话缺失、不可读、不兼容或被占用时,请求失败且不创建替代会话。`session(id)` 仍是当前进程内会话的惰性句柄,不会请求持久化恢复。此 API 不恢复 shell、浏览器或其他外部进程。
`HarnessSession.getWorkingDirectory()` 读取有效目录;`setWorkingDirectory(path)` 修改目录并返回校验后的绝对路径。两个方法都可创建未知 Session,而不启动模型轮次。相对路径以 Session 当前目录解析。起始目录元数据、权限与已有进程保持不变。`HarnessClient` 提供同名方法,并以 `sessionId` 作为首个参数。
+4 -4
View File
@@ -6,14 +6,14 @@
en: 1ea1d8e6d2c14b02
zh: 8e0b9903158667a1
/deepseek-harness-runtime-bin/installed-commands-and-artifacts:
en: 29707be30d04287f
zh: c9dfd1e37f31078b
en: 68407e833c62ee1f
zh: 9590f73d7daf8032
/deepseek-harness-runtime-bin/python-module-api:
en: 78a200e328d96637
zh: f3b7a0b69b7a39e0
/deepseek-harness-runtime-bin/packaged-profile-resolution:
en: af1ec072eb03da7e
zh: 5b540a7301741d41
en: 0f476fb87a1a3349
zh: 359dbd9b24b18b46
/deepseek-harness-runtime-bin/build-and-distribution:
en: a41e8eb7e99b9532
zh: 2789d8f43b1b54be
+2 -2
View File
@@ -25,7 +25,7 @@ primary_runtime = download_primary_runtime()
`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. 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.
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 and package-manager selection. `DSH_OFFICE_SIDECAR` can select an absolute sidecar directory with its standalone Node. 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:
@@ -58,7 +58,7 @@ Unsupported platforms and missing executables or sidecars raise `FileNotFoundErr
Python and the packaged bootstrap share the resource layout: `<cache>/<identity>/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 <name> ...`. That command and the SDK `plugin_manager` tool's package install/remove actions require `pnpm` on `PATH`, or an explicit `plugin-manager.pnpmCommand`. The wheel does not automatically select pnpm from downloaded authoring resources. Ordinary SDK/profile execution and listing/toggling existing plugins do not require pnpm. Every model-facing management action still requires full access or a client approval responder; saved changes affect the shared SDK profile on subsequent launches.
External profile management uses `dsh plugin --profile <name> ...`. After explicit `download_primary_runtime()` preparation, the packaged launcher supplies the selected payload's standalone Node and pnpm to this command and the SDK `plugin_manager` tool. It prepends the standalone Node directory only in package-operation subprocesses, including build scripts; ordinary tools retain their original `PATH`. `DSH_PRIMARY_RUNTIME` overrides the cached selection, and an empty value disables it. A malformed or incomplete selected payload fails startup rather than silently selecting system pnpm. Without a selected Node/pnpm payload, package operations use `pnpm` on `PATH` or explicit `plugin-manager.pnpmCommand`. Downloads never start implicitly. Ordinary SDK/profile execution and listing/toggling existing plugins do not require pnpm. Every model-facing management action still requires full access or a client approval responder; saved changes affect the shared SDK profile on subsequent launches.
## Build and distribution
+2 -2
View File
@@ -25,7 +25,7 @@ primary_runtime = download_primary_runtime()
`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 目录。用户可自行提供 Python 环境和文档库,无需下载创作环境。选择任一种资源即可使用 Office skills;仅有创作环境时,skills 中的 Kit CLI 被禁用。外部创作载荷保留 `primary-runtime/` 及同级 `office-skills/` 布局。SDK 原位读取资源,不复制到 `DSH_HOME`。`sdk-minimal` profile 不挂载这些提供方。
后续启动时,打包 bootstrap 将已下载资源提供给 SDK profile。`DSH_PRIMARY_RUNTIME` 覆盖缓存中的创作环境;空值禁用其查询和包管理器选择。`DSH_OFFICE_SIDECAR` 可选择包含独立 Node 的绝对 sidecar 目录。用户可自行提供 Python 环境和文档库,无需下载创作环境。选择任一种资源即可使用 Office skills;仅有创作环境时,skills 中的 Kit CLI 被禁用。外部创作载荷保留 `primary-runtime/` 及同级 `office-skills/` 布局。SDK 原位读取资源,不复制到 `DSH_HOME`。`sdk-minimal` profile 不挂载这些提供方。
选择 skills 与交付资源相互独立:项目、自定义目录和用户文件系统 skills 优先于同名随包 skills。SDK patch 可以仅禁用 Office 提供方,同时保留 Python 查询:
@@ -58,7 +58,7 @@ Office 操作技能通过 profile 的 shell 命令调用配置的 Node 和 Kit C
Python 与打包的 bootstrap 共享资源布局:`<cache>/<identity>/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 <name> ...`。该命令和 SDK `plugin_manager` 工具的包安装/移除操作要求 `PATH` 中存在 `pnpm`,或显式配置 `plugin-manager.pnpmCommand`。wheel 不会自动选择下载的创作资源中的 pnpm。普通 SDK/profile 运行及列举/切换现有插件不需要 pnpm。每个面向模型的管理操作仍要求完整权限或客户端审批应答方;保存的变更影响后续启动使用的共享 SDK profile。
外部 profile 管理使用 `dsh plugin --profile <name> ...`。显式调用 `download_primary_runtime()` 准备资源后,打包启动器会将所选载荷的独立 Node 和 pnpm 提供给该命令及 SDK `plugin_manager` 工具。独立 Node 目录仅加入包操作子进程(包括构建脚本)的环境;普通工具保留原有 `PATH`。`DSH_PRIMARY_RUNTIME` 覆盖缓存选择,空值禁用它。所选载荷格式错误或不完整时启动失败,不会静默改用系统 pnpm。没有选中包含 Node/pnpm 的载荷时,包操作使用 `PATH` 中的 pnpm 或显式 `plugin-manager.pnpmCommand`。不会隐式触发下载。普通 SDK/profile 运行及列举/切换现有插件不需要 pnpm。每个面向模型的管理操作仍要求完整权限或客户端审批应答方;保存的变更影响后续启动使用的共享 SDK profile。
## 构建与分发
+12 -2
View File
@@ -4,7 +4,7 @@ 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, isAbsolute, join } from 'node:path'
import { delimiter, dirname, isAbsolute, join } from 'node:path'
import { isSea } from 'node:sea'
import { fileURLToPath, pathToFileURL } from 'node:url'
@@ -96,8 +96,18 @@ if (resource !== undefined) {
if (cache !== undefined && existsSync(join(cache, 'primary', 'complete'))) {
process.env.DSH_BUNDLED_PRIMARY_RUNTIME = join(cache, 'primary', 'primary-runtime')
}
let packageManager
const primary = process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME
if (primary !== undefined && primary !== '') {
const { resolvePrimaryRuntime } = await import('@deepseek-ai/dsh-tool-workspace-dependencies')
const { node, pnpm } = await resolvePrimaryRuntime(primary)
if (node !== undefined && pnpm !== undefined) {
packageManager = { command: node, args: [pnpm],
env: { PATH: `${dirname(node)}${delimiter}${process.env.PATH ?? ''}` } }
}
}
const { runCli } = await import('@deepseek-ai/dsh/lib/bin.js')
await runCli()
await runCli({ packageManager })
} else {
Reflect.deleteProperty(process.env, selectorName)
const { runSelectedSubprocessRunner } = await import('@deepseek-ai/dsh-subprocess-local/runner')
+2 -2
View File
@@ -12,8 +12,8 @@
en: 1ae09bebf0f9e19b
zh: 55a0d557a2cae4d8
/deepseek-harness-python-sdk/client-approvals:
en: fc69be1beb8abff1
zh: 91b56781931a6f68
en: 1d18c09832dd4a42
zh: 6e7260b650a1f987
/deepseek-harness-python-sdk/customize-plugins:
en: 69a6a6745c6e89c1
zh: 85e82a4cc31e16e5
+2
View File
@@ -61,6 +61,8 @@ with DeepSeekHarness(dsh_home="/absolute/path/to/saved-home") as harness:
<a id="client-approvals"></a>
## Client approvals
For an interactive full-profile example that explicitly prepares Node/pnpm and answers permission requests while a turn runs, see [`examples/manage_plugins.py`](examples/manage_plugins.py). It uses the existing stdio approval protocol, asks before each operation, and closes the runtime before joining its request worker. Plugin configuration takes effect after runtime restart.
This low-level example receives one approval and rejects it. Use a dedicated request-handling thread or loop for a full run: `next_request()` blocks, and the request handler must run concurrently with `session/wait` or a high-level `run()`. Set `approval_requests=True` on `HarnessClient.initialize()` or `DeepSeekHarnessConfig` only when that handler is ready. Omission preserves the runtime's answerer chain. Decisions are one-shot; invalid responses and lost connections fail closed, and cancellation discards late replies. `ApprovalRequestResult` validates response values when constructing decisions.
```py
+2
View File
@@ -61,6 +61,8 @@ with DeepSeekHarness(dsh_home="/absolute/path/to/saved-home") as harness:
<a id="client-approvals"></a>
## 客户端审批
完整 profile 的交互示例见 [`examples/manage_plugins.py`](examples/manage_plugins.py):它显式准备 Node/pnpm,在运行任务时通过已有 stdio 审批协议逐项询问权限,并在等待请求工作线程结束前关闭运行时。插件配置在运行时重启后生效。
此底层示例接收一次审批并拒绝。完整运行应使用专门处理请求的线程或循环:`next_request()` 会阻塞,请求处理器必须与 `session/wait` 或高层 `run()` 并行运行。仅在处理器就绪时,在 `HarnessClient.initialize()` 或 `DeepSeekHarnessConfig` 设置 `approval_requests=True`;省略时保留运行时应答链。决定只作用于单次操作;无效响应和断连会拒绝放行,取消后丢弃迟到响应。构造决定时,可用 `ApprovalRequestResult` 校验响应值。
```py
+3
View File
@@ -8,6 +8,9 @@
/python-sdk-example/run-the-minimal-agent:
en: f0d785582fe72a91
zh: ee563fb17c775ed3
/python-sdk-example/manage-full-profile-plugins-with-approvals:
en: 654011a3ca04a691
zh: c33714237e9d5c64
/python-sdk-example/add-plugins:
en: 5e1e30dac13f6c9e
zh: 0af98ebb75c88f43
+13
View File
@@ -29,6 +29,19 @@ The bundle does not include `dsh-base`, so every additional row is an explicit p
The persistent PTY can modify any path available to the runtime process, so use a disposable checkout or container.
## Manage full-profile plugins with approvals
[`manage_plugins.py`](manage_plugins.py) explicitly downloads the optional authoring runtime, then starts the full `sdk` profile with `workspace-write` permissions and stdio approvals. It displays each requested operation and requires `y` to grant it once; any other answer rejects it. The prepared Node/pnpm runs package operations even without system pnpm. Downloading this payload also prepares its Python libraries and Office skills.
```sh
python python/sdk/examples/manage_plugins.py \
--dsh-home /absolute/path/to/example-dsh-home \
--workspace /absolute/path/to/disposable-workspace \
"Install and enable the bundle at /absolute/path/to/my-plugin-bundle."
```
The current process keeps its original plugins. After a `restart-required` result, rerun the example with the same home to use the saved configuration. Removing a bundle still used by the running startup composition requires stopping that runtime and using `dsh plugin`.
## Add plugins
Use the runtime wheel's `dsh` command against the same explicit home for persistent profile changes:
+13
View File
@@ -29,6 +29,19 @@ python python/sdk/examples/minimal.py \
持久 PTY 可以修改运行时进程可访问的任何路径,因此只应在一次性 checkout 或容器中使用。
## 通过审批管理完整 profile 的插件
[`manage_plugins.py`](manage_plugins.py) 先显式下载可选创作运行时,再以 `workspace-write` 权限和 stdio 审批启动完整 `sdk` profile。它显示每次请求的操作,只有输入 `y` 才批准一次,其他回答均拒绝。准备好的 Node/pnpm 可在没有系统 pnpm 时执行包操作。下载该载荷也会准备其中的 Python 库和 Office skills。
```sh
python python/sdk/examples/manage_plugins.py \
--dsh-home /absolute/path/to/example-dsh-home \
--workspace /absolute/path/to/disposable-workspace \
"Install and enable the bundle at /absolute/path/to/my-plugin-bundle."
```
当前进程保留原有插件。收到 `restart-required` 后,使用相同 home 重新运行示例以加载已保存的配置。删除仍被当前启动组合使用的组合包,需要先停止该运行时,再使用 `dsh plugin`。
## 添加插件
对同一个显式 home 使用运行时 wheel 包提供的 `dsh` 命令,以进行持久 profile 变更:
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Run a full SDK turn with explicit resource preparation and human permission decisions."""
from __future__ import annotations
import argparse
import os
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
from pathlib import Path
from deepseek_harness import ApprovalRequestParams, DeepSeekHarness
from deepseek_harness_runtime import download_primary_runtime
def main() -> None:
"""Prepare package tools, answer stdio approvals, and print the final response."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("prompt")
parser.add_argument("--dsh-home", type=Path, required=True)
parser.add_argument("--dsh-bin", type=Path)
parser.add_argument("--workspace", type=Path, default=Path.cwd())
args = parser.parse_args()
if os.environ.get("DSH_PRIMARY_RUNTIME") is None:
download_primary_runtime()
with ThreadPoolExecutor(max_workers=2) as worker:
harness = DeepSeekHarness(dsh_home=str(args.dsh_home.resolve()), cwd=str(args.workspace.resolve()),
dsh_bin=None if args.dsh_bin is None else str(args.dsh_bin.resolve()),
approval_requests=True,
env={"DSH_PERMISSION_MODE": "workspace-write"})
try:
harness.start()
run = worker.submit(harness.run, args.prompt)
request = worker.submit(harness.client.next_request)
while True:
ready, _ = wait((run, request), return_when=FIRST_COMPLETED)
if run in ready:
print(run.result().final_response)
break
incoming = request.result()
if incoming.method == "approval/request":
approval = ApprovalRequestParams.model_validate(incoming.payload)
print(f"Session {approval.sessionId}, tool {approval.toolName}: {approval.reason or ''}")
choice = input("Allow this operation once? [y/N] ").strip().lower()
harness.client.respond(incoming.id, {
"outcome": "allowed-once" if choice == "y" else "rejected",
})
else:
harness.client.respond_error(incoming.id, code=-32601, message="Unsupported client request")
request = worker.submit(harness.client.next_request)
finally:
# Closing first releases the pending next_request before the executor joins its workers.
harness.close()
if __name__ == "__main__":
main()
+61 -4
View File
@@ -2,7 +2,7 @@
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 { delimiter, dirname, join } from 'node:path'
import { expect, it } from 'vitest'
async function fixture() {
@@ -23,7 +23,26 @@ async function fixture() {
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/node_modules/@deepseek-ai/dsh/lib/bin.js'), `
export async function runCli(options = {}) {
const result = process.argv[2] ? { argv: process.argv.slice(2) }
: { primary: process.env.DSH_BUNDLED_PRIMARY_RUNTIME, office: process.env.DSH_BUNDLED_OFFICE_CLI };
if (options.packageManager) Object.assign(result, { packageManager: options.packageManager, hostPath: process.env.PATH });
console.log(JSON.stringify(result));
}
`)
await file(join(root, 'node/node_modules/@deepseek-ai/dsh-tool-workspace-dependencies/package.json'), '{"type":"module","exports":"./index.mjs"}')
await file(join(root, 'node/node_modules/@deepseek-ai/dsh-tool-workspace-dependencies/index.mjs'), `
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
export async function resolvePrimaryRuntime(root) {
const manifest = JSON.parse(await readFile(join(root, 'runtime.json'), 'utf8'));
return {
node: manifest.node ? join(root, 'dependencies/node/bin', process.platform === 'win32' ? 'node.exe' : 'node') : undefined,
pnpm: manifest.pnpm ? join(root, 'dependencies/pnpm/bin/pnpm.mjs') : undefined,
};
}
`)
await file(join(root, 'node/primary-runtime.mjs'), `
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
@@ -56,7 +75,7 @@ async function fixture() {
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 },
DSH_PRIMARY_RUNTIME: undefined, DSH_OFFICE_SIDECAR: undefined, DSH_BUNDLED_OFFICE_CLI: undefined, ...extra },
stdio: ['pipe', 'pipe', 'pipe'],
})
let stdout = '', stderr = ''
@@ -72,9 +91,47 @@ async function fixture() {
})
return { child, ready, done }
}
return { root, cache, run }
return { root, cache, run, file }
}
it('supplies downloaded Node and pnpm to profile and CLI package operations without changing the host PATH', async () => {
const { root, cache, run, file } = await fixture()
try {
await run('primary').done
const primary = join(cache, 'a'.repeat(64), 'primary', 'primary-runtime')
await file(join(primary, 'runtime.json'), JSON.stringify({ node: '24.9.0', pnpm: '11.0.0' }))
const node = join(primary, 'dependencies/node/bin', process.platform === 'win32' ? 'node.exe' : 'node')
for (const args of [[], ['plugin', '--profile', 'sdk', 'add', 'local-bundle']]) {
const result = await run(undefined, { PATH: '' }, args).done
expect(result.code, result.stderr).toBe(0)
expect(JSON.parse(result.stdout)).toMatchObject({
packageManager: { command: node, args: [join(primary, 'dependencies/pnpm/bin/pnpm.mjs')],
env: { PATH: `${dirname(node)}${delimiter}` } }, hostPath: '',
})
}
} finally { await rm(root, { recursive: true, force: true }) }
})
it('uses an explicit primary runtime instead of the cache and honors its empty disable value', async () => {
const { root, run, file } = await fixture()
try {
await run('primary').done
const primary = join(root, 'replacement')
await file(join(primary, 'runtime.json'), JSON.stringify({ node: '24.9.0', pnpm: '11.0.0' }))
const replaced = await run(undefined, { DSH_PRIMARY_RUNTIME: primary }).done
expect(replaced.code, replaced.stderr).toBe(0)
expect(JSON.parse(replaced.stdout)).toMatchObject({ packageManager: {
command: join(primary, 'dependencies/node/bin', process.platform === 'win32' ? 'node.exe' : 'node'),
} })
const disabled = await run(undefined, { DSH_PRIMARY_RUNTIME: '' }).done
expect(disabled.code, disabled.stderr).toBe(0)
expect(JSON.parse(disabled.stdout)).not.toHaveProperty('packageManager')
const missing = await run(undefined, { DSH_PRIMARY_RUNTIME: join(root, 'missing') }).done
expect(missing.code).toBe(1)
expect(missing.stderr).toContain('ENOENT')
} finally { await rm(root, { recursive: true, force: true }) }
})
it('starts without downloading, independently downloads either resource, and reuses completed caches', async () => {
const { root, cache, run } = await fixture()
try {
+103 -3
View File
@@ -21,6 +21,7 @@ import tempfile
import threading
import time
import zipfile
from concurrent.futures import ThreadPoolExecutor
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Literal
@@ -47,6 +48,7 @@ MCP_TEXT = "MCP client smoke ok"
PROFILE_PLUGIN_PROMPT = "Verify the Python-installed dsh profile plugin."
PROFILE_PLUGIN_TEXT = "profile plugin smoke ok"
PROFILE_PLUGIN_MARKER = "PYTHON_INSTALLED_DSH_PROFILE_PLUGIN"
PLUGIN_INSTALL_PROMPT = "Install SDK bundle: "
AUTHORING_PROMPT = "Query the packaged Python environment for Office authoring."
IS_WINDOWS = sys.platform == "win32"
MINIMAL_SHELL_TOOL = "pwsh" if IS_WINDOWS else "bash"
@@ -379,6 +381,19 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
result = tool_results[-1]
call_id, tool_name = latest_tool_call(messages, result.get("tool_use_id"))
tool_text = message_text(result.get("content"))
if call_id == "python-plugin-managed-install" and tool_name == "plugin_manager":
if result.get("is_error"):
if "rejected" not in tool_text.lower():
raise AssertionError(f"unexpected plugin approval failure: {tool_text}")
return text_chunks("plugin installation rejected")
outcome = json.loads(tool_text)
if (not outcome.get("changed") or outcome.get("application") != "restart-required"
or outcome.get("restartReason") != "no-hmr"
or outcome.get("packageResult", {}).get("exitCode") != 0):
raise AssertionError(f"approved package installation failed: {outcome}")
if PROFILE_PLUGIN_MARKER in json.dumps(messages):
raise AssertionError("installed plugin became active before the SDK runtime restarted")
return text_chunks("plugin installation saved")
if call_id == "python-plugin-install" and tool_name == "plugin_manager":
outcome = json.loads(tool_text)
if outcome.get("changed") or outcome.get("application") != "failed":
@@ -467,6 +482,13 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
(candidate for candidate in user_prompts if candidate in scenario_prompts),
message_text(latest.get("content")),
)
if prompt.startswith(PLUGIN_INSTALL_PROMPT):
assert_advertised_tool(body, "plugin_manager")
target, _ = json.JSONDecoder().raw_decode(prompt.removeprefix(PLUGIN_INSTALL_PROMPT))
assert isinstance(target, str), target
return tool_call_chunks("python-plugin-managed-install", "plugin_manager", {
"action": "install_bundle", "target": target,
})
if prompt == "check missing plugin package manager":
assert_advertised_tool(body, "plugin_manager")
return tool_call_chunks("python-plugin-install", "plugin_manager", {
@@ -830,7 +852,7 @@ def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--scenario",
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-minimal-in-history", "sdk-dynamic-tools", "sdk-fs-search", "sdk-spawn-node", "sdk-mcp", "sdk-snapshot", "sdk-recovery", "sdk-restart", "sdk-profile-plugin", "sdk-office", "sdk-authoring", "sdk-live", "runner", "direct"),
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-minimal-in-history", "sdk-dynamic-tools", "sdk-fs-search", "sdk-spawn-node", "sdk-mcp", "sdk-snapshot", "sdk-recovery", "sdk-restart", "sdk-profile-plugin", "sdk-plugin-manager", "sdk-office", "sdk-authoring", "sdk-live", "runner", "direct"),
default="all",
)
parser.add_argument("--exe", type=Path)
@@ -849,7 +871,7 @@ def main() -> None:
parser.error("--scenario sdk-profile-plugin requires --installed-wheel")
if args.installed_wheel:
args.exe = assert_installed_wheel_environment()
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-minimal-in-history", "sdk-dynamic-tools", "sdk-fs-search", "sdk-spawn-node", "sdk-snapshot", "sdk-recovery", "sdk-restart", "sdk-office", "sdk-authoring", "runner", "direct"} and args.exe is None:
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-minimal-in-history", "sdk-dynamic-tools", "sdk-fs-search", "sdk-spawn-node", "sdk-snapshot", "sdk-recovery", "sdk-restart", "sdk-office", "sdk-authoring", "sdk-plugin-manager", "runner", "direct"} and args.exe is None:
parser.error("--exe is required for custom, minimal, dynamic-tools, fs-search, spawn-node, snapshot, recovery, restart, office, runner, and direct scenarios")
if args.update_snapshots and args.scenario not in {"all", "sdk-minimal", "sdk-minimal-in-history", "sdk-dynamic-tools", "sdk-snapshot", "sdk-recovery", "sdk-restart", "sdk-authoring"}:
parser.error("--update-snapshots requires --scenario sdk-minimal, sdk-minimal-in-history, sdk-dynamic-tools, sdk-snapshot, sdk-recovery, sdk-restart, sdk-authoring, or all")
@@ -876,6 +898,12 @@ def main() -> None:
return
with MockModel() as model:
if args.scenario == "sdk-plugin-manager":
assert args.exe is not None
with tempfile.TemporaryDirectory(prefix="dsh-sdk-plugin-cache-") as cache:
subprocess.run([str(args.exe.resolve())], check=True, capture_output=True, text=True,
env={**os.environ, "DSH_RESOURCE_CACHE": cache, "DSH_RUNTIME_DOWNLOAD": "primary"})
smoke_sdk_plugin_manager(model.url, args.exe.resolve(), Path(cache))
if args.scenario in {"all", "sdk-authoring"}:
assert args.exe is not None
smoke_sdk_authoring(model.url, args.exe.resolve(), args.update_snapshots)
@@ -1001,9 +1029,80 @@ def _smoke_sdk_authoring_cached(base_url: str, executable: Path, update_snapshot
update_snapshots, AUTHORING_SNAPSHOT_DIRECTORY,
("model-visible.json",),
)
smoke_sdk_plugin_manager(base_url, executable, cache)
print("smoke-python-runtime: bundled Python, default skills, replacement and disabled skills passed")
def smoke_sdk_plugin_manager(base_url: str, executable: Path, cache: Path) -> None:
"""Approve a real local installation with bundled pnpm, then load it after restart."""
from deepseek_harness import ApprovalRequestParams, DeepSeekHarness
with tempfile.TemporaryDirectory(prefix="dsh-sdk-plugin-manager-") as temporary:
root = Path(temporary).resolve()
plugin = root / "plugin"
plugin.mkdir()
(plugin / "package.json").write_text(json.dumps({
"name": "dsh-sdk-managed-fixture", "version": "1.0.0", "type": "module",
"exports": "./index.js", "dsh": {"bundle": {"patch": "./cordis.patch.yml"}},
}))
(plugin / "index.js").write_text(
"export const name = 'sdk-managed-fixture'\n"
"export const inject = ['systemPrompt']\n"
"export function apply(ctx) { ctx.effect(() => ctx.systemPrompt.section({\n"
f" name: 'sdk:managed-fixture', order: 10, text: '{PROFILE_PLUGIN_MARKER}'\n"
"})) }\n"
)
(plugin / "cordis.patch.yml").write_text(json.dumps([{
"insert": [{"id": "sdk-managed-fixture", "name": "dsh-sdk-managed-fixture"}],
}]))
home = root / "home"
environment = {"PATH": "", "DSH_RESOURCE_CACHE": str(cache),
"DSH_PERMISSION_MODE": "workspace-write", "DSH_TELEMETRY_DISABLED": "1"}
config = dict(provider="deepseek-official", model="smoke-model", cwd=str(root),
dsh_bin=str(executable), dsh_home=str(home), env=environment,
api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60)
with ThreadPoolExecutor(max_workers=2) as worker:
harness = DeepSeekHarness(**config, approval_requests=True)
try:
harness.start()
manifest_path = home / "profiles/sdk/package.json"
before = manifest_path.read_bytes()
for decision, expected in (("rejected", "plugin installation rejected"),
("allowed-once", "plugin installation saved")):
run = worker.submit(harness.run, PLUGIN_INSTALL_PROMPT + json.dumps(str(plugin)) + "\n",
session_id=decision)
request = worker.submit(harness.client.next_request).result(timeout=60)
assert request.method == "approval/request", request
approval = ApprovalRequestParams.model_validate(request.payload)
assert approval.toolName == "plugin_manager", approval
assert approval.sessionId == decision, approval
harness.client.respond(request.id, {"outcome": decision})
result = run.result(timeout=60)
assert result.final_response == expected, result.final_response
if decision == "rejected":
assert manifest_path.read_bytes() == before
manifest = json.loads(manifest_path.read_text())
assert "dsh-sdk-managed-fixture" in manifest["dependencies"], manifest
assert "dsh-sdk-managed-fixture" in manifest["dsh"]["profile"]["bundles"], manifest
finally:
harness.close()
with DeepSeekHarness(**config) as restarted:
result = restarted.run(PROFILE_PLUGIN_PROMPT, session_id="installed-after-restart")
assert result.final_response == PROFILE_PLUGIN_TEXT, result.final_response
example = Path(__file__).resolve().parent.parent / "python/sdk/examples/manage_plugins.py"
example_home = root / "example-home"
example_environment = {**os.environ, **environment, "DEEPSEEK_BASE_URL": base_url,
"DEEPSEEK_API_KEY": "sk-keyless-smoke"}
example_args = [sys.executable, str(example), "--dsh-home", str(example_home),
"--workspace", str(root), "--dsh-bin", str(executable)]
for decision, expected in (("n", "plugin installation rejected"), ("y", "plugin installation saved")):
run = subprocess.run([*example_args, PLUGIN_INSTALL_PROMPT + json.dumps(str(plugin)) + "\n"],
input=decision + "\n", env=example_environment, text=True, capture_output=True, timeout=120)
assert run.returncode == 0, run.stderr
assert expected in run.stdout, run.stdout
assert "dsh-sdk-managed-fixture" in json.loads((example_home / "profiles/sdk/package.json").read_text())["dependencies"]
def smoke_sdk_office(executable: Path) -> None:
"""Exercise the relocated profile's bundled Office API and shell commands."""
from deepseek_harness import DeepSeekHarness
@@ -1306,7 +1405,8 @@ def smoke_sdk_default(base_url: str) -> None:
before = (dsh_home / "profiles/sdk/package.json").read_bytes()
with DeepSeekHarness(
provider="deepseek-official", model="smoke-model", cwd=str(root), dsh_home=str(dsh_home),
env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1", "PATH": ""},
env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1", "PATH": "",
"DSH_PRIMARY_RUNTIME": "", "DSH_RESOURCE_CACHE": str(root / "empty-resource-cache")},
api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60,
) as harness:
package_result = harness.run("check missing plugin package manager", session_id="missing-pnpm")